wl-seasonal-hours-clock/src/main.rs

696 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extern crate smithay_client_toolkit as sctk;
use std::fmt;
use std::time::SystemTime;
use chrono::prelude::Local;
use chrono::TimeZone;
use chrono::Timelike;
use sctk::reexports::client::protocol::{wl_shm, wl_surface};
use sctk::shm::AutoMemPool;
use sctk::window::{Event as WEvent, FallbackFrame};
use svg::node::element::path::Data as PathData;
use svg::node::element::{
Circle, Definitions, Group, Line, Path, Rectangle, Style, Text, TextPath,
};
use svg::node::Text as TextNode;
use svg::Document;
sctk::default_environment!(SeasonalClock, desktop);
const HOUR_NAMES: [&str; 24] = [
"Candle", "Ice", "Comet", "Thimble", "Root", "Mist", "Sprout", "Rainbow", "Worm", "Bud",
"Blossom", "Ladybug", "Geese", "Dust", "Peach", "Fog", "Acorn", "Gourd", "Soup", "Crow",
"Mushroom", "Thunder", "Frost", "Lantern",
];
enum Season {
Spring,
Summer,
Autumn,
Winter,
}
impl fmt::Display for Season {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Season::Spring => "spring",
Season::Summer => "summer",
Season::Autumn => "autumn",
Season::Winter => "winter",
}
)
}
}
fn seconds_to_degrees(seconds: i32) -> f32 {
seconds as f32 * 360.0 / 86400.0
}
fn time_to_degrees(timestamp: i32, // should be time/timestamp
) -> f32 {
seconds_to_degrees(timestamp)
}
fn hour_name_path(image_width: u32, outer_r: f32, ring_width: f32) -> Path {
let radius = outer_r - ring_width / 2.0;
let delta_x = radius * (15.0_f32.to_radians() / 2.0).sin();
let delta_y = radius * (1.0 - (15.0_f32.to_radians() / 2.0).cos());
let x1 = (image_width as f32) / 2.0 - delta_x;
let y1 = ((image_width as f32) / 2.0 - radius) + delta_y;
let path_data = PathData::new().move_to((x1, y1)).elliptical_arc_by((
radius,
radius,
15,
0,
1,
2.0 * delta_x,
0,
));
Path::new().set("id", "hour-name-path").set("d", path_data)
}
fn hour_marker(
hour: i32,
image_width: u32,
outer_r: f32,
ring_width: f32,
hour_name_font_size: f32,
utc_hour_font_size: f32,
) -> Group {
let season = match hour {
0..=5 => Season::Winter,
6..=11 => Season::Spring,
12..=17 => Season::Summer,
18..=23 => Season::Autumn,
_ => panic!("Hour out of range"),
};
let rotation = hour * 15;
let delta_x = outer_r * (15f32.to_radians() / 2.0).sin();
let delta_y = outer_r * (1.0 - (15f32.to_radians() / 2.0).cos());
let s_delta_x = 0.0 - ring_width * (15f32.to_radians() / 2.0).sin();
let s_delta_y = ring_width * (15f32.to_radians() / 2.0).cos();
let i_delta_x = -2.0 * (outer_r - ring_width) * (15f32.to_radians() / 2.0).sin();
let x1 = image_width as f32 / 2.0 - delta_x;
let y1 = (image_width as f32 / 2.0 - outer_r) + delta_y;
let utc_hour_y = image_width as f32 / 2.0 - outer_r + ring_width + utc_hour_font_size;
let path_data = PathData::new()
.move_to((x1, y1))
.elliptical_arc_by((outer_r, outer_r, 15, 0, 1, 2.0 * delta_x, 0))
.line_by((s_delta_x, s_delta_y))
.elliptical_arc_by((
outer_r - ring_width,
outer_r - ring_width,
15,
0,
0,
i_delta_x,
0,
))
.close();
let path = Path::new().set("d", path_data);
let hour_name_text_path = TextPath::new()
.set("xlink:href", "#hour-name-path")
.set("startOffset", "50%")
.add(TextNode::new(HOUR_NAMES[hour as usize]));
let hour_name_text = Text::new()
.set("text-anchor", "middle")
.set("dominant-baseline", "mathematical")
.set("font-size", hour_name_font_size)
.add(hour_name_text_path);
let utc_hour_text = Text::new()
.set("class", "utc")
.set(
"transform",
format!("rotate(-7.5, {}, {})", image_width / 2, image_width / 2),
)
.set("x", image_width / 2)
.set("y", utc_hour_y)
.set("text-anchor", "middle")
.set("dominant-baseline", "mathematical")
.set("font-size", utc_hour_font_size)
.add(TextNode::new(format!("U {:02}", hour)));
Group::new()
.set("class", format!("hour {season}"))
.set(
"transform",
format!(
"rotate({}, {}, {})",
rotation as f32 - 172.5,
image_width / 2,
image_width / 2
),
)
.add(path)
.add(hour_name_text)
.add(utc_hour_text)
}
fn get_range_path(
image_width: u32,
radius: f32,
range_name: &str,
start_time: i32,
end_time: i32,
) -> Path {
let start_deg = time_to_degrees(start_time);
let end_deg = time_to_degrees(end_time);
let deg_diff = end_deg - start_deg;
let start_delta_x = radius * start_deg.to_radians().sin();
let start_delta_y = radius * (1.0 - start_deg.to_radians().cos());
let end_delta_x = radius * end_deg.to_radians().sin();
let end_delta_y = radius * (1.0 - end_deg.to_radians().cos());
let large_arc_flag = if deg_diff.abs() >= 180.0 { 0 } else { 1 };
let path_data = PathData::new()
.move_to((image_width / 2, image_width / 2))
.line_to((
image_width as f32 / 2.0 - start_delta_x,
image_width as f32 / 2.0 + radius - start_delta_y,
))
.elliptical_arc_to((
radius,
radius,
deg_diff,
large_arc_flag,
0,
image_width as f32 / 2.0 - end_delta_x,
image_width as f32 / 2.0 + radius - end_delta_y,
))
.close();
Path::new().set("class", range_name).set("d", path_data)
}
fn get_moon_path(image_width: u32, radius: f32, moon_phase: f64) -> Path {
let handle_x_pos = radius as f64 * 1.34;
let handle_y_pos = radius * 0.88;
let min_x = image_width as f64 / 2.0 - handle_x_pos;
let max_x = min_x + 2.0 * handle_x_pos;
let top_y = image_width as f32 / 2.0 - handle_y_pos;
let bottom_y = image_width as f32 / 2.0 + handle_y_pos;
let h1_x: f64;
let h2_x: f64;
if moon_phase < 14.0 {
h1_x = min_x + 2.0 * handle_x_pos * (1.0 - moon_phase / 14.0);
h2_x = max_x;
} else {
h1_x = min_x;
h2_x = max_x + 2.0 * handle_x_pos * (1.0 - moon_phase / 14.0)
}
let path_data = PathData::new()
.move_to((image_width as f32 / 2.0, image_width as f32 / 2.0 - radius))
.cubic_curve_to((
h1_x,
top_y,
h1_x,
bottom_y,
image_width as f32 / 2.0,
image_width as f32 / 2.0 + radius,
))
.cubic_curve_to((
h2_x,
bottom_y,
h2_x,
top_y,
image_width / 2,
image_width as f32 / 2.0 - radius,
));
Path::new().set("class", "moon").set("d", path_data)
}
fn gen_svg() -> Document {
let local_timestamp = Local::now();
let utc_offset = local_timestamp.offset().local_minus_utc();
let local_time = local_timestamp.time().num_seconds_from_midnight() as i32;
// TODO: Make these configurable
let lat = 47.655235;
let long = 19.2868815;
// TODO: These should be calculated instead of hardcoded
let image_width = 700u32;
// Calculate the Moon phase
let unixtime = suncalc::Timestamp(local_timestamp.timestamp_millis());
let moon_illumination = suncalc::moon_illumination(unixtime);
let moon_radius = image_width as f32 * 0.071428571;
let moon_phase = moon_illumination.phase * 28.0;
let sun_times = suncalc::get_times(unixtime, lat, long, None);
let noon = Local
.timestamp_millis(sun_times.solar_noon.0)
.time()
.num_seconds_from_midnight() as i32;
let midnight = Local
.timestamp_millis(sun_times.nadir.0)
.time()
.num_seconds_from_midnight() as i32;
let morning_golden_end = Local
.timestamp_millis(sun_times.golden_hour_end.0)
.time()
.num_seconds_from_midnight() as i32;
let evening_golden_start = Local
.timestamp_millis(sun_times.golden_hour.0)
.time()
.num_seconds_from_midnight() as i32;
let sunrise = Local
.timestamp_millis(sun_times.sunrise.0)
.time()
.num_seconds_from_midnight() as i32;
let sunset = Local
.timestamp_millis(sun_times.sunset.0)
.time()
.num_seconds_from_midnight() as i32;
let dawn = Local
.timestamp_millis(sun_times.dawn.0)
.time()
.num_seconds_from_midnight() as i32;
let dusk = Local
.timestamp_millis(sun_times.dusk.0)
.time()
.num_seconds_from_midnight() as i32;
let local_hour_font_size = image_width as f32 * 0.02357;
let hour_name_font_size = image_width as f32 * 0.019109;
let utc_hour_font_size = image_width as f32 * 0.021462;
let outer_r = (image_width as f32) / 2.0 - 3.0 * hour_name_font_size;
let ring_width = hour_name_font_size * 3.0;
let sun_radius = image_width as f32 * 0.0142871;
let marker_radius = outer_r - ring_width - 2.0 * utc_hour_font_size;
let border = Rectangle::new()
.set("x", 0i32)
.set("y", 0i32)
.set("width", 700i32)
.set("height", 700i32)
.set("id", "border");
let stylesheet = Style::new(
"\
#border {stroke: none; fill: rgb(19, 17, 30); }
.hour path {stroke: rgb(0, 0, 0); stroke-width: 2px;}
.hour text {stroke: none; fill: rgb(238, 187, 85);}
.hour text.utc {stroke: none; fill: rgb(91, 68, 38);}
.winter path {fill: rgb(70, 62, 108);}
.spring path {fill: rgb(55, 87, 55);}
.summer path {fill: rgb(113, 92, 43);}
.autumn path {fill: rgb(108, 68, 44);}
.local-hour {stroke: none; fill: rgb(238, 187, 85);}
.night-time {stroke: none; fill: rgb(19, 17, 30);}
.blue-hour {stroke: none; fill: rgb(9, 1, 119);}
.golden-hour {stroke: none; fill: rgb(170, 132, 44);}
.day-time {stroke: none; fill: rgb(125, 197, 240);}
.marker {stroke: rgb(19, 17, 30); stroke-width: 2px; fill: none;}
.moon-background {stroke: rgb(170, 170, 170); stroke-width: 2px; fill: rgb(19, 17, 30);}
.moon {stroke: none; fill: rgb(170, 170, 170);}
.sun {stroke: none; fill: rgb(238, 187, 85);}
.mid-marker {stroke: red;}
.dial {stroke-width: 2px; stroke: rgb(238, 187, 85);}",
);
let definitions = Definitions::new().add(hour_name_path(image_width, outer_r, ring_width));
let mut local_clock = Group::new().set("id", "local-clock");
for hour in 0i32..24 {
let hour_str = match hour {
0 => "Midnight".to_string(),
12 => "Noon".to_string(),
_ => hour.to_string(),
};
let rotation = hour * 15;
let hour_name = TextNode::new(hour_str);
let hour_node = Text::new()
.set("class", "local-hour")
.set("transform", format!("rotate({}, 350, 350)", 180 + rotation))
.set("x", (image_width as f32) / 2.0)
.set(
"y",
(image_width as f32) / 2.0 - outer_r - local_hour_font_size / 2.0,
)
.set("text-anchor", "middle")
.set("font-size", local_hour_font_size as f32)
.add(hour_name);
local_clock = local_clock.add(hour_node);
}
let mut seasonal_clock = Group::new().set(
"transform",
format!(
"rotate({}, {}, {})",
seconds_to_degrees(utc_offset),
image_width / 2,
image_width / 2
),
);
for hour in 0i32..24 {
seasonal_clock = seasonal_clock.add(hour_marker(
hour,
image_width,
outer_r,
ring_width,
hour_name_font_size,
utc_hour_font_size,
));
}
let daytime_circle = Circle::new()
.set("class", "day-time")
.set("cx", image_width / 2)
.set("cy", image_width / 2)
.set("r", marker_radius);
let golden_hour_path = get_range_path(
image_width,
marker_radius,
"golden-hour",
morning_golden_end,
evening_golden_start,
);
let sun_disc = Circle::new()
.set("class", "sun")
.set("cx", image_width / 2)
.set("cy", image_width as f32 / 2.0 + outer_r / 2.0 + sun_radius)
.set("r", sun_radius)
.set(
"transform",
format!(
"rotate({}, {}, {})",
time_to_degrees(local_time),
image_width / 2,
image_width / 2
),
);
let blue_hour_path = get_range_path(
image_width,
marker_radius,
"blue-hour",
sunrise, // morning_blue_end
sunset, // evening_blue_start
);
let nighttime_path = get_range_path(image_width, marker_radius, "night-time", dawn, dusk);
let marker_circle = Circle::new()
.set("class", "marker")
.set("cx", image_width / 2)
.set("cy", image_width / 2)
.set("r", marker_radius);
let day_parts_group = Group::new()
.set("id", "day-parts")
.add(daytime_circle)
.add(golden_hour_path)
.add(sun_disc)
.add(blue_hour_path)
.add(nighttime_path)
.add(marker_circle);
let moon_circle = Circle::new()
.set("class", "moon-background")
.set("cx", image_width / 2)
.set("cy", image_width / 2)
.set("r", moon_radius);
let moon_group = Group::new()
.set("id", "moon-container")
.add(moon_circle)
.add(get_moon_path(image_width, moon_radius, moon_phase));
let noon_marker = Line::new()
.set("class", "mid-marker")
.set(
"transform",
format!(
"rotate({}, {}, {})",
time_to_degrees(noon),
image_width / 2,
image_width / 2
),
)
.set("x1", image_width / 2)
.set(
"y1",
image_width as f32 / 2.0 + marker_radius - sun_radius as f32,
)
.set("x2", image_width / 2)
.set("y2", image_width as f32 / 2.0 + marker_radius);
let midnight_marker = Line::new()
.set("class", "mid-marker")
.set(
"transform",
format!(
"rotate({}, {}, {})",
time_to_degrees(midnight),
image_width / 2,
image_width / 2
),
)
.set("x1", image_width / 2)
.set(
"y1",
image_width as f32 / 2.0 + marker_radius - sun_radius as f32,
)
.set("x2", image_width / 2)
.set("y2", image_width as f32 / 2.0 + marker_radius);
let dial = Line::new()
.set("id", "dial")
.set("class", "dial")
.set(
"transform",
format!(
"rotate({}, {}, {})",
time_to_degrees(local_time),
image_width / 2,
image_width / 2
),
)
.set("x1", image_width / 2)
.set("y1", image_width as f32 / 2.0 + outer_r * 0.5)
.set("x2", image_width / 2)
.set(
"y2",
image_width as f32 / 2.0 + outer_r - ring_width + hour_name_font_size,
);
Document::new()
.set("viewBox", (0i32, 0i32, 700i32, 700i32))
.set("width", 700i32)
.set("height", 700i32)
.set("xmlns:xlink", "http://www.w3.org/1999/xlink")
.add(stylesheet)
.add(definitions)
.add(border)
.add(local_clock)
.add(day_parts_group)
.add(seasonal_clock)
.add(moon_group)
.add(noon_marker)
.add(midnight_marker)
.add(dial)
}
fn main() {
let (env, display, mut queue) = sctk::new_default_environment!(SeasonalClock, desktop)
.expect("Unable to connect to a Wayland compositor");
let surface = env
.create_surface_with_scale_callback(|dpi, _surface, _dispatch_data| {
println!("dpi changed to {}", dpi);
})
.detach();
let mut next_action = None::<WEvent>;
let mut window = env
.create_window::<FallbackFrame, _>(
surface,
None,
(100, 100),
move |evt, mut dispatch_data| {
let next_action = dispatch_data.get::<Option<WEvent>>().unwrap();
let replace = matches!(
(&evt, &*next_action),
(_, &None)
| (_, &Some(WEvent::Refresh))
| (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
| (&WEvent::Close, _)
);
if replace {
*next_action = Some(evt);
}
},
)
.expect("Failed to create a window !");
window.set_title("Seasonal Hours Clock".to_string());
let mut pool = env
.create_auto_pool()
.expect("Failed to create the memory pool.");
let mut need_redraw = false;
let mut dimensions = (700, 700);
if !env.get_shell().unwrap().needs_configure() {
redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
window.refresh()
}
let now = SystemTime::now();
let mut last_elapsed = 0;
loop {
// Update every second
// TODO: Theres probably a better way to do this…
match now.elapsed() {
Ok(elapsed) => {
let new_elapsed = elapsed.as_secs();
if new_elapsed != last_elapsed {
need_redraw = true;
last_elapsed = new_elapsed;
}
}
Err(e) => {
println!("Error: {:?}", e);
}
}
match next_action.take() {
Some(WEvent::Close) => break,
Some(WEvent::Refresh) => {
window.refresh();
window.surface().commit();
}
Some(WEvent::Configure {
new_size,
states: _,
}) => {
if let Some((w, h)) = new_size {
if dimensions != (w, h) {
dimensions = (w, h);
}
}
window.resize(dimensions.0, dimensions.1);
window.refresh();
need_redraw = true;
}
None => {}
}
if need_redraw {
need_redraw = false;
redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw")
}
if let Err(e) = display.flush() {
if e.kind() != ::std::io::ErrorKind::WouldBlock {
eprintln!("Error while trying to flush the wayland socket: {:?}", e);
}
}
if let Some(guard) = queue.prepare_read() {
if let Err(e) = guard.read_events() {
if e.kind() != ::std::io::ErrorKind::WouldBlock {
eprintln!(
"Error while trying to read from the wayland socked: {:?}",
e
);
}
}
}
queue
.dispatch_pending(&mut next_action, |_, _, _| {})
.expect("Failed to dispatch all messages.");
}
}
fn redraw(
pool: &mut AutoMemPool,
surface: &wl_surface::WlSurface,
(buf_x, buf_y): (u32, u32),
) -> Result<(), ::std::io::Error> {
let document = gen_svg();
let bytes = document.to_string();
let mut opt = usvg::Options::default();
opt.fontdb.load_system_fonts();
opt.font_family = "Liberation Serif".to_string();
let svg_tree = usvg::Tree::from_str(&bytes, &opt.to_ref()).unwrap();
let (canvas, new_buffer) = pool.buffer(
buf_x as i32,
buf_y as i32,
4 * buf_x as i32,
wl_shm::Format::Argb8888,
)?;
let image_size = ::std::cmp::min(buf_x, buf_y);
let move_x = (buf_x - image_size) as f32 / 2.0;
let move_y = (buf_y - image_size) as f32 / 2.0;
let mut pixmap = tiny_skia::Pixmap::new(buf_x, buf_y).unwrap();
// pre-fill the pixmap with our background color so we dont have a white strip at the edges
pixmap.fill(
tiny_skia::Color::from_rgba(19f32 / 255f32, 17f32 / 255f32, 30f32 / 255f32, 1f32).unwrap(),
);
resvg::render(
&svg_tree,
usvg::FitTo::Size(image_size, image_size),
tiny_skia::Transform::from_translate(move_x, move_y),
pixmap.as_mut(),
)
.unwrap();
// We do not have anything to draw yet, so draw an empty surface
for (dst_pixel, src_pixel) in canvas.chunks_exact_mut(4).zip(pixmap.pixels()) {
let r = src_pixel.red() as u32;
let g = src_pixel.green() as u32;
let b = src_pixel.blue() as u32;
let a = src_pixel.alpha() as u32;
let r = ::std::cmp::min(0xFF, (0xFF * (0xFF - a) + a * r) / 0xFF);
let g = ::std::cmp::min(0xFF, (0xFF * (0xFF - a) + a * g) / 0xFF);
let b = ::std::cmp::min(0xFF, (0xFF * (0xFF - a) + a * b) / 0xFF);
let pixel: [u8; 4] = ((0xFF << 24) + (r << 16) + (g << 8) + b).to_ne_bytes();
dst_pixel[0] = pixel[0];
dst_pixel[1] = pixel[1];
dst_pixel[2] = pixel[2];
dst_pixel[3] = pixel[3];
}
surface.attach(Some(&new_buffer), 0, 0);
if surface.as_ref().version() >= 4 {
surface.damage_buffer(0, 0, buf_x as i32, buf_y as i32);
} else {
surface.damage(0, 0, buf_x as i32, buf_y as i32);
}
surface.commit();
Ok(())
}