From e204717b1d9af9dcbb4cad0733953b1f82a73bd9 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 13 May 2026 15:11:15 +0200 Subject: [PATCH] Atom support for `egui::Window` Titlebar (#8154) * part of #7264 * based on https://github.com/emilk/egui/pull/8152 The resize fix allows use to really simplify how the Window Titlebar is rendered. Previously it was using some complex flow to calculate and allocate the height first and then render it later once we knew the windows final width. Since now windows can't shrink past their minimum content widths, I can just show the titlebar inline with the regular content, just outside of the `Resize` container so that it is always visible. This does change what the size of a window means. Before, size was just the size of the contents, while now size (e.g. via min_height) will include the Frames margin and outline, title bar and the contents. Also, the window label now truncates as you shrink the window (meaning windows can now be smaller than their label allows). --------- Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com> --- crates/egui/src/atomics/atom_layout.rs | 2 +- crates/egui/src/containers/area.rs | 4 - .../egui/src/containers/collapsing_header.rs | 28 +- crates/egui/src/containers/resize.rs | 2 +- crates/egui/src/containers/window.rs | 511 ++++++++---------- crates/egui/src/widget_text.rs | 17 - .../tests/snapshots/demos/Bézier Curve.png | 4 +- .../tests/snapshots/demos/Clipboard Test.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Cursor Test.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Grid Test.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../tests/snapshots/demos/ID Test.png | 4 +- .../snapshots/demos/Input Event History.png | 4 +- .../tests/snapshots/demos/Input Test.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Layout Test.png | 4 +- .../snapshots/demos/Manual Layout Test.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Popups.png | 4 +- .../tests/snapshots/demos/SVG Test.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../snapshots/demos/Tessellation Test.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../snapshots/demos/Window Resize Test.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- crates/egui_kittest/tests/regression_tests.rs | 91 +++- .../tests/snapshots/window_outer_size.png | 3 + 50 files changed, 400 insertions(+), 426 deletions(-) create mode 100644 crates/egui_kittest/tests/snapshots/window_outer_size.png diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 7894273f3..1b44c986b 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -226,7 +226,7 @@ impl<'a> AtomLayout<'a> { max_size.x = f32::INFINITY; } - let available_size = ui.available_size().at_most(max_size); + let available_size = ui.available_size().at_most(max_size).at_least(min_size); // The size available for the content let available_inner_size = available_size - frame.total_margin().sum(); diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 09488058d..10be6307a 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -594,10 +594,6 @@ fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 { } impl Prepared { - pub(crate) fn state(&self) -> &AreaState { - &self.state - } - pub(crate) fn state_mut(&mut self) -> &mut AreaState { &mut self.state } diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index de3581288..d921a5cbf 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -2,8 +2,8 @@ use std::hash::Hash; use crate::{ Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle, - TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType, - emath, epaint, pos2, remap, remap_clamp, vec2, + TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, WidgetInfo, WidgetText, WidgetType, emath, + epaint, pos2, remap, remap_clamp, vec2, }; use emath::GuiRounding as _; use epaint::{Shape, StrokeKind}; @@ -81,30 +81,6 @@ impl CollapsingState { } } - /// Will toggle when clicked, etc. - pub(crate) fn show_default_button_with_size( - &mut self, - ui: &mut Ui, - button_size: Vec2, - ) -> Response { - let (_id, rect) = ui.allocate_space(button_size); - let response = ui.interact(rect, self.id, Sense::click()); - response.widget_info(|| { - WidgetInfo::labeled( - WidgetType::Button, - ui.is_enabled(), - if self.is_open() { "Hide" } else { "Show" }, - ) - }); - - if response.clicked() { - self.toggle(ui); - } - let openness = self.openness(ui.ctx()); - paint_default_icon(ui, openness, &response); - response - } - /// Will toggle when clicked, etc. fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response { self.show_button_indented(ui, paint_default_icon) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index c537ac140..679b92c8e 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -50,7 +50,7 @@ pub struct Resize { pub(crate) min_size: Vec2, pub(crate) max_size: Vec2, - default_size: Vec2, + pub(crate) default_size: Vec2, with_stroke: bool, } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 9f25d6131..2c249254f 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -1,9 +1,7 @@ // WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts. -use std::sync::Arc; - use emath::GuiRounding as _; -use epaint::{CornerRadiusF32, RectShape}; +use epaint::CornerRadiusF32; use crate::collapsing_header::CollapsingState; use crate::*; @@ -33,9 +31,9 @@ use super::{Area, Frame, Resize, ScrollArea, area, resize}; /// Note that this is NOT a native OS window. /// To create a new native OS window, use [`crate::Context::show_viewport_deferred`]. #[must_use = "You should call .show()"] -pub struct Window<'open> { - title: WidgetText, - open: Option<&'open mut bool>, +pub struct Window<'a> { + title: Atoms<'a>, + open: Option<&'a mut bool>, area: Area, frame: Option, resize: Resize, @@ -44,14 +42,15 @@ pub struct Window<'open> { default_open: bool, with_title_bar: bool, fade_out: bool, + auto_sized: bool, } -impl<'open> Window<'open> { +impl<'a> Window<'a> { /// The window title is used as a unique [`Id`] and must be unique, and should not change. /// This is true even if you disable the title bar with `.title_bar(false)`. /// If you need a changing title, you must call `window.id(…)` with a fixed id. - pub fn new(title: impl Into) -> Self { - let title = title.into().fallback_text_style(TextStyle::Heading); + pub fn new(title: impl IntoAtoms<'a>) -> Self { + let title: Atoms<'_> = title.into_atoms(); let area = Area::new(Id::new(title.text())).kind(UiKind::Window); Self { title, @@ -61,12 +60,13 @@ impl<'open> Window<'open> { resize: Resize::default() .with_stroke(false) .min_size([96.0, 32.0]) - .default_size([340.0, 420.0]), // Default inner size of a window + .default_size([340.0, 420.0]), // Default outer size of a window (includes frame margins, stroke, and title bar) scroll: ScrollArea::neither().auto_shrink(false), collapsible: true, default_open: true, with_title_bar: true, fade_out: true, + auto_sized: false, } } @@ -118,7 +118,7 @@ impl<'open> Window<'open> { /// * If `*open == true`, the window will have a close button. /// * If the close button is pressed, `*open` will be set to `false`. #[inline] - pub fn open(mut self, open: &'open mut bool) -> Self { + pub fn open(mut self, open: &'a mut bool) -> Self { self.open = Some(open); self } @@ -213,6 +213,9 @@ impl<'open> Window<'open> { } /// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`. + /// + /// The size refers to the *outer* window size, including the frame's `inner_margin`, + /// `outer_margin`, `stroke`, and the title bar. #[inline] pub fn min_size(mut self, min_size: impl Into) -> Self { self.resize = self.resize.min_size(min_size); @@ -234,6 +237,9 @@ impl<'open> Window<'open> { } /// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`. + /// + /// The size refers to the *outer* window size, including the frame's `inner_margin`, + /// `outer_margin`, `stroke`, and the title bar. #[inline] pub fn max_size(mut self, max_size: impl Into) -> Self { self.resize = self.resize.max_size(max_size); @@ -320,6 +326,9 @@ impl<'open> Window<'open> { } /// Set initial size of the window. + /// + /// The size refers to the *outer* window size, including frame margins, stroke, + /// and the title bar. #[inline] pub fn default_size(mut self, default_size: impl Into) -> Self { let default_size: Vec2 = default_size.into(); @@ -345,6 +354,9 @@ impl<'open> Window<'open> { } /// Sets the window size and prevents it from being resized by dragging its edges. + /// + /// The size refers to the *outer* window size, including the frame's `inner_margin`, + /// `outer_margin`, `stroke`, and the title bar. #[inline] pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); @@ -399,6 +411,7 @@ impl<'open> Window<'open> { pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); self.scroll = ScrollArea::neither(); + self.auto_sized = true; self } @@ -473,13 +486,12 @@ impl Window<'_> { default_open, with_title_bar, fade_out, + auto_sized, } = self; let style = ctx.global_style(); - let header_color = - frame.map_or_else(|| style.visuals.widgets.open.weak_bg_fill, |f| f.fill); - let mut window_frame = frame.unwrap_or_else(|| Frame::window(&style)); + let window_frame = frame.unwrap_or_else(|| Frame::window(&style)); let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -507,64 +519,37 @@ impl Window<'_> { let on_top = Some(area_layer_id) == ctx.top_layer_id(); let mut area = area.begin(ctx); - area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text())); - - // Calculate roughly how much larger the full window inner size is compared to the content rect - let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar { - let title_bar_inner_height = ctx - .fonts_mut(|fonts| title.font_height(fonts, &style)) - .at_least(style.spacing.interact_size.y); - let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y; - let half_height = (title_bar_inner_height / 2.0).round() as _; - window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height); - window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height); - - let title_content_spacing = if is_collapsed { - 0.0 - } else { - window_frame.stroke.width - }; - (title_bar_inner_height, title_content_spacing) - } else { - (0.0, 0.0) - }; + area.with_widget_info(|| { + WidgetInfo::labeled( + WidgetType::Window, + true, + title.text().as_deref().unwrap_or(""), + ) + }); { // Prevent window from becoming larger than the constrain rect. + // `resize.max_size` is still in outer-window coordinates here, matching `constrain_rect`. let constrain_rect = area.constrain_rect(); let max_width = constrain_rect.width(); - let max_height = - constrain_rect.height() - title_bar_height_with_margin - title_content_spacing; + let max_height = constrain_rect.height(); resize.max_size.x = resize.max_size.x.min(max_width); resize.max_size.y = resize.max_size.y.min(max_height); } - // First check for resize to avoid frame delay: - let last_frame_outer_rect = area.state().rect(); - let resize_interaction = do_resize_interaction( - ctx, - possible, - area.id(), - area_layer_id, - last_frame_outer_rect, - window_frame, - ); - + // The user-supplied min/max/default sizes on `Window` refer to the *outer* window size + // (the total footprint, including frame margins, stroke, and title bar). `Resize` sizes + // the title bar + inner content area, so we subtract the extra frame margin (the part + // outside of `Resize`). { - let margins = window_frame.total_margin().sum() - + vec2(0.0, title_bar_height_with_margin + title_content_spacing); - - resize_response( - resize_interaction, - ctx, - margins, - area_layer_id, - &mut area, - resize_id, - ); + let frame_margin = window_frame.total_margin().sum(); + resize.min_size = (resize.min_size - frame_margin).at_least(Vec2::ZERO); + resize.max_size = (resize.max_size - frame_margin).at_least(Vec2::ZERO); + resize.default_size = (resize.default_size - frame_margin).at_least(Vec2::ZERO); } let mut area_content_ui = area.content_ui(ctx); + if is_open { // `Area` already takes care of fade-in animations, // so we only need to handle fade-out animations here. @@ -573,55 +558,33 @@ impl Window<'_> { } let content_inner = { - // BEGIN FRAME -------------------------------- - let mut frame = window_frame.begin(&mut area_content_ui); - - let show_close_button = open.is_some(); - - let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); - - let title_bar = if with_title_bar { - let title_bar = TitleBar::new( - &frame.content_ui, - title, - show_close_button, - collapsible, - window_frame, - title_bar_height_with_margin, - ); - resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width - - frame.content_ui.set_min_size(title_bar.inner_rect.size()); - - // Skip the title bar (and separator): - if is_collapsed { - frame.content_ui.add_space(title_bar.inner_rect.height()); - } else { - frame.content_ui.add_space( - title_bar.inner_rect.height() - + title_content_spacing - + window_frame.inner_margin.sum().y, - ); - } - - Some(title_bar) - } else { - None - }; - - let (content_inner, content_response) = collapsing - .show_body_unindented(&mut frame.content_ui, |ui| { - resize.show(ui, |ui| { - if scroll.is_any_scroll_enabled() { - scroll.show(ui, add_contents).inner - } else { - add_contents(ui) - } - }) + let outer_response = window_frame.show(&mut area_content_ui, |ui| { + resize.show(ui, |ui| { + if with_title_bar { + title_ui( + ui, + title, + window_frame, + &mut collapsing, + collapsible, + on_top, + open.as_deref_mut(), + auto_sized, + ); + } + collapsing + .show_body_unindented(ui, |ui| { + if scroll.is_any_scroll_enabled() { + scroll.show(ui, add_contents).inner + } else { + add_contents(ui) + } + }) + .map(|inner| inner.inner) }) - .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); + }); - let outer_rect = frame.end(&mut area_content_ui).rect; + let outer_rect = outer_response.response.rect; // Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window. let resize_interaction = do_resize_interaction( @@ -629,7 +592,7 @@ impl Window<'_> { possible, area.id(), area_layer_id, - last_frame_outer_rect, + outer_rect, window_frame, ); @@ -641,50 +604,25 @@ impl Window<'_> { resize_interaction, ); - // END FRAME -------------------------------- + { + let margins = window_frame.total_margin().sum(); - if let Some(mut title_bar) = title_bar { - title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); - title_bar.inner_rect.max.y = - title_bar.inner_rect.min.y + title_bar_height_with_margin; - - if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = - window_frame.corner_radius - window_frame.stroke.width.round() as u8; - - if !is_collapsed { - round.se = 0; - round.sw = 0; - } - - area_content_ui.painter().set( - *where_to_put_header_background, - RectShape::filled(title_bar.inner_rect, round, header_color), - ); - } - - if false { - ctx.debug_painter().debug_rect( - title_bar.inner_rect, - Color32::LIGHT_BLUE, - "title_bar.rect", - ); - } - - title_bar.ui( - &mut area_content_ui, - content_response.as_ref(), - open.as_deref_mut(), - &mut collapsing, - collapsible, + resize_response( + resize_interaction, + ctx, + margins, + area_layer_id, + &mut area, + resize_id, ); } + // END FRAME -------------------------------- collapsing.store(ctx); paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); - content_inner + outer_response.inner }; let full_response = area.end(ctx, area_content_ui); @@ -992,7 +930,7 @@ fn do_resize_interaction( let side_grab_radius = style.interaction.resize_grab_radius_side; let corner_grab_radius = style.interaction.resize_grab_radius_corner; - let vetrtical_rect = |a: Pos2, b: Pos2| { + let vertical_rect = |a: Pos2, b: Pos2| { Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius)) }; let horizontal_rect = |a: Pos2, b: Pos2| { @@ -1009,14 +947,14 @@ fn do_resize_interaction( if possible.resize_right { let response = side_response( - vetrtical_rect(rect.right_top(), rect.right_bottom()), + vertical_rect(rect.right_top(), rect.right_bottom()), id.with("right"), ); right |= response; } if possible.resize_left { let response = side_response( - vetrtical_rect(rect.left_top(), rect.left_bottom()), + vertical_rect(rect.left_top(), rect.left_bottom()), id.with("left"), ); left |= response; @@ -1177,176 +1115,165 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) // ---------------------------------------------------------------------------- -struct TitleBar { - window_frame: Frame, +/// Show the window titlebar. +/// +/// Should be placed inside a `Frame::window`. The [`Frame`] it was placed inside should be passed as +/// an arg and will be used to paint the divider line at the bottom and the highlighted background +/// when `active` is true. +#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +fn title_ui( + ui: &mut Ui, + mut title: Atoms<'_>, + frame: Frame, + collapsing: &mut CollapsingState, + collapsible: bool, + active: bool, + open: Option<&mut bool>, + auto_sized: bool, +) -> Response { + let shape_idx = ui.painter().add(Shape::Noop); - /// Prepared text in the title - title_galley: Arc, + let mut atoms = Atoms::default(); - /// Size of the title bar in an expanded state. This size become known only - /// after expanding window and painting its content. - /// - /// Does not include the stroke, nor the separator line between the title bar and the window contents. - inner_rect: Rect, -} + let button_size = Vec2::splat(ui.spacing().icon_width); -impl TitleBar { - fn new( - ui: &Ui, - title: WidgetText, - show_close_button: bool, - collapsible: bool, - window_frame: Frame, - title_bar_height_with_margin: f32, - ) -> Self { - if false { - ui.debug_painter() - .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect"); - } + // Since the heading height is higher than the button size, we need to allocate the buttons + // with the headers height as size, otherwise they'd look slightly off-center. + // The shrink is then used to render the buttons with the right size. + let heading_font_height = + ui.fonts_mut(|f| f.row_height(&TextStyle::Heading.resolve(ui.style()))); + let button_allocation_size = Vec2::splat(heading_font_height); + let button_shrink = (button_allocation_size - button_size) / 2.0; - let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y; + let collapse_atom_id = Id::new("__window_collapse_button"); + let close_atom_id = Id::new("__window_close_button"); - let item_spacing = ui.spacing().item_spacing; - let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height)); + let expanded = collapsing.openness(ui.ctx()) > 0.0; - let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical) + if collapsible { + atoms.push_right(Atom::custom(collapse_atom_id, button_allocation_size)); + } - let title_galley = title.into_galley( - ui, - Some(crate::TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Heading, - ); + atoms.push_right(Atom::grow()); - let minimum_width = if collapsible || show_close_button { - // If at least one button is shown we make room for both buttons (since title should be centered): - 2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x + if !auto_sized + && !title.any_shrink() + && let Some(first_text) = title + .iter_mut() + .find(|a| matches!(a.kind, AtomKind::Text(..))) + { + first_text.shrink = true; + } + atoms.extend_right(title); + + atoms.push_right(Atom::grow()); + + if open.is_some() { + atoms.push_right(Atom::custom(close_atom_id, button_allocation_size)); + } + + let spacing = ui.spacing().item_spacing.x; + + let mut child_ui = ui.new_child(UiBuilder::new()); + + let mut layout = AtomLayout::new(atoms) + .gap(spacing) + .fallback_font(TextStyle::Heading) + .wrap_mode(TextWrapMode::Truncate); + + if expanded { + let min_width = if auto_sized { + // During auto size, the resize is essentially disabled, meaning we don't get an + // available_width we can rely on. Instead, check of large the content grew last frame + // and use that for sizing the title bar. Unfortunately this adds a frame delay. + ui.response().rect.width() } else { - left_pad + title_galley.size().x + left_pad + child_ui.available_width() }; - let min_inner_size = vec2(minimum_width, inner_height); - let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size); - if false { - ui.debug_painter() - .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect"); - } - - Self { - window_frame, - title_galley, - inner_rect: min_rect, // First estimate - will be refined later - } + layout = layout.min_size(Vec2::new(min_width, 0.0)); } - /// Finishes painting of the title bar when the window content size already known. - /// - /// # Parameters - /// - /// - `ui`: - /// - `outer_rect`: - /// - `content_response`: if `None`, window is collapsed at this frame, otherwise contains - /// a result of rendering the window content - /// - `open`: if `None`, no "Close" button will be rendered, otherwise renders and processes - /// the "Close" button and writes a `false` if window was closed - /// - `collapsing`: holds the current expanding state. Can be changed by double click on the - /// title if `collapsible` is `true` - /// - `collapsible`: if `true`, double click on the title bar will be handled for a change - /// of `collapsing` state - fn ui( - self, - ui: &mut Ui, - content_response: Option<&Response>, - open: Option<&mut bool>, - collapsing: &mut CollapsingState, - collapsible: bool, - ) { - let window_frame = self.window_frame; - let title_inner_rect = self.inner_rect; + let layout_response = layout.show(&mut child_ui); - if false { - ui.debug_painter() - .debug_rect(self.inner_rect, Color32::RED, "TitleBar"); - } + let mut title_click_rect = layout_response.response.rect + frame.total_margin(); - if collapsible { - // Show collapse-button: - let button_center = Align2::LEFT_CENTER - .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect) - .center(); - let button_size = Vec2::splat(ui.spacing().icon_width); - let button_rect = Rect::from_center_size(button_center, button_size); - let button_rect = button_rect.round_ui(); - - ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| { - collapsing.show_default_button_with_size(ui, button_size); - }); - } - - if let Some(open) = open { - // Add close button now that we know our full width: - if self.close_button_ui(ui).clicked() { - *open = false; - } - } - - let text_pos = - emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect) - .left_top(); - let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); - ui.painter().galley( - text_pos, - Arc::clone(&self.title_galley), - ui.visuals().text_color(), + // Collapse triangle icon + if collapsible && let Some(rect) = layout_response.rect(collapse_atom_id) { + let rect = rect.shrink2(button_shrink); + title_click_rect = title_click_rect.with_min_x(rect.max.x); + let icon_response = child_ui.interact( + rect, + child_ui.auto_id_with("collapse_button"), + Sense::click(), ); - - if let Some(content_response) = content_response { - // Paint separator between title and content: - let content_rect = content_response.rect; - if false { - ui.debug_painter() - .debug_rect(content_rect, Color32::RED, "content_rect"); - } - let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0; - - // To verify the sanity of this, use a very wide window stroke - ui.painter() - .hline(title_inner_rect.x_range(), y, window_frame.stroke); + icon_response.widget_info(|| { + WidgetInfo::labeled( + WidgetType::Button, + child_ui.is_enabled(), + if collapsing.is_open() { "Hide" } else { "Show" }, + ) + }); + if icon_response.clicked() { + collapsing.toggle(&child_ui); } + let openness = collapsing.openness(child_ui.ctx()); + crate::collapsing_header::paint_default_icon(&mut child_ui, openness, &icon_response); + } - // Don't cover the close- and collapse buttons: - let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0)); - - if false { - ui.debug_painter() - .debug_rect(double_click_rect, Color32::GREEN, "double_click_rect"); + // Close button + if let Some(open) = open + && let Some(rect) = layout_response.rect(close_atom_id) + { + let rect = rect.shrink2(button_shrink); + title_click_rect = title_click_rect.with_max_x(rect.min.x); + if close_button(&mut child_ui, rect).clicked() { + *open = false; } + } - let id = ui.unique_id().with("__window_title_bar"); - - if ui - .interact(double_click_rect, id, Sense::CLICK) + if collapsible + && child_ui + .interact( + title_click_rect, + child_ui.auto_id_with("window_title_click"), + Sense::click(), + ) .double_clicked() - && collapsible - { - collapsing.toggle(ui); - } + { + collapsing.toggle(&child_ui); } - /// Paints the "Close" button at the right side of the title bar - /// and processes clicks on it. - /// - /// The button is square and its size is determined by the - /// [`crate::style::Spacing::icon_width`] setting. - fn close_button_ui(&self, ui: &mut Ui) -> Response { - let button_center = Align2::RIGHT_CENTER - .align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect) - .center(); - let button_size = Vec2::splat(ui.spacing().icon_width); - let button_rect = Rect::from_center_size(button_center, button_size); - let button_rect = button_rect.round_to_pixels(ui.pixels_per_point()); - close_button(ui, button_rect) + child_ui.set_clip_rect(Rect::EVERYTHING); + let mut header_frame = frame.shadow(Shadow::NONE); + if active { + header_frame = header_frame.fill(ui.visuals().widgets.open.weak_bg_fill); } + if expanded { + header_frame.corner_radius.sw = 0; + header_frame.corner_radius.se = 0; + } + child_ui + .painter() + .set(shape_idx, header_frame.paint(layout_response.rect)); + + let mut advance_rect = child_ui.min_rect(); + + if auto_sized { + // We may not allocate in the horizontal direction as that would break auto sizing. + // Allocate a rect with 0 width: + advance_rect = advance_rect.with_max_x(advance_rect.min.x); + } + if expanded { + // Account for the margin of the title frame + the margin of the window contents + // - the default ui spacing egui would add on this call + advance_rect.max.y += frame.total_margin().bottom + frame.inner_margin.top as f32 + - child_ui.spacing().item_spacing.y; + } + + ui.advance_cursor_after_rect(advance_rect); + + layout_response.response } /// Paints the "Close" button of the window and processes clicks on it. diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 13e8175a0..8670398fa 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,4 +1,3 @@ -use emath::GuiRounding as _; use epaint::text::{IntoTag, TextFormat, VariationCoords}; use std::fmt::Formatter; use std::{borrow::Cow, sync::Arc}; @@ -692,22 +691,6 @@ impl WidgetText { self.map_rich_text(|text| text.background_color(background_color)) } - /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 { - match self { - Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)), - Self::RichText(text) => text.font_height(fonts, style), - Self::LayoutJob(job) => job.font_height(fonts), - Self::Galley(galley) => { - if let Some(placed_row) = galley.rows.first() { - placed_row.height().round_ui() - } else { - galley.size().y.round_ui() - } - } - } - } - pub fn into_layout_job( self, style: &Style, 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 96d7706ee..aa52b8bda 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:10d64e017d1d0eba736a4471d28b1602a0cb69d8e2ab53f4ee604b01c9343116 -size 32475 +oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599 +size 32422 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 c9a05a629..faadcbef3 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:ab0d730ce0cf5f1d79947601def4f60c0a015e23a5dcd780df65c7ddc7ae7156 -size 27194 +oid sha256:157996b151dd0f450abca6a9cbad4c8237e236300546521e50f95637d9a89c05 +size 27206 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 6134dfad9..12b0c24d3 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fccbda741a056ae30a7bfd7497f7b0adfb337bdb885d247fbdfef43cc24b54c -size 26948 +oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795 +size 26911 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index d43e856aa..17f557c8d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26d247655398bae33c724ad3c3bdcab330d194093b07442708d5069e256f636b -size 76542 +oid sha256:6df3c0a298d48a5f2e1b36207909e20ea545a72a97461a3ae0792d21e0554f93 +size 76567 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png index 60d3993ef..2954609c5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3389491f9f5c54cfc2e295abe76ad53f223c31e9489e1d07a8323cf14fcf37d -size 62628 +oid sha256:e1aa359d3d2767ad0d0ecb635152371b50b3d551f486fb45c6f7fa96969bc8f1 +size 62291 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index c59717683..ec34487f1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e42e801758bb9e6130a4e94bd0857d6f254e68a7dedebec5af5cb7f7d896068 -size 27822 +oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc +size 27253 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index f3c65337f..d5f1d13c5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41e68fd3a679e2a6e3dd81129de5aa1ded3a8f24cc7b1f2ba0b876240d309d9b -size 21019 +oid sha256:9aa1b0f9a1ff5167b7f66894e6d38112e73bd3b6a692b4b468fb6a75717d01bd +size 21009 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 6a050e0e8..99850aa43 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3db502ec416b322e0f98e9737faa52d5d2fd308d47649710fffa0b0bc5996f52 -size 10783 +oid sha256:04f66c848c6f24607d19b809f411a9287ad45067b18da40cf48a77ff65ae9d91 +size 10875 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index b187b4cfd..449c2eac0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39ae3420fe01d696a032d8d052c405c5623a9208fc673f5cf09188b1e6a539b -size 115447 +oid sha256:157424625dbb855e116a29a008172f29697a37b03512ef06a2e92e502d5e4128 +size 101816 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 658cb0922..bacae8846 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:19322248e8335301b2ce31fc1f1352993a374dda945d227784fecea2ee831761 -size 25088 +oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818 +size 25116 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 f6f9a3cda..d7a4c8e1e 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:98f8865d866a6f28ae3e3a16c815770ed691a031b1d06f7d3662a7e94f564606 -size 99318 +oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17 +size 99316 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index ab7de6c2d..b81ed23e0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39208c3c2c95f68fb37880b011f866bedd8dbccee33d163a725cb2a5cc6bb1b3 -size 18290 +oid sha256:165821b21d980c98612a45cf54d9ca3578dec023ab0e2194d2166db087019cc3 +size 18293 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index b899d7356..e9aebce92 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d251ffd6c34e4ffa7b5de7fe5ae57a2d8f48ba7c2e03711da2c3f1a3fd84648c -size 113998 +oid sha256:2027f21fdb132542e01fc592d3150aa876cf2ebb4a4268b8c170ff4f523657f0 +size 114074 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png index 5b3a63aeb..9b1a5e30c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d959e17ee5c8a32534ab98b6f08db884b2fbf25476d8dc2dc90edc79bd87ea4 -size 25821 +oid sha256:03c4660ce72440f5ebf3db05b4bce1e8caf1899907723d30699d9d65d442f562 +size 24540 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 85c205be0..c1627ebba 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:c923f523cc77d678929f294b360f60b9f546ddec66d5317ad0eb44bd61a5f927 -size 51733 +oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7 +size 50512 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index b7d365f3a..7653e9170 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9c98d7bfa08e22e217dd9e7031cc49e4b4486f1a9fdd223bd122be07af72365 -size 22550 +oid sha256:d70cd39499c8f9e0edf689b642ef7cc98115c751bf787305f92323f42aae3fd4 +size 21814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png index a4a82635b..710b51b78 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68afa93605427e12fe527f3ca9613095664b4983f1f585a60f14bc2370c0a1f2 -size 47224 +oid sha256:7705ef738605c31bbf067363f5544ce57e698ab869feb8a88e4be4eabf4de7b6 +size 47087 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 b0cb0c27b..89e4a647b 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:33959851f1bd2386fcca8fb5700133d14d90db5a8f783fbfaa9f3aebb7b0d5b2 -size 23119 +oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076 +size 22994 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png index 28e6d1845..817eebf8f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b918565d66594fe53ed62bb14e357d3489335f3d037ccb088f198d944eb367d -size 65307 +oid sha256:6e00e1dd95278a003c383f5a3f16d25ee4943073171d862db50cb17e90e421bc +size 60272 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 7f0357df1..32dc226c9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8c3c2912a3a11892e65f94792c77c79400a81d8c913109d39e8ce12f5b095c6 -size 33503 +oid sha256:58d56dce5f1e9766dbfdca4ea98fb71f7062b23a0ee4b08ce7f76056513aa9a5 +size 33440 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 59609d76a..b3c10c6a2 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c2c7b0d4913b59ef932f6d6349ddab5cc8619b2e8e9a8b5eb3e055a62e6ed60 -size 38382 +oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6 +size 35378 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 4d4b5c355..9a5859bf5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9567f56f2dd030608798347e1ab755d95730ba5c5dd1721f1c61147be7216e87 -size 18304 +oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0 +size 17686 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 553f8aa0b..2437ae223 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:d19d694e6a70ab6acb45668b391751e82f7beab19f9918e23821c667e8cc9cdb -size 249733 +oid sha256:fc1d270e0171cc055fe99579d3f7ab52c63783c793c59aaf8c82027bf1dd3ab8 +size 216484 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png index 6a18dc9e5..c452f78da 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e1c73e28020371429b3e03d540f72dbf886fd40f0ba08bb194e868bbb3c95ff -size 57230 +oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935 +size 57262 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png index 92d19c470..db3c561ce 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2facd3881e6b107a0dcce9d6e00008a3c9b0f31ad270c35357a87e487180f56 -size 19814 +oid sha256:ebe84ae10b084df7d8373bc4499c5d50a1446a6b707e8220c786a198341d76c8 +size 19189 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index de52524ac..16b02f86d 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:0ad81eb762150360368a97858ef30bb0d5aff72e71743fa40c3fd4d70ec84cdc -size 33400 +oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf +size 30450 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 14603f2f2..603404a9b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c22130891755ac095d73e0494f11ee8e89d0fd0c31a321d3afb969648ece11ef -size 23675 +oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1 +size 23807 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 525f23435..c9b177a18 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:465f33e53ebe15b776e2d6d0710f13f2453f00bab1f6ef4319b3491f1d1d3a26 -size 173487 +oid sha256:10e4658168d6a93779463ebb81520f821cbf6eac59391c60d30f5d40ebdf910d +size 152623 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 34ec70a65..0f2836dd1 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:001f7a1310ddf37be4d9a7f56a95a3079f713b741824a348544561bb16c291fa -size 118614 +oid sha256:ad81eeb593663cb26295e7fc51987b85a7084b32504e26364f54f9c2129cb790 +size 118048 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index b9dea21f0..9473551aa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8105f7c2b519716cc0a45dffd5f08980f53f35c6c6b788592e1d82506cdacccc -size 26665 +oid sha256:3c8aaf89b6d5dad6ae7788b58f642f5d80384777ca80dc3819f1aefc5867b148 +size 26086 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 909b766b9..d64d3fca7 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9e83d5e83e3003830f7f719b02dd93273733a9b72d388aa42083387d02c1a20 -size 76310 +oid sha256:66f35217b0becce12e251c4e7063c31ac92e080340a0318891771fdff54593db +size 67388 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 b1ed6bcde..ad87167a3 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:3d46c87417c49c8462fac6c488e46b1482bbc75f70fe8f7af8391f0d5d28dac3 -size 70271 +oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94 +size 60701 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index 0bd5b9ceb..bda94ab51 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:661570ed4bdf24d52ab049e7d3cf22ef4d50542ee5486d133e0a618a6146da42 -size 98582 +oid sha256:29e23e0e45a6e557e569358c89a34dc768ff0b33b47112f864ae8ed119ac6e6e +size 81119 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 839a15faa..d5513b0ec 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94c4af5715992f4dbb5bbec6ce67eec1e2f66cfc078a3e704ec386bdb482cac4 -size 30064 +oid sha256:7ad81306e153798d72724e37f23a4037bf986f98e0ecbe3e8d294d23a92ef77c +size 29975 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index bea2050ab..b3447e66a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d69b9a12e777ee559a481aec012935a5bfb2ca8b0d48725ecd33e7f0880b2b8 -size 64273 +oid sha256:dde445e82732c45f84acbfa83761348b9b89dd2a4fec500ba5124b0c1e58dd29 +size 63060 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 728dc59b6..a605d136d 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:f6c020860fe9cb7503cea548afbf298ebb9dc620133870b528fe7508d04150c8 -size 13691 +oid sha256:789700adbf20ec5508abdfc95d843197715fbec9f35051799ea18e9927207b30 +size 13650 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 951a6fd41..ecc93a7d4 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:d033935ca18ebee7e3c35629233cc3e3a73766ac8c0627fcdd8a12660eed703c -size 35873 +oid sha256:ab0a8201038b2b066c5aff1fd1a35bc7aed95e74cf50c293eee4a76d66623822 +size 35171 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index 933343f25..adc67e7ab 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77c9058b036770a644f1bfcf9ed1ba7a29a7c98107b1823474c55ccc2880e9e7 -size 485880 +oid sha256:f9c8eecaedce2bbcbdc4b7975a349b9f986463f189041c7ad6bed64809f18bcd +size 445367 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index d1abde4fa..b2d76916a 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bd53b56322123940496700cbd2a73e336fd80eaf49dcb19a958888d66570ddb -size 47159 +oid sha256:01493c64acb62a24d19c69579a8f8cb8e080a60747792bbbb98e1cccdfe9214f +size 47175 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 8f6976ea5..0995b1814 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d15954e6183558141e05e1b566ec4a794d372503baf436da2a8c8c67299056f1 -size 48238 +oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8 +size 48277 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index ab4a99a12..182b7336d 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1738ecf660979888c70b1046dd759fe0b082e4958814fb19077c4fed2fb4bdef -size 44338 +oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251 +size 44357 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index cbac0d342..7d1519d01 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dae7239dc065068147387eb313afdbaa3f0df8b81d060dbbc29f5a6a31ad76c8 -size 44309 +oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2 +size 44347 diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index e6f056da4..d68920974 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,6 +1,7 @@ use egui::accesskit::{self, Role}; use egui::{ - Button, ComboBox, Image, Label, Modifiers, Popup, Pos2, Rect, Vec2, Widget as _, Window, + Align2, Button, ComboBox, FontId, Image, Label, Modifiers, Popup, Pos2, Rect, Stroke, + StrokeKind, Vec2, Widget as _, Window, }; #[cfg(all(feature = "wgpu", feature = "snapshot"))] use egui_kittest::SnapshotResults; @@ -511,3 +512,91 @@ fn window_resize_wraps_to_content_min_width() { window past the non-wrapping label's natural width" ); } + +/// Ensure that the size passed to window is actually treated as outer size (including +/// margins and borders). +#[test] +fn window_fixed_size_is_outer_size() { + use egui::{Color32, Frame, Margin, Pos2, Shape}; + + let outer_pos = Pos2::new(50.0, 50.0); + let outer_size = Vec2::new(300.0, 200.0); + let outer_margin = Margin::same(10); + let expected_rect = Rect::from_min_size(outer_pos, outer_size); + + let mut harness = Harness::builder() + .with_size(Vec2::new(800.0, 600.0)) + .build_ui(move |ui| { + let frame = Frame::window(ui.style()).outer_margin(outer_margin); + Window::new("size_test") + .frame(frame) + .fixed_pos(outer_pos) + .fixed_size(outer_size) + .show(ui.ctx(), |ui| { + // Fill the available space so `Resize` doesn't auto-shrink the window + // below the requested fixed size. + ui.allocate_space(ui.available_size()); + }); + + // Paint a debug rect on top of everything that marks the expected outer + // window rect. In the snapshot this should line up exactly with the + // painted window frame. + let painter = ui.ctx().debug_painter(); + painter.rect_stroke( + expected_rect, + 0.0, + Stroke::new(2.0, Color32::RED), + StrokeKind::Outside, + ); + painter.text( + expected_rect.left_top() + Vec2::new(0.0, -4.0), + Align2::LEFT_BOTTOM, + "should perfectly match the outer window size/position", + FontId::default(), + Color32::RED, + ); + + // Also paint the expected *visible frame* rect (outer rect shrunk by the + // frame's outer_margin). In the snapshot this should line up exactly with + // the painted window frame. + let expected_frame_rect = expected_rect - outer_margin; + painter.debug_rect( + expected_frame_rect, + Color32::GREEN, + "should perfectly match the painted window frame", + ); + }); + + harness.run(); + + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + harness.snapshot("window_outer_size"); + + fn collect_filled_rect_sizes(shape: &Shape, out: &mut Vec) { + match shape { + // Skip stroke-only rects (fill == TRANSPARENT), so the debug overlay + // doesn't trivially satisfy the size check. + Shape::Rect(r) if r.fill != Color32::TRANSPARENT => out.push(r.rect.size()), + Shape::Vec(v) => v.iter().for_each(|s| collect_filled_rect_sizes(s, out)), + _ => {} + } + } + + let mut sizes = Vec::new(); + for clipped in &harness.output().shapes { + collect_filled_rect_sizes(&clipped.shape, &mut sizes); + } + + // The shape will have the inner size + let painted_size = outer_size - outer_margin.sum(); + let found = sizes + .iter() + .any(|s| (s.x - painted_size.x).abs() < 0.5 && (s.y - painted_size.y).abs() < 0.5); + + assert!( + found, + "expected a filled RectShape with size {painted_size:?} (outer size {outer_size:?} \ + minus outer margin {outer_margin:?}) in the paint output, but no painted rect matched. \ + Found filled-rect sizes: {sizes:?}" + ); +} diff --git a/crates/egui_kittest/tests/snapshots/window_outer_size.png b/crates/egui_kittest/tests/snapshots/window_outer_size.png new file mode 100644 index 000000000..bae4f22cb --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/window_outer_size.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cba6fbd64df18b2a41635af59c1f50d1352b4de8ddefd7d5389a4f9e518b6c86 +size 23543