diff --git a/.typos.toml b/.typos.toml index b9d882beb..d4c716172 100644 --- a/.typos.toml +++ b/.typos.toml @@ -18,5 +18,133 @@ teselation = "tessellation" tessalation = "tessellation" tesselation = "tessellation" + +# Use the more common spelling +adaptor = "adapter" +adaptors = "adapters" + +# For consistency we prefer American English: +aeroplane = "airplane" +analogue = "analog" +analyse = "analyze" +appetiser = "appetizer" +arbour = "arbor" +ardour = "arbor" +armour = "armor" +artefact = "artifact" +authorise = "authorize" +behaviour = "behavior" +behavioural = "behavioral" +British = "American" +calibre = "caliber" +# cancelled = "canceled" # winit uses this :( +candour = "candor" +capitalise = "capitalize" +catalogue = "catalog" +centre = "center" +characterise = "characterize" +chequerboard = "checkerboard" +chequered = "checkered" +civilise = "civilize" +clamour = "clamor" +colonise = "colonize" +colour = "color" +coloured = "colored" +cosy = "cozy" +criticise = "criticize" +defence = "defense" +demeanour = "demeanor" +dialogue = "dialog" +distil = "distill" +doughnut = "donut" +dramatise = "dramatize" +draught = "draft" +emphasise = "emphasize" +endeavour = "endeavor" +enrol = "enroll" +epilogue = "epilog" +equalise = "equalize" +favour = "favor" +favourite = "favorite" +fibre = "fiber" +flavour = "flavor" +fulfil = "fufill" +gaol = "jail" +grey = "gray" +greys = "grays" +greyscale = "grayscale" +harbour = "habor" +honour = "honor" +humour = "humor" +instalment = "installment" +instil = "instill" +jewellery = "jewelry" +kerb = "curb" +labour = "labor" +litre = "liter" +lustre = "luster" +meagre = "meager" +metre = "meter" +mobilise = "mobilize" +monologue = "monolog" +naturalise = "naturalize" +neighbour = "neighbor" +neighbourhood = "neighborhood" +normalise = "normalize" +normalised = "normalized" +odour = "odor" +offence = "offense" +organise = "organize" +parlour = "parlor" +plough = "plow" +popularise = "popularize" +pretence = "pretense" +programme = "program" +prologue = "prolog" +rancour = "rancor" +realise = "realize" +recognise = "recognize" +recognised = "recognized" +rigour = "rigor" +rumour = "rumor" +sabre = "saber" +satirise = "satirize" +saviour = "savior" +savour = "savor" +sceptical = "skeptical" +sceptre = "scepter" +sepulchre = "sepulcher" +serialisation = "serialization" +serialise = "serialize" +serialised = "serialized" +skilful = "skillful" +sombre = "somber" +specialisation = "specialization" +specialise = "specialize" +specialised = "specialized" +splendour = "splendor" +standardise = "standardize" +sulphur = "sulfur" +symbolise = "symbolize" +theatre = "theater" +tonne = "ton" +travelogue = "travelog" +tumour = "tumor" +valour = "valor" +vaporise = "vaporize" +vigour = "vigor" + +# null-terminated is the name of the wikipedia article! +# https://en.wikipedia.org/wiki/Null-terminated_string +nullterminated = "null-terminated" +zeroterminated = "null-terminated" +zero-terminated = "null-terminated" + + [files] extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated + +[default] +extend-ignore-re = [ + "#\\[doc\\(alias = .*", # We suggest "grey" in some doc +] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ddedc378..110f92ddd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ There are snapshots test that might need to be updated. Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them. If CI keeps complaining about snapshots (which could happen if you don't use macOS, snapshots in CI are currently rendered with macOS), you can instead run `./scripts/update_snapshots_from_ci.sh` to update your local snapshots from -the last CI run of your PR (which will download the `test_results` artefact). +the last CI run of your PR (which will download the `test_results` artifact). For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md). Snapshots and other big files are stored with git lfs. See [Working with git lfs](#working-with-git-lfs) for more info. If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs. diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 82b666c29..fd502f1a8 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -72,7 +72,7 @@ fn roaming_appdata() -> Option { }; let path = if result == S_OK { - // SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us. + // SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us. let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) }; Some(PathBuf::from(OsString::from_wide(path_slice))) } else { diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 2bffdb780..168d6123a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -3,8 +3,8 @@ use crate::web::string_from_js_value; use super::{ AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event, - native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme_dark, primary_touch_pos, - push_touches, text_from_keyboard_event, theme_from_dark_mode, translate_key, + native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos, + push_touches, text_from_keyboard_event, translate_key, }; use web_sys::{Document, EventTarget, ShadowRoot}; @@ -469,16 +469,19 @@ fn install_color_scheme_change_event( runner_ref: &WebRunner, window: &web_sys::Window, ) -> Result<(), JsValue> { - if let Some(media_query_list) = prefers_color_scheme_dark(window)? { - runner_ref.add_event_listener::( - &media_query_list, - "change", - |event, runner| { - let theme = theme_from_dark_mode(event.matches()); - runner.input.raw.system_theme = Some(theme); - runner.needs_repaint.repaint_asap(); - }, - )?; + for theme in [egui::Theme::Dark, egui::Theme::Light] { + if let Some(media_query_list) = prefers_color_scheme(window, theme)? { + runner_ref.add_event_listener::( + &media_query_list, + "change", + |_event, runner| { + if let Some(theme) = super::system_theme() { + runner.input.raw.system_theme = Some(theme); + runner.needs_repaint.repaint_asap(); + } + }, + )?; + } } Ok(()) diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 2bdd3af63..fdc9d2123 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -40,6 +40,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; +use egui::Theme; use wasm_bindgen::prelude::*; use web_sys::{Document, MediaQueryList, Node}; @@ -113,24 +114,31 @@ pub fn native_pixels_per_point() -> f32 { /// /// `None` means unknown. pub fn system_theme() -> Option { - let dark_mode = prefers_color_scheme_dark(&web_sys::window()?) - .ok()?? - .matches(); - Some(theme_from_dark_mode(dark_mode)) -} - -fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result, JsValue> { - window.match_media("(prefers-color-scheme: dark)") -} - -fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme { - if dark_mode { - egui::Theme::Dark + let window = web_sys::window()?; + if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) { + Some(Theme::Dark) + } else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) { + Some(Theme::Light) } else { - egui::Theme::Light + None } } +fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option { + Some(prefers_color_scheme(window, theme).ok()??.matches()) +} + +fn prefers_color_scheme( + window: &web_sys::Window, + theme: Theme, +) -> Result, JsValue> { + let theme = match theme { + Theme::Dark => "dark", + Theme::Light => "light", + }; + window.match_media(format!("(prefers-color-scheme: {theme})").as_str()) +} + /// Returns the canvas in client coordinates. fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect { let bounding_rect = canvas.get_bounding_client_rect(); diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index debc6c5d1..735c94d73 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -279,13 +279,6 @@ impl WebPainter for WebPainterWgpu { Some((output_frame, capture_buffer)) }; - { - let mut renderer = render_state.renderer.write(); - for id in &textures_delta.free { - renderer.free_texture(id); - } - } - // Submit the commands: both the main buffer and user-defined ones. render_state .queue @@ -307,6 +300,16 @@ impl WebPainter for WebPainterWgpu { frame.present(); } + // Free textures marked for destruction **after** queue submit since they might still be used in the current frame. + // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in. + // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. + { + let mut renderer = render_state.renderer.write(); + for id in &textures_delta.free { + renderer.free_texture(id); + } + } + Ok(()) } diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl index b60d9de9e..2921be74f 100644 --- a/crates/egui-wgpu/src/egui.wgsl +++ b/crates/egui-wgpu/src/egui.wgsl @@ -97,9 +97,8 @@ fn vs_main( @fragment fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4 { - // We always have an sRGB aware texture at the moment. - let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord); - let tex_gamma = gamma_from_linear_rgba(tex_linear); + // We expect "normal" textures that are NOT sRGB-aware. + let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord); var out_color_gamma = in.color * tex_gamma; // Dither the float color down to eight bits to reduce banding. // This step is optional for egui backends. @@ -115,9 +114,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4 { @fragment fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4 { - // We always have an sRGB aware texture at the moment. - let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord); - let tex_gamma = gamma_from_linear_rgba(tex_linear); + // We expect "normal" textures that are NOT sRGB-aware. + let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord); var out_color_gamma = in.color * tex_gamma; // Dither the float color down to eight bits to reduce banding. // This step is optional for egui backends. diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 73df09e43..012613aeb 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -564,15 +564,6 @@ impl Renderer { ); Cow::Borrowed(&image.pixels) } - epaint::ImageData::Font(image) => { - assert_eq!( - width as usize * height as usize, - image.pixels.len(), - "Mismatch between texture size and texel count" - ); - profiling::scope!("font -> sRGBA"); - Cow::Owned(image.srgba_pixels(None).collect::>()) - } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); @@ -638,9 +629,9 @@ impl Renderer { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported. + format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], + view_formats: &[wgpu::TextureFormat::Rgba8Unorm], }) }; let origin = wgpu::Origin3d::ZERO; @@ -699,7 +690,7 @@ impl Renderer { /// /// This enables the application to reference the texture inside an image ui element. /// This effectively enables off-screen rendering inside the egui UI. Texture must have - /// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. + /// the texture format [`wgpu::TextureFormat::Rgba8Unorm`]. pub fn register_native_texture( &mut self, device: &wgpu::Device, @@ -747,7 +738,7 @@ impl Renderer { /// This allows applications to specify individual minification/magnification filters as well as /// custom mipmap and tiling options. /// - /// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`]. + /// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`]. /// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored. #[expect(clippy::needless_pass_by_value)] // false positive pub fn register_native_texture_with_sampler_options( diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 4f4b5b750..ee5ff30d4 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -81,7 +81,7 @@ impl<'a> Atom<'a> { wrap_mode = Some(TextWrapMode::Truncate); } - let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode); + let (intrinsic, kind) = self.kind.into_sized(ui, available_size, wrap_mode); let size = self .size @@ -89,7 +89,7 @@ impl<'a> Atom<'a> { SizedAtom { size, - preferred_size: preferred, + intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()), grow: self.grow, kind, } diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 2672e646b..b85b504a2 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -81,11 +81,10 @@ impl<'a> AtomKind<'a> { ) -> (Vec2, SizedAtomKind<'a>) { match self { AtomKind::Text(text) => { - let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); - ( - galley.size(), // TODO(#5762): calculate the preferred size - SizedAtomKind::Text(galley), - ) + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + let galley = + text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button); + (galley.intrinsic_size, SizedAtomKind::Text(galley)) } AtomKind::Image(image) => { let size = image.load_and_calc_size(ui, available_size); diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index a25a4b7c6..53819fbb0 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> { let mut desired_width = 0.0; - // Preferred width / height is the ideal size of the widget, e.g. the size where the + // intrinsic width / height is the ideal size of the widget, e.g. the size where the // text is not wrapped. Used to set Response::intrinsic_size. - let mut preferred_width = 0.0; - let mut preferred_height = 0.0; + let mut intrinsic_width = 0.0; + let mut intrinsic_height = 0.0; let mut height: f32 = 0.0; @@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> { if atoms.len() > 1 { let gap_space = gap * (atoms.len() as f32 - 1.0); desired_width += gap_space; - preferred_width += gap_space; + intrinsic_width += gap_space; } for (idx, item) in atoms.into_iter().enumerate() { @@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> { let size = sized.size; desired_width += size.x; - preferred_width += sized.preferred_size.x; + intrinsic_width += sized.intrinsic_size.x; height = height.at_least(size.y); - preferred_height = preferred_height.at_least(sized.preferred_size.y); + intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y); sized_items.push(sized); } @@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> { let size = sized.size; desired_width += size.x; - preferred_width += sized.preferred_size.x; + intrinsic_width += sized.intrinsic_size.x; height = height.at_least(size.y); - preferred_height = preferred_height.at_least(sized.preferred_size.y); + intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y); sized_items.insert(index, sized); } @@ -259,7 +259,7 @@ impl<'a> AtomLayout<'a> { let mut response = ui.interact(rect, id, sense); response.intrinsic_size = - Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); + Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size)); AllocatedAtomLayout { sized_atoms: sized_items, diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs index 50fa443a9..f1ae0f81b 100644 --- a/crates/egui/src/atomics/sized_atom.rs +++ b/crates/egui/src/atomics/sized_atom.rs @@ -12,8 +12,8 @@ pub struct SizedAtom<'a> { /// size.x + gap. pub size: Vec2, - /// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`. - pub preferred_size: Vec2, + /// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`. + pub intrinsic_size: Vec2, pub kind: SizedAtomKind<'a>, } diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index d40df8358..5ae4a4b30 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -121,6 +121,7 @@ pub struct Area { new_pos: Option, fade_in: bool, layout: Layout, + sizing_pass: bool, } impl WidgetWithState for Area { @@ -147,6 +148,7 @@ impl Area { anchor: None, fade_in: true, layout: Layout::default(), + sizing_pass: false, } } @@ -357,6 +359,27 @@ impl Area { self.layout = layout; self } + + /// While true, a sizing pass will be done. This means the area will be invisible + /// and the contents will be laid out to estimate the proper containing size of the area. + /// If false, there will be no change to the default area behavior. This is useful if the + /// area contents area dynamic and you need to need to make sure the area adjusts its size + /// accordingly. + /// + /// This should only be set to true during the specific frames you want force a sizing pass. + /// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be + /// visible. + /// + /// # Arguments + /// - resize: If true, the area will be resized to fit its contents. False will keep the + /// default area resizing behavior. + /// + /// Default: `false`. + #[inline] + pub fn sizing_pass(mut self, resize: bool) -> Self { + self.sizing_pass = resize; + self + } } pub(crate) struct Prepared { @@ -410,6 +433,7 @@ impl Area { constrain_rect, fade_in, layout, + sizing_pass: force_sizing_pass, } = self; let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect()); @@ -425,6 +449,10 @@ impl Area { interactable, last_became_visible_at: None, }); + if force_sizing_pass { + sizing_pass = true; + state.size = None; + } state.pivot = pivot; state.interactable = interactable; if let Some(new_pos) = new_pos { diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 434747cda..13b095d35 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -160,6 +160,7 @@ impl From for UiKind { } } +#[must_use = "Call `.show()` to actually display the popup"] pub struct Popup<'a> { id: Id, ctx: Context, @@ -210,6 +211,57 @@ impl<'a> Popup<'a> { } } + /// Show a popup relative to some widget. + /// The popup will be always open. + /// + /// See [`Self::menu`] and [`Self::context_menu`] for common use cases. + pub fn from_response(response: &Response) -> Self { + let mut popup = Self::new( + response.id.with("popup"), + response.ctx.clone(), + response, + response.layer_id, + ); + popup.widget_clicked_elsewhere = response.clicked_elsewhere(); + popup + } + + /// Show a popup relative to some widget, + /// toggling the open state based on the widget's click state. + /// + /// See [`Self::menu`] and [`Self::context_menu`] for common use cases. + pub fn from_toggle_button_response(button_response: &Response) -> Self { + Self::from_response(button_response) + .open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle)) + } + + /// Show a popup when the widget was clicked. + /// Sets the layout to `Layout::top_down_justified(Align::Min)`. + pub fn menu(button_response: &Response) -> Self { + Self::from_toggle_button_response(button_response) + .kind(PopupKind::Menu) + .layout(Layout::top_down_justified(Align::Min)) + .style(menu_style) + .gap(0.0) + } + + /// Show a context menu when the widget was secondary clicked. + /// Sets the layout to `Layout::top_down_justified(Align::Min)`. + /// In contrast to [`Self::menu`], this will open at the pointer position. + pub fn context_menu(response: &Response) -> Self { + Self::menu(response) + .open_memory(if response.secondary_clicked() { + Some(SetOpenCommand::Bool(true)) + } else if response.clicked() { + // Explicitly close the menu if the widget was clicked + // Without this, the context menu would stay open if the user clicks the widget + Some(SetOpenCommand::Bool(false)) + } else { + None + }) + .at_pointer_fixed() + } + /// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`]. #[inline] pub fn kind(mut self, kind: PopupKind) -> Self { @@ -242,49 +294,6 @@ impl<'a> Popup<'a> { self } - /// Show a popup relative to some widget. - /// The popup will be always open. - /// - /// See [`Self::menu`] and [`Self::context_menu`] for common use cases. - pub fn from_response(response: &Response) -> Self { - let mut popup = Self::new( - response.id.with("popup"), - response.ctx.clone(), - response, - response.layer_id, - ); - popup.widget_clicked_elsewhere = response.clicked_elsewhere(); - popup - } - - /// Show a popup when the widget was clicked. - /// Sets the layout to `Layout::top_down_justified(Align::Min)`. - pub fn menu(response: &Response) -> Self { - Self::from_response(response) - .open_memory(response.clicked().then_some(SetOpenCommand::Toggle)) - .kind(PopupKind::Menu) - .layout(Layout::top_down_justified(Align::Min)) - .style(menu_style) - .gap(0.0) - } - - /// Show a context menu when the widget was secondary clicked. - /// Sets the layout to `Layout::top_down_justified(Align::Min)`. - /// In contrast to [`Self::menu`], this will open at the pointer position. - pub fn context_menu(response: &Response) -> Self { - Self::menu(response) - .open_memory(if response.secondary_clicked() { - Some(SetOpenCommand::Bool(true)) - } else if response.clicked() { - // Explicitly close the menu if the widget was clicked - // Without this, the context menu would stay open if the user clicks the widget - Some(SetOpenCommand::Bool(false)) - } else { - None - }) - .at_pointer_fixed() - } - /// Force the popup to be open or closed. #[inline] pub fn open(mut self, open: bool) -> Self { @@ -484,37 +493,18 @@ impl<'a> Popup<'a> { self.gap, expected_popup_size, ) + .unwrap_or_default() } /// Show the popup. /// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is /// no pointer. pub fn show(self, content: impl FnOnce(&mut Ui) -> R) -> Option> { - let best_align = self.get_best_align(); + let hover_pos = self.ctx.pointer_hover_pos(); - let Popup { - id, - ctx, - anchor, - open_kind, - close_behavior, - kind, - info, - layer_id, - rect_align: _, - alternative_aligns: _, - gap, - widget_clicked_elsewhere, - width, - sense, - layout, - frame, - style, - } = self; - - let hover_pos = ctx.pointer_hover_pos(); - if let OpenKind::Memory { set, .. } = open_kind { - ctx.memory_mut(|mem| match set { + let id = self.id; + if let OpenKind::Memory { set } = self.open_kind { + self.ctx.memory_mut(|mem| match set { Some(SetOpenCommand::Bool(open)) => { if open { match self.anchor { @@ -536,10 +526,32 @@ impl<'a> Popup<'a> { }); } - if !open_kind.is_open(id, &ctx) { + if !self.open_kind.is_open(self.id, &self.ctx) { return None; } + let best_align = self.get_best_align(); + + let Popup { + id, + ctx, + anchor, + open_kind, + close_behavior, + kind, + info, + layer_id, + rect_align: _, + alternative_aligns: _, + gap, + widget_clicked_elsewhere, + width, + sense, + layout, + frame, + style, + } = self; + if kind != PopupKind::Tooltip { ctx.pass_state_mut(|fs| { fs.layers diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs index e34ae70eb..8a67c6c5e 100644 --- a/crates/egui/src/containers/sides.rs +++ b/crates/egui/src/containers/sides.rs @@ -1,4 +1,4 @@ -use emath::Align; +use emath::{Align, NumExt as _}; use crate::{Layout, Ui, UiBuilder}; @@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder}; /// /// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right. /// -/// The left widgets are first added to the ui, left-to-right. -/// Then the right widgets are added, right-to-left. +/// The left widgets are added left-to-right. +/// The right widgets are added right-to-left. +/// +/// Which side is first depends on the configuration: +/// - [`Sides::extend`] - left widgets are added first +/// - [`Sides::shrink_left`] - right widgets are added first +/// - [`Sides::shrink_right`] - left widgets are added first /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder}; pub struct Sides { height: Option, spacing: Option, + kind: SidesKind, + wrap_mode: Option, +} + +#[derive(Clone, Copy, Debug, Default)] +enum SidesKind { + #[default] + Extend, + ShrinkLeft, + ShrinkRight, } impl Sides { @@ -68,58 +83,175 @@ impl Sides { self } + /// Try to shrink widgets on the left side. + /// + /// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the + /// remaining space. + #[inline] + pub fn shrink_left(mut self) -> Self { + self.kind = SidesKind::ShrinkLeft; + self + } + + /// Try to shrink widgets on the right side. + /// + /// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the + /// remaining space. + #[inline] + pub fn shrink_right(mut self) -> Self { + self.kind = SidesKind::ShrinkRight; + self + } + + /// Extend the left and right sides to fill the available space. + /// + /// This is the default behavior. + /// The left widgets will be added first, followed by the right widgets. + #[inline] + pub fn extend(mut self) -> Self { + self.kind = SidesKind::Extend; + self + } + + /// The text wrap mode for the shrinking side. + /// + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Truncate the text on the shrinking side. + /// + /// This is a shortcut for [`Self::wrap_mode`]. + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn truncate(mut self) -> Self { + self.wrap_mode = Some(crate::TextWrapMode::Truncate); + self + } + + /// Wrap the text on the shrinking side. + /// + /// This is a shortcut for [`Self::wrap_mode`]. + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn wrap(mut self) -> Self { + self.wrap_mode = Some(crate::TextWrapMode::Wrap); + self + } + pub fn show( self, ui: &mut Ui, add_left: impl FnOnce(&mut Ui) -> RetL, add_right: impl FnOnce(&mut Ui) -> RetR, ) -> (RetL, RetR) { - let Self { height, spacing } = self; + let Self { + height, + spacing, + mut kind, + mut wrap_mode, + } = self; let height = height.unwrap_or_else(|| ui.spacing().interact_size.y); let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x); let mut top_rect = ui.available_rect_before_wrap(); top_rect.max.y = top_rect.min.y + height; - let result_left; - let result_right; - - let left_rect = { - let left_max_rect = top_rect; - let mut left_ui = ui.new_child( - UiBuilder::new() - .max_rect(left_max_rect) - .layout(Layout::left_to_right(Align::Center)), - ); - result_left = add_left(&mut left_ui); - left_ui.min_rect() - }; - - let right_rect = { - let right_max_rect = top_rect.with_min_x(left_rect.max.x); - let mut right_ui = ui.new_child( - UiBuilder::new() - .max_rect(right_max_rect) - .layout(Layout::right_to_left(Align::Center)), - ); - result_right = add_right(&mut right_ui); - right_ui.min_rect() - }; - - let mut final_rect = left_rect.union(right_rect); - let min_width = left_rect.width() + spacing + right_rect.width(); - if ui.is_sizing_pass() { - // Make as small as possible: - final_rect.max.x = left_rect.min.x + min_width; - } else { - // If the rects overlap, make sure we expand the allocated rect so that the parent - // ui knows we overflowed, and resizes: - final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); + kind = SidesKind::Extend; + wrap_mode = None; } - ui.advance_cursor_after_rect(final_rect); + match kind { + SidesKind::ShrinkLeft => { + let (right_rect, result_right) = Self::create_ui( + ui, + top_rect, + Layout::right_to_left(Align::Center), + add_right, + None, + ); + let available_width = top_rect.width() - right_rect.width() - spacing; + let left_rect_constraint = + top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0)); + let (left_rect, result_left) = Self::create_ui( + ui, + left_rect_constraint, + Layout::left_to_right(Align::Center), + add_left, + wrap_mode, + ); - (result_left, result_right) + ui.advance_cursor_after_rect(left_rect.union(right_rect)); + (result_left, result_right) + } + SidesKind::ShrinkRight => { + let (left_rect, result_left) = Self::create_ui( + ui, + top_rect, + Layout::left_to_right(Align::Center), + add_left, + None, + ); + let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing); + let (right_rect, result_right) = Self::create_ui( + ui, + right_rect_constraint, + Layout::right_to_left(Align::Center), + add_right, + wrap_mode, + ); + + ui.advance_cursor_after_rect(left_rect.union(right_rect)); + (result_left, result_right) + } + SidesKind::Extend => { + let (left_rect, result_left) = Self::create_ui( + ui, + top_rect, + Layout::left_to_right(Align::Center), + add_left, + None, + ); + let right_max_rect = top_rect.with_min_x(left_rect.max.x); + let (right_rect, result_right) = Self::create_ui( + ui, + right_max_rect, + Layout::right_to_left(Align::Center), + add_right, + None, + ); + + let mut final_rect = left_rect.union(right_rect); + let min_width = left_rect.width() + spacing + right_rect.width(); + + if ui.is_sizing_pass() { + final_rect.max.x = left_rect.min.x + min_width; + } else { + final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); + } + + ui.advance_cursor_after_rect(final_rect); + (result_left, result_right) + } + } + } + + fn create_ui( + ui: &mut Ui, + max_rect: emath::Rect, + layout: Layout, + add_content: impl FnOnce(&mut Ui) -> Ret, + wrap_mode: Option, + ) -> (emath::Rect, Ret) { + let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout)); + if let Some(wrap_mode) = wrap_mode { + child_ui.style_mut().wrap_mode = Some(wrap_mode); + } + let result = add_content(&mut child_ui); + (child_ui.min_rect(), result) } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 54ae49036..f3bc73cec 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -78,7 +78,7 @@ impl Default for WrappedTextureManager { // Will be filled in later let font_id = tex_mngr.alloc( "egui_font_texture".into(), - epaint::FontImage::new([0, 0]).into(), + epaint::ColorImage::filled([0, 0], Color32::TRANSPARENT).into(), Default::default(), ); assert_eq!( @@ -610,6 +610,8 @@ impl ContextImpl { log::trace!("Adding new fonts"); } + let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage; + let mut is_new = false; let fonts = self @@ -624,13 +626,14 @@ impl ContextImpl { Fonts::new( pixels_per_point, max_texture_side, + text_alpha_from_coverage, self.font_definitions.clone(), ) }); { profiling::scope!("Fonts::begin_pass"); - fonts.begin_pass(pixels_per_point, max_texture_side); + fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage); } if is_new && self.memory.options.preload_font_glyphs { @@ -1149,7 +1152,7 @@ impl Context { ID clashes happens when things like Windows or CollapsingHeaders share names,\n\ or when things like Plot and Grid:s aren't given unique id_salt:s.\n\n\ Sometimes the solution is to use ui.push_id.", - if below { "above" } else { "below" }) + if below { "above" } else { "below" }), ); } } @@ -1216,6 +1219,51 @@ impl Context { self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder)); } + #[cfg(feature = "accesskit")] + self.write(|ctx| { + use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation}; + let viewport = ctx.viewport_for(ctx.viewport_id()); + + viewport + .input + .consume_accesskit_action_requests(res.id, |request| { + // TODO(lucasmerlin): Correctly handle the scroll unit: + // https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621 + const DISTANCE: f32 = 100.0; + + match &request.action { + accesskit::Action::ScrollIntoView => { + viewport.this_pass.scroll_target = [ + Some(ScrollTarget::new( + res.rect.x_range(), + Some(Align::Center), + ScrollAnimation::none(), + )), + Some(ScrollTarget::new( + res.rect.y_range(), + Some(Align::Center), + ScrollAnimation::none(), + )), + ]; + } + accesskit::Action::ScrollDown => { + viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP; + } + accesskit::Action::ScrollUp => { + viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN; + } + accesskit::Action::ScrollLeft => { + viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT; + } + accesskit::Action::ScrollRight => { + viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT; + } + _ => return false, + }; + true + }); + }); + res } diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index f253a1dfe..1361c1c49 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -91,6 +91,8 @@ pub fn hit_test( } } + close.retain(|rect| !rect.interact_rect.any_nan()); // Protect against bad input and transforms + // When using layer transforms it is common to stack layers close to each other. // For instance, you may have a resize-separator on a panel, with two // transform-layers on either side. diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index d87bd5669..a3ebf532e 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -824,6 +824,23 @@ impl InputState { }) } + #[cfg(feature = "accesskit")] + pub fn consume_accesskit_action_requests( + &mut self, + id: crate::Id, + mut consume: impl FnMut(&accesskit::ActionRequest) -> bool, + ) { + let accesskit_id = id.accesskit_id(); + self.events.retain(|event| { + if let Event::AccessKitActionRequest(request) = event { + if request.target == accesskit_id { + return !consume(request); + } + } + true + }); + } + #[cfg(feature = "accesskit")] pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool { self.accesskit_action_requests(id, action).next().is_some() @@ -1448,7 +1465,10 @@ impl PointerState { } if let Some(pos) = self.hover_pos() { - return rect.intersects_ray(pos, self.direction()); + let dir = self.direction(); + if dir != Vec2::ZERO { + return rect.intersects_ray(pos, self.direction()); + } } false } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index b4daa9326..9c4686bde 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -161,12 +161,10 @@ //! //! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. //! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). -//! * egui prefers linear color spaces for all blending so: -//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). -//! * Otherwise: remember to decode gamma in the fragment shader. -//! * Decode the gamma of the incoming vertex colors in your vertex shader. -//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). -//! * Otherwise: gamma-encode the colors before you write them again. +//! * egui prefers gamma color spaces for all blending so: +//! * Do NOT use an sRGBA-aware texture (NOT `GL_SRGB8_ALPHA8`). +//! * Multiply texture and vertex colors in gamma space +//! * Turn OFF sRGBA/gamma framebuffer (NO `GL_FRAMEBUFFER_SRGB`). //! //! //! # Understanding immediate mode @@ -467,7 +465,7 @@ pub use emath::{ remap_clamp, vec2, }; pub use epaint::{ - ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback, + ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback, PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 53d9172b0..8f98de305 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -408,11 +408,11 @@ impl Options { .show(ui, |ui| { theme_preference.radio_buttons(ui); - std::sync::Arc::make_mut(match theme { + let style = std::sync::Arc::make_mut(match theme { Theme::Dark => dark_style, Theme::Light => light_style, - }) - .ui(ui); + }); + style.ui(ui); }); CollapsingHeader::new("โœ’ Painting") diff --git a/crates/egui/src/memory/theme.rs b/crates/egui/src/memory/theme.rs index dd4d3d6f9..555edaedd 100644 --- a/crates/egui/src/memory/theme.rs +++ b/crates/egui/src/memory/theme.rs @@ -89,9 +89,32 @@ impl ThemePreference { /// Show radio-buttons to switch between light mode, dark mode and following the system theme. pub fn radio_buttons(&mut self, ui: &mut crate::Ui) { ui.horizontal(|ui| { - ui.selectable_value(self, Self::Light, "โ˜€ Light"); - ui.selectable_value(self, Self::Dark, "๐ŸŒ™ Dark"); - ui.selectable_value(self, Self::System, "๐Ÿ’ป System"); + let system_theme = ui.ctx().input(|i| i.raw.system_theme); + + ui.selectable_value(self, Self::System, "๐Ÿ’ป System") + .on_hover_ui(|ui| { + ui.label("Follow the system theme preference."); + + ui.add_space(4.0); + + if let Some(system_theme) = system_theme { + ui.label(format!( + "The current system theme is: {}", + match system_theme { + Theme::Dark => "dark", + Theme::Light => "light", + } + )); + } else { + ui.label("The system theme is unknown."); + } + }); + + ui.selectable_value(self, Self::Dark, "๐ŸŒ™ Dark") + .on_hover_text("Use the dark mode theme"); + + ui.selectable_value(self, Self::Light, "โ˜€ Light") + .on_hover_text("Use the light mode theme"); }); } } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 373142f61..fe273970e 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -83,6 +83,7 @@ impl Painter { } /// If set, colors will be modified to look like this + #[deprecated = "Use `multiply_opacity` instead"] pub fn set_fade_to_color(&mut self, fade_to_color: Option) { self.fade_to_color = fade_to_color; } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 354269483..364b3fffc 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -3,7 +3,7 @@ #![allow(clippy::if_same_then_else)] use emath::Align; -use epaint::{CornerRadius, Shadow, Stroke, text::FontTweak}; +use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak}; use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; use crate::{ @@ -11,6 +11,7 @@ use crate::{ WidgetText, ecolor::Color32, emath::{Rangef, Rect, Vec2, pos2, vec2}, + reset_button_with, }; /// How to format numbers in e.g. a [`crate::DragValue`]. @@ -920,6 +921,9 @@ pub struct Visuals { /// this is more to provide a convenient summary of the rest of the settings. pub dark_mode: bool, + /// ADVANCED: Controls how we render text. + pub text_alpha_from_coverage: AlphaFromCoverage, + /// Override default text color for all text. /// /// This is great for setting the color of text for any widget. @@ -935,6 +939,17 @@ pub struct Visuals { /// it is disabled, non-interactive, hovered etc. pub override_text_color: Option, + /// How strong "weak" text is. + /// + /// Ignored if [`Self::weak_text_color`] is set. + pub weak_text_alpha: f32, + + /// Color of "weak" text. + /// + /// If `None`, the color is [`Self::text_color`] + /// multiplied by [`Self::weak_text_alpha`]. + pub weak_text_color: Option, + /// Visual styles of widgets pub widgets: Widgets, @@ -952,6 +967,11 @@ pub struct Visuals { /// that needs to look different from other interactive stuff. pub extreme_bg_color: Color32, + /// The background color of [`crate::TextEdit`]. + /// + /// Defaults to [`Self::extreme_bg_color`]. + pub text_edit_bg_color: Option, + /// Background color behind code-styled monospaced labels. pub code_bg_color: Color32, @@ -1019,6 +1039,9 @@ pub struct Visuals { /// How to display numeric color values. pub numeric_color_space: NumericColorSpace, + + /// How much to modify the alpha of a disabled widget. + pub disabled_alpha: f32, } impl Visuals { @@ -1034,7 +1057,8 @@ impl Visuals { } pub fn weak_text_color(&self) -> Color32 { - self.gray_out(self.text_color()) + self.weak_text_color + .unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha)) } #[inline(always)] @@ -1042,6 +1066,11 @@ impl Visuals { self.widgets.active.text_color() } + /// The background color of [`crate::TextEdit`]. + pub fn text_edit_bg_color(&self) -> Color32 { + self.text_edit_bg_color.unwrap_or(self.extreme_bg_color) + } + /// Window background color. #[inline(always)] pub fn window_fill(&self) -> Color32 { @@ -1054,17 +1083,32 @@ impl Visuals { } /// When fading out things, we fade the colors towards this. - // TODO(emilk): replace with an alpha #[inline(always)] + #[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."] pub fn fade_out_to_color(&self) -> Color32 { self.widgets.noninteractive.weak_bg_fill } - /// Returned a "grayed out" version of the given color. + /// Disabled widgets have their alpha modified by this. + #[inline(always)] + pub fn disabled_alpha(&self) -> f32 { + self.disabled_alpha + } + + /// Returns a "disabled" version of the given color. + /// + /// This function modifies the opcacity of the given color. + /// If this is undesirable use [`gray_out`](Self::gray_out). + #[inline(always)] + pub fn disable(&self, color: Color32) -> Color32 { + color.gamma_multiply(self.disabled_alpha()) + } + + /// Returns a "grayed out" version of the given color. #[doc(alias = "grey_out")] #[inline(always)] pub fn gray_out(&self, color: Color32) -> Color32 { - crate::ecolor::tint_color_towards(color, self.fade_out_to_color()) + crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill) } } @@ -1333,12 +1377,16 @@ impl Visuals { pub fn dark() -> Self { Self { dark_mode: true, + text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT, override_text_color: None, + weak_text_alpha: 0.6, + weak_text_color: None, widgets: Widgets::default(), selection: Selection::default(), hyperlink_color: Color32::from_rgb(90, 170, 255), faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background + text_edit_bg_color: None, // use `extreme_bg_color` by default code_bg_color: Color32::from_gray(64), warn_fg_color: Color32::from_rgb(255, 143, 0), // orange error_fg_color: Color32::from_rgb(255, 0, 0), // red @@ -1384,6 +1432,7 @@ impl Visuals { image_loading_spinners: true, numeric_color_space: NumericColorSpace::GammaByte, + disabled_alpha: 0.5, } } @@ -1391,6 +1440,7 @@ impl Visuals { pub fn light() -> Self { Self { dark_mode: false, + text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT, widgets: Widgets::light(), selection: Selection::light(), hyperlink_color: Color32::from_rgb(0, 155, 255), @@ -1680,11 +1730,11 @@ impl Style { ui.end_row(); }); - ui.collapsing("๐Ÿ”  Text Styles", |ui| text_styles_ui(ui, text_styles)); + ui.collapsing("๐Ÿ”  Text styles", |ui| text_styles_ui(ui, text_styles)); ui.collapsing("๐Ÿ“ Spacing", |ui| spacing.ui(ui)); ui.collapsing("โ˜ Interaction", |ui| interaction.ui(ui)); ui.collapsing("๐ŸŽจ Visuals", |ui| visuals.ui(ui)); - ui.collapsing("๐Ÿ”„ Scroll Animation", |ui| scroll_animation.ui(ui)); + ui.collapsing("๐Ÿ”„ Scroll animation", |ui| scroll_animation.ui(ui)); #[cfg(debug_assertions)] ui.collapsing("๐Ÿ› Debug", |ui| debug.ui(ui)); @@ -2022,13 +2072,17 @@ impl WidgetVisuals { impl Visuals { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { - dark_mode: _, + dark_mode, + text_alpha_from_coverage, override_text_color: _, + weak_text_alpha, + weak_text_color, widgets, selection, hyperlink_color, faint_bg_color, extreme_bg_color, + text_edit_bg_color, code_bg_color, warn_fg_color, error_fg_color, @@ -2063,44 +2117,115 @@ impl Visuals { image_loading_spinners, numeric_color_space, + disabled_alpha, } = self; - ui.collapsing("Background Colors", |ui| { - ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons"); - ui_color(ui, window_fill, "Windows"); - ui_color(ui, panel_fill, "Panels"); - ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( - "Used for faint accentuation of interactive things, like striped grids.", - ); - ui_color(ui, extreme_bg_color, "Extreme") - .on_hover_text("Background of plots and paintings"); + fn ui_optional_color( + ui: &mut Ui, + color: &mut Option, + default_value: Color32, + label: impl Into, + ) -> Response { + let label_response = ui.label(label); + + ui.horizontal(|ui| { + let mut set = color.is_some(); + ui.checkbox(&mut set, ""); + if set { + let color = color.get_or_insert(default_value); + ui.color_edit_button_srgba(color); + } else { + *color = None; + }; + }); + + ui.end_row(); + + label_response + } + + ui.collapsing("Background colors", |ui| { + Grid::new("background_colors") + .num_columns(2) + .show(ui, |ui| { + fn ui_color( + ui: &mut Ui, + color: &mut Color32, + label: impl Into, + ) -> Response { + let label_response = ui.label(label); + ui.color_edit_button_srgba(color); + ui.end_row(); + label_response + } + + ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons"); + ui_color(ui, window_fill, "Windows"); + ui_color(ui, panel_fill, "Panels"); + ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( + "Used for faint accentuation of interactive things, like striped grids.", + ); + ui_color(ui, extreme_bg_color, "Extreme") + .on_hover_text("Background of plots and paintings"); + + ui_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit") + .on_hover_text("Background of TextEdit"); + }); }); ui.collapsing("Text color", |ui| { - ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label"); - ui_text_color( - ui, - &mut widgets.inactive.fg_stroke.color, - "Unhovered button", - ); - ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button"); - ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button"); + fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into) { + ui.label(label.into().color(*color)); + ui.color_edit_button_srgba(color); + ui.end_row(); + } - ui_text_color(ui, warn_fg_color, RichText::new("Warnings")); - ui_text_color(ui, error_fg_color, RichText::new("Errors")); + Grid::new("text_color").num_columns(2).show(ui, |ui| { + ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label"); - ui_text_color(ui, hyperlink_color, "hyperlink_color"); + ui_text_color( + ui, + &mut widgets.inactive.fg_stroke.color, + "Unhovered button", + ); + ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button"); + ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button"); - ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui( - |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("For monospaced inlined text "); - ui.code("like this"); - ui.label("."); + ui_text_color(ui, warn_fg_color, RichText::new("Warnings")); + ui_text_color(ui, error_fg_color, RichText::new("Errors")); + + ui_text_color(ui, hyperlink_color, "hyperlink_color"); + + ui.label(RichText::new("Code background").code()) + .on_hover_ui(|ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("For monospaced inlined text "); + ui.code("like this"); + ui.label("."); + }); }); - }, - ); + ui.color_edit_button_srgba(code_bg_color); + ui.end_row(); + + ui.label("Weak text alpha"); + ui.add_enabled( + weak_text_color.is_none(), + DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0), + ); + ui.end_row(); + + ui_optional_color( + ui, + weak_text_color, + widgets.noninteractive.text_color(), + "Weak text color", + ); + }); + + ui.add_space(4.0); + + text_alpha_from_coverage_ui(ui, text_alpha_from_coverage); }); ui.collapsing("Text cursor", |ui| { @@ -2191,12 +2316,60 @@ impl Visuals { ui.label("Color picker type"); numeric_color_space.toggle_button_ui(ui); }); + + ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha")); }); - ui.vertical_centered(|ui| reset_button(ui, self, "Reset visuals")); + let dark_mode = *dark_mode; + ui.vertical_centered(|ui| { + reset_button_with( + ui, + self, + "Reset visuals", + if dark_mode { + Self::dark() + } else { + Self::light() + }, + ); + }); } } +fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) { + let mut dark_mode_special = + *text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq; + + ui.horizontal(|ui| { + ui.label("Text rendering:"); + + ui.checkbox(&mut dark_mode_special, "Dark-mode special"); + + if dark_mode_special { + *text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq; + } else { + let mut gamma = match text_alpha_from_coverage { + AlphaFromCoverage::Linear => 1.0, + AlphaFromCoverage::Gamma(gamma) => *gamma, + AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same + }; + + ui.add( + DragValue::new(&mut gamma) + .speed(0.01) + .range(0.1..=4.0) + .prefix("Gamma: "), + ); + + if gamma == 1.0 { + *text_alpha_from_coverage = AlphaFromCoverage::Linear; + } else { + *text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma); + } + } + }); +} + impl TextCursorStyle { fn ui(&mut self, ui: &mut Ui) { let Self { @@ -2310,22 +2483,6 @@ fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive) -> im } } -fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into) -> Response { - ui.horizontal(|ui| { - ui.color_edit_button_srgba(color); - ui.label(label); - }) - .response -} - -fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into) -> Response { - ui.horizontal(|ui| { - ui.color_edit_button_srgba(color); - ui.label(label.into().color(*color)); - }) - .response -} - impl HandleShape { pub fn ui(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 0abd9c054..c24ca211b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -27,7 +27,7 @@ use crate::{ vec2, widgets, widgets::{ Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton, - SelectableLabel, Separator, Spinner, TextEdit, Widget, color_picker, + Separator, Spinner, TextEdit, Widget, color_picker, }, }; // ---------------------------------------------------------------------------- @@ -522,7 +522,7 @@ impl Ui { self.enabled = false; if self.is_visible() { self.painter - .set_fade_to_color(Some(self.visuals().fade_out_to_color())); + .multiply_opacity(self.visuals().disabled_alpha()); } } @@ -2077,13 +2077,13 @@ impl Ui { Checkbox::new(checked, atoms).ui(self) } - /// Acts like a checkbox, but looks like a [`SelectableLabel`]. + /// Acts like a checkbox, but looks like a [`Button::selectable`]. /// /// Click to toggle to bool. /// /// See also [`Self::checkbox`]. - pub fn toggle_value(&mut self, selected: &mut bool, text: impl Into) -> Response { - let mut response = self.selectable_label(*selected, text); + pub fn toggle_value<'a>(&mut self, selected: &mut bool, atoms: impl IntoAtoms<'a>) -> Response { + let mut response = self.selectable_label(*selected, atoms); if response.clicked() { *selected = !*selected; response.mark_changed(); @@ -2134,10 +2134,10 @@ impl Ui { /// Show a label which can be selected or not. /// - /// See also [`SelectableLabel`] and [`Self::toggle_value`]. + /// See also [`Button::selectable`] and [`Self::toggle_value`]. #[must_use = "You should check if the user clicked this with `if ui.selectable_label(โ€ฆ).clicked() { โ€ฆ } "] - pub fn selectable_label(&mut self, checked: bool, text: impl Into) -> Response { - SelectableLabel::new(checked, text).ui(self) + pub fn selectable_label<'a>(&mut self, checked: bool, text: impl IntoAtoms<'a>) -> Response { + Button::selectable(checked, text).ui(self) } /// Show selectable text. It is selected if `*current_value == selected_value`. @@ -2145,12 +2145,12 @@ impl Ui { /// /// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`. /// - /// See also [`SelectableLabel`] and [`Self::toggle_value`]. - pub fn selectable_value( + /// See also [`Button::selectable`] and [`Self::toggle_value`]. + pub fn selectable_value<'a, Value: PartialEq>( &mut self, current_value: &mut Value, selected_value: Value, - text: impl Into, + text: impl IntoAtoms<'a>, ) -> Response { let mut response = self.selectable_label(*current_value == selected_value, text); if response.clicked() && *current_value != selected_value { @@ -2963,8 +2963,8 @@ impl Ui { if is_anything_being_dragged && !can_accept_what_is_being_dragged { // When dragging something else, show that it can't be dropped here: - fill = self.visuals().gray_out(fill); - stroke.color = self.visuals().gray_out(stroke.color); + fill = self.visuals().disable(fill); + stroke.color = self.visuals().disable(stroke.color); } frame.frame.fill = fill; diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 9f5bb1e31..bf09fc845 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -438,7 +438,7 @@ impl ViewportBuilder { } /// macOS: Set to `true` to allow the window to be moved by dragging the background. - /// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders. + /// Enabling this feature can result in unexpected behavior with draggable UI widgets such as sliders. #[inline] pub fn with_movable_by_background(mut self, value: bool) -> Self { self.movable_by_window_background = Some(value); diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index aa75eabd8..d836c0701 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -29,6 +29,7 @@ pub struct Button<'a> { stroke: Option, small: bool, frame: Option, + frame_when_inactive: bool, min_size: Vec2, corner_radius: Option, selected: bool, @@ -44,6 +45,7 @@ impl<'a> Button<'a> { stroke: None, small: false, frame: None, + frame_when_inactive: true, min_size: Vec2::ZERO, corner_radius: None, selected: false, @@ -52,6 +54,27 @@ impl<'a> Button<'a> { } } + /// Show a selectable button. + /// + /// Equivalent to: + /// ```rust + /// # use egui::{Button, IntoAtoms, __run_test_ui}; + /// # __run_test_ui(|ui| { + /// let selected = true; + /// ui.add(Button::new("toggle me").selected(selected).frame_when_inactive(!selected).frame(true)); + /// # }); + /// ``` + /// + /// See also: + /// - [`Ui::selectable_value`] + /// - [`Ui::selectable_label`] + pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self { + Self::new(atoms) + .selected(selected) + .frame_when_inactive(selected) + .frame(true) + } + /// Creates a button with an image. The size of the image as displayed is defined by the provided size. /// /// Note: In contrast to [`Button::new`], this limits the image size to the default font height @@ -138,6 +161,18 @@ impl<'a> Button<'a> { self } + /// If `false`, the button will not have a frame when inactive. + /// + /// Default: `true`. + /// + /// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting + /// has no effect. + #[inline] + pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self { + self.frame_when_inactive = frame_when_inactive; + self + } + /// By default, buttons senses clicks. /// Change this to a drag-button with `Sense::drag()`. #[inline] @@ -220,6 +255,7 @@ impl<'a> Button<'a> { stroke, small, frame, + frame_when_inactive, mut min_size, corner_radius, selected, @@ -243,9 +279,9 @@ impl<'a> Button<'a> { let text = layout.text().map(String::from); - let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame); + let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame); - let mut button_padding = if has_frame { + let mut button_padding = if has_frame_margin { ui.spacing().button_padding } else { Vec2::ZERO @@ -262,13 +298,22 @@ impl<'a> Button<'a> { let response = if ui.is_rect_visible(prepared.response.rect) { let visuals = ui.style().interact_selectable(&prepared.response, selected); + let visible_frame = if frame_when_inactive { + has_frame_margin + } else { + has_frame_margin + && (prepared.response.hovered() + || prepared.response.is_pointer_button_down_on() + || prepared.response.has_focus()) + }; + if image_tint_follows_text_color { prepared.map_images(|image| image.tint(visuals.text_color())); } prepared.fallback_text_color = visuals.text_color(); - if has_frame { + if visible_frame { let stroke = stroke.unwrap_or(visuals.bg_stroke); let fill = fill.unwrap_or(visuals.weak_bg_fill); prepared.frame = prepared diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 0ec4e9c90..08b377843 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -278,7 +278,8 @@ impl<'a> Image<'a> { } /// Set alt text for the image. This will be shown when the image fails to load. - /// It will also be read to screen readers. + /// + /// It will also be used for accessibility (e.g. read by screen readers). #[inline] pub fn alt_text(mut self, label: impl Into) -> Self { self.alt_text = Some(label.into()); @@ -672,7 +673,7 @@ pub fn paint_texture_load_result( rect: Rect, show_loading_spinner: Option, options: &ImageOptions, - alt: Option<&str>, + alt_text: Option<&str>, ) { match tlr { Ok(TexturePoll::Ready { texture }) => { @@ -697,9 +698,9 @@ pub fn paint_texture_load_result( 0.0, TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color), ); - if let Some(alt) = alt { + if let Some(alt_text) = alt_text { job.append( - alt, + alt_text, ui.spacing().item_spacing.x, TextFormat::simple(font_id, ui.visuals().text_color()), ); diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index d303b181b..9cf003c94 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -22,6 +22,8 @@ mod slider; mod spinner; pub mod text_edit; +#[expect(deprecated)] +pub use self::selected_label::SelectableLabel; pub use self::{ button::Button, checkbox::Checkbox, @@ -35,7 +37,6 @@ pub use self::{ label::Label, progress_bar::ProgressBar, radio_button::RadioButton, - selected_label::SelectableLabel, separator::Separator, slider::{Slider, SliderClamping, SliderOrientation}, spinner::Spinner, diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 4b2ee9ae2..536ef43da 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -1,88 +1,13 @@ -use crate::{ - NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType, -}; +#![expect(deprecated, clippy::new_ret_no_self)] -/// One out of several alternatives, either selected or not. -/// Will mark selected items with a different background color. -/// An alternative to [`crate::RadioButton`] and [`crate::Checkbox`]. -/// -/// Usually you'd use [`Ui::selectable_value`] or [`Ui::selectable_label`] instead. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// #[derive(PartialEq)] -/// enum Enum { First, Second, Third } -/// let mut my_enum = Enum::First; -/// -/// ui.selectable_value(&mut my_enum, Enum::First, "First"); -/// -/// // is equivalent to: -/// -/// if ui.add(egui::SelectableLabel::new(my_enum == Enum::First, "First")).clicked() { -/// my_enum = Enum::First -/// } -/// # }); -/// ``` -#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] -pub struct SelectableLabel { - selected: bool, - text: WidgetText, -} +use crate::WidgetText; + +#[deprecated = "Use `Button::selectable()` instead"] +pub struct SelectableLabel {} impl SelectableLabel { - pub fn new(selected: bool, text: impl Into) -> Self { - Self { - selected, - text: text.into(), - } - } -} - -impl Widget for SelectableLabel { - fn ui(self, ui: &mut Ui) -> Response { - let Self { selected, text } = self; - - let button_padding = ui.spacing().button_padding; - let total_extra = button_padding + button_padding; - - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); - - let mut desired_size = total_extra + galley.size(); - desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - let (rect, response) = ui.allocate_at_least(desired_size, Sense::click()); - response.widget_info(|| { - WidgetInfo::selected( - WidgetType::SelectableLabel, - ui.is_enabled(), - selected, - galley.text(), - ) - }); - - if ui.is_rect_visible(response.rect) { - let text_pos = ui - .layout() - .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) - .min; - - let visuals = ui.style().interact_selectable(&response, selected); - - if selected || response.hovered() || response.highlighted() || response.has_focus() { - let rect = rect.expand(visuals.expansion); - - ui.painter().rect( - rect, - visuals.corner_radius, - visuals.weak_bg_fill, - visuals.bg_stroke, - epaint::StrokeKind::Inside, - ); - } - - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - - response + #[deprecated = "Use `Button::selectable()` instead"] + pub fn new(selected: bool, text: impl Into) -> super::Button<'static> { + crate::Button::selectable(selected, text) } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index d5a006564..bcd65c7bf 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -63,7 +63,7 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { text: &'t mut dyn TextBuffer, @@ -207,7 +207,7 @@ impl<'t> TextEdit<'t> { self } - /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::extreme_bg_color`]. + /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::text_edit_bg_color`]. // TODO(bircni): remove this once #3284 is implemented #[inline] pub fn background_color(mut self, color: Color32) -> Self { @@ -428,7 +428,7 @@ impl TextEdit<'_> { let where_to_put_background = ui.painter().add(Shape::Noop); let background_color = self .background_color - .unwrap_or(ui.visuals().extreme_bg_color); + .unwrap_or_else(|| ui.visuals().text_edit_bg_color()); let output = self.show_content(ui); if frame { diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 08f5fc98f..f06e16cba 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cf6d0b20f127f22d49daefed27fc2d0ca43d645fe1486cf7f6fcbb676bdec82 -size 179065 +oid sha256:fc3dbdcd483d4da7a9c1a00f0245a7882997fbcd2d26f8d6a6d2d855f3382063 +size 179724 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index a13af2e71..e6f108e98 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e37b3ce49c9ccc1a64beb58b176e23ab6c1fa2d897f676b0de85e510e6bfa85 -size 100845 +oid sha256:c8ad2c2d494e2287b878049091688069e4d86b69ae72b89cb7ecbe47d8c35e33 +size 100766 diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 961990b90..63b3b2a89 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -69,6 +69,6 @@ fn test_demo_app() { // Can't use Harness::run because fractal clock keeps requesting repaints harness.run_steps(4); - results.add(harness.try_snapshot(&anchor.to_string())); + results.add(harness.try_snapshot(anchor.to_string())); } } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index e0c86f0db..02f098e67 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -168,6 +168,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let fonts = egui::epaint::text::Fonts::new( pixels_per_point, max_texture_side, + egui::epaint::AlphaFromCoverage::default(), egui::FontDefinitions::default(), ); { @@ -210,7 +211,11 @@ pub fn criterion_benchmark(c: &mut Criterion) { let mut rng = rand::rng(); b.iter(|| { - fonts.begin_pass(pixels_per_point, max_texture_side); + fonts.begin_pass( + pixels_per_point, + max_texture_side, + egui::epaint::AlphaFromCoverage::default(), + ); // Delete a random character, simulating a user making an edit in a long file: let mut new_string = string.clone(); diff --git a/crates/egui_demo_lib/data/ring.png b/crates/egui_demo_lib/data/ring.png new file mode 100644 index 000000000..f82db9196 Binary files /dev/null and b/crates/egui_demo_lib/data/ring.png differ diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 0e3a0d2c2..7f24d7284 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -373,7 +373,7 @@ mod tests { use crate::{Demo as _, demo::demo_app_windows::DemoGroups}; use egui_kittest::kittest::{NodeT as _, Queryable as _}; - use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; + use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults}; #[test] fn demos_should_match_snapshot() { @@ -410,12 +410,13 @@ mod tests { harness.run_ok(); let mut options = SnapshotOptions::default(); - // The Bรฉzier Curve demo needs a threshold of 2.1 to pass on linux + if name == "Bรฉzier Curve" { - options.threshold = 2.1; + // The Bรฉzier Curve demo needs a threshold of 2.1 to pass on linux: + options = options.threshold(OsThreshold::new(0.0).linux(2.1)); } - results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options)); + results.add(harness.try_snapshot_options(format!("demos/{name}"), &options)); } } diff --git a/crates/egui_demo_lib/src/demo/password.rs b/crates/egui_demo_lib/src/demo/password.rs index f22b5aa8a..04b3c6f37 100644 --- a/crates/egui_demo_lib/src/demo/password.rs +++ b/crates/egui_demo_lib/src/demo/password.rs @@ -27,7 +27,7 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response { let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Toggle the `show_plaintext` bool with a button: let response = ui - .add(egui::SelectableLabel::new(show_plaintext, "๐Ÿ‘")) + .selectable_label(show_plaintext, "๐Ÿ‘") .on_hover_text("Show/hide password"); if response.clicked() { diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index c1a325a5f..cb08cf24e 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -374,7 +374,7 @@ mod tests { harness.fit_contents(); harness.run(); - harness.snapshot(&format!("tessellation_test/{name}")); + harness.snapshot(format!("tessellation_test/{name}")); } } } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 31f5d279a..214646d49 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -319,16 +319,27 @@ mod tests { date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), ..Default::default() }; - let mut harness = Harness::builder() - .with_pixels_per_point(2.0) - .with_size(Vec2::new(380.0, 550.0)) - .build_ui(|ui| { - egui_extras::install_image_loaders(ui.ctx()); - demo.ui(ui); - }); - harness.fit_contents(); + for pixels_per_point in [1, 2] { + for theme in [egui::Theme::Light, egui::Theme::Dark] { + let mut harness = Harness::builder() + .with_pixels_per_point(pixels_per_point as f32) + .with_theme(theme) + .with_size(Vec2::new(380.0, 550.0)) + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + demo.ui(ui); + }); - harness.snapshot("widget_gallery"); + harness.fit_contents(); + + let theme_name = match theme { + egui::Theme::Light => "light", + egui::Theme::Dark => "dark", + }; + let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}"); + harness.snapshot(&image_name); + } + } } } diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index e14bfca07..e6f8b2c2d 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -159,7 +159,7 @@ impl ColorTest { ui.separator(); // TODO(emilk): test color multiplication (image tint), - // to make sure vertex and texture color multiplication is done in linear space. + // to make sure vertex and texture color multiplication is done in gamma space. ui.label("Gamma interpolation:"); self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); @@ -191,8 +191,8 @@ impl ColorTest { ui.separator(); - ui.label("Linear interpolation (texture sampling):"); - self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear); + ui.label("Texture interpolation (texture sampling) should be in gamma space:"); + self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); } fn show_gradients( @@ -245,11 +245,10 @@ impl ColorTest { let g = Gradient::endpoints(left, right); match interpolation { - Interpolation::Linear => { - // texture sampler is sRGBA aware, and should therefore be linear - self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); - } + Interpolation::Linear => {} Interpolation::Gamma => { + self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); + // vertex shader uses gamma self.vertex_gradient( ui, @@ -330,7 +329,10 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon #[derive(Clone, Copy)] enum Interpolation { + /// egui used to want Linear interpolation for some things, but now we're always in gamma space. + #[expect(unused)] Linear, + Gamma, } @@ -745,7 +747,7 @@ mod tests { harness.fit_contents(); - results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"))); + results.add(harness.try_snapshot(format!("rendering_test/dpi_{dpi:.2}"))); } } } diff --git a/crates/egui_demo_lib/tests/image_blending.rs b/crates/egui_demo_lib/tests/image_blending.rs new file mode 100644 index 000000000..c8e5775a8 --- /dev/null +++ b/crates/egui_demo_lib/tests/image_blending.rs @@ -0,0 +1,25 @@ +use egui::{hex_color, include_image}; +use egui_kittest::Harness; + +#[test] +fn test_image_blending() { + for pixels_per_point in [1.0, 2.0] { + let mut harness = Harness::builder() + .with_pixels_per_point(pixels_per_point) + .build_ui(|ui| { + egui_extras::install_image_loaders(ui.ctx()); + egui::Frame::new() + .fill(hex_color!("#5981FF")) + .show(ui, |ui| { + ui.add( + egui::Image::new(include_image!("../data/ring.png")) + .max_height(18.0) + .tint(egui::Color32::GRAY), + ); + }); + }); + harness.run(); + harness.fit_contents(); + harness.snapshot(format!("image_blending/image_x{pixels_per_point}")); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bรฉzier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bรฉzier Curve.png index 8bceea77e..0bf7d928c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bรฉzier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bรฉzier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbe9f58cce2466360b4b93b03afaaee36711b3017ddff1b2b56bfe49ea91a076 -size 31306 +oid sha256:13262df01a7f2cd5655b8b0bb9379ae02a851c877314375f047a7d749908125c +size 31368 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index ec9510008..449c88683 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4f807098e0bc56eaacabb76d646a76036cc66a7a6e54b1c934fa9fecb5b0170 -size 26470 +oid sha256:27d5aa7b7e6bd5f59c1765e98ca4588545284456e4cc255799ea797950e09850 +size 26461 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 64c6b76ec..41d3995db 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b999914adab3d44c614bdf3b28abd268a4ff6162c5680b43035b3f71cb69bb -size 23999 +oid sha256:6d5f3129e34e22b15245212904e0a3537a0c7e70f1d35fd3e9c784af707038b5 +size 24018 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index 394bea644..7dbb397fa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816 -size 99087 +oid sha256:5d05c74583024825d82f1fe8dbeb2a793e366016e87a639f51d46945831de82a +size 99106 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png index 91548c427..aca535ad1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fc9e2ec3253a30ac9649995b019b6b23d745dba07a327886f574a15c0e99e84 -size 50082 +oid sha256:e0a49139611dd5f4e97874e8f7b0e12b649da5f373ff7ee80a7ff678f7f8ecc7 +size 50321 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png index 857cd2d6c..0e2bdbf80 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3110fab8444cb41dffe8b27277fa5dafd0d335aaf13dca511bcccc8b53fb25c8 -size 24046 +oid sha256:17f7065c47712f140e4a9fd9eed61a7118fe12cd79cf0745642a02921eaa596b +size 24065 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 9953ac6c1..e87c842a1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df1e4a1e355100056713e751a8979d4201d0e4aab5513ba2f7a3e4852e1347dd -size 264340 +oid sha256:cfc5dd77728ee0b3d319c5851698305851b6713eb054a6eb5b618e9670f58ae5 +size 277018 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index ea8f9c857..760c84e8f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e -size 35121 +oid sha256:aabc0e3821a2d9b21708e9b8d9be02ad55055ccabe719a93af921dba2384b4b3 +size 34297 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 49b223e7d..f13fc54db 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88 -size 179653 +oid sha256:1bd15215f3ec1b365b8c51987f629d5653e4f40e84c34756aea0dc863af27c1e +size 179906 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 92e94b78f..c26e7e4f6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301 -size 115320 +oid sha256:7e80bf8c79e6e431806c85385a0bd9262796efc0a1e74d431a1b896dde0b8651 +size 115338 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index 723bb5995..462a40ad9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90d56d40004f61628e3f66cfac817c426cd18eb4b9c69ea1b3a6fe5e75e3f05 -size 70354 +oid sha256:a3f8873c9cfb80ddeb1ccc0fa04c1c84ea936db1685238f5d29ee6e89f55e457 +size 68814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 53d6c8a3d..b3dbb2ea1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4d6a15094eee5d96a8af5c44ea9d0c962d650ee9b867344c86d1229e526dcb5 -size 12822 +oid sha256:26e4828e42f54da24d032f384f8829e42bcebaee072923f277db582f84302911 +size 12847 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index a45e2be68..217419e00 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02abc0cbab97e572218f422f4b167957869d4e2b4b388355444c20148d998015 -size 35200 +oid sha256:4a4520aa68d6752992fd2f87090a317e6e5e24b5cdb5ee2e82daf07f9471ca80 +size 35251 diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png new file mode 100644 index 000000000..7ef5676bb --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e057c0bba4ec4c30e890c39153bd6dd17c511f410bfb894e66ef3ef9973d8fd4 +size 807 diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png new file mode 100644 index 000000000..89fad98f9 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/image_blending/image_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8b573f58a41efe26a0bf335e27cc123ffd4c13b24576e46d96ddedfed68b606 +size 2027 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index 51b8d8540..200de9835 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f6cf5b14056522d06f0cb1e56bafd7e5ab7a9033eb358748d43d748bb0ceef1 -size 553177 +oid sha256:39bd11647241521c0ad5c7163a1af4f1aa86792018657091a2d47bb7f2c48b47 +size 598408 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index 3e73d0abb..ea9298ad6 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd3bd1f64995db34a14dbc860ae8b8e269073ed7b8f10d10ce8f99b613cfc999 -size 769357 +oid sha256:080a59163ab60d60738cfab5afac7cfbddfb585d8a0ee7d03540924b458badea +size 833822 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 4b9a5194e..86ec338c7 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f12e6145f3a1c3fda6dede3daeb0e52ed2bffb35531d823133224a477798a14a -size 907800 +oid sha256:216d3d028f48f4bfbd6aca0a25500655d4eb4d5581a387397ed0b048d57fc0c3 +size 984737 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index c5f324368..3b239324a 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05bdcfd2c34b6d7badede14f5495dce34e5e9cfe421314f40dcea15e9f865736 -size 1024735 +oid sha256:399fc3d64e7ff637425effe6c80d684be1cf8bb9b555458352d0ae6c62bddb5a +size 1109505 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 8e4481d06..8d4a1b365 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8365c89f6b823f01464a9310bab7717bf25305b335cdeecf21711c7dca9f053f -size 1140082 +oid sha256:30ce4874a1adb8acf8c8e404f3e19e683eca3133cdef15befbc50d6c09618094 +size 1241773 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index de8c8b321..854ee6b29 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b38021057ec6b5bb39c41bd4afaf5e9ff38687216d52d5bba8cbf7b6fdfe9a4f -size 1291518 +oid sha256:135fbe5f4ee485ee671931b64c16410b9073d40bedb23dc2545fc863448e8c63 +size 1398091 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 2fdbaff3d..852bc6bb2 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac90da596084a880487035b276177e98d711854143373d59860f01733b1c0cd -size 45592 +oid sha256:1b0fe7aa33506c59142aff764c6b229e8c55b35c8038312b22ea2659987a081a +size 45578 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 5eb8bf536..49ba9ad07 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e412d424aac7b9cbdfdb8e36bd598e6cbc77183da7733c94c5f20e70699b8b4a -size 87263 +oid sha256:3a3512ea7235640db79e86aa84039621784270201c9213c910f15e7451e5600b +size 87336 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index e9e1a078d..6130a530e 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:222a32da21c69ee46e847e29fb05fd5e1d2de6bb7a22358549bc426f8243fdcb -size 119671 +oid sha256:dc4918a534f26b72d42ef20221e26c0f91a0252870a1987c1fe4cc4aa3c74872 +size 119406 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index a08a658eb..7969d6bee 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d42e11f50a9522dd5ae73e8f8336bfb01493751705055a63abea3f5258f7c9c1 -size 51626 +oid sha256:71182570a65693839fd7cd7390025731ab3f3f88ab55bc67d8be6466fe5a2c11 +size 51843 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index 677783cc5..49141c40d 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b567d4038fd73986c80d2bd12197a6df037fde043545993fa9fe4160d0af446c -size 54829 +oid sha256:a0dc0294f990730da34fcbbc53f44280306ec6179826c20a6c9ee946e1148b61 +size 55042 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index 7816cfdb0..e8f61ae77 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf40a1f56a6e280002719c6556fe477c93fa7fe88d398372ed36efaa1b83a62 -size 55282 +oid sha256:3004adfe5a864bdc768ceb42d1f09b6debcaf5413f8fea4d36b0aff99e4584f9 +size 55511 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 6005e865a..139648c38 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33621731155ebb463fb01ea41ab20272885250efcd7d5c7683c10936b296e14d -size 36446 +oid sha256:b99360833f59a212a965a13d52485ab8ad0e6420b9288b2d6936507067c22a85 +size 36395 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index 713e01fcc..10ad7603b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:186bd8a3146ad8f1977955e3f7fa593877ad1bf1e8376d32f446c67f36a2aafe -size 36493 +oid sha256:82aa004f668f0ac6b493717b4bff8436ccc1e991c7fb3fcde5b5f3a123c06b9f +size 36428 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png deleted file mode 100644 index bcb09fe26..000000000 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07 -size 153136 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png new file mode 100644 index 000000000..9cd2d630e --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e21bb01ae6e4226402a97b7086b49604cdde6b41a6770199df68dc940cd9a45 +size 64748 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png new file mode 100644 index 000000000..f881f639c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0626bc45888ad250bf4b49c7f7f462a93ab91e3a2817fd7d0902411043c97132 +size 153289 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png new file mode 100644 index 000000000..5b88cc531 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:919a82c95468300bcd09471eb31d53d25d50cdcb02c27ddbc759d24e65da92b6 +size 59398 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png new file mode 100644 index 000000000..a1971cad6 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a55e39a640b0e2cc992286a86dcf38460f1abcc7b964df9022549ca1a94c4df5 +size 146408 diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index d13134e21..001e988c2 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -100,9 +100,9 @@ impl BytesLoader for FileLoader { let entry = entry.get_mut(); *entry = Poll::Ready(result); ctx.request_repaint(); - log::trace!("finished loading {uri:?}"); + log::trace!("Finished loading {uri:?}"); } else { - log::trace!("cancelled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading."); + log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading."); } } }) diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index a5e7b3c1f..98fe25a45 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -172,12 +172,7 @@ impl Painter { let supported_extensions = gl.supported_extensions(); log::trace!("OpenGL extensions: {supported_extensions:?}"); - let srgb_textures = shader_version == ShaderVersion::Es300 // WebGL2 always support sRGB - || supported_extensions.iter().any(|extension| { - // EXT_sRGB, GL_ARB_framebuffer_sRGB, GL_EXT_sRGB, GL_EXT_texture_sRGB_decode, โ€ฆ - extension.contains("sRGB") - }); - log::debug!("SRGB texture Support: {:?}", srgb_textures); + let srgb_textures = false; // egui wants normal sRGB-unaware textures let supports_srgb_framebuffer = !cfg!(target_arch = "wasm32") && supported_extensions.iter().any(|extension| { @@ -202,11 +197,10 @@ impl Painter { &gl, glow::FRAGMENT_SHADER, &format!( - "{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n#define SRGB_TEXTURES {}\n{}\n{}", + "{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n{}\n{}", shader_version_declaration, shader_version.is_new_shader_interface() as i32, dithering as i32, - srgb_textures as i32, shader_prefix, FRAG_SRC ), @@ -534,23 +528,6 @@ impl Painter { self.upload_texture_srgb(delta.pos, image.size, delta.options, data); } - egui::ImageData::Font(image) => { - assert_eq!( - image.width() * image.height(), - image.pixels.len(), - "Mismatch between texture size and texel count" - ); - - let data: Vec = { - profiling::scope!("font -> sRGBA"); - image - .srgba_pixels(None) - .flat_map(|a| a.to_array()) - .collect() - }; - - self.upload_texture_srgb(delta.pos, image.size, delta.options, &data); - } }; } diff --git a/crates/egui_glow/src/shader/fragment.glsl b/crates/egui_glow/src/shader/fragment.glsl index f2792ed04..07a931b53 100644 --- a/crates/egui_glow/src/shader/fragment.glsl +++ b/crates/egui_glow/src/shader/fragment.glsl @@ -43,25 +43,8 @@ vec3 dither_interleaved(vec3 rgb, float levels) { return rgb + noise / (levels - 1.0); } -// 0-1 sRGB gamma from 0-1 linear -vec3 srgb_gamma_from_linear(vec3 rgb) { - bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); - vec3 lower = rgb * vec3(12.92); - vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); - return mix(higher, lower, vec3(cutoff)); -} - -// 0-1 sRGBA gamma from 0-1 linear -vec4 srgba_gamma_from_linear(vec4 rgba) { - return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a); -} - void main() { -#if SRGB_TEXTURES - vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc)); -#else vec4 texture_in_gamma = texture2D(u_sampler, v_tc); -#endif // We multiply the colors in gamma space, because that's the only way to get text to look right. vec4 frag_color_gamma = v_rgba_in_gamma * texture_in_gamma; diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 3b10ba37a..021019a89 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -7,6 +7,7 @@ use std::marker::PhantomData; pub struct HarnessBuilder { pub(crate) screen_rect: Rect, pub(crate) pixels_per_point: f32, + pub(crate) theme: egui::Theme, pub(crate) max_steps: u64, pub(crate) step_dt: f32, pub(crate) state: PhantomData, @@ -19,6 +20,7 @@ impl Default for HarnessBuilder { Self { screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), pixels_per_point: 1.0, + theme: egui::Theme::Dark, state: PhantomData, renderer: Box::new(LazyRenderer::default()), max_steps: 4, @@ -45,6 +47,13 @@ impl HarnessBuilder { self } + /// Set the desired theme (dark or light). + #[inline] + pub fn with_theme(mut self, theme: egui::Theme) -> Self { + self.theme = theme; + self + } + /// Set the maximum number of steps to run when calling [`Harness::run`]. /// /// Default is 4. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index ac67c8dad..6adefe53b 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -28,6 +28,7 @@ pub use builder::*; pub use node::*; pub use renderer::*; +use egui::style::ScrollAnimation; use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; use kittest::Queryable; @@ -55,6 +56,10 @@ impl Display for ExceededMaxStepsError { /// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure. /// In _most cases_ it should be fine to just store the state in the closure itself. /// The state functions are useful if you need to access the state after the harness has been created. +/// +/// Some egui style options are changed from the defaults: +/// - The cursor blinking is disabled +/// - The scroll animation is disabled pub struct Harness<'a, State = ()> { pub ctx: egui::Context, input: egui::RawInput, @@ -86,6 +91,7 @@ impl<'a, State> Harness<'a, State> { let HarnessBuilder { screen_rect, pixels_per_point, + theme, max_steps, step_dt, state: _, @@ -93,9 +99,14 @@ impl<'a, State> Harness<'a, State> { wait_for_pending_images, } = builder; let ctx = ctx.unwrap_or_default(); + ctx.set_theme(theme); ctx.enable_accesskit(); - // Disable cursor blinking so it doesn't interfere with snapshots - ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false); + ctx.all_styles_mut(|style| { + // Disable cursor blinking so it doesn't interfere with snapshots + style.visuals.text_cursor.blink = false; + style.scroll_animation = ScrollAnimation::none(); + style.animation_time = 0.0; + }); let mut input = egui::RawInput { screen_rect: Some(screen_rect), ..Default::default() @@ -562,7 +573,8 @@ impl<'a, State> Harness<'a, State> { .expect("Missing root viewport") } - fn root(&self) -> Node<'_> { + /// The root node of the test harness. + pub fn root(&self) -> Node<'_> { Node { accesskit_node: self.kittest.root(), queue: &self.queued_events, diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs index 51a0cc3a0..94940ffff 100644 --- a/crates/egui_kittest/src/node.rs +++ b/crates/egui_kittest/src/node.rs @@ -159,4 +159,49 @@ impl Node<'_> { pub fn is_focused(&self) -> bool { self.accesskit_node.is_focused() } + + /// Scroll the node into view. + pub fn scroll_to_me(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollIntoView, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node down (100px). + pub fn scroll_down(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollDown, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node up (100px). + pub fn scroll_up(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollUp, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node left (100px). + pub fn scroll_left(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollLeft, + target: self.accesskit_node.id(), + data: None, + })); + } + + /// Scroll the [`egui::ScrollArea`] containing this node right (100px). + pub fn scroll_right(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::ScrollRight, + target: self.accesskit_node.id(), + data: None, + })); + } } diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 3c7dc265a..8a457ec52 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -13,16 +13,117 @@ pub struct SnapshotOptions { /// wgpu backends). pub threshold: f32, + /// The number of pixels that can differ before the snapshot is considered a failure. + /// Preferably, you should use `threshold` to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + /// If `None`, the default is `0` (meaning no pixels can differ). + /// If `Some`, the value can be set per OS + pub failed_pixel_count_threshold: usize, + /// The path where the snapshots will be saved. /// The default is `tests/snapshots`. pub output_path: PathBuf, } +/// Helper struct to define the number of pixels that can differ before the snapshot is considered a failure. +/// This is useful if you want to set different thresholds for different operating systems. +/// +/// The default values are 0 / 0.0 +/// +/// Example usage: +/// ```no_run +/// use egui_kittest::{OsThreshold, SnapshotOptions}; +/// let mut harness = egui_kittest::Harness::new_ui(|ui| { +/// ui.label("Hi!"); +/// }); +/// harness.snapshot_options( +/// "os_threshold_example", +/// &SnapshotOptions::new() +/// .threshold(OsThreshold::new(0.0).windows(10.0)) +/// .failed_pixel_count_threshold(OsThreshold::new(0).windows(10).macos(53) +/// )) +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct OsThreshold { + pub windows: T, + pub macos: T, + pub linux: T, + pub fallback: T, +} + +impl From for OsThreshold { + fn from(value: usize) -> Self { + Self::new(value) + } +} + +impl OsThreshold +where + T: Copy, +{ + /// Use the same value for all + pub fn new(same: T) -> Self { + Self { + windows: same, + macos: same, + linux: same, + fallback: same, + } + } + + /// Set the threshold for Windows. + #[inline] + pub fn windows(mut self, threshold: T) -> Self { + self.windows = threshold; + self + } + + /// Set the threshold for macOS. + #[inline] + pub fn macos(mut self, threshold: T) -> Self { + self.macos = threshold; + self + } + + /// Set the threshold for Linux. + #[inline] + pub fn linux(mut self, threshold: T) -> Self { + self.linux = threshold; + self + } + + /// Get the threshold for the current operating system. + pub fn threshold(&self) -> T { + if cfg!(target_os = "windows") { + self.windows + } else if cfg!(target_os = "macos") { + self.macos + } else if cfg!(target_os = "linux") { + self.linux + } else { + self.fallback + } + } +} + +impl From> for usize { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + +impl From> for f32 { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + impl Default for SnapshotOptions { fn default() -> Self { Self { threshold: 0.6, output_path: PathBuf::from("tests/snapshots"), + failed_pixel_count_threshold: 0, // Default is 0, meaning no pixels can differ } } } @@ -37,8 +138,8 @@ impl SnapshotOptions { /// The default is `0.6` (which is enough for most egui tests to pass across different /// wgpu backends). #[inline] - pub fn threshold(mut self, threshold: f32) -> Self { - self.threshold = threshold; + pub fn threshold(mut self, threshold: impl Into) -> Self { + self.threshold = threshold.into(); self } @@ -49,6 +150,20 @@ impl SnapshotOptions { self.output_path = output_path.into(); self } + + /// Change the number of pixels that can differ before the snapshot is considered a failure. + /// + /// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + #[inline] + pub fn failed_pixel_count_threshold( + mut self, + failed_pixel_count_threshold: impl Into>, + ) -> Self { + let failed_pixel_count_threshold = failed_pixel_count_threshold.into().threshold(); + self.failed_pixel_count_threshold = failed_pixel_count_threshold; + self + } } #[derive(Debug)] @@ -58,7 +173,7 @@ pub enum SnapshotError { /// Name of the test name: String, - /// Count of pixels that were different + /// Count of pixels that were different (above the per-pixel threshold). diff: i32, /// Path where the diff image was saved @@ -195,15 +310,24 @@ fn should_update_snapshots() -> bool { /// reading or writing the snapshot. pub fn try_image_snapshot_options( new: &image::RgbaImage, - name: &str, + name: impl Into, + options: &SnapshotOptions, +) -> SnapshotResult { + try_image_snapshot_options_impl(new, name.into(), options) +} + +fn try_image_snapshot_options_impl( + new: &image::RgbaImage, + name: String, options: &SnapshotOptions, ) -> SnapshotResult { let SnapshotOptions { threshold, output_path, + failed_pixel_count_threshold, } = options; - let parent_path = if let Some(parent) = PathBuf::from(name).parent() { + let parent_path = if let Some(parent) = PathBuf::from(&name).parent() { output_path.join(parent) } else { output_path.clone() @@ -269,7 +393,7 @@ pub fn try_image_snapshot_options( return update_snapshot(); } else { return Err(SnapshotError::SizeMismatch { - name: name.to_owned(), + name, expected: previous.dimensions(), actual: new.dimensions(), }); @@ -280,19 +404,24 @@ pub fn try_image_snapshot_options( let result = dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None); - if let Some((diff, result_image)) = result { + if let Some((num_wrong_pixels, result_image)) = result { result_image .save(diff_path.clone()) .map_err(|err| SnapshotError::WriteSnapshot { path: diff_path.clone(), err, })?; + if should_update_snapshots() { update_snapshot() } else { + if num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64 { + return Ok(()); + } + Err(SnapshotError::Diff { - name: name.to_owned(), - diff, + name, + diff: num_wrong_pixels, diff_path, }) } @@ -314,7 +443,7 @@ pub fn try_image_snapshot_options( /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error /// reading or writing the snapshot. -pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotResult { +pub fn try_image_snapshot(current: &image::RgbaImage, name: impl Into) -> SnapshotResult { try_image_snapshot_options(current, name, &SnapshotOptions::default()) } @@ -335,7 +464,11 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotRes /// Panics if the image does not match the snapshot or if there was an error reading or writing the /// snapshot. #[track_caller] -pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &SnapshotOptions) { +pub fn image_snapshot_options( + current: &image::RgbaImage, + name: impl Into, + options: &SnapshotOptions, +) { match try_image_snapshot_options(current, name, options) { Ok(_) => {} Err(err) => { @@ -354,7 +487,7 @@ pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: & /// Panics if the image does not match the snapshot or if there was an error reading or writing the /// snapshot. #[track_caller] -pub fn image_snapshot(current: &image::RgbaImage, name: &str) { +pub fn image_snapshot(current: &image::RgbaImage, name: impl Into) { match try_image_snapshot(current, name) { Ok(_) => {} Err(err) => { @@ -385,13 +518,13 @@ impl Harness<'_, State> { /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. pub fn try_snapshot_options( &mut self, - name: &str, + name: impl Into, options: &SnapshotOptions, ) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; - try_image_snapshot_options(&image, name, options) + try_image_snapshot_options(&image, name.into(), options) } /// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot. @@ -402,7 +535,7 @@ impl Harness<'_, State> { /// # Errors /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. - pub fn try_snapshot(&mut self, name: &str) -> SnapshotResult { + pub fn try_snapshot(&mut self, name: impl Into) -> SnapshotResult { let image = self .render() .map_err(|err| SnapshotError::RenderError { err })?; @@ -428,7 +561,7 @@ impl Harness<'_, State> { /// Panics if the image does not match the snapshot, if there was an error reading or writing the /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + pub fn snapshot_options(&mut self, name: impl Into, options: &SnapshotOptions) { match self.try_snapshot_options(name, options) { Ok(_) => {} Err(err) => { @@ -446,7 +579,7 @@ impl Harness<'_, State> { /// Panics if the image does not match the snapshot, if there was an error reading or writing the /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn snapshot(&mut self, name: &str) { + pub fn snapshot(&mut self, name: impl Into) { match self.try_snapshot(name) { Ok(_) => {} Err(err) => { @@ -467,7 +600,7 @@ impl Harness<'_, State> { )] pub fn try_wgpu_snapshot_options( &mut self, - name: &str, + name: impl Into, options: &SnapshotOptions, ) -> SnapshotResult { self.try_snapshot_options(name, options) @@ -477,7 +610,7 @@ impl Harness<'_, State> { since = "0.31.0", note = "Use `try_snapshot` instead. This function will be removed in 0.32" )] - pub fn try_wgpu_snapshot(&mut self, name: &str) -> SnapshotResult { + pub fn try_wgpu_snapshot(&mut self, name: impl Into) -> SnapshotResult { self.try_snapshot(name) } @@ -485,7 +618,7 @@ impl Harness<'_, State> { since = "0.31.0", note = "Use `snapshot_options` instead. This function will be removed in 0.32" )] - pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + pub fn wgpu_snapshot_options(&mut self, name: impl Into, options: &SnapshotOptions) { self.snapshot_options(name, options); } diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png new file mode 100644 index 000000000..32969d743 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d76e55327de17163bc9c7e128c28153f95db3229dec919352a024eb80544f1 +size 7399 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png new file mode 100644 index 000000000..361925d04 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b7b3145401b7cf9815a652a0914b230892ffda3b5e23fea530dafee9c0c3d3 +size 8110 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index b4e49642f..6d66c5f5a 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,5 +1,5 @@ -use egui::{Modifiers, Vec2, include_image}; -use egui_kittest::Harness; +use egui::{Modifiers, ScrollArea, Vec2, include_image}; +use egui_kittest::{Harness, SnapshotResults}; use kittest::Queryable as _; #[test] @@ -81,3 +81,60 @@ fn should_wait_for_images() { harness.snapshot("should_wait_for_images"); } + +fn test_scroll_harness() -> Harness<'static, bool> { + Harness::builder() + .with_size(Vec2::new(100.0, 200.0)) + .build_ui_state( + |ui, state| { + ScrollArea::vertical().show(ui, |ui| { + for i in 0..20 { + ui.label(format!("Item {i}")); + } + if ui.button("Hidden Button").clicked() { + *state = true; + }; + }); + }, + false, + ) +} + +#[test] +fn test_scroll_to_me() { + let mut harness = test_scroll_harness(); + let mut results = SnapshotResults::new(); + + results.add(harness.try_snapshot("test_scroll_initial")); + + harness.get_by_label("Hidden Button").scroll_to_me(); + + harness.run(); + results.add(harness.try_snapshot("test_scroll_scrolled")); + + harness.get_by_label("Hidden Button").click(); + harness.run(); + + assert!( + harness.state(), + "The button was not clicked after scrolling." + ); +} + +#[test] +fn test_scroll_down() { + let mut harness = test_scroll_harness(); + + let button = harness.get_by_label("Hidden Button"); + button.scroll_down(); + button.scroll_down(); + harness.run(); + + harness.get_by_label("Hidden Button").click(); + harness.run(); + + assert!( + harness.state(), + "The button was not clicked after scrolling down. (Probably not scrolled enough / at all)" + ); +} diff --git a/crates/emath/src/align.rs b/crates/emath/src/align.rs index 89b28d4af..a672c456e 100644 --- a/crates/emath/src/align.rs +++ b/crates/emath/src/align.rs @@ -146,7 +146,7 @@ impl Align { // ---------------------------------------------------------------------------- /// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Align2(pub [Align; 2]); @@ -298,3 +298,9 @@ impl std::ops::IndexMut for Align2 { pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect { Align2::CENTER_CENTER.align_size_within_rect(size, frame) } + +impl std::fmt::Debug for Align2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Align2({:?}, {:?})", self.x(), self.y()) + } +} diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index dc63315b6..8810fe361 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{Div, Mul, Pos2, Rangef, Rot2, Vec2, lerp, pos2, vec2}; +use crate::{Div, Mul, NumExt as _, Pos2, Rangef, Rot2, Vec2, lerp, pos2, vec2}; /// A rectangular region of space. /// @@ -341,11 +341,13 @@ impl Rect { self.max - self.min } + /// Note: this can be negative. #[inline(always)] pub fn width(&self) -> f32 { self.max.x - self.min.x } + /// Note: this can be negative. #[inline(always)] pub fn height(&self) -> f32 { self.max.y - self.min.y @@ -373,9 +375,10 @@ impl Rect { } } + /// This is never negative, and instead returns zero for negative rectangles. #[inline(always)] pub fn area(&self) -> f32 { - self.width() * self.height() + self.width().at_least(0.0) * self.height().at_least(0.0) } /// The distance from the rect to the position. @@ -651,7 +654,7 @@ impl Rect { pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { debug_assert!( d.is_normalized(), - "expected normalized direction, but `d` has length {}", + "Debug assert: expected normalized direction, but `d` has length {}", d.length() ); diff --git a/crates/emath/src/rect_align.rs b/crates/emath/src/rect_align.rs index 5a8102ad1..31580405f 100644 --- a/crates/emath/src/rect_align.rs +++ b/crates/emath/src/rect_align.rs @@ -7,9 +7,9 @@ use crate::{Align2, Pos2, Rect, Vec2}; /// /// There are helper constants for the 12 common menu positions: /// ```text -/// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -/// โ”‚ TOP_START โ”‚ โ”‚ TOP โ”‚ โ”‚ TOP_END โ”‚ -/// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +/// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +/// โ”‚ TOP_START โ”‚ โ”‚ TOP โ”‚ โ”‚ TOP_END โ”‚ +/// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ /// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” /// โ”‚LEFT_STARTโ”‚ โ”‚ โ”‚ โ”‚RIGHT_STARTโ”‚ /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ @@ -19,9 +19,9 @@ use crate::{Align2, Pos2, Rect, Vec2}; /// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” /// โ”‚ LEFT_END โ”‚ โ”‚ โ”‚ โ”‚ RIGHT_END โ”‚ /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -/// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -/// โ”‚BOTTOM_STARTโ”‚ โ”‚BOTTOMโ”‚ โ”‚BOTTOM_ENDโ”‚ -/// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +/// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +/// โ”‚BOTTOM_STARTโ”‚ โ”‚BOTTOMโ”‚ โ”‚BOTTOM_ENDโ”‚ +/// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ /// ``` // There is no `new` function on purpose, since writing out `parent` and `child` is more // reasonable. @@ -235,45 +235,34 @@ impl RectAlign { [self.flip_x(), self.flip_y(), self.flip()] } - /// Look for the [`RectAlign`] that fits best in the available space. + /// Look for the first alternative [`RectAlign`] that allows the child rect to fit + /// inside the `screen_rect`. + /// + /// If no alternative fits, the first is returned. + /// If no alternatives are given, `None` is returned. /// /// See also: /// - [`RectAlign::symmetries`] to calculate alternatives /// - [`RectAlign::MENU_ALIGNS`] for the 12 common menu positions pub fn find_best_align( - mut values_to_try: impl Iterator, - available_space: Rect, + values_to_try: impl Iterator, + screen_rect: Rect, parent_rect: Rect, gap: f32, - size: Vec2, - ) -> Self { - let area = size.x * size.y; - - let blocked_area = |pos: Self| { - let rect = pos.align_rect(&parent_rect, size, gap); - area - available_space.intersect(rect).area() - }; - - let first = values_to_try.next().unwrap_or_default(); - - if blocked_area(first) == 0.0 { - return first; - } - - let mut best_area = blocked_area(first); - let mut best = first; + expected_size: Vec2, + ) -> Option { + let mut first_choice = None; for align in values_to_try { - let blocked = blocked_area(align); - if blocked == 0.0 { - return align; - } - if blocked < best_area { - best = align; - best_area = blocked; + first_choice = first_choice.or(Some(align)); // Remember the first alternative + + let suggested_popup_rect = align.align_rect(&parent_rect, expected_size, gap); + + if screen_rect.contains_rect(suggested_popup_rect) { + return Some(align); } } - best + first_choice } } diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 676e1d0fd..d4b10a216 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -1,8 +1,8 @@ use criterion::{Criterion, black_box, criterion_group, criterion_main}; use epaint::{ - ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, TessellationOptions, - Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path, + AlphaFromCoverage, ClippedShape, Color32, Mesh, PathStroke, Pos2, Rect, Shape, Stroke, + TessellationOptions, Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path, }; #[global_allocator] @@ -66,7 +66,7 @@ fn tessellate_circles(c: &mut Criterion) { let pixels_per_point = 2.0; let options = TessellationOptions::default(); - let atlas = TextureAtlas::new([4096, 256]); + let atlas = TextureAtlas::new([4096, 256], AlphaFromCoverage::default()); let font_tex_size = atlas.size(); let prepared_discs = atlas.prepared_discs(); diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 8fcef2df7..e14ea869e 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -7,24 +7,20 @@ use std::sync::Arc; /// /// To load an image file, see [`ColorImage::from_rgba_unmultiplied`]. /// -/// In order to paint the image on screen, you first need to convert it to +/// This is currently an enum with only one variant, but more image types may be added in the future. /// -/// See also: [`ColorImage`], [`FontImage`]. -#[derive(Clone, PartialEq)] +/// See also: [`ColorImage`]. +#[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageData { /// RGBA image. Color(Arc), - - /// Used for the font texture. - Font(FontImage), } impl ImageData { pub fn size(&self) -> [usize; 2] { match self { Self::Color(image) => image.size, - Self::Font(image) => image.size, } } @@ -38,7 +34,7 @@ impl ImageData { pub fn bytes_per_pixel(&self) -> usize { match self { - Self::Color(_) | Self::Font(_) => 4, + Self::Color(_) => 4, } } } @@ -271,6 +267,37 @@ impl ColorImage { } Self::new([width, height], output) } + + /// Clone a sub-region as a new image. + pub fn region_by_pixels(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self { + assert!( + x + w <= self.width(), + "x + w should be <= self.width(), but x: {}, w: {}, width: {}", + x, + w, + self.width() + ); + assert!( + y + h <= self.height(), + "y + h should be <= self.height(), but y: {}, h: {}, height: {}", + y, + h, + self.height() + ); + + let mut pixels = Vec::with_capacity(w * h); + for y in y..y + h { + let offset = y * self.width() + x; + pixels.extend(&self.pixels[offset..(offset + w)]); + } + assert_eq!( + pixels.len(), + w * h, + "pixels.len should be w * h, but got {}", + pixels.len() + ); + Self::new([w, h], pixels) + } } impl std::ops::Index<(usize, usize)> for ColorImage { @@ -318,127 +345,56 @@ impl std::fmt::Debug for ColorImage { // ---------------------------------------------------------------------------- -/// A single-channel image designed for the font texture. -/// -/// Each value represents "coverage", i.e. how much a texel is covered by a character. -/// -/// This is roughly interpreted as the opacity of a white image. -#[derive(Clone, Default, PartialEq)] +/// How to convert font coverage values into alpha and color values. +// +// This whole thing is less than rigorous. +// Ideally we should do this in a shader instead, and use different computations +// for different text colors. +// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis. +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct FontImage { - /// width, height - pub size: [usize; 2], - - /// The coverage value. +pub enum AlphaFromCoverage { + /// `alpha = coverage`. /// - /// Often you want to use [`Self::srgba_pixels`] instead. - pub pixels: Vec, -} - -impl FontImage { - pub fn new(size: [usize; 2]) -> Self { - Self { - size, - pixels: vec![0.0; size[0] * size[1]], - } - } - - #[inline] - pub fn width(&self) -> usize { - self.size[0] - } - - #[inline] - pub fn height(&self) -> usize { - self.size[1] - } - - /// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom. + /// Looks good for black-on-white text, i.e. light mode. /// - /// `gamma` should normally be set to `None`. + /// Same as [`Self::Gamma`]`(1.0)`, but more efficient. + Linear, + + /// `alpha = coverage^gamma`. + Gamma(f32), + + /// `alpha = 2 * coverage - coverage^2` /// - /// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`. - #[inline] - pub fn srgba_pixels(&self, gamma: Option) -> impl ExactSizeIterator + '_ { - // This whole function is less than rigorous. - // Ideally we should do this in a shader instead, and use different computations - // for different text colors. - // See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis. - self.pixels.iter().map(move |coverage| { - let alpha = if let Some(gamma) = gamma { - coverage.powf(gamma) - } else { - // alpha = coverage * coverage; // recommended by the article for WHITE text (using linear blending) - - // The following is recommended by the article for BLACK text (using linear blending). - // Very similar to a gamma of 0.5, but produces sharper text. - // In practice it works well for all text colors (better than a gamma of 0.5, for instance). - // See https://www.desmos.com/calculator/w0ndf5blmn for a visual comparison. - 2.0 * coverage - coverage * coverage - }; - Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha)) - }) - } - - /// Clone a sub-region as a new image. - pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> Self { - assert!( - x + w <= self.width(), - "x + w should be <= self.width(), but x: {}, w: {}, width: {}", - x, - w, - self.width() - ); - assert!( - y + h <= self.height(), - "y + h should be <= self.height(), but y: {}, h: {}, height: {}", - y, - h, - self.height() - ); - - let mut pixels = Vec::with_capacity(w * h); - for y in y..y + h { - let offset = y * self.width() + x; - pixels.extend(&self.pixels[offset..(offset + w)]); - } - assert_eq!( - pixels.len(), - w * h, - "pixels.len should be w * h, but got {}", - pixels.len() - ); - Self { - size: [w, h], - pixels, - } - } + /// This looks good for white-on-black text, i.e. dark mode. + /// + /// Very similar to a gamma of 0.5, but produces sharper text. + /// See for a comparison to gamma=0.5. + #[default] + TwoCoverageMinusCoverageSq, } -impl std::ops::Index<(usize, usize)> for FontImage { - type Output = f32; +impl AlphaFromCoverage { + /// A good-looking default for light mode (black-on-white text). + pub const LIGHT_MODE_DEFAULT: Self = Self::Linear; - #[inline] - fn index(&self, (x, y): (usize, usize)) -> &f32 { - let [w, h] = self.size; - assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); - &self.pixels[y * w + x] - } -} + /// A good-looking default for dark mode (white-on-black text). + pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq; -impl std::ops::IndexMut<(usize, usize)> for FontImage { - #[inline] - fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 { - let [w, h] = self.size; - assert!(x < w && y < h, "x: {x}, y: {y}, w: {w}, h: {h}"); - &mut self.pixels[y * w + x] - } -} - -impl From for ImageData { + /// Convert coverage to alpha. #[inline(always)] - fn from(image: FontImage) -> Self { - Self::Font(image) + pub fn alpha_from_coverage(&self, coverage: f32) -> f32 { + match self { + Self::Linear => coverage, + Self::Gamma(gamma) => coverage.powf(*gamma), + Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage, + } + } + + #[inline(always)] + pub fn color_from_coverage(&self, coverage: f32) -> Color32 { + let alpha = self.alpha_from_coverage(coverage); + Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha)) } } @@ -447,7 +403,7 @@ impl From for ImageData { /// A change to an image. /// /// Either a whole new image, or an update to a rectangular region of it. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "The painter must take care of this"] pub struct ImageDelta { diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 264966809..f02889d97 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -50,7 +50,7 @@ pub use self::{ color::ColorMode, corner_radius::CornerRadius, corner_radius_f32::CornerRadiusF32, - image::{ColorImage, FontImage, ImageData, ImageDelta}, + image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta}, margin::Margin, margin_f32::*, mesh::{Mesh, Mesh16, Vertex}, diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index bf9db964b..9505dc49b 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -130,10 +130,12 @@ impl TextShape { num_vertices: _, num_indices: _, pixels_per_point: _, + intrinsic_size, } = Arc::make_mut(galley); *rect = transform.scaling * *rect; *mesh_bounds = transform.scaling * *mesh_bounds; + *intrinsic_size = transform.scaling * *intrinsic_size; for text::PlacedRow { pos, row } in rows { *pos *= transform.scaling; @@ -179,7 +181,12 @@ mod tests { #[test] fn text_bounding_box_under_rotation() { - let fonts = Fonts::new(1.0, 1024, FontDefinitions::default()); + let fonts = Fonts::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let font = FontId::monospace(12.0); let mut t = crate::Shape::text( diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 72fffaad0..dd095c443 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -279,12 +279,13 @@ impl FontImpl { } else { let glyph_pos = { let atlas = &mut self.atlas.lock(); + let text_alpha_from_coverage = atlas.text_alpha_from_coverage; let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); glyph.draw(|x, y, v| { if 0.0 < v { let px = glyph_pos.0 + x as usize; let py = glyph_pos.1 + y as usize; - image[(px, py)] = v; + image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v); } }); glyph_pos diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 6e90b5666..a08f3206e 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{ - TextureAtlas, + AlphaFromCoverage, TextureAtlas, mutex::{Mutex, MutexGuard}, text::{ Galley, LayoutJob, LayoutSection, @@ -430,36 +430,56 @@ impl Fonts { pub fn new( pixels_per_point: f32, max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { let fonts_and_cache = FontsAndCache { - fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + fonts: FontsImpl::new( + pixels_per_point, + max_texture_side, + text_alpha_from_coverage, + definitions, + ), galley_cache: Default::default(), }; Self(Arc::new(Mutex::new(fonts_and_cache))) } /// Call at the start of each frame with the latest known - /// `pixels_per_point` and `max_texture_side`. + /// `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`. /// /// Call after painting the previous frame, but before using [`Fonts`] for the new frame. /// - /// This function will react to changes in `pixels_per_point` and `max_texture_side`, + /// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`, /// as well as notice when the font atlas is getting full, and handle that. - pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) { + pub fn begin_pass( + &self, + pixels_per_point: f32, + max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, + ) { let mut fonts_and_cache = self.0.lock(); let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point; let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side; + let text_alpha_from_coverage_changed = + fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage; let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8; - let needs_recreate = - pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full; + let needs_recreate = pixels_per_point_changed + || max_texture_side_changed + || text_alpha_from_coverage_changed + || font_atlas_almost_full; if needs_recreate { let definitions = fonts_and_cache.fonts.definitions.clone(); *fonts_and_cache = FontsAndCache { - fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + fonts: FontsImpl::new( + pixels_per_point, + max_texture_side, + text_alpha_from_coverage, + definitions, + ), galley_cache: Default::default(), }; } @@ -497,7 +517,7 @@ impl Fonts { /// The full font atlas image. #[inline] - pub fn image(&self) -> crate::FontImage { + pub fn image(&self) -> crate::ColorImage { self.lock().fonts.atlas.lock().image().clone() } @@ -642,6 +662,7 @@ impl FontsImpl { pub fn new( pixels_per_point: f32, max_texture_side: usize, + text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { assert!( @@ -651,7 +672,7 @@ impl FontsImpl { let texture_width = max_texture_side.at_most(16 * 1024); let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways. - let atlas = TextureAtlas::new([texture_width, initial_height]); + let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage); let atlas = Arc::new(Mutex::new(atlas)); @@ -1051,6 +1072,7 @@ mod tests { use core::f32; use super::*; + use crate::text::{TextWrapping, layout}; use crate::{Stroke, text::TextFormat}; use ecolor::Color32; use emath::Align; @@ -1120,6 +1142,7 @@ mod tests { let mut fonts = FontsImpl::new( pixels_per_point, max_texture_side, + AlphaFromCoverage::default(), FontDefinitions::default(), ); @@ -1161,4 +1184,60 @@ mod tests { } } } + + #[test] + fn test_intrinsic_size() { + let pixels_per_point = [1.0, 1.3, 2.0, 0.867]; + let max_widths = [40.0, 80.0, 133.0, 200.0]; + let rounded_output_to_gui = [false, true]; + + for pixels_per_point in pixels_per_point { + let mut fonts = FontsImpl::new( + pixels_per_point, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); + + for &max_width in &max_widths { + for round_output_to_gui in rounded_output_to_gui { + for mut job in jobs() { + job.wrap = TextWrapping::wrap_at_width(max_width); + + job.round_output_to_gui = round_output_to_gui; + + let galley_wrapped = layout(&mut fonts, job.clone().into()); + + job.wrap = TextWrapping::no_max_width(); + + let text = job.text.clone(); + let galley_unwrapped = layout(&mut fonts, job.into()); + + let intrinsic_size = galley_wrapped.intrinsic_size; + let unwrapped_size = galley_unwrapped.size(); + + let difference = (intrinsic_size - unwrapped_size).length().abs(); + similar_asserts::assert_eq!( + format!("{intrinsic_size:.4?}"), + format!("{unwrapped_size:.4?}"), + "Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?} + Difference: {difference:.8?} + wrapped rows: {}, unwrapped rows: {} + pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}", + galley_wrapped.rows.len(), + galley_unwrapped.rows.len() + ); + similar_asserts::assert_eq!( + format!("{intrinsic_size:.4?}"), + format!("{unwrapped_size:.4?}"), + "Unwrapped galley intrinsic size should exactly match its size. \ + {:.8?} vs {:8?}", + galley_unwrapped.intrinsic_size, + galley_unwrapped.size(), + ); + } + } + } + } + } } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 3777d860c..6dc0aa03f 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -82,6 +82,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { num_indices: 0, pixels_per_point: fonts.pixels_per_point(), elided: true, + intrinsic_size: Vec2::ZERO, }; } @@ -94,6 +95,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let point_scale = PointScale::new(fonts.pixels_per_point()); + let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs); + let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { @@ -124,7 +127,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { } // Calculate the Y positions and tessellate the text: - galley_from_rows(point_scale, job, rows, elided) + galley_from_rows(point_scale, job, rows, elided, intrinsic_size) } // Ignores the Y coordinate. @@ -190,6 +193,46 @@ fn layout_section( } } +/// Calculate the intrinsic size of the text. +/// +/// The result is eventually passed to `Response::intrinsic_size`. +/// This works by calculating the size of each `Paragraph` (instead of each `Row`). +fn calculate_intrinsic_size( + point_scale: PointScale, + job: &LayoutJob, + paragraphs: &[Paragraph], +) -> Vec2 { + let mut intrinsic_size = Vec2::ZERO; + for (idx, paragraph) in paragraphs.iter().enumerate() { + if paragraph.glyphs.is_empty() { + if idx == 0 { + intrinsic_size.y += point_scale.round_to_pixel(paragraph.empty_paragraph_height); + } + continue; + } + intrinsic_size.x = f32::max( + paragraph + .glyphs + .last() + .map(|l| l.max_x()) + .unwrap_or_default(), + intrinsic_size.x, + ); + + let mut height = paragraph + .glyphs + .iter() + .map(|g| g.line_height) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(paragraph.empty_paragraph_height); + if idx == 0 { + height = f32::max(height, job.first_row_min_height); + } + intrinsic_size.y += point_scale.round_to_pixel(height); + } + intrinsic_size +} + // Ignores the Y coordinate. fn rows_from_paragraphs( paragraphs: Vec, @@ -610,6 +653,7 @@ fn galley_from_rows( job: Arc, mut rows: Vec, elided: bool, + intrinsic_size: Vec2, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; @@ -680,6 +724,7 @@ fn galley_from_rows( num_vertices, num_indices, pixels_per_point: point_scale.pixels_per_point, + intrinsic_size, }; if galley.job.round_output_to_gui { @@ -1034,11 +1079,18 @@ fn is_cjk_break_allowed(c: char) -> bool { #[cfg(test)] mod tests { + use crate::AlphaFromCoverage; + use super::{super::*, *}; #[test] fn test_zero_max_width() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default()); layout_job.wrap.max_width = 0.0; let galley = layout(&mut fonts, layout_job.into()); @@ -1049,7 +1101,12 @@ mod tests { fn test_truncate_with_newline() { // No matter where we wrap, we should be appending the newline character. - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let text_format = TextFormat { font_id: FontId::monospace(12.0), ..Default::default() @@ -1094,7 +1151,12 @@ mod tests { #[test] fn test_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section( "ๆ—ฅๆœฌ่ชžใจEnglishใฎๆททๅœจใ—ใŸๆ–‡็ซ ".into(), TextFormat::default(), @@ -1109,7 +1171,12 @@ mod tests { #[test] fn test_pre_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section( "ๆ—ฅๆœฌ่ชžใจEnglishใฎๆททๅœจใ—ใŸๆ–‡็ซ ".into(), TextFormat::default(), @@ -1124,7 +1191,12 @@ mod tests { #[test] fn test_truncate_width() { - let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut fonts = FontsImpl::new( + 1.0, + 1024, + AlphaFromCoverage::default(), + FontDefinitions::default(), + ); let mut layout_job = LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default()); layout_job.wrap.max_width = f32::INFINITY; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 016bfe104..36d92479e 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -560,6 +560,12 @@ pub struct Galley { /// so that we can warn if this has changed once we get to /// tessellation. pub pixels_per_point: f32, + + /// This is the size that a non-wrapped, non-truncated, non-justified version of the text + /// would have. + /// + /// Useful for advanced layouting. + pub intrinsic_size: Vec2, } #[derive(Clone, Debug, PartialEq)] @@ -821,6 +827,8 @@ impl Galley { .at_most(rect.min.x + self.job.wrap.max_width) .floor_ui(); } + + self.intrinsic_size = self.intrinsic_size.round_ui(); } /// Append each galley under the previous one. @@ -836,6 +844,7 @@ impl Galley { num_vertices: 0, num_indices: 0, pixels_per_point, + intrinsic_size: Vec2::ZERO, }; for (i, galley) in galleys.iter().enumerate() { @@ -872,6 +881,9 @@ impl Galley { // Note that if `galley.elided` is true this will be the last `Galley` in // the vector and the loop will end. merged_galley.elided |= galley.elided; + merged_galley.intrinsic_size.x = + f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x); + merged_galley.intrinsic_size.y += galley.intrinsic_size.y; } if merged_galley.job.round_output_to_gui { diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index 8e174e544..36dd1b48e 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -1,6 +1,7 @@ +use ecolor::Color32; use emath::{Rect, remap_clamp}; -use crate::{FontImage, ImageDelta}; +use crate::{AlphaFromCoverage, ColorImage, ImageDelta}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Rectu { @@ -57,7 +58,7 @@ pub struct PreparedDisc { /// More characters can be added, possibly expanding the texture. #[derive(Clone)] pub struct TextureAtlas { - image: FontImage, + image: ColorImage, /// What part of the image that is dirty dirty: Rectu, @@ -72,18 +73,22 @@ pub struct TextureAtlas { /// pre-rasterized discs of radii `2^i`, where `i` is the index. discs: Vec, + + /// Controls how to convert glyph coverage to alpha. + pub(crate) text_alpha_from_coverage: AlphaFromCoverage, } impl TextureAtlas { - pub fn new(size: [usize; 2]) -> Self { + pub fn new(size: [usize; 2], text_alpha_from_coverage: AlphaFromCoverage) -> Self { assert!(size[0] >= 1024, "Tiny texture atlas"); let mut atlas = Self { - image: FontImage::new(size), + image: ColorImage::filled(size, Color32::TRANSPARENT), dirty: Rectu::EVERYTHING, cursor: (0, 0), row_height: 0, overflowed: false, discs: vec![], // will be filled in below + text_alpha_from_coverage, }; // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: @@ -93,7 +98,7 @@ impl TextureAtlas { (0, 0), "Expected the first allocation to be at (0, 0), but was at {pos:?}" ); - image[pos] = 1.0; + image[pos] = Color32::WHITE; // Allocate a series of anti-aliased discs used to render small filled circles: // TODO(emilk): these circles can be packed A LOT better. @@ -116,7 +121,7 @@ impl TextureAtlas { let coverage = remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0); image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = - coverage; + text_alpha_from_coverage.color_from_coverage(coverage); } } atlas.discs.push(PrerasterizedDisc { @@ -184,7 +189,7 @@ impl TextureAtlas { /// The full font atlas image. #[inline] - pub fn image(&self) -> &FontImage { + pub fn image(&self) -> &ColorImage { &self.image } @@ -200,14 +205,14 @@ impl TextureAtlas { } else { let pos = [dirty.min_x, dirty.min_y]; let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y]; - let region = self.image.region(pos, size); + let region = self.image.region_by_pixels(pos, size); Some(ImageDelta::partial(pos, region, texture_options)) } } /// Returns the coordinates of where the rect ended up, /// and invalidates the region. - pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) { + pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut ColorImage) { /// On some low-precision GPUs (my old iPad) characters get muddled up /// if we don't add some empty pixels between the characters. /// On modern high-precision GPUs this is not needed. @@ -254,13 +259,15 @@ impl TextureAtlas { } } -fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool { +fn resize_to_min_height(image: &mut ColorImage, required_height: usize) -> bool { while required_height >= image.height() { image.size[1] *= 2; // double the height } if image.width() * image.height() > image.pixels.len() { - image.pixels.resize(image.width() * image.height(), 0.0); + image + .pixels + .resize(image.width() * image.height(), Color32::TRANSPARENT); true } else { false diff --git a/crates/epaint/src/textures.rs b/crates/epaint/src/textures.rs index cc191a75f..0944a9052 100644 --- a/crates/epaint/src/textures.rs +++ b/crates/epaint/src/textures.rs @@ -271,7 +271,7 @@ pub enum TextureWrapMode { /// What has been allocated and freed during the last period. /// /// These are commands given to the integration painter. -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "The painter must take care of this"] pub struct TexturesDelta { diff --git a/scripts/update_snapshots_from_ci.sh b/scripts/update_snapshots_from_ci.sh index c42ffb0fd..c15360365 100755 --- a/scripts/update_snapshots_from_ci.sh +++ b/scripts/update_snapshots_from_ci.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# This script searches for the last CI run with your branch name, downloads the test_results artefact +# This script searches for the last CI run with your branch name, downloads the test_results artifact # and replaces your existing snapshots with the new ones. # Make sure you have the gh cli installed and authenticated before running this script. # If prompted to select a default repo, choose the emilk/egui one diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index 7dbda11d9..f15fb0ce6 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad14068e60fa678ee749925dd3713ee2b12a83ec1bca9c413bdeb9bc27d8ac20 -size 407795 +oid sha256:d59882afca42e766dddc36450a3331ca247a130e3796f99d0335ac370a7c3610 +size 425517 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long.png b/tests/egui_tests/tests/snapshots/sides/default_long.png new file mode 100644 index 000000000..2d66f3665 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7ceaa95512c67dcbf1c8ba5a8f33bf4833c2e863d09903fb71b5aa2822cc086 +size 7889 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short.png b/tests/egui_tests/tests/snapshots/sides/default_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png new file mode 100644 index 000000000..39e1bab98 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88e1557dffa7295e7e7e37ed175fcec40aab939f9b67137a1ce33811e8ae4722 +size 7148 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png new file mode 100644 index 000000000..3326d9527 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508209ca303751ef323301b25bb3878410742ea79339b75363d2681b98d2712b +size 7068 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png new file mode 100644 index 000000000..36929a413 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c9e39c18fc5bb1fc02a86dbf02e3ffca5537dbe8986d5c5b50cb4984c97466 +size 9085 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png new file mode 100644 index 000000000..47398293f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e9ba0acb573853ef5b3dedb1156d99cdf80338ccb160093960e8aaa41bd5df +size 9048 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png new file mode 100644 index 000000000..15e9dc464 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png new file mode 100644 index 000000000..756a3068f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png new file mode 100644 index 000000000..6f3189261 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png index 8c8e9630c..364f7771f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button.png +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f64e581b97df6694cb7c85ee7728a955e3c1a851ab660e8b6091eee1885bbe -size 9719 +oid sha256:a573976aacbb629c88c285089fca28ba7998a0c28ecee9f783920d67929a1e2d +size 9735 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index c71c2aeb0..c38571a6e 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d39ec25b91f5f5d68305d2cb7cc0285d715fe30ccbd66369efbe7327d1899b52 -size 10753 +oid sha256:9fbb9aca2006aeca555c138f1ebdb89409026f1bed48da74cd0fa03dcd8facbe +size 10746 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 42f8ff02a..4be868a30 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46d86987ba895ead9b28efcc37e1b4374f34eedebac83d1db9eaa8e5a3202ee3 -size 13203 +oid sha256:8ff776897760d300a4f26c10578be0d9afed7b4ae9f95f941914e641c2a10cb8 +size 13798 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 114baa35d..ffabcae40 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9151f1c9d8a769ac2143a684cabf5d9ed1e453141fff555da245092003f1df1 -size 13563 +oid sha256:9cd6a7f38c876cc345eae1a5e01f7668d4642b70181198fe0f09570815e47da8 +size 13489 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png index 40852f3c2..113839a2f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e03cf99a3d28f73d4a72c0e616dc54198663b94bf5cffda694cf4eb4dee01be8 -size 13445 +oid sha256:ec75c3fccec8d6a72b808aba593f8c289618b6f95db08eb3cdb20a255b9d986e +size 13450 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index dbe3c13b6..56b5bb0e3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e86a37c7b259a6bad61897545d927d75e8307916dc78d256e4d33c410fcd6876 -size 7306 +oid sha256:c7e66a490236b306ce03c504d29490cdadc3708a79e21e3b46d11df8eb22a26b +size 7309 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png index a42ad5012..8e89197e1 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a172cfadc91467529e5546e686673be73ba0071a55d55abc7a41fb1d07214d -size 11700 +oid sha256:895914fa37608ff68c5ae7fdd22d0363da26907c78d4980f6bf1ed19f7e5f388 +size 11697 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png index 81f995515..67d80fed3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ef91dfedc74cae59099bce32b2e42cb04649e84442e8010282a9c1ff2a7f2c8 -size 12469 +oid sha256:0e0c4277eebadb0c350b5110d5ea7ff9292ab2b0231d6b36e9ada3aeefc7c198 +size 12510 diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png index 6c8348559..7e868c0e7 100644 --- a/tests/egui_tests/tests/snapshots/visuals/slider.png +++ b/tests/egui_tests/tests/snapshots/visuals/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1892358a4552af3f529141d314cd18e4cf55a629d870798278a5470e3e0a8a94 -size 11030 +oid sha256:ec09e0e3432668c0d08bbba0aa8608c4eefba33d57f2335fdf105d144791406d +size 11036 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 5f2a64b8d..1e1e4d394 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7300a0b88d4fdb6c1e543bfaf50e8964b2f84aaaf8197267b671d0cf3c8da30a -size 7033 +oid sha256:9353e6d39d309e7a6e6c0a17be819809c2dbea8979e9e73b3c73b67b07124a36 +size 7031 diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index abc9f2d05..98e90c1c5 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -69,3 +69,35 @@ fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult { harness.try_snapshot(name) } + +#[test] +fn test_intrinsic_size() { + let mut intrinsic_size = None; + for wrapping in [ + TextWrapMode::Extend, + TextWrapMode::Wrap, + TextWrapMode::Truncate, + ] { + _ = HarnessBuilder::default() + .with_size(Vec2::new(100.0, 100.0)) + .build_ui(|ui| { + ui.style_mut().wrap_mode = Some(wrapping); + let response = ui.add(Button::new( + "Hello world this is a long text that should be wrapped.", + )); + if let Some(current_intrinsic_size) = intrinsic_size { + assert_eq!( + Some(current_intrinsic_size), + response.intrinsic_size, + "For wrapping: {wrapping:?}" + ); + } + assert!( + response.intrinsic_size.is_some(), + "intrinsic_size should be set for `Button`" + ); + intrinsic_size = response.intrinsic_size; + }); + } + assert_eq!(intrinsic_size.unwrap().round(), Vec2::new(305.0, 18.0)); +} diff --git a/tests/egui_tests/tests/test_sides.rs b/tests/egui_tests/tests/test_sides.rs new file mode 100644 index 000000000..52d35db4c --- /dev/null +++ b/tests/egui_tests/tests/test_sides.rs @@ -0,0 +1,76 @@ +use egui::{TextWrapMode, Vec2, containers::Sides}; +use egui_kittest::{Harness, SnapshotResults}; + +#[test] +fn sides_container_tests() { + let mut results = SnapshotResults::new(); + + test_variants("default", |sides| sides, &mut results); + + test_variants( + "shrink_left", + |sides| sides.shrink_left().truncate(), + &mut results, + ); + + test_variants( + "shrink_right", + |sides| sides.shrink_right().truncate(), + &mut results, + ); + + test_variants( + "wrap_left", + |sides| sides.shrink_left().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); + + test_variants( + "wrap_right", + |sides| sides.shrink_right().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); +} + +fn test_variants( + name: &str, + mut create_sides: impl FnMut(Sides) -> Sides, + results: &mut SnapshotResults, +) { + for (variant_name, left_text, right_text, fit_contents) in [ + ("short", "Left", "Right", false), + ( + "long", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + false, + ), + ("short_fit_contents", "Left", "Right", true), + ( + "long_fit_contents", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + true, + ), + ] { + let mut harness = Harness::builder() + .with_size(Vec2::new(400.0, 50.0)) + .build_ui(|ui| { + create_sides(Sides::new()).show( + ui, + |left| { + left.label(left_text); + }, + |right| { + right.label(right_text); + }, + ); + }); + + if fit_contents { + harness.fit_contents(); + } + + results.add(harness.try_snapshot(format!("sides/{name}_{variant_name}"))); + } +} diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 4ec317521..a6050e95b 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -244,7 +244,7 @@ fn test_widget_layout(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> Sna }); harness.fit_contents(); - harness.try_snapshot(&format!("layout/{name}")) + harness.try_snapshot(format!("layout/{name}")) } /// Utility to create a snapshot test of the different states of a egui widget. @@ -370,7 +370,7 @@ impl<'a> VisualTests<'a> { harness.fit_contents(); - harness.try_snapshot(&format!("visuals/{}", self.name)) + harness.try_snapshot(format!("visuals/{}", self.name)) } }