use adw::prelude::*; use fixed::types::{I8F8, U128F0}; use glib::{Object, Sender}; use gtk::subclass::prelude::*; use lights_core::{ App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI, }; use std::{ cell::RefCell, env, rc::Rc, sync::mpsc::{Receiver, TryRecvError}, }; const WIDTH: i32 = 640; const HEIGHT: i32 = 480; pub struct Update { dashboard: DashboardPattern, lights: BodyPattern, } pub struct DashboardLightsPrivate { lights: Rc>, } #[glib::object_subclass] impl ObjectSubclass for DashboardLightsPrivate { const NAME: &'static str = "DashboardLights"; type Type = DashboardLights; type ParentType = gtk::DrawingArea; fn new() -> Self { Self { lights: Rc::new(RefCell::new(OFF_DASHBOARD)), } } } impl ObjectImpl for DashboardLightsPrivate {} impl WidgetImpl for DashboardLightsPrivate {} impl DrawingAreaImpl for DashboardLightsPrivate {} glib::wrapper! { pub struct DashboardLights(ObjectSubclass) @extends gtk::DrawingArea, gtk::Widget; } impl DashboardLights { pub fn new() -> Self { let s: Self = Object::builder().build(); s.set_width_request(WIDTH); s.set_height_request(100); s.set_draw_func({ let s = s.clone(); move |_, context, width, _| { let start = width as f64 / 2. - 150.; let lights = s.imp().lights.borrow(); for i in 0..3 { context.set_source_rgb( lights[i].r.into(), lights[i].g.into(), lights[i].b.into(), ); context.rectangle(start + 100. * i as f64, 10., 80., 80.); let _ = context.fill(); } } }); s } pub fn set_lights(&self, lights: DashboardPattern) { *self.imp().lights.borrow_mut() = lights; self.queue_draw(); } } pub struct BikeLightsPrivate { lights: Rc>, } #[glib::object_subclass] impl ObjectSubclass for BikeLightsPrivate { const NAME: &'static str = "BikeLights"; type Type = BikeLights; type ParentType = gtk::DrawingArea; fn new() -> Self { Self { lights: Rc::new(RefCell::new(OFF_BODY)), } } } impl ObjectImpl for BikeLightsPrivate {} impl WidgetImpl for BikeLightsPrivate {} impl DrawingAreaImpl for BikeLightsPrivate {} glib::wrapper! { pub struct BikeLights(ObjectSubclass) @extends gtk::DrawingArea, gtk::Widget; } impl BikeLights { pub fn new() -> Self { let s: Self = Object::builder().build(); s.set_width_request(WIDTH); s.set_height_request(640); let center = WIDTH as f64 / 2.; s.set_draw_func({ let s = s.clone(); move |_, context, _, _| { let lights = s.imp().lights.borrow(); for i in 0..30 { context.set_source_rgb( lights[i].r.into(), lights[i].g.into(), lights[i].b.into(), ); context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.); let _ = context.fill(); } for i in 0..30 { context.set_source_rgb( lights[i + 30].r.into(), lights[i + 30].g.into(), lights[i + 30].b.into(), ); context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.); let _ = context.fill(); } } }); s } pub fn set_lights(&self, lights: [RGB; 60]) { *self.imp().lights.borrow_mut() = lights; self.queue_draw(); } } struct GTKUI { tx: Sender, rx: Receiver, } impl UI for GTKUI { fn check_event(&self) -> Option { match self.rx.try_recv() { Ok(event) => Some(event), Err(TryRecvError::Empty) => None, Err(TryRecvError::Disconnected) => None, } } fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) { self.tx .send(Update { dashboard: dashboard_lights, lights, }) .unwrap(); } } fn main() { let adw_app = adw::Application::builder() .application_id("com.luminescent-dreams.bike-light-simulator") .build(); adw_app.connect_activate(move |adw_app| { let (update_tx, update_rx) = gtk::glib::MainContext::channel::(gtk::glib::Priority::DEFAULT); let (event_tx, event_rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let mut bike_app = App::new(Box::new(GTKUI { tx: update_tx, rx: event_rx, })); loop { bike_app.tick(Instant(U128F0::from( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(), ))); std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64))); } }); let window = adw::ApplicationWindow::builder() .application(adw_app) .default_width(WIDTH) .default_height(HEIGHT) .build(); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); let controls = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); let dashboard_lights = DashboardLights::new(); let bike_lights = BikeLights::new(); let left_button = gtk::Button::builder().label("L").build(); let brake_button = gtk::Button::builder().label("Brakes").build(); let right_button = gtk::Button::builder().label("R").build(); left_button.connect_clicked({ let event_tx = event_tx.clone(); move |_| { let _ = event_tx.send(Event::LeftBlinker); } }); brake_button.connect_clicked({ let event_tx = event_tx.clone(); move |_| { let _ = event_tx.send(Event::Brake); } }); right_button.connect_clicked({ let event_tx = event_tx.clone(); move |_| { let _ = event_tx.send(Event::RightBlinker); } }); controls.append(&left_button); controls.append(&brake_button); controls.append(&right_button); layout.append(&controls); let pattern_controls = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); let previous_pattern = gtk::Button::builder().label("Previous").build(); let next_pattern = gtk::Button::builder().label("Next").build(); previous_pattern.connect_clicked({ let event_tx = event_tx.clone(); move |_| { let _ = event_tx.send(Event::PreviousPattern); } }); next_pattern.connect_clicked({ let event_tx = event_tx.clone(); move |_| { let _ = event_tx.send(Event::NextPattern); } }); pattern_controls.append(&previous_pattern); pattern_controls.append(&next_pattern); layout.append(&pattern_controls); layout.append(&dashboard_lights); layout.append(&bike_lights); update_rx.attach(None, { let dashboard_lights = dashboard_lights.clone(); let bike_lights = bike_lights.clone(); move |Update { dashboard, lights }| { dashboard_lights.set_lights(dashboard); bike_lights.set_lights(lights); glib::ControlFlow::Continue } }); window.set_content(Some(&layout)); window.present(); }); let args: Vec = env::args().collect(); ApplicationExtManual::run_with_args(&adw_app, &args); }