diff --git a/CHANGELOG.md b/CHANGELOG.md index 4965ec97ebc..6ded790d2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). * Renamed `Frame::margin` to `Frame::inner_margin`. * Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)). +* `dark-light` (dark mode detection) is now an opt-in feature for `eframe` and `egui_glow` ([#1437](https://github.com/emilk/egui/pull/1437)). ### Fixed 🐛 * Fixed ComboBoxes always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)). diff --git a/Cargo.lock b/Cargo.lock index 2d4bb847919..6112e122c10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61caed9aec6daeee1ea38ccf5fb225e4f96c1eeead1b4a5c267324a63cf02326" +checksum = "d54a65e0d4f66f8536c98cb3ca81ca33b7e2ca43442465507a3a62291ec0d9e4" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -423,9 +423,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd" +checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5" dependencies = [ "smallvec", ] @@ -1080,9 +1080,11 @@ dependencies = [ name = "egui_extras" version = "0.17.0" dependencies = [ + "chrono", "egui", "image", "resvg", + "serde", "tiny-skia", "usvg", ] @@ -1164,9 +1166,9 @@ dependencies = [ [[package]] name = "enum-map" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7848397e7221a27d81cb7f07498d563f09b23fcd52ce9f74a6a110ed28f7cd4f" +checksum = "82605a2a3d13a9661b07ba27f39d00496aa347c9c236b1a3b8201c1b6d761408" dependencies = [ "enum-map-derive", "serde", @@ -1305,14 +1307,14 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db856ee8cca3b9f23dd11c13bf3d4854b663ae86ed0c4a627a354431fc265f66" +checksum = "122fa73a5566372f9df09768a16e8e3dad7ad18abe07835f1f0b71f84078ba4c" dependencies = [ "fontconfig-parser", "log", "memmap2 0.5.3", - "ttf-parser 0.15.0", + "ttf-parser", ] [[package]] @@ -1425,9 +1427,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" dependencies = [ "cfg-if 1.0.0", "libc", @@ -2297,11 +2299,11 @@ dependencies = [ [[package]] name = "owned_ttf_parser" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef05f2882a8b3e7acc10c153ade2631f7bfc8ce00d2bf3fb8f4e9d2ae6ea5c3" +checksum = "4fb1e509cfe7a12db2a90bfa057dfcdbc55a347f5da677c506b53dd099cfec9d" dependencies = [ - "ttf-parser 0.14.0", + "ttf-parser", ] [[package]] @@ -2450,9 +2452,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8f1882177b17c98ec33a51f5910ecbf4db92ca0def706781a1f8d0c661f393" +checksum = "02cd7d51cea7e2fa6bbcb8af5fbcad15b871451bfc2d20ed72dff2f4ae072a84" dependencies = [ "bitflags", "crc32fast", @@ -2490,9 +2492,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro-crate" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dada8c9981fcf32929c3c0f0cd796a9284aca335565227ed88c83babb1d43dc" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -2775,7 +2777,7 @@ dependencies = [ "bitflags", "bytemuck", "smallvec", - "ttf-parser 0.15.0", + "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-general-category", @@ -2836,9 +2838,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" +checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" [[package]] name = "serde" @@ -3337,12 +3339,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ttf-parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" - [[package]] name = "ttf-parser" version = "0.15.0" @@ -3488,7 +3484,7 @@ dependencies = [ "simplecss", "siphasher", "svgtypes", - "ttf-parser 0.15.0", + "ttf-parser", "unicode-bidi", "unicode-script", "unicode-vo", diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index ae83aa92259..bf100752c68 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -18,6 +18,7 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG * You can now load/save state in `App::update` * Changed `App::update` to take `&mut Frame` instead of `&Frame`. * `Frame` is no longer `Clone` or `Sync`. +* Add `glow` (OpenGL) context to `Frame` ([#1425](https://github.com/emilk/egui/pull/1425)). ## 0.17.0 - 2022-02-22 diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index 5b2f4b40fbf..f1a50935f55 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -42,6 +42,8 @@ screen_reader = [ "egui_web/screen_reader", ] +dark-light = [ "egui-winit/dark-light"] # detect dark mode system preference + [dependencies] egui = { version = "0.17.0", path = "../egui", default-features = false } diff --git a/egui-winit/CHANGELOG.md b/egui-winit/CHANGELOG.md index ccc2a29b39a..e030432e570 100644 --- a/egui-winit/CHANGELOG.md +++ b/egui-winit/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the `egui-winit` integration will be noted in this file. ## Unreleased +* Reexport `egui` crate ## 0.17.0 - 2022-02-22 diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index e3533e829ce..b34d3a5d124 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -149,6 +149,7 @@ pub struct EpiIntegration { impl EpiIntegration { pub fn new( integration_name: &'static str, + gl: std::rc::Rc, max_texture_side: usize, window: &winit::window::Window, storage: Option>, @@ -169,6 +170,7 @@ impl EpiIntegration { }, output: Default::default(), storage, + gl, }; if prefer_dark_mode == Some(true) { diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 731724cf24b..a43f7121fab 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -5,6 +5,7 @@ #![allow(clippy::manual_range_contains)] +pub use egui; pub use winit; pub mod clipboard; diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index dd7d9151913..dbadf5a0f7c 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -201,7 +201,7 @@ impl SidePanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - if let Some(pointer) = ui.ctx().latest_pointer_pos() { + if let Some(pointer) = ui.ctx().pointer_latest_pos() { let we_are_on_top = ui .ctx() .layer_id_at(pointer) @@ -217,9 +217,9 @@ impl SidePanel { && ui.input().pointer.any_down() && mouse_over_resize_line { - ui.memory().interaction.drag_id = Some(resize_id); + ui.memory().set_dragged_id(resize_id); } - is_resizing = ui.memory().interaction.drag_id == Some(resize_id); + is_resizing = ui.memory().is_being_dragged(resize_id); if is_resizing { let width = (pointer.x - side.side_x(panel_rect)).abs(); let width = diff --git a/egui/src/context.rs b/egui/src/context.rs index 1171f05ff9d..8f3abf3fd52 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -885,7 +885,7 @@ impl Context { /// Latest reported pointer position. /// When tapping a touch screen, this will be `None`. #[inline(always)] - pub(crate) fn latest_pointer_pos(&self) -> Option { + pub fn pointer_latest_pos(&self) -> Option { self.input().pointer.latest_pos() } diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 4956918c35d..91a1a8a4609 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -52,7 +52,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo response .on_hover_cursor(CursorIcon::ZoomIn) .on_hover_ui_at_pointer(|ui| { - if let Some(pos) = ui.ctx().latest_pointer_pos() { + if let Some(pos) = ui.ctx().pointer_latest_pos() { let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w); let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h); diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 4dabc78eeff..3893a39ecd7 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -418,6 +418,11 @@ impl Memory { self.interaction.drag_id == Some(id) } + #[inline(always)] + pub fn set_dragged_id(&mut self, id: Id) { + self.interaction.drag_id = Some(id); + } + /// Forget window positions, sizes etc. /// Can be used to auto-layout windows. pub fn reset_areas(&mut self) { diff --git a/egui/src/style.rs b/egui/src/style.rs index 721bee5107f..96c3f8be0ea 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -636,7 +636,7 @@ impl Visuals { widgets: Widgets::default(), selection: Selection::default(), hyperlink_color: Color32::from_rgb(90, 170, 255), - faint_bg_color: Color32::from_gray(24), + faint_bg_color: Color32::from_gray(35), extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background code_bg_color: Color32::from_gray(64), window_rounding: Rounding::same(6.0), @@ -658,7 +658,7 @@ impl Visuals { widgets: Widgets::light(), selection: Selection::light(), hyperlink_color: Color32::from_rgb(0, 155, 255), - faint_bg_color: Color32::from_gray(245), + faint_bg_color: Color32::from_gray(242), extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background code_bg_color: Color32::from_gray(230), window_shadow: Shadow::big_light(), diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 8515d79ccfd..cd69d02a602 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -792,7 +792,7 @@ impl Ui { /// So one can think of `cursor` as a constraint on the available region. /// /// If something has already been added, this will point to `style.spacing.item_spacing` beyond the latest child. - /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the min_rect. + /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the `min_rect`. pub fn cursor(&self) -> Rect { self.placer.cursor() } diff --git a/egui_demo_lib/Cargo.toml b/egui_demo_lib/Cargo.toml index a41f3059ee9..272e56dbdd3 100644 --- a/egui_demo_lib/Cargo.toml +++ b/egui_demo_lib/Cargo.toml @@ -20,15 +20,21 @@ all-features = true [features] -default = ["chrono"] +default = ["datetime"] # Enable additional checks if debug assertions are enabled (debug builds). extra_debug_asserts = ["egui/extra_debug_asserts"] # Always enable additional checks. extra_asserts = ["egui/extra_asserts"] +datetime = ["egui_extras/chrono", "chrono"] http = ["egui_extras", "ehttp", "image", "poll-promise"] -persistence = ["egui/persistence", "epi/persistence", "serde"] +persistence = [ + "egui/persistence", + "epi/persistence", + "egui_extras/persistence", + "serde", +] serialize = ["egui/serialize", "serde"] syntax_highlighting = ["syntect"] @@ -45,6 +51,7 @@ unicode_names2 = { version = "0.5.0", default-features = false } # feature "http": egui_extras = { version = "0.17.0", path = "../egui_extras", optional = true, features = [ "image", + "datepicker", ] } ehttp = { version = "0.2.0", optional = true } image = { version = "0.24", optional = true, default-features = false, features = [ @@ -64,7 +71,6 @@ serde = { version = "1", optional = true, features = ["derive"] } [dev-dependencies] criterion = { version = "0.3", default-features = false } - [[bench]] name = "benchmark" harness = false diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index f6eb1600d06..a501f22b047 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -29,6 +29,8 @@ impl Default for Demos { Box::new(super::plot_demo::PlotDemo::default()), Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), + Box::new(super::strip_demo::StripDemo::default()), + Box::new(super::table_demo::TableDemo::default()), Box::new(super::text_edit::TextEdit::default()), Box::new(super::widget_gallery::WidgetGallery::default()), Box::new(super::window_options::WindowOptions::default()), diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 61118f7d7ee..76ee6e12e82 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -21,6 +21,8 @@ pub mod password; pub mod plot_demo; pub mod scrolling; pub mod sliders; +pub mod strip_demo; +pub mod table_demo; pub mod tests; pub mod text_edit; pub mod toggle_switch; diff --git a/egui_demo_lib/src/apps/demo/strip_demo.rs b/egui_demo_lib/src/apps/demo/strip_demo.rs new file mode 100644 index 00000000000..ebaeb2b2b01 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/strip_demo.rs @@ -0,0 +1,107 @@ +use egui::Color32; +use egui_extras::{Size, StripBuilder}; + +/// Shows off a table with dynamic layout +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Default)] +pub struct StripDemo {} + +impl super::Demo for StripDemo { + fn name(&self) -> &'static str { + "▣ Strip Demo" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(true) + .default_width(400.0) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for StripDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + StripBuilder::new(ui) + .size(Size::exact(50.0)) + .size(Size::remainder()) + .size(Size::relative(0.5).at_least(60.0)) + .size(Size::exact(10.0)) + .vertical(|mut strip| { + strip.cell(|ui| { + ui.painter() + .rect_filled(ui.available_rect_before_wrap(), 0.0, Color32::BLUE); + ui.label("Full width and 50px height"); + }); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + strip.cell(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + Color32::RED, + ); + ui.label("remaining height and 50% of the width"); + }); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 3).vertical(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + Color32::YELLOW, + ); + ui.label("one third of the box left of me but same width"); + }); + }); + }); + }); + }); + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::exact(60.0)) + .size(Size::remainder()) + .size(Size::exact(70.0)) + .horizontal(|mut strip| { + strip.empty(); + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::exact(60.0)) + .size(Size::remainder()) + .vertical(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + Color32::GOLD, + ); + ui.label("60x60"); + }); + }); + }); + strip.empty(); + strip.cell(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + Color32::GREEN, + ); + ui.label("height: half the available - at least 60px, width: 70px"); + }); + }); + }); + strip.cell(|ui| { + ui.vertical_centered(|ui| { + ui.add(crate::__egui_github_link_file!()); + }); + }); + }); + } +} diff --git a/egui_demo_lib/src/apps/demo/table_demo.rs b/egui_demo_lib/src/apps/demo/table_demo.rs new file mode 100644 index 00000000000..641e003f2c6 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/table_demo.rs @@ -0,0 +1,129 @@ +use egui::TextStyle; +use egui_extras::{Size, StripBuilder, TableBuilder}; + +/// Shows off a table with dynamic layout +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Default)] +pub struct TableDemo { + virtual_scroll: bool, + resizable: bool, +} + +impl super::Demo for TableDemo { + fn name(&self) -> &'static str { + "☰ Table Demo" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(true) + .default_width(400.0) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for TableDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.checkbox(&mut self.virtual_scroll, "Virtual scroll"); + ui.checkbox(&mut self.resizable, "Resizable columns"); + + // Leave room for the source code link after the table demo: + StripBuilder::new(ui) + .size(Size::remainder()) // for the table + .size(Size::exact(10.0)) // for the source code link + .vertical(|mut strip| { + strip.cell(|ui| { + self.table_ui(ui); + }); + strip.cell(|ui| { + ui.vertical_centered(|ui| { + ui.add(crate::__egui_github_link_file!()); + }); + }); + }); + } +} + +impl TableDemo { + fn table_ui(&mut self, ui: &mut egui::Ui) { + let text_height = TextStyle::Body.resolve(ui.style()).size; + + TableBuilder::new(ui) + .striped(true) + .column(Size::initial(60.0).at_least(40.0)) + .column(Size::initial(60.0).at_least(40.0)) + .column(Size::remainder().at_least(60.0)) + .resizable(self.resizable) + .header(20.0, |mut header| { + header.col(|ui| { + ui.centered_and_justified(|ui| { + ui.heading("Row"); + }); + }); + header.col(|ui| { + ui.centered_and_justified(|ui| { + ui.heading("Clock"); + }); + }); + header.col(|ui| { + ui.centered_and_justified(|ui| { + ui.heading("Content"); + }); + }); + }) + .body(|mut body| { + if self.virtual_scroll { + body.rows(text_height, 100_000, |row_index, mut row| { + row.col(|ui| { + ui.label(row_index.to_string()); + }); + row.col(|ui| { + ui.label(clock_emoji(row_index)); + }); + row.col(|ui| { + ui.add( + egui::Label::new("Thousands of rows of even height").wrap(false), + ); + }); + }); + } else { + for row_index in 0..20 { + let thick = row_index % 6 == 0; + let row_height = if thick { 30.0 } else { 18.0 }; + body.row(row_height, |mut row| { + row.col(|ui| { + ui.centered_and_justified(|ui| { + ui.label(row_index.to_string()); + }); + }); + row.col(|ui| { + ui.centered_and_justified(|ui| { + ui.label(clock_emoji(row_index)); + }); + }); + row.col(|ui| { + ui.centered_and_justified(|ui| { + ui.style_mut().wrap = Some(false); + if thick { + ui.heading("Extra thick row"); + } else { + ui.label("Normal row"); + } + }); + }); + }); + } + } + }); + } +} + +fn clock_emoji(row_index: usize) -> String { + char::from_u32(0x1f550 + row_index as u32 % 24) + .unwrap() + .to_string() +} diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 48628c2f8d9..5c79c46ed9e 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "datetime")] +mod serde_date_format; + #[derive(Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum Enum { @@ -17,6 +20,9 @@ pub struct WidgetGallery { string: String, color: egui::Color32, animate_progress_bar: bool, + #[cfg(feature = "datetime")] + #[serde(with = "serde_date_format")] + date: chrono::Date, #[cfg_attr(feature = "serde", serde(skip))] texture: Option, } @@ -32,6 +38,8 @@ impl Default for WidgetGallery { string: Default::default(), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), animate_progress_bar: false, + #[cfg(feature = "datetime")] + date: chrono::offset::Utc::now().date(), texture: None, } } @@ -102,6 +110,7 @@ impl WidgetGallery { string, color, animate_progress_bar, + date, texture, } = self; @@ -201,6 +210,13 @@ impl WidgetGallery { } ui.end_row(); + #[cfg(feature = "datetime")] + { + ui.add(doc_link_label("DatePickerButton", "DatePickerButton")); + ui.add(egui_extras::DatePickerButton::new(date)); + ui.end_row(); + } + ui.add(doc_link_label("Separator", "separator")); ui.separator(); ui.end_row(); diff --git a/egui_demo_lib/src/apps/demo/widget_gallery/serde_date_format.rs b/egui_demo_lib/src/apps/demo/widget_gallery/serde_date_format.rs new file mode 100644 index 00000000000..4a70ec7c802 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/widget_gallery/serde_date_format.rs @@ -0,0 +1,23 @@ +use chrono::{Date, NaiveDate, Utc}; +use serde::{self, Deserialize, Deserializer, Serializer}; + +const FORMAT: &str = "%Y-%m-%d"; + +pub fn serialize(date: &Date, serializer: S) -> Result +where + S: Serializer, +{ + let s = date.format(FORMAT).to_string(); + serializer.serialize_str(&s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + + NaiveDate::parse_from_str(&s, FORMAT) + .map(|naive_date| Date::from_utc(naive_date, Utc)) + .map_err(serde::de::Error::custom) +} diff --git a/egui_demo_lib/src/lib.rs b/egui_demo_lib/src/lib.rs index 87e9b2d9a93..fcb16c2f9d5 100644 --- a/egui_demo_lib/src/lib.rs +++ b/egui_demo_lib/src/lib.rs @@ -100,7 +100,7 @@ fn test_egui_zero_window_size() { /// Time of day as seconds since midnight. Used for clock in demo app. pub(crate) fn seconds_since_midnight() -> Option { - #[cfg(feature = "chrono")] + #[cfg(feature = "datetime")] { use chrono::Timelike; let time = chrono::Local::now().time(); @@ -108,6 +108,6 @@ pub(crate) fn seconds_since_midnight() -> Option { time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64); Some(seconds_since_midnight) } - #[cfg(not(feature = "chrono"))] + #[cfg(not(feature = "datetime"))] None } diff --git a/egui_extras/CHANGELOG.md b/egui_extras/CHANGELOG.md index 23fb3ddd0b1..986f1f90e1b 100644 --- a/egui_extras/CHANGELOG.md +++ b/egui_extras/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the `egui_extras` integration will be noted in this file. ## Unreleased +* Added `Strip`, `Table` and `DatePicker` ([#963](https://github.com/emilk/egui/pull/963)). ## 0.17.0 - 2022-02-22 diff --git a/egui_extras/Cargo.toml b/egui_extras/Cargo.toml index 7988b34083a..172dbbddffe 100644 --- a/egui_extras/Cargo.toml +++ b/egui_extras/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "egui_extras" version = "0.17.0" -authors = ["Emil Ernerfeldt "] +authors = [ + "Dominik Rössler ", + "Emil Ernerfeldt ", + "René Rössler ", +] description = "Extra functionality and widgets for the egui GUI library" edition = "2021" rust-version = "1.56" @@ -25,12 +29,20 @@ default = [] # Support loading svg images svg = ["resvg", "tiny-skia", "usvg"] +# Datepicker widget +datepicker = ["chrono"] + +# Persistence +persistence = ["serde"] [dependencies] egui = { version = "0.17.0", path = "../egui", default-features = false } # Optional dependencies: +# Date operations needed for datepicker widget +chrono = { version = "0.4", optional = true } + # Add support for loading images with the `image` crate. # You also need to ALSO opt-in to the image formats you want to support, like so: # image = { version = "0.24", features = ["jpeg", "png"] } @@ -40,3 +52,6 @@ image = { version = "0.24", optional = true, default-features = false } resvg = { version = "0.22", optional = true } tiny-skia = { version = "0.6", optional = true } usvg = { version = "0.22", optional = true } + +# feature "persistence": +serde = { version = "1", features = ["derive"], optional = true } diff --git a/egui_extras/src/datepicker/button.rs b/egui_extras/src/datepicker/button.rs new file mode 100644 index 00000000000..a017a850c9d --- /dev/null +++ b/egui_extras/src/datepicker/button.rs @@ -0,0 +1,132 @@ +use super::popup::DatePickerPopup; +use chrono::{Date, Utc}; +use egui::{Area, Button, Frame, Key, Order, RichText, Ui, Widget}; + +#[derive(Default, Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct DatePickerButtonState { + pub picker_visible: bool, +} + +pub struct DatePickerButton<'a> { + selection: &'a mut Date, + id_source: Option<&'a str>, + combo_boxes: bool, + arrows: bool, + calendar: bool, + calendar_week: bool, +} + +impl<'a> DatePickerButton<'a> { + pub fn new(selection: &'a mut Date) -> Self { + Self { + selection, + id_source: None, + combo_boxes: true, + arrows: true, + calendar: true, + calendar_week: true, + } + } + + /// Add id source. + /// Must be set if multiple date picker buttons are in the same Ui. + pub fn id_source(mut self, id_source: &'a str) -> Self { + self.id_source = Some(id_source); + self + } + + /// Show combo boxes in date picker popup. (Default: true) + pub fn combo_boxes(mut self, combo_boxes: bool) -> Self { + self.combo_boxes = combo_boxes; + self + } + + /// Show arrows in date picker popup. (Default: true) + pub fn arrows(mut self, arrows: bool) -> Self { + self.arrows = arrows; + self + } + + /// Show calendar in date picker popup. (Default: true) + pub fn calendar(mut self, calendar: bool) -> Self { + self.calendar = calendar; + self + } + + /// Show calendar week in date picker popup. (Default: true) + pub fn calendar_week(mut self, week: bool) -> Self { + self.calendar_week = week; + self + } +} + +impl<'a> Widget for DatePickerButton<'a> { + fn ui(self, ui: &mut Ui) -> egui::Response { + let id = ui.make_persistent_id(&self.id_source); + let mut button_state = ui + .memory() + .data + .get_persisted::(id) + .unwrap_or_default(); + + let mut text = RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d"))); + let visuals = ui.visuals().widgets.open; + if button_state.picker_visible { + text = text.color(visuals.text_color()); + } + let mut button = Button::new(text); + if button_state.picker_visible { + button = button.fill(visuals.bg_fill).stroke(visuals.bg_stroke); + } + let button_response = ui.add(button); + if button_response.clicked() { + button_state.picker_visible = true; + ui.memory().data.insert_persisted(id, button_state.clone()); + } + + if button_state.picker_visible { + let width = 333.0; + let mut pos = button_response.rect.left_bottom(); + let width_with_padding = width + + ui.style().spacing.item_spacing.x + + ui.style().spacing.window_margin.left + + ui.style().spacing.window_margin.right; + if pos.x + width_with_padding > ui.clip_rect().right() { + pos.x = button_response.rect.right() - width_with_padding; + } + //TODO: Better positioning + + let area_response = Area::new(ui.make_persistent_id(&self.id_source)) + .order(Order::Foreground) + .fixed_pos(pos) + .show(ui.ctx(), |ui| { + let frame = Frame::popup(ui.style()); + frame.show(ui, |ui| { + ui.set_min_width(width); + ui.set_max_width(width); + + DatePickerPopup { + selection: self.selection, + button_id: id, + combo_boxes: self.combo_boxes, + arrows: self.arrows, + calendar: self.calendar, + calendar_week: self.calendar_week, + } + .draw(ui); + }) + }) + .response; + + if !button_response.clicked() + && (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere()) + { + button_state.picker_visible = false; + ui.memory().data.insert_persisted(id, button_state); + } + } + + button_response + } +} diff --git a/egui_extras/src/datepicker/mod.rs b/egui_extras/src/datepicker/mod.rs new file mode 100644 index 00000000000..b163a3a716f --- /dev/null +++ b/egui_extras/src/datepicker/mod.rs @@ -0,0 +1,34 @@ +mod button; +mod popup; + +pub use button::DatePickerButton; +use chrono::{Date, Datelike, Duration, NaiveDate, Utc, Weekday}; + +#[derive(Debug)] +struct Week { + number: u8, + days: Vec>, +} + +fn month_data(year: i32, month: u32) -> Vec { + let first = Date::from_utc(NaiveDate::from_ymd(year, month, 1), Utc); + let mut start = first; + while start.weekday() != Weekday::Mon { + start = start.checked_sub_signed(Duration::days(1)).unwrap(); + } + let mut weeks = vec![]; + let mut week = vec![]; + while start < first || start.month() == first.month() || start.weekday() != Weekday::Mon { + week.push(start); + + if start.weekday() == Weekday::Sun { + weeks.push(Week { + number: start.iso_week().week() as u8, + days: week.drain(..).collect(), + }); + } + start = start.checked_add_signed(Duration::days(1)).unwrap(); + } + + weeks +} diff --git a/egui_extras/src/datepicker/popup.rs b/egui_extras/src/datepicker/popup.rs new file mode 100644 index 00000000000..0a280100da0 --- /dev/null +++ b/egui_extras/src/datepicker/popup.rs @@ -0,0 +1,408 @@ +use super::{button::DatePickerButtonState, month_data}; +use crate::{Size, StripBuilder, TableBuilder}; +use chrono::{Date, Datelike, NaiveDate, Utc, Weekday}; +use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2}; + +#[derive(Default, Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +struct DatePickerPopupState { + year: i32, + month: u32, + day: u32, + setup: bool, +} + +impl DatePickerPopupState { + fn last_day_of_month(&self) -> u32 { + let date: Date = Date::from_utc(NaiveDate::from_ymd(self.year, self.month, 1), Utc); + date.with_day(31) + .map(|_| 31) + .or_else(|| date.with_day(30).map(|_| 30)) + .or_else(|| date.with_day(29).map(|_| 29)) + .unwrap_or(28) + } +} + +pub(crate) struct DatePickerPopup<'a> { + pub selection: &'a mut Date, + pub button_id: Id, + pub combo_boxes: bool, + pub arrows: bool, + pub calendar: bool, + pub calendar_week: bool, +} + +impl<'a> DatePickerPopup<'a> { + pub fn draw(&mut self, ui: &mut Ui) { + let id = ui.make_persistent_id("date_picker"); + let today = chrono::offset::Utc::now().date(); + let mut popup_state = ui + .memory() + .data + .get_persisted::(id) + .unwrap_or_default(); + if !popup_state.setup { + popup_state.year = self.selection.year(); + popup_state.month = self.selection.month(); + popup_state.day = self.selection.day(); + popup_state.setup = true; + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + + let weeks = month_data(popup_state.year, popup_state.month); + let mut close = false; + let height = 20.0; + let spacing = 2.0; + ui.spacing_mut().item_spacing = Vec2::splat(spacing); + StripBuilder::new(ui) + .clip(false) + .sizes( + Size::exact(height), + match (self.combo_boxes, self.arrows) { + (true, true) => 2, + (true, false) | (false, true) => 1, + (false, false) => 0, + }, + ) + .sizes( + Size::exact((spacing + height) * (weeks.len() + 1) as f32), + if self.calendar { 1 } else { 0 }, + ) + .size(Size::exact(height)) + .vertical(|mut strip| { + if self.combo_boxes { + strip.strip(|builder| { + builder.sizes(Size::remainder(), 3).horizontal(|mut strip| { + strip.cell(|ui| { + ComboBox::from_id_source("date_picker_year") + .selected_text(popup_state.year.to_string()) + .show_ui(ui, |ui| { + for year in today.year() - 5..today.year() + 10 { + if ui + .selectable_value( + &mut popup_state.year, + year, + year.to_string(), + ) + .changed() + { + ui.memory() + .data + .insert_persisted(id, popup_state.clone()); + } + } + }); + }); + strip.cell(|ui| { + ComboBox::from_id_source("date_picker_month") + .selected_text(month_name(popup_state.month)) + .show_ui(ui, |ui| { + for month in 1..=12 { + if ui + .selectable_value( + &mut popup_state.month, + month, + month_name(month), + ) + .changed() + { + ui.memory() + .data + .insert_persisted(id, popup_state.clone()); + } + } + }); + }); + strip.cell(|ui| { + ComboBox::from_id_source("date_picker_day") + .selected_text(popup_state.day.to_string()) + .show_ui(ui, |ui| { + for day in 1..=popup_state.last_day_of_month() { + if ui + .selectable_value( + &mut popup_state.day, + day, + day.to_string(), + ) + .changed() + { + ui.memory() + .data + .insert_persisted(id, popup_state.clone()); + } + } + }); + }); + }); + }); + } + + if self.arrows { + strip.strip(|builder| { + builder.sizes(Size::remainder(), 6).horizontal(|mut strip| { + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui + .button("<<<") + .on_hover_text("substract one year") + .clicked() + { + popup_state.year -= 1; + popup_state.day = + popup_state.day.min(popup_state.last_day_of_month()); + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui + .button("<<") + .on_hover_text("substract one month") + .clicked() + { + popup_state.month -= 1; + if popup_state.month == 0 { + popup_state.month = 12; + popup_state.year -= 1; + } + popup_state.day = + popup_state.day.min(popup_state.last_day_of_month()); + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button("<").on_hover_text("substract one day").clicked() { + popup_state.day -= 1; + if popup_state.day == 0 { + popup_state.month -= 1; + if popup_state.month == 0 { + popup_state.year -= 1; + popup_state.month = 12; + } + popup_state.day = popup_state.last_day_of_month(); + } + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button(">").on_hover_text("add one day").clicked() { + popup_state.day += 1; + if popup_state.day > popup_state.last_day_of_month() { + popup_state.day = 1; + popup_state.month += 1; + if popup_state.month > 12 { + popup_state.month = 1; + popup_state.year += 1; + } + } + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button(">>").on_hover_text("add one month").clicked() { + popup_state.month += 1; + if popup_state.month > 12 { + popup_state.month = 1; + popup_state.year += 1; + } + popup_state.day = + popup_state.day.min(popup_state.last_day_of_month()); + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button(">>>").on_hover_text("add one year").clicked() { + popup_state.year += 1; + popup_state.day = + popup_state.day.min(popup_state.last_day_of_month()); + ui.memory().data.insert_persisted(id, popup_state.clone()); + } + }); + }); + }); + }); + } + + if self.calendar { + strip.cell(|ui| { + ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0); + TableBuilder::new(ui) + .scroll(false) + .clip(false) + .columns(Size::remainder(), if self.calendar_week { 8 } else { 7 }) + .header(height, |mut header| { + if self.calendar_week { + header.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::TopDown), + |ui| { + ui.label("Week"); + }, + ); + }); + } + + //TODO: Locale + for name in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] { + header.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::TopDown), + |ui| { + ui.label(name); + }, + ); + }); + } + }) + .body(|mut body| { + for week in weeks { + body.row(height, |mut row| { + if self.calendar_week { + row.col(|ui| { + ui.label(week.number.to_string()); + }); + } + for day in week.days { + row.col(|ui| { + ui.with_layout( + Layout::top_down_justified(Align::Center), + |ui| { + let fill_color = if popup_state.year + == day.year() + && popup_state.month == day.month() + && popup_state.day == day.day() + { + ui.visuals().selection.bg_fill + } else if day.weekday() == Weekday::Sat + || day.weekday() == Weekday::Sun + { + if ui.visuals().dark_mode { + Color32::DARK_RED + } else { + Color32::LIGHT_RED + } + } else { + ui.visuals().extreme_bg_color + }; + + let mut text_color = ui + .visuals() + .widgets + .inactive + .text_color(); + + if day.month() != popup_state.month { + text_color = + text_color.linear_multiply(0.5); + }; + + let button_response = ui.add( + Button::new( + RichText::new( + day.day().to_string(), + ) + .color(text_color), + ) + .fill(fill_color), + ); + + if day == today { + // Encircle today's date + let stroke = ui + .visuals() + .widgets + .inactive + .fg_stroke; + ui.painter().circle_stroke( + button_response.rect.center(), + 8.0, + stroke, + ); + } + + if button_response.clicked() { + popup_state.year = day.year(); + popup_state.month = day.month(); + popup_state.day = day.day(); + ui.memory().data.insert_persisted( + id, + popup_state.clone(), + ); + } + }, + ); + }); + } + }); + } + }); + }); + } + + strip.strip(|builder| { + builder.sizes(Size::remainder(), 3).horizontal(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button("Cancel").clicked() { + close = true; + } + }); + }); + strip.cell(|ui| { + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { + if ui.button("Save").clicked() { + *self.selection = Date::from_utc( + NaiveDate::from_ymd( + popup_state.year, + popup_state.month, + popup_state.day, + ), + Utc, + ); + close = true; + } + }); + }); + }); + }); + }); + + if close { + popup_state.setup = false; + ui.memory().data.insert_persisted(id, popup_state); + + ui.memory() + .data + .get_persisted_mut_or_default::(self.button_id) + .picker_visible = false; + } + } +} + +fn month_name(i: u32) -> &'static str { + match i { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => panic!("Unknown month: {}", i), + } +} diff --git a/egui_extras/src/layout.rs b/egui_extras/src/layout.rs new file mode 100644 index 00000000000..634518aa3be --- /dev/null +++ b/egui_extras/src/layout.rs @@ -0,0 +1,156 @@ +use egui::{Pos2, Rect, Response, Sense, Ui}; + +#[derive(Clone, Copy)] +pub(crate) enum CellSize { + /// Absolute size in points + Absolute(f32), + /// Take all available space + Remainder, +} + +/// Cells are positioned in two dimensions, cells go in one direction and form lines. +/// +/// In a strip there's only one line which goes in the direction of the strip: +/// +/// In a horizontal strip, a `[StripLayout]` with horizontal `[CellDirection]` is used. +/// Its cells go from left to right inside this `[StripLayout]`. +/// +/// In a table there's a `[StripLayout]` for each table row with a horizontal `[CellDirection]`. +/// Its cells go from left to right. And the lines go from top to bottom. +pub(crate) enum CellDirection { + /// Cells go from left to right. + Horizontal, + /// Cells go from top to bottom. + Vertical, +} + +/// Positions cells in `[CellDirection]` and starts a new line on `[StripLayout::end_line]` +pub struct StripLayout<'l> { + ui: &'l mut Ui, + direction: CellDirection, + rect: Rect, + cursor: Pos2, + max: Pos2, + pub(crate) clip: bool, +} + +impl<'l> StripLayout<'l> { + pub(crate) fn new(ui: &'l mut Ui, direction: CellDirection, clip: bool) -> Self { + let rect = ui.available_rect_before_wrap(); + let pos = rect.left_top(); + + Self { + ui, + rect, + cursor: pos, + max: pos, + direction, + clip, + } + } + + pub fn current_y(&self) -> f32 { + self.rect.top() + } + + fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect { + Rect { + min: self.cursor, + max: Pos2 { + x: match width { + CellSize::Absolute(width) => self.cursor.x + width, + CellSize::Remainder => self.rect.right(), + }, + y: match height { + CellSize::Absolute(height) => self.cursor.y + height, + CellSize::Remainder => self.rect.bottom(), + }, + }, + } + } + + fn set_pos(&mut self, rect: Rect) { + self.max.x = self.max.x.max(rect.right()); + self.max.y = self.max.y.max(rect.bottom()); + + match self.direction { + CellDirection::Horizontal => { + self.cursor.x = rect.right() + self.ui.spacing().item_spacing.x; + } + CellDirection::Vertical => { + self.cursor.y = rect.bottom() + self.ui.spacing().item_spacing.y; + } + } + } + + pub(crate) fn empty(&mut self, width: CellSize, height: CellSize) { + self.set_pos(self.cell_rect(&width, &height)); + } + + pub(crate) fn add( + &mut self, + width: CellSize, + height: CellSize, + add_contents: impl FnOnce(&mut Ui), + ) -> Response { + let rect = self.cell_rect(&width, &height); + let used_rect = self.cell(rect, add_contents); + self.set_pos(rect); + self.ui.allocate_rect(rect.union(used_rect), Sense::hover()) + } + + pub(crate) fn add_striped( + &mut self, + width: CellSize, + height: CellSize, + add_contents: impl FnOnce(&mut Ui), + ) -> Response { + let rect = self.cell_rect(&width, &height); + + // Make sure we don't have a gap in the stripe background: + let rect = rect.expand2(egui::vec2(0.5 * self.ui.spacing().item_spacing.x, 0.0)); + + self.ui + .painter() + .rect_filled(rect, 0.0, self.ui.visuals().faint_bg_color); + + self.add(width, height, add_contents) + } + + /// only needed for layouts with multiple lines, like [`Table`]. + pub fn end_line(&mut self) { + match self.direction { + CellDirection::Horizontal => { + self.cursor.y = self.max.y; + self.cursor.x = self.rect.left(); + } + CellDirection::Vertical => { + self.cursor.x = self.max.x; + self.cursor.y = self.rect.top(); + } + } + } + + fn cell(&mut self, rect: Rect, add_contents: impl FnOnce(&mut Ui)) -> Rect { + let mut child_ui = self.ui.child_ui(rect, *self.ui.layout()); + + if self.clip { + let mut clip_rect = child_ui.clip_rect(); + clip_rect.min = clip_rect.min.max(rect.min); + clip_rect.max = clip_rect.max.min(rect.max); + child_ui.set_clip_rect(clip_rect); + } + + add_contents(&mut child_ui); + child_ui.min_rect() + } + + /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size + pub fn allocate_rect(&mut self) -> Response { + let mut rect = self.rect; + rect.set_right(self.max.x); + rect.set_bottom(self.max.y); + + self.ui.allocate_rect(rect, Sense::hover()) + } +} diff --git a/egui_extras/src/lib.rs b/egui_extras/src/lib.rs index c711144238c..014b75dd8c3 100644 --- a/egui_extras/src/lib.rs +++ b/egui_extras/src/lib.rs @@ -3,6 +3,20 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] +#[cfg(feature = "chrono")] +mod datepicker; + pub mod image; +mod layout; +mod sizing; +mod strip; +mod table; + +#[cfg(feature = "chrono")] +pub use crate::datepicker::DatePickerButton; pub use crate::image::RetainedImage; +pub(crate) use crate::layout::StripLayout; +pub use crate::sizing::Size; +pub use crate::strip::*; +pub use crate::table::*; diff --git a/egui_extras/src/sizing.rs b/egui_extras/src/sizing.rs new file mode 100644 index 00000000000..8fbb60299ee --- /dev/null +++ b/egui_extras/src/sizing.rs @@ -0,0 +1,179 @@ +/// Size hint for table column/strip cell. +#[derive(Clone, Debug, Copy)] +pub enum Size { + /// Absolute size in points, with a given range of allowed sizes to resize within. + Absolute { initial: f32, range: (f32, f32) }, + /// Relative size relative to all available space. + Relative { fraction: f32, range: (f32, f32) }, + /// Multiple remainders each get the same space. + Remainder { range: (f32, f32) }, +} + +impl Size { + /// Exactly this big, with no room for resize. + pub fn exact(points: f32) -> Self { + Self::Absolute { + initial: points, + range: (points, points), + } + } + + /// Initially this big, but can resize. + pub fn initial(points: f32) -> Self { + Self::Absolute { + initial: points, + range: (0.0, f32::INFINITY), + } + } + + /// Relative size relative to all available space. Values must be in range `0.0..=1.0`. + pub fn relative(fraction: f32) -> Self { + egui::egui_assert!(0.0 <= fraction && fraction <= 1.0); + Self::Relative { + fraction, + range: (0.0, f32::INFINITY), + } + } + + /// Multiple remainders each get the same space. + pub fn remainder() -> Self { + Self::Remainder { + range: (0.0, f32::INFINITY), + } + } + + /// Won't shrink below this size (in points). + pub fn at_least(mut self, minimum: f32) -> Self { + match &mut self { + Self::Absolute { range, .. } + | Self::Relative { range, .. } + | Self::Remainder { range, .. } => { + range.0 = minimum; + } + } + self + } + + /// Won't grow above this size (in points). + pub fn at_most(mut self, maximum: f32) -> Self { + match &mut self { + Self::Absolute { range, .. } + | Self::Relative { range, .. } + | Self::Remainder { range, .. } => { + range.1 = maximum; + } + } + self + } + + /// Allowed range of movement (in points), if in a resizable [`Table`]. + pub fn range(self) -> (f32, f32) { + match self { + Self::Absolute { range, .. } + | Self::Relative { range, .. } + | Self::Remainder { range, .. } => range, + } + } +} + +#[derive(Clone)] +pub struct Sizing { + pub(crate) sizes: Vec, +} + +impl Sizing { + pub fn new() -> Self { + Self { sizes: vec![] } + } + + pub fn add(&mut self, size: Size) { + self.sizes.push(size); + } + + pub fn to_lengths(&self, length: f32, spacing: f32) -> Vec { + if self.sizes.is_empty() { + return vec![]; + } + + let mut remainders = 0; + let sum_non_remainder = self + .sizes + .iter() + .map(|&size| match size { + Size::Absolute { initial, .. } => initial, + Size::Relative { + fraction, + range: (min, max), + } => { + assert!(0.0 <= fraction && fraction <= 1.0); + (length * fraction).clamp(min, max) + } + Size::Remainder { .. } => { + remainders += 1; + 0.0 + } + }) + .sum::() + + spacing * (self.sizes.len() - 1) as f32; + + let avg_remainder_length = if remainders == 0 { + 0.0 + } else { + let mut remainder_length = length - sum_non_remainder; + let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor(); + self.sizes.iter().for_each(|&size| { + if let Size::Remainder { range: (min, _max) } = size { + if avg_remainder_length < min { + remainder_length -= min; + remainders -= 1; + } + } + }); + if remainders > 0 { + 0.0f32.max(remainder_length / remainders as f32) + } else { + 0.0 + } + }; + + self.sizes + .iter() + .map(|&size| match size { + Size::Absolute { initial, .. } => initial, + Size::Relative { + fraction, + range: (min, max), + } => (length * fraction).clamp(min, max), + Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max), + }) + .collect() + } +} + +impl From> for Sizing { + fn from(sizes: Vec) -> Self { + Self { sizes } + } +} + +#[test] +fn test_sizing() { + let sizing: Sizing = vec![].into(); + assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]); + + let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into(); + assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]); + assert_eq!(sizing.to_lengths(30.0, 0.0), vec![20.0, 10.0]); + assert_eq!(sizing.to_lengths(20.0, 0.0), vec![20.0, 0.0]); + assert_eq!(sizing.to_lengths(10.0, 0.0), vec![20.0, 0.0]); + assert_eq!(sizing.to_lengths(20.0, 10.0), vec![20.0, 0.0]); + assert_eq!(sizing.to_lengths(30.0, 10.0), vec![20.0, 0.0]); + assert_eq!(sizing.to_lengths(40.0, 10.0), vec![20.0, 10.0]); + assert_eq!(sizing.to_lengths(110.0, 10.0), vec![50.0, 50.0]); + + let sizing: Sizing = vec![Size::relative(0.5).at_least(10.0), Size::exact(10.0)].into(); + assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 10.0]); + assert_eq!(sizing.to_lengths(30.0, 0.0), vec![15.0, 10.0]); + assert_eq!(sizing.to_lengths(20.0, 0.0), vec![10.0, 10.0]); + assert_eq!(sizing.to_lengths(10.0, 0.0), vec![10.0, 10.0]); +} diff --git a/egui_extras/src/strip.rs b/egui_extras/src/strip.rs new file mode 100644 index 00000000000..2f15087768a --- /dev/null +++ b/egui_extras/src/strip.rs @@ -0,0 +1,171 @@ +use crate::{ + layout::{CellDirection, CellSize, StripLayout}, + sizing::Sizing, + Size, +}; +use egui::{Response, Ui}; + +/// Builder for creating a new [`Strip`]. +/// +/// This can be used to do dynamic layouts. +/// +/// In contrast to normal egui behavior, strip cells do *not* grow with its children! +/// +/// After adding size hints with `[Self::column]`/`[Self::columns]` the strip can be build with `[Self::horizontal]`/`[Self::vertical]`. +/// +/// ### Example +/// ``` +/// # egui::__run_test_ui(|ui| { +/// use egui_extras::{StripBuilder, Size}; +/// StripBuilder::new(ui) +/// .size(Size::remainder().at_least(100.0)) +/// .size(Size::exact(40.0)) +/// .vertical(|mut strip| { +/// strip.strip(|builder| { +/// builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { +/// strip.cell(|ui| { +/// ui.label("Top Left"); +/// }); +/// strip.cell(|ui| { +/// ui.label("Top Right"); +/// }); +/// }); +/// }); +/// strip.cell(|ui| { +/// ui.label("Fixed"); +/// }); +/// }); +/// # }); +/// ``` +pub struct StripBuilder<'a> { + ui: &'a mut Ui, + sizing: Sizing, + clip: bool, +} + +impl<'a> StripBuilder<'a> { + /// Create new strip builder. + pub fn new(ui: &'a mut Ui) -> Self { + let sizing = Sizing::new(); + + Self { + ui, + sizing, + clip: true, + } + } + + /// Should we clip the contents of each cell? Default: `true`. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Add size hint for one column/row. + pub fn size(mut self, size: Size) -> Self { + self.sizing.add(size); + self + } + + /// Add size hint for several columns/rows at once. + pub fn sizes(mut self, size: Size, count: usize) -> Self { + for _ in 0..count { + self.sizing.add(size); + } + self + } + + /// Build horizontal strip: Cells are positions from left to right. + /// Takes the available horizontal width, so there can't be anything right of the strip or the container will grow slowly! + /// + /// Returns a `[egui::Response]` for hover events. + pub fn horizontal(self, strip: F) -> Response + where + F: for<'b> FnOnce(Strip<'a, 'b>), + { + let widths = self.sizing.to_lengths( + self.ui.available_rect_before_wrap().width() - self.ui.spacing().item_spacing.x, + self.ui.spacing().item_spacing.x, + ); + let mut layout = StripLayout::new(self.ui, CellDirection::Horizontal, self.clip); + strip(Strip { + layout: &mut layout, + direction: CellDirection::Horizontal, + sizes: &widths, + }); + layout.allocate_rect() + } + + /// Build vertical strip: Cells are positions from top to bottom. + /// Takes the full available vertical height, so there can't be anything below of the strip or the container will grow slowly! + /// + /// Returns a `[egui::Response]` for hover events. + pub fn vertical(self, strip: F) -> Response + where + F: for<'b> FnOnce(Strip<'a, 'b>), + { + let heights = self.sizing.to_lengths( + self.ui.available_rect_before_wrap().height() - self.ui.spacing().item_spacing.y, + self.ui.spacing().item_spacing.y, + ); + let mut layout = StripLayout::new(self.ui, CellDirection::Vertical, self.clip); + strip(Strip { + layout: &mut layout, + direction: CellDirection::Vertical, + sizes: &heights, + }); + layout.allocate_rect() + } +} + +/// A Strip of cells which go in one direction. Each cell has a fixed size. +/// In contrast to normal egui behavior, strip cells do *not* grow with its children! +pub struct Strip<'a, 'b> { + layout: &'b mut StripLayout<'a>, + direction: CellDirection, + sizes: &'b [f32], +} + +impl<'a, 'b> Strip<'a, 'b> { + fn next_cell_size(&mut self) -> (CellSize, CellSize) { + assert!( + !self.sizes.is_empty(), + "Tried using more strip cells than available." + ); + let size = self.sizes[0]; + self.sizes = &self.sizes[1..]; + + match self.direction { + CellDirection::Horizontal => (CellSize::Absolute(size), CellSize::Remainder), + CellDirection::Vertical => (CellSize::Remainder, CellSize::Absolute(size)), + } + } + + /// Add empty cell + pub fn empty(&mut self) { + let (width, height) = self.next_cell_size(); + self.layout.empty(width, height); + } + + /// Add cell contents. + pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) { + let (width, height) = self.next_cell_size(); + self.layout.add(width, height, add_contents); + } + + /// Add strip as cell + pub fn strip(&mut self, strip_builder: impl FnOnce(StripBuilder<'_>)) { + let clip = self.layout.clip; + self.cell(|ui| { + strip_builder(StripBuilder::new(ui).clip(clip)); + }); + } +} + +impl<'a, 'b> Drop for Strip<'a, 'b> { + fn drop(&mut self) { + while !self.sizes.is_empty() { + self.empty(); + } + } +} diff --git a/egui_extras/src/table.rs b/egui_extras/src/table.rs new file mode 100644 index 00000000000..6d7b8ec6f53 --- /dev/null +++ b/egui_extras/src/table.rs @@ -0,0 +1,457 @@ +//! Table view with (optional) fixed header and scrolling body. +//! Cell widths are precalculated with given size hints so we can have tables like this: +//! | fixed size | all available space/minimum | 30% of available width | fixed size | +//! Takes all available height, so if you want something below the table, put it in a strip. + +use crate::{ + layout::{CellDirection, CellSize}, + sizing::Sizing, + Size, StripLayout, +}; + +use egui::{Response, Ui}; + +/// Builder for a [`Table`] with (optional) fixed header and scrolling body. +/// +/// Cell widths are precalculated with given size hints so we can have tables like this: +/// +/// | fixed size | all available space/minimum | 30% of available width | fixed size | +/// +/// In contrast to normal egui behavior, columns/rows do *not* grow with its children! +/// Takes all available height, so if you want something below the table, put it in a strip. +/// +/// ### Example +/// ``` +/// # egui::__run_test_ui(|ui| { +/// use egui_extras::{TableBuilder, Size}; +/// TableBuilder::new(ui) +/// .column(Size::remainder().at_least(100.0)) +/// .column(Size::exact(40.0)) +/// .header(20.0, |mut header| { +/// header.col(|ui| { +/// ui.heading("Growing"); +/// }); +/// header.col(|ui| { +/// ui.heading("Fixed"); +/// }); +/// }) +/// .body(|mut body| { +/// body.row(30.0, |mut row| { +/// row.col(|ui| { +/// ui.label("first row growing cell"); +/// }); +/// row.col(|ui| { +/// ui.button("action"); +/// }); +/// }); +/// }); +/// # }); +/// ``` +pub struct TableBuilder<'a> { + ui: &'a mut Ui, + sizing: Sizing, + scroll: bool, + striped: bool, + resizable: bool, + clip: bool, +} + +impl<'a> TableBuilder<'a> { + pub fn new(ui: &'a mut Ui) -> Self { + let sizing = Sizing::new(); + + Self { + ui, + sizing, + scroll: true, + striped: false, + resizable: false, + clip: true, + } + } + + /// Enable scrollview in body (default: true) + pub fn scroll(mut self, scroll: bool) -> Self { + self.scroll = scroll; + self + } + + /// Enable striped row background (default: false) + pub fn striped(mut self, striped: bool) -> Self { + self.striped = striped; + self + } + + /// Make the columns resizable by dragging. + /// + /// If the _last_ column is [`Size::Remainder`], then it won't be resizable + /// (and instead use up the remainder). + /// + /// Default is `false`. + /// + /// If you have multiple [`Table`]:s in the same [`Ui`] + /// you will need to give them unique id:s with [`Ui::push_id`]. + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Should we clip the contents of each cell? Default: `true`. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Add size hint for column + pub fn column(mut self, width: Size) -> Self { + self.sizing.add(width); + self + } + + /// Add size hint for several columns at once. + pub fn columns(mut self, size: Size, count: usize) -> Self { + for _ in 0..count { + self.sizing.add(size); + } + self + } + + fn available_width(&self) -> f32 { + self.ui.available_rect_before_wrap().width() + - if self.scroll { + self.ui.spacing().item_spacing.x + self.ui.spacing().scroll_bar_width + } else { + 0.0 + } + } + + /// Create a header row which always stays visible and at the top + pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> { + let available_width = self.available_width(); + + let Self { + ui, + sizing, + scroll, + striped, + resizable, + clip, + } = self; + + let resize_id = resizable.then(|| ui.id().with("__table_resize")); + let widths = if let Some(resize_id) = resize_id { + ui.data().get_persisted(resize_id) + } else { + None + }; + let widths = widths + .unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x)); + + let table_top = ui.cursor().top(); + + { + let mut layout = StripLayout::new(ui, CellDirection::Horizontal, clip); + header(TableRow { + layout: &mut layout, + widths: &widths, + striped: false, + height, + }); + layout.allocate_rect(); + } + + Table { + ui, + table_top, + resize_id, + sizing, + available_width, + widths, + scroll, + striped, + clip, + } + } + + /// Create table body without a header row + pub fn body(self, body: F) + where + F: for<'b> FnOnce(TableBody<'b>), + { + let available_width = self.available_width(); + + let Self { + ui, + sizing, + scroll, + striped, + resizable, + clip, + } = self; + + let resize_id = resizable.then(|| ui.id().with("__table_resize")); + let widths = if let Some(resize_id) = resize_id { + ui.data().get_persisted(resize_id) + } else { + None + }; + let widths = widths + .unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x)); + + let table_top = ui.cursor().top(); + + Table { + ui, + table_top, + resize_id, + sizing, + available_width, + widths, + scroll, + striped, + clip, + } + .body(body); + } +} + +/// Table struct which can construct a [`TableBody`]. +/// +/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`]. +pub struct Table<'a> { + ui: &'a mut Ui, + table_top: f32, + resize_id: Option, + sizing: Sizing, + available_width: f32, + widths: Vec, + scroll: bool, + striped: bool, + clip: bool, +} + +impl<'a> Table<'a> { + /// Create table body after adding a header row + pub fn body(self, body: F) + where + F: for<'b> FnOnce(TableBody<'b>), + { + let Table { + ui, + table_top, + resize_id, + sizing, + mut available_width, + widths, + scroll, + striped, + clip, + } = self; + + let avail_rect = ui.available_rect_before_wrap(); + + let mut new_widths = widths.clone(); + + egui::ScrollArea::new([false, scroll]) + .auto_shrink([true; 2]) + .show(ui, move |ui| { + let layout = StripLayout::new(ui, CellDirection::Horizontal, clip); + + body(TableBody { + layout, + widths, + striped, + row_nr: 0, + start_y: avail_rect.top(), + end_y: avail_rect.bottom(), + }); + }); + + let bottom = ui.min_rect().bottom(); + + // TODO: fix frame-delay by interacting before laying out (but painting later). + if let Some(resize_id) = resize_id { + let spacing_x = ui.spacing().item_spacing.x; + let mut x = avail_rect.left() - spacing_x * 0.5; + for (i, width) in new_widths.iter_mut().enumerate() { + x += *width + spacing_x; + + // If the last column is Size::Remainder, then let it fill the remainder! + let last_column = i + 1 == sizing.sizes.len(); + if last_column { + if let Size::Remainder { range: (min, max) } = sizing.sizes[i] { + let eps = 0.1; // just to avoid some rounding errors. + *width = (available_width - eps).clamp(min, max); + break; + } + } + + let resize_id = ui.id().with("__panel_resize").with(i); + + let mut p0 = egui::pos2(x, table_top); + let mut p1 = egui::pos2(x, bottom); + let line_rect = egui::Rect::from_min_max(p0, p1) + .expand(ui.style().interaction.resize_grab_radius_side); + let mouse_over_resize_line = ui.rect_contains_pointer(line_rect); + + if ui.input().pointer.any_pressed() + && ui.input().pointer.any_down() + && mouse_over_resize_line + { + ui.memory().set_dragged_id(resize_id); + } + let is_resizing = ui.memory().is_being_dragged(resize_id); + if is_resizing { + if let Some(pointer) = ui.ctx().pointer_latest_pos() { + let new_width = *width + pointer.x - x; + let (min, max) = sizing.sizes[i].range(); + let new_width = new_width.clamp(min, max); + let x = x - *width + new_width; + p0.x = x; + p1.x = x; + + *width = new_width; + } + } + + let dragging_something_else = + ui.input().pointer.any_down() || ui.input().pointer.any_pressed(); + let resize_hover = mouse_over_resize_line && !dragging_something_else; + + if resize_hover || is_resizing { + ui.output().cursor_icon = egui::CursorIcon::ResizeHorizontal; + } + + let stroke = if is_resizing { + ui.style().visuals.widgets.active.bg_stroke + } else if resize_hover { + ui.style().visuals.widgets.hovered.bg_stroke + } else { + // ui.visuals().widgets.inactive.bg_stroke + ui.visuals().widgets.noninteractive.bg_stroke + }; + ui.painter().line_segment([p0, p1], stroke); + + available_width -= *width + spacing_x; + } + + ui.data().insert_persisted(resize_id, new_widths); + } + } +} + +/// The body of a table. +/// Is created by calling `body` on a [`Table`] (after adding a header row) or [`TableBuilder`] (without a header row). +pub struct TableBody<'a> { + layout: StripLayout<'a>, + widths: Vec, + striped: bool, + row_nr: usize, + start_y: f32, + end_y: f32, +} + +impl<'a> TableBody<'a> { + /// Add rows with same height. + /// + /// Is a lot more performant than adding each individual row as non visible rows must not be rendered + pub fn rows(mut self, height: f32, rows: usize, mut row: impl FnMut(usize, TableRow<'_, '_>)) { + let delta = self.layout.current_y() - self.start_y; + let mut start = 0; + + if delta < 0.0 { + start = (-delta / height).floor() as usize; + + let skip_height = start as f32 * height; + TableRow { + layout: &mut self.layout, + widths: &self.widths, + striped: false, + height: skip_height, + } + .col(|_| ()); // advances the cursor + } + + let max_height = self.end_y - self.start_y; + let count = (max_height / height).ceil() as usize; + let end = rows.min(start + count); + + for idx in start..end { + row( + idx, + TableRow { + layout: &mut self.layout, + widths: &self.widths, + striped: self.striped && idx % 2 == 0, + height, + }, + ); + } + + if rows - end > 0 { + let skip_height = (rows - end) as f32 * height; + + TableRow { + layout: &mut self.layout, + widths: &self.widths, + striped: false, + height: skip_height, + } + .col(|_| ()); // advances the cursor + } + } + + /// Add row with individual height + pub fn row(&mut self, height: f32, row: impl FnOnce(TableRow<'a, '_>)) { + row(TableRow { + layout: &mut self.layout, + widths: &self.widths, + striped: self.striped && self.row_nr % 2 == 0, + height, + }); + + self.row_nr += 1; + } +} + +impl<'a> Drop for TableBody<'a> { + fn drop(&mut self) { + self.layout.allocate_rect(); + } +} + +/// The row of a table. +/// Is created by [`TableRow`] for each created [`TableBody::row`] or each visible row in rows created by calling [`TableBody::rows`]. +pub struct TableRow<'a, 'b> { + layout: &'b mut StripLayout<'a>, + widths: &'b [f32], + striped: bool, + height: f32, +} + +impl<'a, 'b> TableRow<'a, 'b> { + /// Add the contents of a column. + pub fn col(&mut self, add_contents: impl FnOnce(&mut Ui)) -> Response { + assert!( + !self.widths.is_empty(), + "Tried using more table columns than available." + ); + + let width = self.widths[0]; + self.widths = &self.widths[1..]; + let width = CellSize::Absolute(width); + let height = CellSize::Absolute(self.height); + + if self.striped { + self.layout.add_striped(width, height, add_contents) + } else { + self.layout.add(width, height, add_contents) + } + } +} + +impl<'a, 'b> Drop for TableRow<'a, 'b> { + fn drop(&mut self) { + self.layout.end_line(); + } +} diff --git a/egui_glow/Cargo.toml b/egui_glow/Cargo.toml index 717d8d7bcd5..3d62c53e76d 100644 --- a/egui_glow/Cargo.toml +++ b/egui_glow/Cargo.toml @@ -53,6 +53,8 @@ screen_reader = ["egui-winit/screen_reader"] # if you want to use glow painter on web disable this feature. winit = ["egui-winit", "glutin"] +dark-light = ["egui-winit/dark-light"] # detect dark mode system preference + [dependencies] egui = { version = "0.17.0", path = "../egui", default-features = false, features = [ @@ -67,7 +69,6 @@ tracing = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] egui-winit = { version = "0.17.0", path = "../egui-winit", optional = true, default-features = false, features = [ - "dark-light", "epi_backend", ] } glutin = { version = "0.28.0", optional = true } diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 8b919ea9897..fde2bb3041a 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -1,4 +1,3 @@ -use crate::*; use egui_winit::winit; struct RequestRepaintEvent; @@ -50,6 +49,7 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi let mut integration = egui_winit::epi::EpiIntegration::new( "egui_glow", + gl.clone(), painter.max_texture_side(), gl_window.window(), storage, @@ -86,6 +86,10 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi std::thread::sleep(std::time::Duration::from_millis(10)); } + let screen_size_in_pixels: [u32; 2] = gl_window.window().inner_size().into(); + + crate::painter::clear(&gl, screen_size_in_pixels, app.clear_color()); + let egui::FullOutput { platform_output, needs_repaint, @@ -97,24 +101,14 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi let clipped_primitives = integration.egui_ctx.tessellate(shapes); - // paint: - { - let color = app.clear_color(); - unsafe { - use glow::HasContext as _; - gl.disable(glow::SCISSOR_TEST); - gl.clear_color(color[0], color[1], color[2], color[3]); - gl.clear(glow::COLOR_BUFFER_BIT); - } - painter.paint_and_update_textures( - gl_window.window().inner_size().into(), - integration.egui_ctx.pixels_per_point(), - &clipped_primitives, - &textures_delta, - ); - - gl_window.swap_buffers().unwrap(); - } + painter.paint_and_update_textures( + screen_size_in_pixels, + integration.egui_ctx.pixels_per_point(), + &clipped_primitives, + &textures_delta, + ); + + gl_window.swap_buffers().unwrap(); { *control_flow = if integration.should_quit() { diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index e26e86a106b..35b2ff40197 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -627,11 +627,16 @@ impl Painter { } } -pub fn clear(gl: &glow::Context, dimension: [u32; 2], clear_color: egui::Rgba) { +pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: egui::Rgba) { unsafe { gl.disable(glow::SCISSOR_TEST); - gl.viewport(0, 0, dimension[0] as i32, dimension[1] as i32); + gl.viewport( + 0, + 0, + screen_size_in_pixels[0] as i32, + screen_size_in_pixels[1] as i32, + ); let clear_color: Color32 = clear_color.into(); gl.clear_color( diff --git a/egui_web/README.md b/egui_web/README.md index 5dc5f940d99..2cca8c2c51c 100644 --- a/egui_web/README.md +++ b/egui_web/README.md @@ -14,16 +14,16 @@ Check out [eframe_template](https://github.com/emilk/eframe_template) for an exa ## Downsides with using egui on the web -`egui_web` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challanges and serious downsides. +`egui_web` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides. * Rendering: Getting pixel-perfect rendering right on the web is very difficult. * Search: you cannot search an egui web page like you would a normal web page. * Bringing up an on-screen keyboard on mobile: there is no JS function to do this, so `egui_web` fakes it by adding some invisible DOM elements. It doesn't always work. * Mobile text editing is not as good as for a normal web app. -* Accessibility: There is an experimental screen reader for `egui_web`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity conserns). +* Accessibility: There is an experimental screen reader for `egui_web`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns). * No integration with browser settings for colors and fonts. * On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui. In many ways, `egui_web` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work). -The suggested use for `egui_web` are for web apps where performance and responsiveness are more important than accessability and mobile text editing. +The suggested use for `egui_web` are for web apps where performance and responsiveness are more important than accessibility and mobile text editing. diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index f8dfa12b241..23d43cf0e4d 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -155,6 +155,7 @@ impl AppRunner { }, output: Default::default(), storage: Some(Box::new(LocalStorage::default())), + gl: painter.gl().clone(), }; let needs_repaint: std::sync::Arc = Default::default(); @@ -274,12 +275,14 @@ impl AppRunner { Ok((needs_repaint, clipped_primitives)) } + pub fn clear_color_buffer(&self) { + self.painter.clear(self.app.clear_color()); + } + /// Paint the results of the last call to [`Self::logic`]. pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { let textures_delta = std::mem::take(&mut self.textures_delta); - self.painter.clear(self.app.clear_color()); - self.painter.paint_and_update_textures( clipped_primitives, self.egui_ctx.pixels_per_point(), diff --git a/egui_web/src/glow_wrapping.rs b/egui_web/src/glow_wrapping.rs index a66cf5d4df5..80d6fab5ab9 100644 --- a/egui_web/src/glow_wrapping.rs +++ b/egui_web/src/glow_wrapping.rs @@ -32,6 +32,10 @@ impl WrappedGlowPainter { } impl WrappedGlowPainter { + pub fn gl(&self) -> &std::rc::Rc { + self.painter.gl() + } + pub fn max_texture_side(&self) -> usize { self.painter.max_texture_side() } @@ -48,7 +52,7 @@ impl WrappedGlowPainter { self.painter.free_texture(tex_id); } - pub fn clear(&mut self, clear_color: Rgba) { + pub fn clear(&self, clear_color: Rgba) { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color); } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 1e227919d20..899fde79aca 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -339,6 +339,7 @@ fn paint_and_schedule(runner_ref: &AppRunnerRef, panicked: Arc) -> R fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.lock(); if runner_lock.needs_repaint.fetch_and_clear() { + runner_lock.clear_color_buffer(); let (needs_repaint, clipped_primitives) = runner_lock.logic()?; runner_lock.paint(&clipped_primitives)?; if needs_repaint { diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 4a8ea63666f..45ce20365c2 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -248,8 +248,6 @@ pub struct IconData { /// /// It provides methods to inspect the surroundings (are we on the web?), /// allocate textures, and change settings (e.g. window size). -/// -/// [`Frame`] is cheap to clone and is safe to pass to other threads. pub struct Frame { /// Information about the integration. #[doc(hidden)] @@ -262,6 +260,10 @@ pub struct Frame { /// A place where you can store custom data in a way that persists when you restart the app. #[doc(hidden)] pub storage: Option>, + + /// A reference to the underlying [`glow`] (OpenGL) context. + #[doc(hidden)] + pub gl: std::rc::Rc, } impl Frame { @@ -285,6 +287,19 @@ impl Frame { self.storage.as_deref_mut() } + /// A reference to the underlying [`glow`] (OpenGL) context. + /// + /// This can be used, for instance, to: + /// * Render things to offscreen buffers. + /// * Read the pixel buffer from the previous frame (`glow::Context::read_pixels`). + /// * Render things behind the egui windows. + /// + /// Note that all egui painting is deferred to after the call to [`App::update`] + /// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on). + pub fn gl(&self) -> &std::rc::Rc { + &self.gl + } + /// Signal the app to stop/exit/quit the app (only works for native apps, not web apps). /// The framework will not quit immediately, but at the end of the this frame. pub fn quit(&mut self) {