1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

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>
This commit is contained in:
Lucas Meurer
2026-05-13 15:11:15 +02:00
committed by GitHub
parent 571d366056
commit e204717b1d
50 changed files with 400 additions and 426 deletions

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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<Frame>,
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<WidgetText>) -> 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<Vec2>) -> 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<Vec2>) -> 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<Vec2>) -> 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<Vec2>) -> 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<Galley>,
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.

View File

@@ -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,

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:10d64e017d1d0eba736a4471d28b1602a0cb69d8e2ab53f4ee604b01c9343116
size 32475
oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
size 32422

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab0d730ce0cf5f1d79947601def4f60c0a015e23a5dcd780df65c7ddc7ae7156
size 27194
oid sha256:157996b151dd0f450abca6a9cbad4c8237e236300546521e50f95637d9a89c05
size 27206

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6fccbda741a056ae30a7bfd7497f7b0adfb337bdb885d247fbdfef43cc24b54c
size 26948
oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
size 26911

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26d247655398bae33c724ad3c3bdcab330d194093b07442708d5069e256f636b
size 76542
oid sha256:6df3c0a298d48a5f2e1b36207909e20ea545a72a97461a3ae0792d21e0554f93
size 76567

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3389491f9f5c54cfc2e295abe76ad53f223c31e9489e1d07a8323cf14fcf37d
size 62628
oid sha256:e1aa359d3d2767ad0d0ecb635152371b50b3d551f486fb45c6f7fa96969bc8f1
size 62291

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e42e801758bb9e6130a4e94bd0857d6f254e68a7dedebec5af5cb7f7d896068
size 27822
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
size 27253

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41e68fd3a679e2a6e3dd81129de5aa1ded3a8f24cc7b1f2ba0b876240d309d9b
size 21019
oid sha256:9aa1b0f9a1ff5167b7f66894e6d38112e73bd3b6a692b4b468fb6a75717d01bd
size 21009

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3db502ec416b322e0f98e9737faa52d5d2fd308d47649710fffa0b0bc5996f52
size 10783
oid sha256:04f66c848c6f24607d19b809f411a9287ad45067b18da40cf48a77ff65ae9d91
size 10875

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c39ae3420fe01d696a032d8d052c405c5623a9208fc673f5cf09188b1e6a539b
size 115447
oid sha256:157424625dbb855e116a29a008172f29697a37b03512ef06a2e92e502d5e4128
size 101816

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19322248e8335301b2ce31fc1f1352993a374dda945d227784fecea2ee831761
size 25088
oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
size 25116

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98f8865d866a6f28ae3e3a16c815770ed691a031b1d06f7d3662a7e94f564606
size 99318
oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
size 99316

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39208c3c2c95f68fb37880b011f866bedd8dbccee33d163a725cb2a5cc6bb1b3
size 18290
oid sha256:165821b21d980c98612a45cf54d9ca3578dec023ab0e2194d2166db087019cc3
size 18293

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d251ffd6c34e4ffa7b5de7fe5ae57a2d8f48ba7c2e03711da2c3f1a3fd84648c
size 113998
oid sha256:2027f21fdb132542e01fc592d3150aa876cf2ebb4a4268b8c170ff4f523657f0
size 114074

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d959e17ee5c8a32534ab98b6f08db884b2fbf25476d8dc2dc90edc79bd87ea4
size 25821
oid sha256:03c4660ce72440f5ebf3db05b4bce1e8caf1899907723d30699d9d65d442f562
size 24540

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c923f523cc77d678929f294b360f60b9f546ddec66d5317ad0eb44bd61a5f927
size 51733
oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
size 50512

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9c98d7bfa08e22e217dd9e7031cc49e4b4486f1a9fdd223bd122be07af72365
size 22550
oid sha256:d70cd39499c8f9e0edf689b642ef7cc98115c751bf787305f92323f42aae3fd4
size 21814

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:68afa93605427e12fe527f3ca9613095664b4983f1f585a60f14bc2370c0a1f2
size 47224
oid sha256:7705ef738605c31bbf067363f5544ce57e698ab869feb8a88e4be4eabf4de7b6
size 47087

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33959851f1bd2386fcca8fb5700133d14d90db5a8f783fbfaa9f3aebb7b0d5b2
size 23119
oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
size 22994

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b918565d66594fe53ed62bb14e357d3489335f3d037ccb088f198d944eb367d
size 65307
oid sha256:6e00e1dd95278a003c383f5a3f16d25ee4943073171d862db50cb17e90e421bc
size 60272

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8c3c2912a3a11892e65f94792c77c79400a81d8c913109d39e8ce12f5b095c6
size 33503
oid sha256:58d56dce5f1e9766dbfdca4ea98fb71f7062b23a0ee4b08ce7f76056513aa9a5
size 33440

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c2c7b0d4913b59ef932f6d6349ddab5cc8619b2e8e9a8b5eb3e055a62e6ed60
size 38382
oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
size 35378

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9567f56f2dd030608798347e1ab755d95730ba5c5dd1721f1c61147be7216e87
size 18304
oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
size 17686

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d19d694e6a70ab6acb45668b391751e82f7beab19f9918e23821c667e8cc9cdb
size 249733
oid sha256:fc1d270e0171cc055fe99579d3f7ab52c63783c793c59aaf8c82027bf1dd3ab8
size 216484

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e1c73e28020371429b3e03d540f72dbf886fd40f0ba08bb194e868bbb3c95ff
size 57230
oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
size 57262

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2facd3881e6b107a0dcce9d6e00008a3c9b0f31ad270c35357a87e487180f56
size 19814
oid sha256:ebe84ae10b084df7d8373bc4499c5d50a1446a6b707e8220c786a198341d76c8
size 19189

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ad81eb762150360368a97858ef30bb0d5aff72e71743fa40c3fd4d70ec84cdc
size 33400
oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
size 30450

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c22130891755ac095d73e0494f11ee8e89d0fd0c31a321d3afb969648ece11ef
size 23675
oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
size 23807

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:465f33e53ebe15b776e2d6d0710f13f2453f00bab1f6ef4319b3491f1d1d3a26
size 173487
oid sha256:10e4658168d6a93779463ebb81520f821cbf6eac59391c60d30f5d40ebdf910d
size 152623

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:001f7a1310ddf37be4d9a7f56a95a3079f713b741824a348544561bb16c291fa
size 118614
oid sha256:ad81eeb593663cb26295e7fc51987b85a7084b32504e26364f54f9c2129cb790
size 118048

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8105f7c2b519716cc0a45dffd5f08980f53f35c6c6b788592e1d82506cdacccc
size 26665
oid sha256:3c8aaf89b6d5dad6ae7788b58f642f5d80384777ca80dc3819f1aefc5867b148
size 26086

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9e83d5e83e3003830f7f719b02dd93273733a9b72d388aa42083387d02c1a20
size 76310
oid sha256:66f35217b0becce12e251c4e7063c31ac92e080340a0318891771fdff54593db
size 67388

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d46c87417c49c8462fac6c488e46b1482bbc75f70fe8f7af8391f0d5d28dac3
size 70271
oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
size 60701

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:661570ed4bdf24d52ab049e7d3cf22ef4d50542ee5486d133e0a618a6146da42
size 98582
oid sha256:29e23e0e45a6e557e569358c89a34dc768ff0b33b47112f864ae8ed119ac6e6e
size 81119

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94c4af5715992f4dbb5bbec6ce67eec1e2f66cfc078a3e704ec386bdb482cac4
size 30064
oid sha256:7ad81306e153798d72724e37f23a4037bf986f98e0ecbe3e8d294d23a92ef77c
size 29975

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d69b9a12e777ee559a481aec012935a5bfb2ca8b0d48725ecd33e7f0880b2b8
size 64273
oid sha256:dde445e82732c45f84acbfa83761348b9b89dd2a4fec500ba5124b0c1e58dd29
size 63060

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6c020860fe9cb7503cea548afbf298ebb9dc620133870b528fe7508d04150c8
size 13691
oid sha256:789700adbf20ec5508abdfc95d843197715fbec9f35051799ea18e9927207b30
size 13650

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d033935ca18ebee7e3c35629233cc3e3a73766ac8c0627fcdd8a12660eed703c
size 35873
oid sha256:ab0a8201038b2b066c5aff1fd1a35bc7aed95e74cf50c293eee4a76d66623822
size 35171

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77c9058b036770a644f1bfcf9ed1ba7a29a7c98107b1823474c55ccc2880e9e7
size 485880
oid sha256:f9c8eecaedce2bbcbdc4b7975a349b9f986463f189041c7ad6bed64809f18bcd
size 445367

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bd53b56322123940496700cbd2a73e336fd80eaf49dcb19a958888d66570ddb
size 47159
oid sha256:01493c64acb62a24d19c69579a8f8cb8e080a60747792bbbb98e1cccdfe9214f
size 47175

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d15954e6183558141e05e1b566ec4a794d372503baf436da2a8c8c67299056f1
size 48238
oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
size 48277

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1738ecf660979888c70b1046dd759fe0b082e4958814fb19077c4fed2fb4bdef
size 44338
oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
size 44357

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dae7239dc065068147387eb313afdbaa3f0df8b81d060dbbc29f5a6a31ad76c8
size 44309
oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
size 44347

View File

@@ -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<Vec2>) {
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:?}"
);
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cba6fbd64df18b2a41635af59c1f50d1352b4de8ddefd7d5389a4f9e518b6c86
size 23543