From d0f29d134951e35c6c99cc4f4fd158ff3a8f03c5 Mon Sep 17 00:00:00 2001 From: Ilya Zlobintsev Date: Sun, 9 Feb 2025 15:33:30 +0200 Subject: [PATCH] feat: wip egui plot --- Cargo.lock | 222 ++++++++++++++++++++- lact-gui/Cargo.toml | 2 + lact-gui/src/app/graphs_window/mod.rs | 1 + lact-gui/src/app/graphs_window/plot/imp.rs | 121 +++++------ lact-gui/src/app/graphs_window/plot/mod.rs | 11 +- 5 files changed, 293 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acbf562..66f475d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "addr2line" version = "0.24.2" @@ -597,12 +613,73 @@ dependencies = [ "syn", ] +[[package]] +name = "ecolor" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "egui" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6" +dependencies = [ + "ahash", + "bitflags 2.8.0", + "emath", + "epaint", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui_glow" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_plot" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1794c66fb727dac28dffed2e4b548e5118d1cccc331d368a35411d68725dde71" +dependencies = [ + "ahash", + "egui", + "emath", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "emath" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2" +dependencies = [ + "bytemuck", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -648,6 +725,41 @@ dependencies = [ "syn", ] +[[package]] +name = "epaint" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6" + +[[package]] +name = "epoxy" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b96028ce3ff03972312fd8243281858e80fc0f9838b1f035676b6c199214d9e" +dependencies = [ + "gl_generator", + "libc", + "pkg-config", + "shared_library", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -932,6 +1044,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "gl_generator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a795170cbd85b5a7baa58d6d7525cae6a03e486859860c220f7ebbbdd379d0a" +dependencies = [ + "khronos_api", + "log", + "xml-rs 0.7.0", +] + +[[package]] +name = "gl_loader" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "glib" version = "0.20.7" @@ -982,6 +1115,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gobject-sys" version = "0.20.7" @@ -1047,6 +1192,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gtk-egui-area" +version = "0.1.1" +dependencies = [ + "egui", + "egui_glow", + "epoxy", + "gl_loader", + "gtk4", + "libloading 0.8.6", +] + [[package]] name = "gtk4" version = "0.9.5" @@ -1257,6 +1414,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "khronos_api" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037ab472c33f67b5fbd3e9163a2645319e5356fcd355efa6d4eb7fff4bbcb554" + [[package]] name = "kqueue" version = "1.0.8" @@ -1358,6 +1521,8 @@ dependencies = [ "anyhow", "cairo-rs", "chrono", + "egui_plot", + "gtk-egui-area", "gtk4", "itertools", "lact-client", @@ -1613,6 +1778,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -1751,6 +1922,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pango" version = "0.20.7" @@ -1909,6 +2089,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" + [[package]] name = "quote" version = "1.0.38" @@ -2208,6 +2394,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2248,6 +2444,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -2580,6 +2785,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "uds_windows" version = "1.1.0" @@ -2645,7 +2856,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81086c28be67a8759cd80cbb3c8f7b520e0874605fc5eb74d5a1c9c2d1878e79" dependencies = [ - "xml-rs", + "xml-rs 0.8.25", ] [[package]] @@ -3003,6 +3214,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1cb601d29fe2c2ac60a2b2e5e293994d87a1f6fa9687a31a15270f909be9c2" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "xml-rs" version = "0.8.25" diff --git a/lact-gui/Cargo.toml b/lact-gui/Cargo.toml index b746f70..9d0532e 100644 --- a/lact-gui/Cargo.toml +++ b/lact-gui/Cargo.toml @@ -34,8 +34,10 @@ plotters = { version = "0.3.5", default-features = false, features = [ plotters-cairo = "0.7.0" cairo-rs = { version = "0.20", default-features = false } itertools = "0.13.0" +gtk-egui-area = { path = "../../gtk-egui-area" } thread-priority = "1.1.0" +egui_plot = "0.31.0" [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/lact-gui/src/app/graphs_window/mod.rs b/lact-gui/src/app/graphs_window/mod.rs index 0336bd4..41aa428 100644 --- a/lact-gui/src/app/graphs_window/mod.rs +++ b/lact-gui/src/app/graphs_window/mod.rs @@ -38,6 +38,7 @@ impl relm4::Component for GraphsWindow { set_margin_all: 10, set_row_spacing: 20, set_column_spacing: 20, + set_column_homogeneous: true, attach[0, 0, 1, 1]: temperature_plot = &Plot { set_title: "Temperature", diff --git a/lact-gui/src/app/graphs_window/plot/imp.rs b/lact-gui/src/app/graphs_window/plot/imp.rs index fe5d7ce..c4f4ce4 100644 --- a/lact-gui/src/app/graphs_window/plot/imp.rs +++ b/lact-gui/src/app/graphs_window/plot/imp.rs @@ -1,14 +1,18 @@ use chrono::NaiveDateTime; +use egui_plot::AxisHints; +use egui_plot::PlotBounds; +use egui_plot::PlotPoint; +use egui_plot::PlotPoints; use glib::Properties; use gtk::{glib, prelude::*, subclass::prelude::*}; +use gtk_egui_area::egui; +use gtk_egui_area::EguiArea; use std::cell::Cell; use std::cell::RefCell; use std::collections::BTreeMap; -use super::render_thread::{RenderRequest, RenderThread}; - #[derive(Properties, Default)] #[properties(wrapper_type = super::Plot)] pub struct Plot { @@ -24,7 +28,6 @@ pub struct Plot { secondary_y_label_area_relative_size: Cell, pub(super) data: RefCell, pub(super) dirty: Cell, - render_thread: RenderThread, #[property(get, set)] time_period_seconds: Cell, } @@ -33,7 +36,7 @@ pub struct Plot { impl ObjectSubclass for Plot { const NAME: &'static str = "Plot"; type Type = super::Plot; - type ParentType = gtk::Widget; + type ParentType = gtk::Box; } #[glib::derived_properties] @@ -41,58 +44,62 @@ impl ObjectImpl for Plot { fn constructed(&self) { self.parent_constructed(); - let obj = self.obj(); + let obj = self.obj().clone(); obj.set_height_request(250); obj.set_hexpand(true); obj.set_vexpand(true); - } -} -impl WidgetImpl for Plot { - fn snapshot(&self, snapshot: >k::Snapshot) { - let width = self.obj().width() as u32; - let height = self.obj().height() as u32; + let area = EguiArea::new(move |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + let data = obj.imp().data.borrow(); - if width == 0 || height == 0 { - return; - } + egui_plot::Plot::new(obj.title()) + .legend(egui_plot::Legend::default().position(egui_plot::Corner::RightBottom)) + .custom_x_axes(vec![AxisHints::new_x().label("Time").formatter( + |point, _| { + let time = chrono::DateTime::from_timestamp_millis(point.value as i64) + .unwrap() + .naive_local() + .time(); + time.to_string() + }, + )]) + .show(ui, |plot_ui| { + let max_x = data + .line_series_iter() + .next() + .and_then(|(_, points)| points.last()) + .map(|point| point.x) + .unwrap_or_default(); + let min_x = max_x - (obj.time_period_seconds() * 1000) as f64; - let last_texture = self.render_thread.get_last_texture(); - let size_changed = last_texture - .as_ref() - .map(|texture| (texture.width() as u32, texture.height() as u32) != (width, height)) - .unwrap_or(true); + let bounds = PlotBounds::from_min_max( + [min_x, -f64::INFINITY], + [max_x, f64::INFINITY], + ); + plot_ui.set_plot_bounds(bounds); + plot_ui.set_auto_bounds([false, true]); - if self.dirty.replace(false) || size_changed { - self.render_thread.replace_render_request(RenderRequest { - data: self.data.borrow().clone(), - width, - height, - title: self.title.borrow().clone(), - value_suffix: self.value_suffix.borrow().clone(), - secondary_value_suffix: self.secondary_value_suffix.borrow().clone(), - y_label_area_relative_size: self.y_label_area_relative_size.get(), - secondary_y_label_relative_area_size: self - .secondary_y_label_area_relative_size - .get(), - supersample_factor: 4, - time_period_seconds: self.time_period_seconds.get(), + for (name, points) in data.line_series_iter() { + plot_ui.line( + egui_plot::Line::new(PlotPoints::Borrowed(points)).name(name), + ); + } + }) }); - } - - // Rendering is always behind by at least one frame, but it's not an issue - if let Some(texture) = last_texture { - let bounds = gtk::graphene::Rect::new(0.0, 0.0, width as f32, height as f32); - // Uses by default Trillinear texture filtering, which is quite good at 4x supersampling - snapshot.append_texture(&texture, &bounds); - } + }); + self.obj().append(&area); } } +impl WidgetImpl for Plot {} + +impl BoxImpl for Plot {} + #[derive(Default, Clone)] pub struct PlotData { - pub(super) line_series: BTreeMap>, - pub(super) secondary_line_series: BTreeMap>, + pub(super) line_series: BTreeMap>, + pub(super) secondary_line_series: BTreeMap>, pub(super) throttling: Vec<(i64, (String, bool))>, } @@ -109,7 +116,10 @@ impl PlotData { self.line_series .entry(name.to_owned()) .or_default() - .push((time.and_utc().timestamp_millis(), point)); + .push(PlotPoint::new( + time.and_utc().timestamp_millis() as f64, + point, + )); } pub fn push_secondary_line_series_with_time( @@ -121,7 +131,10 @@ impl PlotData { self.secondary_line_series .entry(name.to_owned()) .or_default() - .push((time.and_utc().timestamp_millis(), point)); + .push(PlotPoint::new( + time.and_utc().timestamp_millis() as f64, + point, + )); } pub fn push_throttling(&mut self, name: &str, point: bool) { @@ -134,11 +147,11 @@ impl PlotData { )); } - pub fn line_series_iter(&self) -> impl Iterator)> { + pub fn line_series_iter(&self) -> impl Iterator)> { self.line_series.iter() } - pub fn secondary_line_series_iter(&self) -> impl Iterator)> { + pub fn secondary_line_series_iter(&self) -> impl Iterator)> { self.secondary_line_series.iter() } @@ -151,23 +164,17 @@ impl PlotData { pub fn trim_data(&mut self, last_seconds: i64) { // Limit data to N seconds for data in self.line_series.values_mut() { - let maximum_point = data - .last() - .map(|(date_time, _)| *date_time) - .unwrap_or_default(); + let maximum_point = data.last().map(|point| point.x as i64).unwrap_or_default(); - data.retain(|(time_point, _)| ((maximum_point - *time_point) / 1000) < last_seconds); + data.retain(|point| ((maximum_point - point.x as i64) / 1000) < last_seconds); } self.line_series.retain(|_, data| !data.is_empty()); for data in self.secondary_line_series.values_mut() { - let maximum_point = data - .last() - .map(|(date_time, _)| *date_time) - .unwrap_or_default(); + let maximum_point = data.last().map(|point| point.x as i64).unwrap_or_default(); - data.retain(|(time_point, _)| ((maximum_point - *time_point) / 1000) < last_seconds); + data.retain(|point| ((maximum_point - point.x as i64) / 1000) < last_seconds); } self.secondary_line_series diff --git a/lact-gui/src/app/graphs_window/plot/mod.rs b/lact-gui/src/app/graphs_window/plot/mod.rs index 4fb539b..077d0aa 100644 --- a/lact-gui/src/app/graphs_window/plot/mod.rs +++ b/lact-gui/src/app/graphs_window/plot/mod.rs @@ -1,17 +1,16 @@ -mod cubic_spline; +// mod cubic_spline; mod imp; -mod render_thread; -mod to_texture_ext; - -use std::cell::RefMut; +// mod render_thread; +// mod to_texture_ext; pub use imp::PlotData; +use std::cell::RefMut; use gtk::glib::{self, subclass::types::ObjectSubclassIsExt, Object}; glib::wrapper! { pub struct Plot(ObjectSubclass) - @extends gtk::Widget; + @extends gtk::Widget, gtk::Box; } impl Default for Plot {