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

405 lines
12 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::time::SystemTime;
use chrono::prelude::Local;
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, Group, Line, Path, Rectangle, Style, Text};
use svg::node::Text as TextNode;
use svg::Document;
sctk::default_environment!(SeasonalClock, desktop);
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_marker(
hour: i32,
image_width: u32,
outer_r: f32,
ring_width: f32,
utc_hour_font_size: f32,
) -> Group {
let rotation = hour * 15;
let utc_hour_y = image_width as f32 / 2.0 - outer_r + ring_width + utc_hour_font_size;
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", "hour")
.set(
"transform",
format!(
"rotate({}, {}, {})",
rotation as f32 - 172.5,
image_width / 2,
image_width / 2
),
)
.add(utc_hour_text)
}
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 {
// These should be calculated
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: 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 local_hour_font_size = 16.5;
let utc_hour_font_size = image_width as f32 * 0.021462;
let hour_name_font_size = 13.37699;
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 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 text.utc {stroke: none; fill: rgb(91, 68, 38);}
.local-hour {stroke: none; fill: rgb(238, 187, 85);}
.moon-background {stroke: rgb(170, 170, 170); stroke-width: 2px; fill: rgb(19, 17, 30);}
.moon {stroke: none; fill: rgb(170, 170, 170);}
.dial {stroke-width: 2px; stroke: rgb(238, 187, 85);}",
);
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,
utc_hour_font_size,
));
}
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 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(border)
.add(local_clock)
.add(seasonal_clock)
.add(moon_group)
.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();
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(())
}