Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ui::is_sizing_pass for better size estimation of Areas, and menus in particular #4557

Merged
merged 14 commits into from
May 29, 2024
Merged
8 changes: 8 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3628,6 +3628,14 @@ dependencies = [
"env_logger",
]

[[package]]
name = "test_size_pass"
version = "0.1.0"
dependencies = [
"eframe",
"env_logger",
]

[[package]]
name = "test_viewports"
version = "0.1.0"
Expand Down
86 changes: 71 additions & 15 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub struct Area {
constrain_rect: Option<Rect>,
order: Order,
default_pos: Option<Pos2>,
default_size: Vec2,
pivot: Align2,
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
Expand All @@ -87,6 +88,7 @@ impl Area {
enabled: true,
order: Order::Middle,
default_pos: None,
default_size: Vec2::NAN,
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
Expand Down Expand Up @@ -163,6 +165,35 @@ impl Area {
self
}

/// The size used for the [`Ui::max_rect`] the first frame.
///
/// Text will wrap at this width, and images that expand to fill the available space
/// will expand to this size.
///
/// If the contents are smaller than this size, the area will shrink to fit the contents.
/// If the contents overflow, the area will grow.
///
/// If not set, [`style::Spacing::default_area_size`] will be used.
#[inline]
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.default_size = default_size.into();
self
}

/// See [`Self::default_size`].
#[inline]
pub fn default_width(mut self, default_width: f32) -> Self {
self.default_size.x = default_width;
self
}

/// See [`Self::default_size`].
#[inline]
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_size.y = default_height;
self
}

/// Positions the window and prevents it from being moved
#[inline]
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
Expand Down Expand Up @@ -247,7 +278,7 @@ pub(crate) struct Prepared {
/// This is so that we use the first frame to calculate the window size,
/// and then can correctly position the window and its contents the next frame,
/// without having one frame where the window is wrongly positioned or sized.
temporarily_invisible: bool,
sizing_pass: bool,
}

impl Area {
Expand All @@ -272,6 +303,7 @@ impl Area {
interactable,
enabled,
default_pos,
default_size,
new_pos,
pivot,
anchor,
Expand All @@ -292,11 +324,29 @@ impl Area {
if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
}
let mut state = state.unwrap_or_else(|| State {
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
pivot,
size: Vec2::ZERO,
interactable,
let mut state = state.unwrap_or_else(|| {
// during the sizing pass we will use this as the max size
let mut size = default_size;

let default_area_size = ctx.style().spacing.default_area_size;
if size.x.is_nan() {
size.x = default_area_size.x;
}
if size.y.is_nan() {
size.y = default_area_size.y;
}

if constrain {
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
size = size.at_most(constrain_rect.size());
}

State {
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
pivot,
size,
interactable,
}
});
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.interactable = interactable;
Expand Down Expand Up @@ -365,7 +415,7 @@ impl Area {
enabled,
constrain,
constrain_rect,
temporarily_invisible: is_new,
sizing_pass: is_new,
}
}

Expand Down Expand Up @@ -431,12 +481,7 @@ impl Prepared {
}
};

let max_rect = Rect::from_min_max(
self.state.left_top_pos(),
constrain_rect
.max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
);
let max_rect = Rect::from_min_size(self.state.left_top_pos(), self.state.size);

let clip_rect = constrain_rect; // Don't paint outside our bounds

Expand All @@ -448,7 +493,9 @@ impl Prepared {
clip_rect,
);
ui.set_enabled(self.enabled);
ui.set_visible(!self.temporarily_invisible);
if self.sizing_pass {
ui.set_sizing_pass();
}
ui
}

Expand All @@ -461,11 +508,20 @@ impl Prepared {
enabled: _,
constrain: _,
constrain_rect: _,
temporarily_invisible: _,
sizing_pass,
} = self;

state.size = content_ui.min_size();

if sizing_pass {
// If during the sizing pass we measure our width to `123.45` and
// then try to wrap to exactly that next frame,
// we may accidentally wrap the last letter of some text.
// We only do this after the initial sizing pass though;
// otherwise we could end up with for-ever expanding areas.
state.size = state.size.ceil();
}

ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));

move_response
Expand Down
3 changes: 2 additions & 1 deletion crates/egui/src/containers/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ impl Frame {
self.show_dyn(ui, Box::new(add_contents))
}

fn show_dyn<'c, R>(
/// Show using dynamic dispatch.
pub fn show_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
Expand Down
8 changes: 2 additions & 6 deletions crates/egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,11 @@ fn show_tooltip_area_dyn<'c, R>(
Area::new(area_id)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.default_width(ctx.style().spacing.tooltip_width)
.constrain_to(ctx.screen_rect())
.interactable(false)
.show(ctx, |ui| {
Frame::popup(&ctx.style())
.show(ui, |ui| {
ui.set_max_width(ui.spacing().tooltip_width);
add_contents(ui)
})
.inner
Frame::popup(&ctx.style()).show_dyn(ui, add_contents).inner
})
}

Expand Down
20 changes: 5 additions & 15 deletions crates/egui/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ pub(crate) fn submenu_button<R>(
}

/// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>(
fn menu_popup<'c, R>(
ctx: &Context,
menu_id: Id,
menu_state_arc: &Arc<RwLock<MenuState>>,
menu_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
let pos = {
Expand All @@ -150,14 +150,14 @@ pub(crate) fn menu_ui<'c, R>(
.fixed_pos(pos)
.constrain_to(ctx.screen_rect())
.interactable(true)
.default_width(ctx.style().spacing.menu_width)
.sense(Sense::hover());

let area_response = area.show(ctx, |ui| {
set_menu_style(ui.style_mut());

Frame::menu(ui.style())
.show(ui, |ui| {
ui.set_max_width(ui.spacing().menu_width);
ui.set_menu_state(Some(menu_state_arc.clone()));
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
.inner
Expand Down Expand Up @@ -306,8 +306,7 @@ impl MenuRoot {
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (MenuResponse, Option<InnerResponse<R>>) {
if self.id == button.id {
let inner_response =
MenuState::show(&button.ctx, &self.menu_state, self.id, add_contents);
let inner_response = menu_popup(&button.ctx, &self.menu_state, self.id, add_contents);
let menu_state = self.menu_state.read();

if menu_state.response.is_close() {
Expand Down Expand Up @@ -593,23 +592,14 @@ impl MenuState {
self.response = MenuResponse::Close;
}

pub fn show<R>(
ctx: &Context,
menu_state: &Arc<RwLock<Self>>,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
crate::menu::menu_ui(ctx, id, menu_state, add_contents)
}

fn show_submenu<R>(
&mut self,
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = Self::show(ctx, sub, id, add_contents);
let inner_response = menu_popup(ctx, sub, id, add_contents);
(sub.read().response, inner_response.inner)
})?;
self.cascade_close_response(sub_response);
Expand Down
21 changes: 19 additions & 2 deletions crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,21 @@ pub struct Spacing {
/// This is the spacing between the icon and the text
pub icon_spacing: f32,

/// The size used for the [`Ui::max_rect`] the first frame.
///
/// Text will wrap at this width, and images that expand to fill the available space
/// will expand to this size.
///
/// If the contents are smaller than this size, the area will shrink to fit the contents.
/// If the contents overflow, the area will grow.
pub default_area_size: Vec2,

/// Width of a tooltip (`on_hover_ui`, `on_hover_text` etc).
pub tooltip_width: f32,

/// The default width of a menu.
/// The default wrapping width of a menu.
///
/// Items longer than this will wrap to a new line.
pub menu_width: f32,

/// Horizontal distance between a menu and a submenu.
Expand Down Expand Up @@ -1073,8 +1084,9 @@ impl Default for Spacing {
icon_width: 14.0,
icon_width_inner: 8.0,
icon_spacing: 4.0,
default_area_size: vec2(600.0, 400.0),
tooltip_width: 600.0,
menu_width: 150.0,
menu_width: 400.0,
menu_spacing: 2.0,
combo_height: 200.0,
scroll: Default::default(),
Expand Down Expand Up @@ -1459,6 +1471,7 @@ impl Spacing {
icon_width,
icon_width_inner,
icon_spacing,
default_area_size,
tooltip_width,
menu_width,
menu_spacing,
Expand Down Expand Up @@ -1509,6 +1522,10 @@ impl Spacing {
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
ui.end_row();

ui.label("Default area size");
ui.add(two_drag_values(default_area_size, 0.0..=1000.0));
ui.end_row();

ui.label("TextEdit width");
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
ui.end_row();
Expand Down
37 changes: 36 additions & 1 deletion crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub struct Ui {
/// and all widgets will assume a gray style.
enabled: bool,

/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
sizing_pass: bool,

/// Indicates whether this Ui belongs to a Menu.
menu_state: Option<Arc<RwLock<MenuState>>>,
}
Expand All @@ -82,6 +86,7 @@ impl Ui {
style,
placer: Placer::new(max_rect, Layout::default()),
enabled: true,
sizing_pass: false,
menu_state: None,
};

Expand All @@ -108,9 +113,18 @@ impl Ui {
pub fn child_ui_with_id_source(
&mut self,
max_rect: Rect,
layout: Layout,
mut layout: Layout,
id_source: impl Hash,
) -> Self {
if self.sizing_pass {
// During the sizing pass we want widgets to use up as little space as possible,
// so that we measure the only the space we _need_.
layout.cross_justify = false;
if layout.cross_align == Align::Center {
layout.cross_align = Align::Min;
}
}

debug_assert!(!max_rect.any_nan());
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
Expand All @@ -121,6 +135,7 @@ impl Ui {
style: self.style.clone(),
placer: Placer::new(max_rect, layout),
enabled: self.enabled,
sizing_pass: self.sizing_pass,
menu_state: self.menu_state.clone(),
};

Expand All @@ -140,6 +155,26 @@ impl Ui {

// -------------------------------------------------

/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
///
/// This will also turn the Ui invisible.
/// Should be called right after [`Self::new`], if at all.
#[inline]
pub fn set_sizing_pass(&mut self) {
self.sizing_pass = true;
self.set_visible(false);
}

/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
#[inline]
pub fn is_sizing_pass(&self) -> bool {
self.sizing_pass
}

// -------------------------------------------------

/// A unique identity of this [`Ui`].
#[inline]
pub fn id(&self) -> Id {
Expand Down
Loading
Loading