From c6375efa22e3db269cfc85e4f61a7bb90436c6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Mon, 9 Sep 2024 10:36:13 +0200 Subject: [PATCH 01/53] Add `WidgetType::RadioGroup` (#5081) Extracted out of #4805 I'm using this widget type in [`egui-theme-switch`] but since it's not built in I have to call `accesskit_node_builder` which is a bit cumbersome :) * [x] I have followed the instructions in the PR template [`egui-theme-switch`]: https://github.com/bash/egui-theme-switch/blob/main/src/lib.rs --- crates/egui/src/data/output.rs | 1 + crates/egui/src/lib.rs | 3 +++ crates/egui/src/response.rs | 1 + 3 files changed, 5 insertions(+) diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 58df5da230a..5d50afec371 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -640,6 +640,7 @@ impl WidgetInfo { WidgetType::Button => "button", WidgetType::Checkbox => "checkbox", WidgetType::RadioButton => "radio", + WidgetType::RadioGroup => "radio group", WidgetType::SelectableLabel => "selectable", WidgetType::ComboBox => "combo", WidgetType::Slider => "slider", diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 9a93a56fa90..78a526c181d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -618,6 +618,9 @@ pub enum WidgetType { RadioButton, + /// A group of radio buttons. + RadioGroup, + SelectableLabel, ComboBox, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index d6a9a8a0c7f..3a415e36961 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -992,6 +992,7 @@ impl Response { } WidgetType::Checkbox => Role::CheckBox, WidgetType::RadioButton => Role::RadioButton, + WidgetType::RadioGroup => Role::RadioGroup, WidgetType::SelectableLabel => Role::Button, WidgetType::ComboBox => Role::ComboBox, WidgetType::Slider => Role::Slider, From 49fb163ae9211d63e6c2eafdb62a8ed714e679c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Mon, 9 Sep 2024 10:36:56 +0200 Subject: [PATCH 02/53] Add return value to `with_accessibility_parent` (#5083) Extracted out of #4805 In [`egui-theme-switch`] I'm allocating a response inside the closure passed to `with_accessibility_parent` so that my radio buttons have the radio group as parent. I'm working around the lack of return value with a custom extension trait for now: [`ContextExt`] * [x] I have followed the instructions in the PR template [`egui-theme-switch`]: https://github.com/bash/egui-theme-switch/blob/main/src/lib.rs [`ContextExt`]: https://github.com/bash/egui-theme-switch/blob/main/src/context_ext.rs --- crates/egui/src/context.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 162ff4a739e..81f0c0a9624 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2887,9 +2887,9 @@ impl Context { /// the function is still called, but with no other effect. /// /// No locks are held while the given closure is called. - #[allow(clippy::unused_self)] + #[allow(clippy::unused_self, clippy::let_and_return)] #[inline] - pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce()) { + pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce() -> R) -> R { // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls #[cfg(feature = "accesskit")] self.frame_state_mut(|fs| { @@ -2898,7 +2898,7 @@ impl Context { } }); - f(); + let result = f(); #[cfg(feature = "accesskit")] self.frame_state_mut(|fs| { @@ -2906,6 +2906,8 @@ impl Context { assert_eq!(state.parent_stack.pop(), Some(_id)); } }); + + result } /// If AccessKit support is active for the current frame, get or create From 9000d16d836c277b5ab25298352c82852c9cf79f Mon Sep 17 00:00:00 2001 From: Simon <614955+simgt@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:11:52 +0200 Subject: [PATCH 03/53] Export module `egui::frame` (#5087) Remove the crate visibility of the frame module. Useful at least when using `Frame::begin` as otherwise the returned type is opaque to library users and prevents from creating containers that use `Frame` with a similar interface. Alternative is to only export `frame::Prepared` as `PreparedFrame` or something, but I saw that other submodules of containers are already public. * Closes #2106 * [x] I have followed the instructions in the PR template --- crates/egui/src/containers/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 0f05b29bd5c..3dd75a4458e 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod area; pub mod collapsing_header; mod combo_box; -pub(crate) mod frame; +pub mod frame; pub mod panel; pub mod popup; pub(crate) mod resize; From 1c293d4cc881967782d21209a2d674574e0138c8 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 9 Sep 2024 14:02:06 +0200 Subject: [PATCH 04/53] Update `glow` to 0.14 (#4952) Before making this PR, I did take notice of a similar PR, https://github.com/emilk/egui/pull/4833, but as it appears to be abandoned, I decided to make this PR. **Missing** One of the checks doesn't pass as wgpu still uses glow `0.13.1` ```shell cargo deny --all-features --log-level error --target aarch64-apple-darwin check ``` * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 18 +++++++++++++++--- Cargo.toml | 2 +- crates/eframe/src/native/glow_integration.rs | 4 ---- crates/egui_glow/examples/pure_glow.rs | 2 -- deny.toml | 2 ++ 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cc5aa5c845..f11f4d03164 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,7 +1177,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow", + "glow 0.14.0", "glutin", "glutin-winit", "home", @@ -1328,7 +1328,7 @@ dependencies = [ "document-features", "egui", "egui-winit", - "glow", + "glow 0.14.0", "glutin", "glutin-winit", "log", @@ -1831,6 +1831,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glow" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f865cbd94bd355b89611211e49508da98a1fce0ad755c1e8448fb96711b24528" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glutin" version = "0.32.0" @@ -4504,7 +4516,7 @@ dependencies = [ "block", "cfg_aliases 0.1.1", "core-graphics-types", - "glow", + "glow 0.13.1", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", diff --git a/Cargo.toml b/Cargo.toml index 09e2f42d840..90f4d7c17c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ backtrace = "0.3" bytemuck = "1.7.2" criterion = { version = "0.5.1", default-features = false } document-features = " 0.2.8" -glow = "0.13" +glow = "0.14" glutin = "0.32.0" glutin-winit = "0.5.0" home = "0.5.9" diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index c1a3f5eaf89..1a666095709 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -5,10 +5,6 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -// `clippy::arc_with_non_send_sync`: `glow::Context` was accidentally non-Sync in glow 0.13, -// but that will be fixed in future releases of glow. -// https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e -#![allow(clippy::arc_with_non_send_sync)] #![allow(clippy::undocumented_unsafe_blocks)] use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; diff --git a/crates/egui_glow/examples/pure_glow.rs b/crates/egui_glow/examples/pure_glow.rs index 4145e1d6310..d151be377d5 100644 --- a/crates/egui_glow/examples/pure_glow.rs +++ b/crates/egui_glow/examples/pure_glow.rs @@ -3,8 +3,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![allow(rustdoc::missing_crate_level_docs)] // it's an example #![allow(clippy::undocumented_unsafe_blocks)] -#![allow(clippy::arc_with_non_send_sync)] -// `clippy::arc_with_non_send_sync`: `glow::Context` was accidentally non-Sync in glow 0.13, but that will be fixed in future releases of glow: https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e #![allow(unsafe_code)] use std::num::NonZeroU32; diff --git a/deny.toml b/deny.toml index 5b3c546528d..f5291700dfc 100644 --- a/deny.toml +++ b/deny.toml @@ -59,6 +59,8 @@ skip = [ { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' { name = "windows-core" }, # old version via accesskit_windows { name = "windows" }, # old version via accesskit_windows + { name = "glow" }, # wgpu uses an old `glow`, but realistically no one uses _both_ `egui_wgpu` and `egui_glow`, so we won't get a duplicate dependency + ] skip-tree = [ { name = "criterion" }, # dev-dependency From 1ccd056d191c1b62b66b341da1fe7237ec782880 Mon Sep 17 00:00:00 2001 From: Tiaan Louw Date: Mon, 9 Sep 2024 14:34:13 +0200 Subject: [PATCH 05/53] Remove reference to egui::Color32. (#5011) As most of the code refers to types in epaint, it makes sense to use the Color32 alias from epaint as well. - [x] I have followed the instructions in the PR template --- crates/egui-wgpu/src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 476685707ad..899cfbed7a1 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -542,7 +542,7 @@ impl Renderer { "Mismatch between texture size and texel count" ); crate::profile_scope!("font -> sRGBA"); - Cow::Owned(image.srgba_pixels(None).collect::>()) + Cow::Owned(image.srgba_pixels(None).collect::>()) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); From f897405a826c0cdad42e987029be65664411d49c Mon Sep 17 00:00:00 2001 From: YgorSouza <43298013+YgorSouza@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:50:56 +0200 Subject: [PATCH 06/53] Use precomputed lookup table in Color32::from_rgba_unmultiplied (#5088) Improves performances significantly (about 40 times) according to the benchmarks. * Closes * [x] I have followed the instructions in the PR template --- crates/ecolor/src/color32.rs | 41 +++++++++++++++------------ crates/epaint/benches/benchmark.rs | 45 +++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index ca257303dae..9025b6ee548 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -1,6 +1,4 @@ -use crate::{ - fast_round, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba, -}; +use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; /// This format is used for space-efficient color representation (32 bits). /// @@ -95,21 +93,28 @@ impl Color32 { /// From `sRGBA` WITHOUT premultiplied alpha. #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - if a == 255 { - Self::from_rgb(r, g, b) // common-case optimization - } else if a == 0 { - Self::TRANSPARENT // common-case optimization - } else { - let r_lin = linear_f32_from_gamma_u8(r); - let g_lin = linear_f32_from_gamma_u8(g); - let b_lin = linear_f32_from_gamma_u8(b); - let a_lin = linear_f32_from_linear_u8(a); - - let r = gamma_u8_from_linear_f32(r_lin * a_lin); - let g = gamma_u8_from_linear_f32(g_lin * a_lin); - let b = gamma_u8_from_linear_f32(b_lin * a_lin); - - Self::from_rgba_premultiplied(r, g, b, a) + use std::sync::OnceLock; + match a { + // common-case optimization + 0 => Self::TRANSPARENT, + // common-case optimization + 255 => Self::from_rgb(r, g, b), + a => { + static LOOKUP_TABLE: OnceLock<[u8; 256 * 256]> = OnceLock::new(); + let lut = LOOKUP_TABLE.get_or_init(|| { + use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8}; + core::array::from_fn(|i| { + let [value, alpha] = (i as u16).to_ne_bytes(); + let value_lin = linear_f32_from_gamma_u8(value); + let alpha_lin = linear_f32_from_linear_u8(alpha); + gamma_u8_from_linear_f32(value_lin * alpha_lin) + }) + }); + + let [r, g, b] = + [r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]); + Self::from_rgba_premultiplied(r, g, b, a) + } } } diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 07a743ae404..e723638b849 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -223,6 +223,46 @@ fn thin_large_line_uv(c: &mut Criterion) { }); } +fn rgba_values() -> [[u8; 4]; 1000] { + core::array::from_fn(|i| [5, 7, 11, 13].map(|m| (i * m) as u8)) +} + +fn from_rgba_unmultiplied_0(c: &mut Criterion) { + c.bench_function("from_rgba_unmultiplied_0", move |b| { + let values = black_box(rgba_values().map(|[r, g, b, _]| [r, g, b, 0])); + b.iter(|| { + for [r, g, b, a] in values { + let color = ecolor::Color32::from_rgba_unmultiplied(r, g, b, a); + black_box(color); + } + }); + }); +} + +fn from_rgba_unmultiplied_other(c: &mut Criterion) { + c.bench_function("from_rgba_unmultiplied_other", move |b| { + let values = black_box(rgba_values().map(|[r, g, b, a]| [r, g, b, a.clamp(1, 254)])); + b.iter(|| { + for [r, g, b, a] in values { + let color = ecolor::Color32::from_rgba_unmultiplied(r, g, b, a); + black_box(color); + } + }); + }); +} + +fn from_rgba_unmultiplied_255(c: &mut Criterion) { + c.bench_function("from_rgba_unmultiplied_255", move |b| { + let values = black_box(rgba_values().map(|[r, g, b, _]| [r, g, b, 255])); + b.iter(|| { + for [r, g, b, a] in values { + let color = ecolor::Color32::from_rgba_unmultiplied(r, g, b, a); + black_box(color); + } + }); + }); +} + criterion_group!( benches, single_dashed_lines, @@ -235,6 +275,9 @@ criterion_group!( thick_line_uv, thick_large_line_uv, thin_line_uv, - thin_large_line_uv + thin_large_line_uv, + from_rgba_unmultiplied_0, + from_rgba_unmultiplied_other, + from_rgba_unmultiplied_255, ); criterion_main!(benches); From 89b6055f9cc8954e0a27d17bae2d10f089592330 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 10 Sep 2024 11:04:13 +0200 Subject: [PATCH 07/53] Add `Ui::with_visual_transform` (#5055) * [X] I have followed the instructions in the PR template This allows you to transform widgets without having to put them on a new layer. Example usage: https://github.com/user-attachments/assets/6b547782-f15e-42ce-835f-e8febe8d2d65 ```rust use eframe::egui; use eframe::egui::{Button, Frame, InnerResponse, Label, Pos2, RichText, UiBuilder, Widget}; use eframe::emath::TSTransform; use eframe::NativeOptions; use egui::{CentralPanel, Sense, WidgetInfo}; pub fn main() -> eframe::Result { eframe::run_simple_native("focus test", NativeOptions::default(), |ctx, _frame| { CentralPanel::default().show(ctx, |ui| { let response = ui.ctx().read_response(ui.next_auto_id()); let pressed = response .as_ref() .is_some_and(|r| r.is_pointer_button_down_on()); let hovered = response.as_ref().is_some_and(|r| r.hovered()); let target_scale = match (pressed, hovered) { (true, _) => 0.94, (_, true) => 1.06, _ => 1.0, }; let scale = ui .ctx() .animate_value_with_time(ui.id().with("Down"), target_scale, 0.1); let mut center = response .as_ref() .map(|r| r.rect.center()) .unwrap_or_else(|| Pos2::new(0.0, 0.0)); if center.any_nan() { center = Pos2::new(0.0, 0.0); } let transform = TSTransform::from_translation(center.to_vec2()) * TSTransform::from_scaling(scale) * TSTransform::from_translation(-center.to_vec2()); ui.with_visual_transform(transform, |ui| { Button::new(RichText::new("Yaaaay").size(20.0)) .sense(Sense::click()) .ui(ui) }); }); }) } ``` --- crates/egui/src/layers.rs | 14 +++++++++++++- crates/egui/src/ui.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 3a657ba013f..66bbd338387 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -124,10 +124,14 @@ impl PaintList { self.0.is_empty() } + pub fn next_idx(&self) -> ShapeIdx { + ShapeIdx(self.0.len()) + } + /// Returns the index of the new [`Shape`] that can be used with `PaintList::set`. #[inline(always)] pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx { - let idx = ShapeIdx(self.0.len()); + let idx = self.next_idx(); self.0.push(ClippedShape { clip_rect, shape }); idx } @@ -171,6 +175,14 @@ impl PaintList { } } + /// Transform each [`Shape`] and clip rectangle in range by this much, in-place + pub fn transform_range(&mut self, start: ShapeIdx, end: ShapeIdx, transform: TSTransform) { + for ClippedShape { clip_rect, shape } in &mut self.0[start.0..end.0] { + *clip_rect = transform.mul_rect(*clip_rect); + shape.transform(transform); + } + } + /// Read-only access to all held shapes. pub fn all_entries(&self) -> impl ExactSizeIterator { self.0.iter() diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index ac31bf4856c..0a16bafe5cd 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2690,6 +2690,33 @@ impl Ui { (InnerResponse { inner, response }, payload) } + + /// Create a new Scope and transform its contents via a [`emath::TSTransform`]. + /// This only affects visuals, inputs will not be transformed. So this is mostly useful + /// to create visual effects on interactions, e.g. scaling a button on hover / click. + /// + /// Check out [`Context::set_transform_layer`] for a persistent transform that also affects + /// inputs. + pub fn with_visual_transform( + &mut self, + transform: emath::TSTransform, + add_contents: impl FnOnce(&mut Self) -> R, + ) -> InnerResponse { + let start_idx = self.ctx().graphics(|gx| { + gx.get(self.layer_id()) + .map_or(crate::layers::ShapeIdx(0), |l| l.next_idx()) + }); + + let r = self.scope_dyn(UiBuilder::new(), Box::new(add_contents)); + + self.ctx().graphics_mut(|g| { + let list = g.entry(self.layer_id()); + let end_idx = list.next_idx(); + list.transform_range(start_idx, end_idx, transform); + }); + + r + } } /// # Menus From f4697bc007447c6c2674beb4e25f599fb7afa093 Mon Sep 17 00:00:00 2001 From: lampsitter <96946613+lampsitter@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:38:26 +0200 Subject: [PATCH 08/53] Use Style's font size in egui_extras::syntax_highlighting (#5090) * Closes https://github.com/emilk/egui/issues/3549 * [X] I have followed the instructions in the PR template The syntax highlighting font size was always hardcoded to 12 or 10 depending on what case it was hitting (so not consistent). This is particularly noticeable when you increase the font size to something larger for the rest of the ui. With this the default monospace font size is used by default. Since the issue is closely related to #3549 I decided to implement the ability to use override_font_id too. ## Visualized Default monospace is set to 15 in all the pictures Before/After without syntect: ![normal](https://github.com/user-attachments/assets/0d058720-47ff-49e7-af77-30d48f5e138c) Before/after _with_ syntect: ![syntect](https://github.com/user-attachments/assets/e5c380fe-ced1-40ee-b4b1-c26cec18a840) Font override after without/with syntect (monospace = 20): ![override](https://github.com/user-attachments/assets/efd1b759-3f97-4673-864a-5a18afc64099) ### Breaking changes - `CodeTheme::dark` and `CodeTheme::light` takes in the font size - `CodeTheme::from_memory` takes in `Style` - `highlight` function takes in `Style` --- crates/egui_demo_app/src/apps/http_app.rs | 6 +- crates/egui_demo_lib/src/demo/code_editor.rs | 12 +- crates/egui_demo_lib/src/demo/code_example.rs | 3 +- crates/egui_demo_lib/src/lib.rs | 2 +- crates/egui_extras/src/syntax_highlighting.rs | 129 ++++++++++++++---- 5 files changed, 118 insertions(+), 34 deletions(-) diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index d6b57284267..abc728fd5b2 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -224,7 +224,11 @@ fn syntax_highlighting( let extension = extension_and_rest.first()?; let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style()); Some(ColoredText(egui_extras::syntax_highlighting::highlight( - ctx, &theme, text, extension, + ctx, + &ctx.style(), + &theme, + text, + extension, ))) } diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index 4dad60d3cb4..fe39e1be8f1 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -67,7 +67,8 @@ impl crate::View for CodeEditor { }); } - let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = + egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style()); ui.collapsing("Theme", |ui| { ui.group(|ui| { theme.ui(ui); @@ -76,8 +77,13 @@ impl crate::View for CodeEditor { }); let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { - let mut layout_job = - egui_extras::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); + let mut layout_job = egui_extras::syntax_highlighting::highlight( + ui.ctx(), + ui.style(), + &theme, + string, + language, + ); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 18a251077b2..3db90ad3e34 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -130,7 +130,8 @@ impl crate::View for CodeExample { ui.separator(); - let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = + egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style()); ui.collapsing("Theme", |ui| { theme.ui(ui); theme.store_in_memory(ui.ctx()); diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index ce18e0910ee..23c28bcb65c 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -21,7 +21,7 @@ pub use rendering_test::ColorTest; /// View some Rust code with syntax highlighting and selection. pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) { let language = "rs"; - let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style()); egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language); } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 8ddd4606507..e782677c95c 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -6,6 +6,7 @@ #![allow(clippy::mem_forget)] // False positive from enum_map macro use egui::text::LayoutJob; +use egui::TextStyle; /// View some code with syntax highlighting and selection. pub fn code_view_ui( @@ -14,29 +15,53 @@ pub fn code_view_ui( code: &str, language: &str, ) -> egui::Response { - let layout_job = highlight(ui.ctx(), theme, code, language); + let layout_job = highlight(ui.ctx(), ui.style(), theme, code, language); ui.add(egui::Label::new(layout_job).selectable(true)) } /// Add syntax highlighting to a code string. /// /// The results are memoized, so you can call this every frame without performance penalty. -pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob { - impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter { - fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { - self.highlight(theme, code, lang) +pub fn highlight( + ctx: &egui::Context, + style: &egui::Style, + theme: &CodeTheme, + code: &str, + language: &str, +) -> LayoutJob { + // We take in both context and style so that in situations where ui is not available such as when + // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available + // (ui.ctx(), ui.style()) can be used + + impl egui::util::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> + for Highlighter + { + fn compute( + &mut self, + (font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str), + ) -> LayoutJob { + self.highlight(font_id.clone(), theme, code, lang) } } type HighlightCache = egui::util::cache::FrameCache; + let font_id = style + .override_font_id + .clone() + .unwrap_or_else(|| TextStyle::Monospace.resolve(style)); + ctx.memory_mut(|mem| { mem.caches .cache::() - .get((theme, code, language)) + .get((&font_id, theme, code, language)) }) } +fn monospace_font_size(style: &egui::Style) -> f32 { + TextStyle::Monospace.resolve(style).size +} + // ---------------------------------------------------------------------------- #[cfg(not(feature = "syntect"))] @@ -128,6 +153,8 @@ pub struct CodeTheme { #[cfg(feature = "syntect")] syntect_theme: SyntectTheme, + #[cfg(feature = "syntect")] + font_id: egui::FontId, #[cfg(not(feature = "syntect"))] formats: enum_map::EnumMap, @@ -135,40 +162,75 @@ pub struct CodeTheme { impl Default for CodeTheme { fn default() -> Self { - Self::dark() + Self::dark(12.0) } } impl CodeTheme { /// Selects either dark or light theme based on the given style. pub fn from_style(style: &egui::Style) -> Self { + let font_id = style + .override_font_id + .clone() + .unwrap_or_else(|| TextStyle::Monospace.resolve(style)); + if style.visuals.dark_mode { - Self::dark() + Self::dark_with_font_id(font_id) } else { - Self::light() + Self::light_with_font_id(font_id) } } + /// ### Example + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// use egui_extras::syntax_highlighting::CodeTheme; + /// let theme = CodeTheme::dark(12.0); + /// # }); + /// ``` + pub fn dark(font_size: f32) -> Self { + Self::dark_with_font_id(egui::FontId::monospace(font_size)) + } + + /// ### Example + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// use egui_extras::syntax_highlighting::CodeTheme; + /// let theme = CodeTheme::light(12.0); + /// # }); + /// ``` + pub fn light(font_size: f32) -> Self { + Self::light_with_font_id(egui::FontId::monospace(font_size)) + } + /// Load code theme from egui memory. /// /// There is one dark and one light theme stored at any one time. - pub fn from_memory(ctx: &egui::Context) -> Self { + pub fn from_memory(ctx: &egui::Context, style: &egui::Style) -> Self { #![allow(clippy::needless_return)] - let (id, default) = if ctx.style().visuals.dark_mode { - (egui::Id::new("dark"), Self::dark as fn() -> Self) + let (id, default) = if style.visuals.dark_mode { + (egui::Id::new("dark"), Self::dark as fn(f32) -> Self) } else { - (egui::Id::new("light"), Self::light as fn() -> Self) + (egui::Id::new("light"), Self::light as fn(f32) -> Self) }; #[cfg(feature = "serde")] { - return ctx.data_mut(|d| d.get_persisted(id).unwrap_or_else(default)); + return ctx.data_mut(|d| { + d.get_persisted(id) + .unwrap_or_else(|| default(monospace_font_size(style))) + }); } #[cfg(not(feature = "serde"))] { - return ctx.data_mut(|d| d.get_temp(id).unwrap_or_else(default)); + return ctx.data_mut(|d| { + d.get_temp(id) + .unwrap_or_else(|| default(monospace_font_size(style))) + }); } } @@ -192,17 +254,19 @@ impl CodeTheme { #[cfg(feature = "syntect")] impl CodeTheme { - pub fn dark() -> Self { + fn dark_with_font_id(font_id: egui::FontId) -> Self { Self { dark_mode: true, syntect_theme: SyntectTheme::Base16MochaDark, + font_id, } } - pub fn light() -> Self { + fn light_with_font_id(font_id: egui::FontId) -> Self { Self { dark_mode: false, syntect_theme: SyntectTheme::SolarizedLight, + font_id, } } @@ -220,8 +284,10 @@ impl CodeTheme { #[cfg(not(feature = "syntect"))] impl CodeTheme { - pub fn dark() -> Self { - let font_id = egui::FontId::monospace(10.0); + // The syntect version takes it by value. This could be avoided by specializing the from_style + // function, but at the cost of more code duplication. + #[allow(clippy::needless_pass_by_value)] + fn dark_with_font_id(font_id: egui::FontId) -> Self { use egui::{Color32, TextFormat}; Self { dark_mode: true, @@ -236,8 +302,9 @@ impl CodeTheme { } } - pub fn light() -> Self { - let font_id = egui::FontId::monospace(10.0); + // The syntect version takes it by value + #[allow(clippy::needless_pass_by_value)] + fn light_with_font_id(font_id: egui::FontId) -> Self { use egui::{Color32, TextFormat}; Self { dark_mode: false, @@ -291,9 +358,9 @@ impl CodeTheme { }); let reset_value = if self.dark_mode { - Self::dark() + Self::dark(monospace_font_size(ui.style())) } else { - Self::light() + Self::light(monospace_font_size(ui.style())) }; if ui @@ -348,12 +415,18 @@ impl Default for Highlighter { impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] - fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { + fn highlight( + &self, + font_id: egui::FontId, + theme: &CodeTheme, + code: &str, + lang: &str, + ) -> LayoutJob { self.highlight_impl(theme, code, lang).unwrap_or_else(|| { // Fallback: LayoutJob::simple( code.into(), - egui::FontId::monospace(12.0), + font_id, if theme.dark_mode { egui::Color32::LIGHT_GRAY } else { @@ -377,8 +450,8 @@ impl Highlighter { .find_syntax_by_name(language) .or_else(|| self.ps.find_syntax_by_extension(language))?; - let theme = theme.syntect_theme.syntect_key_name(); - let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]); + let syn_theme = theme.syntect_theme.syntect_key_name(); + let mut h = HighlightLines::new(syntax, &self.ts.themes[syn_theme]); use egui::text::{LayoutSection, TextFormat}; @@ -402,7 +475,7 @@ impl Highlighter { leading_space: 0.0, byte_range: as_byte_range(text, range), format: TextFormat { - font_id: egui::FontId::monospace(12.0), + font_id: theme.font_id.clone(), color: text_color, italics, underline, From b5627c7d4084c667d8ffefcd9131bfe5b6009e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Wed, 11 Sep 2024 17:52:53 +0200 Subject: [PATCH 09/53] Make Light & Dark Visuals Customizable When Following The System Theme (#4744) * Closes * [x] I have followed the instructions in the PR template --- Unfortunately, this PR contains a bunch of breaking changes because `Context` no longer has one style, but two. I could try to add some of the methods back if that's desired. The most subtle change is probably that `style_mut` mutates both the dark and the light style (which from the usage in egui itself felt like the right choice but might be surprising to users). I decided to deviate a bit from the data structure suggested in the linked issue. Instead of this: ```rust pub theme: Theme, // Dark or Light pub follow_system_theme: bool, // Change [`Self::theme`] based on `RawInput::system_theme`? ``` I decided to add a `ThemePreference` enum and track the current system theme separately. This has a couple of benefits: * The user's theme choice is not magically overwritten on the next frame. * A widget for changing the theme preference only needs to know the `ThemePreference` and not two values. * Persisting the `theme_preference` is fine (as opposed to persisting the `theme` field which may actually be the system theme). The `small_toggle_button` currently only toggles between dark and light (so you can never get back to following the system). I think it's easy to improve on this in a follow-up PR :) I made the function `pub(crate)` for now because it should eventually be a method on `ThemePreference`, not `Theme`. To showcase the new capabilities I added a new example that uses different "accent" colors in dark and light mode: A screenshot of egui's widget gallery demo in dark mode
using a purple accent color instead of the default blue accent A screenshot of egui's widget gallery demo in light
mode using a green accent color instead of the default blue accent --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 11 ++ crates/eframe/src/epi.rs | 2 +- crates/egui/src/context.rs | 121 +++++++++++++++--- crates/egui/src/lib.rs | 2 +- crates/egui/src/memory/mod.rs | 81 +++++++----- crates/egui/src/memory/theme.rs | 71 ++++++++++ crates/egui/src/style.rs | 51 ++------ crates/egui/src/ui.rs | 6 +- crates/egui/src/widgets/color_picker.rs | 2 +- crates/egui/src/widgets/mod.rs | 26 ++-- crates/egui_demo_app/src/wrap_app.rs | 2 +- crates/egui_demo_lib/src/demo/scrolling.rs | 9 +- crates/egui_extras/src/syntax_highlighting.rs | 4 +- examples/custom_font_style/src/main.rs | 6 +- examples/custom_keypad/src/main.rs | 2 +- examples/custom_style/Cargo.toml | 23 ++++ examples/custom_style/README.md | 7 + examples/custom_style/screenshot.png | Bin 0 -> 127741 bytes examples/custom_style/src/main.rs | 69 ++++++++++ examples/custom_window_frame/src/main.rs | 2 +- examples/screenshot/src/main.rs | 4 +- tests/test_ui_stack/src/main.rs | 2 +- 22 files changed, 381 insertions(+), 122 deletions(-) create mode 100644 examples/custom_style/Cargo.toml create mode 100644 examples/custom_style/README.md create mode 100644 examples/custom_style/screenshot.png create mode 100644 examples/custom_style/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index f11f4d03164..d787f6aba03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1052,6 +1052,17 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_style" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_demo_lib", + "egui_extras", + "env_logger", + "image", +] + [[package]] name = "custom_window_frame" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 0fec5a070fd..f567507c4da 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -54,7 +54,7 @@ pub struct CreationContext<'s> { /// The egui Context. /// /// You can use this to customize the look of egui, e.g to call [`egui::Context::set_fonts`], - /// [`egui::Context::set_visuals`] etc. + /// [`egui::Context::set_visuals_of`] etc. pub egui_ctx: egui::Context, /// Information about the surrounding environment. diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 81f0c0a9624..dda34db920f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -21,7 +21,7 @@ use crate::{ layers::GraphicLayers, load, load::{Bytes, Loaders, SizedTexture}, - memory::Options, + memory::{Options, Theme}, menu, os::OperatingSystem, output::FullOutput, @@ -487,7 +487,7 @@ impl ContextImpl { }); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { - let interact_radius = self.memory.options.style.interaction.interact_radius; + let interact_radius = self.memory.options.style().interaction.interact_radius; crate::hit_test::hit_test( &viewport.prev_frame.widgets, @@ -583,7 +583,7 @@ impl ContextImpl { crate::profile_scope!("preload_font_glyphs"); // Preload the most common characters for the most common fonts. // This is not very important to do, but may save a few GPU operations. - for font_id in self.memory.options.style.text_styles.values() { + for font_id in self.memory.options.style().text_styles.values() { fonts.lock().fonts.font(font_id).preload_common_characters(); } } @@ -1245,7 +1245,7 @@ impl Context { pub fn register_widget_info(&self, id: Id, make_info: impl Fn() -> crate::WidgetInfo) { #[cfg(debug_assertions)] self.write(|ctx| { - if ctx.memory.options.style.debug.show_interactive_widgets { + if ctx.memory.options.style().debug.show_interactive_widgets { ctx.viewport().this_frame.widgets.set_info(id, make_info()); } }); @@ -1612,12 +1612,37 @@ impl Context { } } - /// The [`Style`] used by all subsequent windows, panels etc. + /// Does the OS use dark or light mode? + /// This is used when the theme preference is set to [`crate::ThemePreference::System`]. + pub fn system_theme(&self) -> Option { + self.memory(|mem| mem.options.system_theme) + } + + /// The [`Theme`] used to select the appropriate [`Style`] (dark or light) + /// used by all subsequent windows, panels etc. + pub fn theme(&self) -> Theme { + self.options(|opt| opt.theme()) + } + + /// The [`Theme`] used to select between dark and light [`Self::style`] + /// as the active style used by all subsequent windows, panels etc. + /// + /// Example: + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.set_theme(egui::Theme::Light); // Switch to light mode + /// ``` + pub fn set_theme(&self, theme_preference: impl Into) { + self.options_mut(|opt| opt.theme_preference = theme_preference.into()); + } + + /// The currently active [`Style`] used by all subsequent windows, panels etc. pub fn style(&self) -> Arc