diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index e287444b295..37d151bad08 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -6,6 +6,29 @@ use crate::*; // ---------------------------------------------------------------------------- +fn when_was_a_toolip_last_shown_id() -> Id { + Id::new("when_was_a_toolip_last_shown") +} + +pub fn seconds_since_last_tooltip(ctx: &Context) -> f32 { + let when_was_a_toolip_last_shown = + ctx.data(|d| d.get_temp::(when_was_a_toolip_last_shown_id())); + + if let Some(when_was_a_toolip_last_shown) = when_was_a_toolip_last_shown { + let now = ctx.input(|i| i.time); + (now - when_was_a_toolip_last_shown) as f32 + } else { + f32::INFINITY + } +} + +fn remember_that_tooltip_was_shown(ctx: &Context) { + let now = ctx.input(|i| i.time); + ctx.data_mut(|data| data.insert_temp::(when_was_a_toolip_last_shown_id(), now)); +} + +// ---------------------------------------------------------------------------- + /// Show a tooltip at the current pointer position (if any). /// /// Most of the time it is easier to use [`Response::on_hover_ui`]. @@ -17,7 +40,7 @@ use crate::*; /// ``` /// # egui::__run_test_ui(|ui| { /// if ui.ui_contains_pointer() { -/// egui::show_tooltip(ui.ctx(), egui::Id::new("my_tooltip"), |ui| { +/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { /// ui.label("Helpful text"); /// }); /// } @@ -25,10 +48,11 @@ use crate::*; /// ``` pub fn show_tooltip( ctx: &Context, + parent_layer: LayerId, widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - show_tooltip_at_pointer(ctx, widget_id, add_contents) + show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents) } /// Show a tooltip at the current pointer position (if any). @@ -42,7 +66,7 @@ pub fn show_tooltip( /// ``` /// # egui::__run_test_ui(|ui| { /// if ui.ui_contains_pointer() { -/// egui::show_tooltip_at_pointer(ui.ctx(), egui::Id::new("my_tooltip"), |ui| { +/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { /// ui.label("Helpful text"); /// }); /// } @@ -50,11 +74,18 @@ pub fn show_tooltip( /// ``` pub fn show_tooltip_at_pointer( ctx: &Context, + parent_layer: LayerId, widget_id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { ctx.input(|i| i.pointer.hover_pos()).map(|pointer_pos| { - show_tooltip_at(ctx, widget_id, pointer_pos + vec2(16.0, 16.0), add_contents) + show_tooltip_at( + ctx, + parent_layer, + widget_id, + pointer_pos + vec2(16.0, 16.0), + add_contents, + ) }) } @@ -63,14 +94,16 @@ pub fn show_tooltip_at_pointer( /// If the tooltip does not fit under the area, it tries to place it above it instead. pub fn show_tooltip_for( ctx: &Context, + parent_layer: LayerId, widget_id: Id, widget_rect: &Rect, add_contents: impl FnOnce(&mut Ui) -> R, ) -> R { let is_touch_screen = ctx.input(|i| i.any_touches()); let allow_placing_below = !is_touch_screen; // There is a finger below. - show_tooltip_at_avoid_dyn( + show_tooltip_at_dyn( ctx, + parent_layer, widget_id, allow_placing_below, widget_rect, @@ -83,14 +116,16 @@ pub fn show_tooltip_for( /// Returns `None` if the tooltip could not be placed. pub fn show_tooltip_at( ctx: &Context, + parent_layer: LayerId, widget_id: Id, suggested_position: Pos2, add_contents: impl FnOnce(&mut Ui) -> R, ) -> R { let allow_placing_below = true; let rect = Rect::from_center_size(suggested_position, Vec2::ZERO); - show_tooltip_at_avoid_dyn( + show_tooltip_at_dyn( ctx, + parent_layer, widget_id, allow_placing_below, &rect, @@ -98,21 +133,34 @@ pub fn show_tooltip_at( ) } -fn show_tooltip_at_avoid_dyn<'c, R>( +fn show_tooltip_at_dyn<'c, R>( ctx: &Context, + parent_layer: LayerId, widget_id: Id, allow_placing_below: bool, widget_rect: &Rect, add_contents: Box R + 'c>, ) -> R { - // if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work. - let mut state = ctx.frame_state(|fs| { - fs.tooltip_state + let mut widget_rect = *widget_rect; + if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { + widget_rect = transform * widget_rect; + } + + remember_that_tooltip_was_shown(ctx); + + let mut state = ctx.frame_state_mut(|fs| { + // Remember that this is the widget showing the tooltip: + fs.layers + .entry(parent_layer) + .or_default() + .widget_with_tooltip = Some(widget_id); + + fs.tooltips .widget_tooltips .get(&widget_id) .copied() .unwrap_or(PerWidgetTooltipState { - bounding_rect: *widget_rect, + bounding_rect: widget_rect, tooltip_count: 0, }) }); @@ -151,7 +199,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( state.tooltip_count += 1; state.bounding_rect = state.bounding_rect.union(response.rect); - ctx.frame_state_mut(|fs| fs.tooltip_state.widget_tooltips.insert(widget_id, state)); + ctx.frame_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); inner } @@ -159,7 +207,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( /// What is the id of the next tooltip for this widget? pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id { let tooltip_count = ctx.frame_state(|fs| { - fs.tooltip_state + fs.tooltips .widget_tooltips .get(&widget_id) .map_or(0, |state| state.tooltip_count) @@ -235,12 +283,17 @@ fn find_tooltip_position( /// ``` /// # egui::__run_test_ui(|ui| { /// if ui.ui_contains_pointer() { -/// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text"); +/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text"); /// } /// # }); /// ``` -pub fn show_tooltip_text(ctx: &Context, widget_id: Id, text: impl Into) -> Option<()> { - show_tooltip(ctx, widget_id, |ui| { +pub fn show_tooltip_text( + ctx: &Context, + parent_layer: LayerId, + widget_id: Id, + text: impl Into, +) -> Option<()> { + show_tooltip(ctx, parent_layer, widget_id, |ui| { crate::widgets::Label::new(text).ui(ui); }) } @@ -323,53 +376,61 @@ pub fn popup_above_or_below_widget( close_behavior: PopupCloseBehavior, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - if parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { - let (mut pos, pivot) = match above_or_below { - AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), - AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), - }; - if let Some(transform) = parent_ui - .ctx() - .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) - { - pos = transform * pos; - } + if !parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { + return None; + } + + let (mut pos, pivot) = match above_or_below { + AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), + AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), + }; + if let Some(transform) = parent_ui + .ctx() + .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) + { + pos = transform * pos; + } - let frame = Frame::popup(parent_ui.style()); - let frame_margin = frame.total_margin(); - let inner_width = widget_response.rect.width() - frame_margin.sum().x; - - let response = Area::new(popup_id) - .kind(UiKind::Popup) - .order(Order::Foreground) - .fixed_pos(pos) - .default_width(inner_width) - .pivot(pivot) - .show(parent_ui.ctx(), |ui| { - frame - .show(ui, |ui| { - ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { - ui.set_min_width(inner_width); - add_contents(ui) - }) - .inner + let frame = Frame::popup(parent_ui.style()); + let frame_margin = frame.total_margin(); + let inner_width = widget_response.rect.width() - frame_margin.sum().x; + + parent_ui.ctx().frame_state_mut(|fs| { + fs.layers + .entry(parent_ui.layer_id()) + .or_default() + .open_popups + .insert(popup_id) + }); + + let response = Area::new(popup_id) + .kind(UiKind::Popup) + .order(Order::Foreground) + .fixed_pos(pos) + .default_width(inner_width) + .pivot(pivot) + .show(parent_ui.ctx(), |ui| { + frame + .show(ui, |ui| { + ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { + ui.set_min_width(inner_width); + add_contents(ui) }) .inner - }); - - let should_close = match close_behavior { - PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), - PopupCloseBehavior::CloseOnClickOutside => { - widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() - } - PopupCloseBehavior::IgnoreClicks => false, - }; - - if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close { - parent_ui.memory_mut(|mem| mem.close_popup()); + }) + .inner + }); + + let should_close = match close_behavior { + PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), + PopupCloseBehavior::CloseOnClickOutside => { + widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() } - Some(response.inner) - } else { - None + PopupCloseBehavior::IgnoreClicks => false, + }; + + if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close { + parent_ui.memory_mut(|mem| mem.close_popup()); } + Some(response.inner) } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 4643b17d931..c1c179f2ca0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -220,18 +220,17 @@ pub struct ViewportState { pub input: InputState, - /// State that is collected during a frame and then cleared - pub frame_state: FrameState, + /// State that is collected during a frame and then cleared. + pub this_frame: FrameState, + + /// The final [`FrameState`] from last frame. + /// + /// Only read from. + pub prev_frame: FrameState, /// Has this viewport been updated this frame? pub used: bool, - /// Written to during the frame. - pub widgets_this_frame: WidgetRects, - - /// Read - pub widgets_prev_frame: WidgetRects, - /// State related to repaint scheduling. repaint: ViewportRepaintInfo, @@ -451,12 +450,12 @@ impl ContextImpl { let screen_rect = viewport.input.screen_rect; - viewport.frame_state.begin_frame(screen_rect); + viewport.this_frame.begin_frame(screen_rect); { let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.widgets_prev_frame.layer_ids().collect(); + let mut layers: Vec = viewport.prev_frame.widgets.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -472,7 +471,7 @@ impl ContextImpl { let interact_radius = self.memory.options.style.interaction.interact_radius; crate::hit_test::hit_test( - &viewport.widgets_prev_frame, + &viewport.prev_frame.widgets, &layers, &self.memory.layer_transforms, pos, @@ -484,7 +483,7 @@ impl ContextImpl { viewport.interact_widgets = crate::interaction::interact( &viewport.interact_widgets, - &viewport.widgets_prev_frame, + &viewport.prev_frame.widgets, &viewport.hits, &viewport.input, self.memory.interaction_mut(), @@ -513,7 +512,7 @@ impl ContextImpl { builder.set_transform(accesskit::Affine::scale(pixels_per_point.into())); let mut node_builders = IdMap::default(); node_builders.insert(id, builder); - viewport.frame_state.accesskit_state = Some(AccessKitFrameState { + viewport.this_frame.accesskit_state = Some(AccessKitFrameState { node_builders, parent_stack: vec![id], }); @@ -573,12 +572,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { - let state = self - .viewport() - .frame_state - .accesskit_state - .as_mut() - .unwrap(); + let state = self.viewport().this_frame.accesskit_state.as_mut().unwrap(); let builders = &mut state.node_builders; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); @@ -774,8 +768,11 @@ impl Context { /// ``` pub fn begin_frame(&self, new_input: RawInput) { crate::profile_function!(); - self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); + self.write(|ctx| ctx.begin_frame_mut(new_input)); + + // Plugs run just after the frame has started: + self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); } } @@ -876,15 +873,27 @@ impl Context { } /// Read-only access to [`FrameState`]. + /// + /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. #[inline] pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.write(move |ctx| reader(&ctx.viewport().frame_state)) + self.write(move |ctx| reader(&ctx.viewport().this_frame)) } /// Read-write access to [`FrameState`]. + /// + /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. #[inline] pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.viewport().frame_state)) + self.write(move |ctx| writer(&mut ctx.viewport().this_frame)) + } + + /// Read-only access to the [`FrameState`] from the previous frame. + /// + /// This is swapped at the end of each frame. + #[inline] + pub(crate) fn prev_frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().prev_frame)) } /// Read-only access to [`Fonts`]. @@ -1030,7 +1039,7 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.widgets_this_frame.insert(w.layer_id, w); + viewport.this_frame.widgets.insert(w.layer_id, w); if w.sense.focusable { ctx.memory.interested_in_focus(w.id); @@ -1069,9 +1078,10 @@ impl Context { self.write(|ctx| { let viewport = ctx.viewport(); viewport - .widgets_this_frame + .this_frame + .widgets .get(id) - .or_else(|| viewport.widgets_prev_frame.get(id)) + .or_else(|| viewport.prev_frame.widgets.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1095,7 +1105,8 @@ impl Context { enabled, } = widget_rect; - let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id)); + // previous frame + "highlight next frame" == "highlight this frame" + let highlighted = self.prev_frame_state(|fs| fs.highlight_next_frame.contains(&id)); let mut res = Response { ctx: self.clone(), @@ -1216,7 +1227,7 @@ impl Context { #[cfg(debug_assertions)] self.write(|ctx| { if ctx.memory.options.style.debug.show_interactive_widgets { - ctx.viewport().widgets_this_frame.set_info(id, make_info()); + ctx.viewport().this_frame.widgets.set_info(id, make_info()); } }); @@ -1807,6 +1818,7 @@ impl Context { crate::gui_zoom::zoom_with_keyboard(self); } + // Plugins run just before the frame ends. self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); #[cfg(debug_assertions)] @@ -1828,7 +1840,7 @@ impl Context { let paint_widget_id = |id: Id, text: &str, color: Color32| { if let Some(widget) = - self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).copied()) + self.write(|ctx| ctx.viewport().this_frame.widgets.get(id).copied()) { paint_widget(&widget, text, color); } @@ -1836,7 +1848,7 @@ impl Context { if self.style().debug.show_interactive_widgets { // Show all interactive widgets: - let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); + let rects = self.write(|ctx| ctx.viewport().this_frame.widgets.clone()); for (layer_id, rects) in rects.layers() { let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { @@ -1874,7 +1886,7 @@ impl Context { paint_widget_id(id, "contains_pointer", Color32::BLUE); } - let widget_rects = self.write(|w| w.viewport().widgets_this_frame.clone()); + let widget_rects = self.write(|w| w.viewport().this_frame.widgets.clone()); let mut contains_pointer: Vec = contains_pointer.iter().copied().collect(); contains_pointer.sort_by_key(|&id| { @@ -1941,7 +1953,7 @@ impl ContextImpl { viewport.repaint.frame_nr += 1; - self.memory.end_frame(&viewport.frame_state.used_ids); + self.memory.end_frame(&viewport.this_frame.used_ids); if let Some(fonts) = self.fonts.get(&pixels_per_point.into()) { let tex_mngr = &mut self.tex_manager.0.write(); @@ -1978,7 +1990,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] { crate::profile_scope!("accesskit"); - let state = viewport.frame_state.accesskit_state.take(); + let state = viewport.this_frame.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); let nodes = { @@ -2011,21 +2023,15 @@ impl ContextImpl { let mut repaint_needed = false; - { - if self.memory.options.repaint_on_widget_change { - crate::profile_function!("compare-widget-rects"); - if viewport.widgets_prev_frame != viewport.widgets_this_frame { - repaint_needed = true; // Some widget has moved - } + if self.memory.options.repaint_on_widget_change { + crate::profile_function!("compare-widget-rects"); + if viewport.prev_frame.widgets != viewport.this_frame.widgets { + repaint_needed = true; // Some widget has moved } - - std::mem::swap( - &mut viewport.widgets_prev_frame, - &mut viewport.widgets_this_frame, - ); - viewport.widgets_this_frame.clear(); } + std::mem::swap(&mut viewport.prev_frame, &mut viewport.this_frame); + if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); } else if let Some(delay) = viewport.input.wants_repaint_after() { @@ -2208,7 +2214,7 @@ impl Context { /// How much space is used by panels and windows. pub fn used_rect(&self) -> Rect { self.write(|ctx| { - let mut used = ctx.viewport().frame_state.used_by_panels; + let mut used = ctx.viewport().this_frame.used_by_panels; for (_id, window) in ctx.memory.areas().visible_windows() { used = used.union(window.rect()); } @@ -2875,7 +2881,7 @@ impl Context { ) -> Option { self.write(|ctx| { ctx.viewport() - .frame_state + .this_frame .accesskit_state .is_some() .then(|| ctx.accesskit_node_builder(id)) diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 1a4f5a71161..184ab0d64a8 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -1,13 +1,19 @@ +use ahash::{HashMap, HashSet}; + use crate::{id::IdSet, *}; +/// Reset at the start of each frame. #[derive(Clone, Debug, Default)] pub struct TooltipFrameState { + /// If a tooltip has been shown this frame, where was it? + /// This is used to prevent multiple tooltips to cover each other. pub widget_tooltips: IdMap, } impl TooltipFrameState { pub fn clear(&mut self) { - self.widget_tooltips.clear(); + let Self { widget_tooltips } = self; + widget_tooltips.clear(); } } @@ -20,6 +26,20 @@ pub struct PerWidgetTooltipState { pub tooltip_count: usize, } +#[derive(Clone, Debug, Default)] +pub struct PerLayerState { + /// Is there any open popup (menus, combo-boxes, etc)? + /// + /// Does NOT include tooltips. + pub open_popups: HashSet, + + /// Which widget is showing a tooltip (if any)? + /// + /// Only one widget per layer may show a tooltip. + /// But if a tooltip contains a tooltip, you can show a tooltip on top of a tooltip. + pub widget_with_tooltip: Option, +} + #[cfg(feature = "accesskit")] #[derive(Clone)] pub struct AccessKitFrameState { @@ -27,13 +47,25 @@ pub struct AccessKitFrameState { pub parent_stack: Vec, } -/// State that is collected during a frame and then cleared. -/// Short-term (single frame) memory. +/// State that is collected during a frame, then saved for the next frame, +/// and then cleared. +/// +/// One per viewport. #[derive(Clone)] pub struct FrameState { /// All [`Id`]s that were used this frame. pub used_ids: IdMap, + /// All widgets produced this frame. + pub widgets: WidgetRects, + + /// Per-layer state. + /// + /// Not all layers registers themselves there though. + pub layers: HashMap, + + pub tooltips: TooltipFrameState, + /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`CentralPanel`] does not change this. /// This is the area available to Window's. @@ -46,11 +78,6 @@ pub struct FrameState { /// How much space is used by panels. pub used_by_panels: Rect, - /// If a tooltip has been shown this frame, where was it? - /// This is used to prevent multiple tooltips to cover each other. - /// Reset at the start of each frame. - pub tooltip_state: TooltipFrameState, - /// The current scroll area should scroll to this range (horizontal, vertical). pub scroll_target: [Option<(Rangef, Option)>; 2], @@ -68,10 +95,7 @@ pub struct FrameState { #[cfg(feature = "accesskit")] pub accesskit_state: Option, - /// Highlight these widgets this next frame. Read from this. - pub highlight_this_frame: IdSet, - - /// Highlight these widgets the next frame. Write to this. + /// Highlight these widgets the next frame. pub highlight_next_frame: IdSet, #[cfg(debug_assertions)] @@ -82,15 +106,16 @@ impl Default for FrameState { fn default() -> Self { Self { used_ids: Default::default(), + widgets: Default::default(), + layers: Default::default(), + tooltips: Default::default(), available_rect: Rect::NAN, unused_rect: Rect::NAN, used_by_panels: Rect::NAN, - tooltip_state: Default::default(), scroll_target: [None, None], scroll_delta: Vec2::default(), #[cfg(feature = "accesskit")] accesskit_state: None, - highlight_this_frame: Default::default(), highlight_next_frame: Default::default(), #[cfg(debug_assertions)] @@ -104,15 +129,16 @@ impl FrameState { crate::profile_function!(); let Self { used_ids, + widgets, + tooltips, + layers, available_rect, unused_rect, used_by_panels, - tooltip_state, scroll_target, scroll_delta, #[cfg(feature = "accesskit")] accesskit_state, - highlight_this_frame, highlight_next_frame, #[cfg(debug_assertions)] @@ -120,10 +146,12 @@ impl FrameState { } = self; used_ids.clear(); + widgets.clear(); + tooltips.clear(); + layers.clear(); *available_rect = screen_rect; *unused_rect = screen_rect; *used_by_panels = Rect::NOTHING; - tooltip_state.clear(); *scroll_target = [None, None]; *scroll_delta = Vec2::default(); @@ -137,7 +165,7 @@ impl FrameState { *accesskit_state = None; } - *highlight_this_frame = std::mem::take(highlight_next_frame); + highlight_next_frame.clear(); } /// How much space is still available after panels has been added. diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index e2fd693b786..abd6028b9c7 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -726,6 +726,9 @@ pub struct PointerState { /// Current velocity of pointer. velocity: Vec2, + /// Current direction of pointer. + direction: Vec2, + /// Recent movement of the pointer. /// Used for calculating velocity of pointer. pos_history: History, @@ -774,7 +777,8 @@ impl Default for PointerState { delta: Vec2::ZERO, motion: None, velocity: Vec2::ZERO, - pos_history: History::new(0..1000, 0.1), + direction: Vec2::ZERO, + pos_history: History::new(2..1000, 0.1), down: Default::default(), press_origin: None, press_start_time: None, @@ -889,6 +893,7 @@ impl PointerState { // When dragging a slider and the mouse leaves the viewport, we still want the drag to work, // so we don't treat this as a `PointerEvent::Released`. // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. + self.pos_history.clear(); } Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, _ => {} @@ -920,6 +925,8 @@ impl PointerState { self.last_move_time = time; } + self.direction = self.pos_history.velocity().unwrap_or_default().normalized(); + self.started_decidedly_dragging = self.is_decidedly_dragging() && !was_decidedly_dragging; self @@ -944,11 +951,22 @@ impl PointerState { } /// Current velocity of pointer. + /// + /// This is smoothed over a few frames, + /// but can be ZERO when frame-rate is bad. #[inline(always)] pub fn velocity(&self) -> Vec2 { self.velocity } + /// Current direction of the pointer. + /// + /// This is less sensitive to bad framerate than [`Self::velocity`]. + #[inline(always)] + pub fn direction(&self) -> Vec2 { + self.direction + } + /// Where did the current click/drag originate? /// `None` if no mouse button is down. #[inline(always)] @@ -1284,6 +1302,7 @@ impl PointerState { delta, motion, velocity, + direction, pos_history: _, down, press_origin, @@ -1304,6 +1323,7 @@ impl PointerState { "velocity: [{:3.0} {:3.0}] points/sec", velocity.x, velocity.y )); + ui.label(format!("direction: {direction:?}")); ui.label(format!("down: {down:#?}")); ui.label(format!("press_origin: {press_origin:?}")); ui.label(format!("press_start_time: {press_start_time:?} s")); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 4980f6f9823..893a1bd540b 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -135,6 +135,7 @@ pub(crate) fn submenu_button( /// wrapper for the contents of every menu. fn menu_popup<'c, R>( ctx: &Context, + parent_layer: LayerId, menu_state_arc: &Arc>, menu_id: Id, add_contents: impl FnOnce(&mut Ui) -> R + 'c, @@ -145,7 +146,17 @@ fn menu_popup<'c, R>( menu_state.rect.min }; - let area = Area::new(menu_id.with("__menu")) + let area_id = menu_id.with("__menu"); + + ctx.frame_state_mut(|fs| { + fs.layers + .entry(parent_layer) + .or_default() + .open_popups + .insert(area_id) + }); + + let area = Area::new(area_id) .kind(UiKind::Menu) .order(Order::Foreground) .fixed_pos(pos) @@ -320,7 +331,13 @@ impl MenuRoot { add_contents: impl FnOnce(&mut Ui) -> R, ) -> (MenuResponse, Option>) { if self.id == button.id { - let inner_response = menu_popup(&button.ctx, &self.menu_state, self.id, add_contents); + let inner_response = menu_popup( + &button.ctx, + button.layer_id, + &self.menu_state, + self.id, + add_contents, + ); let menu_state = self.menu_state.read(); let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape)); @@ -580,10 +597,10 @@ impl SubMenu { self.parent_state .write() .submenu_button_interaction(ui, sub_id, &response); - let inner = self - .parent_state - .write() - .show_submenu(ui.ctx(), sub_id, add_contents); + let inner = + self.parent_state + .write() + .show_submenu(ui.ctx(), ui.layer_id(), sub_id, add_contents); InnerResponse::new(inner, response) } } @@ -624,11 +641,12 @@ impl MenuState { fn show_submenu( &mut self, ctx: &Context, + parent_layer: LayerId, id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { let (sub_response, response) = self.submenu(id).map(|sub| { - let inner_response = menu_popup(ctx, sub, id, add_contents); + let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents); (sub.read().response, inner_response.inner) })?; self.cascade_close_response(sub_response); @@ -683,7 +701,7 @@ impl MenuState { if let Some(sub_menu) = self.current_submenu() { if let Some(pos) = pointer.hover_pos() { let rect = sub_menu.read().rect; - return rect.intersects_ray(pos, pointer.velocity().normalized()); + return rect.intersects_ray(pos, pointer.direction().normalized()); } } false diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 534524e32fe..8ad0066923a 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -290,11 +290,14 @@ impl Painter { let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color); let rect = anchor.anchor_size(pos, galley.size()); let frame_rect = rect.expand(2.0); - self.add(Shape::rect_filled( - frame_rect, - 0.0, - Color32::from_black_alpha(150), - )); + + let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5; + let bg_color = if is_text_bright { + Color32::from_black_alpha(150) + } else { + Color32::from_white_alpha(150) + }; + self.add(Shape::rect_filled(frame_rect, 0.0, bg_color)); self.galley(rect.min, galley, color); frame_rect } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index aee287176f2..04b11fde280 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, - menu, AreaState, ComboBox, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui, - WidgetRect, WidgetText, + menu, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui, WidgetRect, + WidgetText, }; // ---------------------------------------------------------------------------- @@ -545,7 +545,13 @@ impl Response { /// Show this UI when hovering if the widget is disabled. pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { if !self.enabled && self.should_show_hover_ui() { - crate::containers::show_tooltip_for(&self.ctx, self.id, &self.rect, add_contents); + crate::containers::show_tooltip_for( + &self.ctx, + self.layer_id, + self.id, + &self.rect, + add_contents, + ); } self } @@ -553,7 +559,12 @@ impl Response { /// Like `on_hover_ui`, but show the ui next to cursor. pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self { if self.enabled && self.should_show_hover_ui() { - crate::containers::show_tooltip_at_pointer(&self.ctx, self.id, add_contents); + crate::containers::show_tooltip_at_pointer( + &self.ctx, + self.layer_id, + self.id, + add_contents, + ); } self } @@ -562,14 +573,13 @@ impl Response { /// /// This can be used to give attention to a widget during a tutorial. pub fn show_tooltip_ui(&self, add_contents: impl FnOnce(&mut Ui)) { - let mut rect = self.rect; - if let Some(transform) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) - { - rect = transform * rect; - } - crate::containers::show_tooltip_for(&self.ctx, self.id, &rect, add_contents); + crate::containers::show_tooltip_for( + &self.ctx, + self.layer_id, + self.id, + &self.rect, + add_contents, + ); } /// Always show this tooltip, even if disabled and the user isn't hovering it. @@ -591,12 +601,22 @@ impl Response { return true; } - let is_tooltip_open = self.is_tooltip_open(); + let any_open_popups = self.ctx.prev_frame_state(|fs| { + fs.layers + .get(&self.layer_id) + .map_or(false, |layer| !layer.open_popups.is_empty()) + }); + if any_open_popups { + // Hide tooltips if the user opens a popup (menu, combo-box, etc) in the same layer. + return false; + } - if is_tooltip_open { - let (pointer_pos, pointer_vel) = self + let is_our_tooltip_open = self.is_tooltip_open(); + + if is_our_tooltip_open { + let (pointer_pos, pointer_dir) = self .ctx - .input(|i| (i.pointer.hover_pos(), i.pointer.velocity())); + .input(|i| (i.pointer.hover_pos(), i.pointer.direction())); if let Some(pointer_pos) = pointer_pos { if self.rect.contains(pointer_pos) { @@ -609,9 +629,10 @@ impl Response { let layer_id = LayerId::new(Order::Tooltip, tooltip_id); let tooltip_has_interactive_widget = self.ctx.viewport(|vp| { - vp.widgets_prev_frame + vp.prev_frame + .widgets .get_layer(layer_id) - .any(|w| w.sense.interactive()) + .any(|w| w.enabled && w.sense.interactive()) }); if tooltip_has_interactive_widget { @@ -624,7 +645,7 @@ impl Response { if let Some(pos) = pointer_pos { let pointer_in_area_or_on_the_way_there = rect.contains(pos) - || rect.intersects_ray(pos, pointer_vel.normalized()); + || rect.intersects_ray(pos, pointer_dir.normalized()); if pointer_in_area_or_on_the_way_there { return true; @@ -634,6 +655,22 @@ impl Response { } } + let is_other_tooltip_open = self.ctx.prev_frame_state(|fs| { + if let Some(already_open_tooltip) = fs + .layers + .get(&self.layer_id) + .and_then(|layer| layer.widget_with_tooltip) + { + already_open_tooltip != self.id + } else { + false + } + }); + if is_other_tooltip_open { + // We only allow one tooltip per layer. First one wins. It is up to that tooltip to close itself. + return false; + } + // Fast early-outs: if self.enabled { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { @@ -643,21 +680,6 @@ impl Response { return false; } - if self.context_menu_opened() { - return false; - } - - if ComboBox::is_open(&self.ctx, self.id) { - return false; // Don't cover the open ComboBox with a tooltip - } - - let when_was_a_toolip_last_shown_id = Id::new("when_was_a_toolip_last_shown"); - let now = self.ctx.input(|i| i.time); - - let when_was_a_toolip_last_shown = self - .ctx - .data(|d| d.get_temp::(when_was_a_toolip_last_shown_id)); - let tooltip_delay = self.ctx.style().interaction.tooltip_delay; let tooltip_grace_time = self.ctx.style().interaction.tooltip_grace_time; @@ -666,10 +688,10 @@ impl Response { // another widget should show the tooltip for that widget right away. // Let the user quickly move over some dead space to hover the next thing - let tooltip_was_recently_shown = when_was_a_toolip_last_shown - .map_or(false, |time| ((now - time) as f32) < tooltip_grace_time); + let tooltip_was_recently_shown = + crate::popup::seconds_since_last_tooltip(&self.ctx) < tooltip_grace_time; - if !tooltip_was_recently_shown && !is_tooltip_open { + if !tooltip_was_recently_shown && !is_our_tooltip_open { if self.ctx.style().interaction.show_tooltips_only_when_still { // We only show the tooltip when the mouse pointer is still. if !self.ctx.input(|i| i.pointer.is_still()) { @@ -702,10 +724,6 @@ impl Response { // All checks passed: show the tooltip! - // Remember that we're showing a tooltip - self.ctx - .data_mut(|data| data.insert_temp::(when_was_a_toolip_last_shown_id, now)); - true } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index ec5a4729eee..7988820d6af 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -154,13 +154,15 @@ impl LabelSelectionState { } pub fn load(ctx: &Context) -> Self { - ctx.data(|data| data.get_temp::(Id::NULL)) + let id = Id::new(ctx.viewport_id()); + ctx.data(|data| data.get_temp::(id)) .unwrap_or_default() } pub fn store(self, ctx: &Context) { + let id = Id::new(ctx.viewport_id()); ctx.data_mut(|data| { - data.insert_temp(Id::NULL, self); + data.insert_temp(id, self); }); } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index f24a8a3792a..a7a7712b7e2 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2621,13 +2621,32 @@ fn register_rect(ui: &Ui, rect: Rect) { // Paint rectangle around widget: { + // Print width and height: + let text_color = if ui.visuals().dark_mode { + Color32::WHITE + } else { + Color32::BLACK + }; + painter.debug_text( + rect.left_center() + 2.0 * Vec2::LEFT, + Align2::RIGHT_CENTER, + text_color, + format!("H: {:.1}", rect.height()), + ); + painter.debug_text( + rect.center_top(), + Align2::CENTER_BOTTOM, + text_color, + format!("W: {:.1}", rect.width()), + ); + + // Paint rect: let rect_fg_color = if is_clicking { Color32::WHITE } else { Color32::LIGHT_BLUE }; let rect_bg_color = Color32::BLUE.gamma_multiply(0.5); - painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color)); } @@ -2655,7 +2674,7 @@ fn register_rect(ui: &Ui, rect: Rect) { let screen_rect = ui.ctx().screen_rect(); let y = if galley.size().y <= rect.top() { // Above - rect.top() - galley.size().y + rect.top() - galley.size().y - 16.0 } else { // Below rect.bottom() diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 0772e76347e..d2085d4fb49 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -68,6 +68,14 @@ impl Label { self } + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Extend`], + /// disabling wrapping and truncating, and instead expanding the parent [`Ui`]. + #[inline] + pub fn extend(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Extend); + self + } + /// Can the user select the text with the mouse? /// /// Overrides [`crate::style::Interaction::selectable_labels`]. diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 3d4eac39bdf..b0b8747dff3 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -107,12 +107,12 @@ impl Default for Tests { fn default() -> Self { Self::from_demos(vec![ Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), - Box::::default(), Box::::default(), ]) } diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index f059803eda2..a52d07e64dd 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -58,6 +58,8 @@ const NUM_MANUAL_ROWS: usize = 20; impl crate::View for TableDemo { fn ui(&mut self, ui: &mut egui::Ui) { + let mut reset = false; + ui.vertical(|ui| { ui.horizontal(|ui| { ui.checkbox(&mut self.striped, "Striped"); @@ -102,6 +104,8 @@ impl crate::View for TableDemo { self.scroll_to_row = Some(self.scroll_to_row_slider); } } + + reset = ui.button("Reset").clicked(); }); ui.separator(); @@ -115,7 +119,7 @@ impl crate::View for TableDemo { .vertical(|mut strip| { strip.cell(|ui| { egui::ScrollArea::horizontal().show(ui, |ui| { - self.table_ui(ui); + self.table_ui(ui, reset); }); }); strip.cell(|ui| { @@ -128,7 +132,7 @@ impl crate::View for TableDemo { } impl TableDemo { - fn table_ui(&mut self, ui: &mut egui::Ui) { + fn table_ui(&mut self, ui: &mut egui::Ui, reset: bool) { use egui_extras::{Column, TableBuilder}; let text_height = egui::TextStyle::Body @@ -142,9 +146,14 @@ impl TableDemo { .resizable(self.resizable) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::auto()) + .column( + Column::remainder() + .at_least(40.0) + .clip(true) + .resizable(true), + ) .column(Column::auto()) - .column(Column::initial(100.0).range(40.0..=300.0)) - .column(Column::initial(100.0).at_least(40.0).clip(true)) + .column(Column::remainder()) .column(Column::remainder()) .min_scrolled_height(0.0) .max_scroll_height(available_height); @@ -157,19 +166,23 @@ impl TableDemo { table = table.scroll_to_row(row_index, None); } + if reset { + table.reset(); + } + table .header(20.0, |mut header| { header.col(|ui| { ui.strong("Row"); }); header.col(|ui| { - ui.strong("Interaction"); + ui.strong("Clipped text"); }); header.col(|ui| { ui.strong("Expanding content"); }); header.col(|ui| { - ui.strong("Clipped text"); + ui.strong("Interaction"); }); header.col(|ui| { ui.strong("Content"); @@ -187,13 +200,13 @@ impl TableDemo { ui.label(row_index.to_string()); }); row.col(|ui| { - ui.checkbox(&mut self.checked, "Click me"); + ui.label(long_text(row_index)); }); row.col(|ui| { expanding_content(ui); }); row.col(|ui| { - ui.label(long_text(row_index)); + ui.checkbox(&mut self.checked, "Click me"); }); row.col(|ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); @@ -217,13 +230,13 @@ impl TableDemo { ui.label(row_index.to_string()); }); row.col(|ui| { - ui.checkbox(&mut self.checked, "Click me"); + ui.label(long_text(row_index)); }); row.col(|ui| { expanding_content(ui); }); row.col(|ui| { - ui.label(long_text(row_index)); + ui.checkbox(&mut self.checked, "Click me"); }); row.col(|ui| { ui.add( @@ -245,13 +258,13 @@ impl TableDemo { ui.label(row_index.to_string()); }); row.col(|ui| { - ui.checkbox(&mut self.checked, "Click me"); + ui.label(long_text(row_index)); }); row.col(|ui| { expanding_content(ui); }); row.col(|ui| { - ui.label(long_text(row_index)); + ui.checkbox(&mut self.checked, "Click me"); }); row.col(|ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); @@ -280,14 +293,7 @@ impl TableDemo { } fn expanding_content(ui: &mut egui::Ui) { - let width = ui.available_width().clamp(20.0, 200.0); - let height = ui.available_height(); - let (rect, _response) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover()); - ui.painter().hline( - rect.x_range(), - rect.center().y, - (1.0, ui.visuals().text_color()), - ); + ui.add(egui::Separator::default().horizontal()); } fn long_text(row_index: usize) -> String { diff --git a/crates/egui_demo_lib/src/demo/tests/table_test.rs b/crates/egui_demo_lib/src/demo/tests/grid_test.rs similarity index 96% rename from crates/egui_demo_lib/src/demo/tests/table_test.rs rename to crates/egui_demo_lib/src/demo/tests/grid_test.rs index 72e39ff00dc..511aa428f67 100644 --- a/crates/egui_demo_lib/src/demo/tests/table_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/grid_test.rs @@ -1,5 +1,5 @@ #[derive(PartialEq)] -pub struct TableTest { +pub struct GridTest { num_cols: usize, num_rows: usize, min_col_width: f32, @@ -7,7 +7,7 @@ pub struct TableTest { text_length: usize, } -impl Default for TableTest { +impl Default for GridTest { fn default() -> Self { Self { num_cols: 4, @@ -19,9 +19,9 @@ impl Default for TableTest { } } -impl crate::Demo for TableTest { +impl crate::Demo for GridTest { fn name(&self) -> &'static str { - "Table Test" + "Grid Test" } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { @@ -32,7 +32,7 @@ impl crate::Demo for TableTest { } } -impl crate::View for TableTest { +impl crate::View for GridTest { fn ui(&mut self, ui: &mut egui::Ui) { ui.add( egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"), diff --git a/crates/egui_demo_lib/src/demo/tests/mod.rs b/crates/egui_demo_lib/src/demo/tests/mod.rs index 497f384c885..78ad0a01185 100644 --- a/crates/egui_demo_lib/src/demo/tests/mod.rs +++ b/crates/egui_demo_lib/src/demo/tests/mod.rs @@ -1,17 +1,17 @@ mod cursor_test; +mod grid_test; mod id_test; mod input_event_history; mod input_test; mod layout_test; mod manual_layout_test; -mod table_test; mod window_resize_test; pub use cursor_test::CursorTest; +pub use grid_test::GridTest; pub use id_test::IdTest; pub use input_event_history::InputEventHistory; pub use input_test::InputTest; pub use layout_test::LayoutTest; pub use manual_layout_test::ManualLayoutTest; -pub use table_test::TableTest; pub use window_resize_test::WindowResizeTest; diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index d75d638e47b..1252f209776 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -58,6 +58,9 @@ impl<'a> DatePickerPopup<'a> { let height = 20.0; let spacing = 2.0; ui.spacing_mut().item_spacing = Vec2::splat(spacing); + + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text + StripBuilder::new(ui) .clip(false) .sizes( diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index cc51e05d221..1c8f6b0c967 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -150,14 +150,16 @@ impl<'l> StripLayout<'l> { let used_rect = child_ui.min_rect(); - self.set_pos(max_rect); - - let allocation_rect = if flags.clip { + let allocation_rect = if self.ui.is_sizing_pass() { + used_rect + } else if flags.clip { max_rect } else { max_rect.union(used_rect) }; + self.set_pos(allocation_rect); + self.ui.advance_cursor_after_rect(allocation_rect); let response = child_ui.interact(max_rect, child_ui.id(), self.sense); @@ -191,12 +193,12 @@ impl<'l> StripLayout<'l> { fn cell( &mut self, flags: StripLayoutFlags, - rect: Rect, + max_rect: Rect, child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Ui { let mut child_ui = self.ui.child_ui_with_id_source( - rect, + max_rect, self.cell_layout, child_ui_id_source, Some(egui::UiStackInfo::new(egui::UiKind::TableCell)), @@ -205,7 +207,7 @@ impl<'l> StripLayout<'l> { if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); let margin = margin.min(0.5 * self.ui.spacing().item_spacing); - let clip_rect = rect.expand2(margin); + let clip_rect = max_rect.expand2(margin); child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect())); } diff --git a/crates/egui_extras/src/sizing.rs b/crates/egui_extras/src/sizing.rs index 2c380ae6179..770306770df 100644 --- a/crates/egui_extras/src/sizing.rs +++ b/crates/egui_extras/src/sizing.rs @@ -49,26 +49,20 @@ impl Size { /// Won't shrink below this size (in points). #[inline] pub fn at_least(mut self, minimum: f32) -> Self { - match &mut self { - Self::Absolute { range, .. } - | Self::Relative { range, .. } - | Self::Remainder { range, .. } => { - range.min = minimum; - } - } + self.range_mut().min = minimum; self } /// Won't grow above this size (in points). #[inline] pub fn at_most(mut self, maximum: f32) -> Self { - match &mut self { - Self::Absolute { range, .. } - | Self::Relative { range, .. } - | Self::Remainder { range, .. } => { - range.max = maximum; - } - } + self.range_mut().max = maximum; + self + } + + #[inline] + pub fn with_range(mut self, range: Rangef) -> Self { + *self.range_mut() = range; self } @@ -80,6 +74,29 @@ impl Size { | Self::Remainder { range, .. } => range, } } + + pub fn range_mut(&mut self) -> &mut Rangef { + match self { + Self::Absolute { range, .. } + | Self::Relative { range, .. } + | Self::Remainder { range, .. } => range, + } + } + + #[inline] + pub fn is_absolute(&self) -> bool { + matches!(self, Self::Absolute { .. }) + } + + #[inline] + pub fn is_relative(&self) -> bool { + matches!(self, Self::Relative { .. }) + } + + #[inline] + pub fn is_remainder(&self) -> bool { + matches!(self, Self::Remainder { .. }) + } } #[derive(Clone, Default)] @@ -97,7 +114,7 @@ impl Sizing { return vec![]; } - let mut remainders = 0; + let mut num_remainders = 0; let sum_non_remainder = self .sizes .iter() @@ -108,28 +125,28 @@ impl Sizing { range.clamp(length * fraction) } Size::Remainder { .. } => { - remainders += 1; + num_remainders += 1; 0.0 } }) .sum::() + spacing * (self.sizes.len() - 1) as f32; - let avg_remainder_length = if remainders == 0 { + let avg_remainder_length = if num_remainders == 0 { 0.0 } else { let mut remainder_length = length - sum_non_remainder; - let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor(); - self.sizes.iter().for_each(|&size| { + let avg_remainder_length = 0.0f32.max(remainder_length / num_remainders as f32).floor(); + for &size in &self.sizes { if let Size::Remainder { range } = size { if avg_remainder_length < range.min { remainder_length -= range.min; - remainders -= 1; + num_remainders -= 1; } } - }); - if remainders > 0 { - 0.0f32.max(remainder_length / remainders as f32) + } + if num_remainders > 0 { + 0.0f32.max(remainder_length / num_remainders as f32) } else { 0.0 } diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 0c57831f7e8..fd6abfee210 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -156,8 +156,7 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing { InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width), InitialColumnSize::Remainder => Size::remainder(), } - .at_least(column.width_range.min) - .at_most(column.width_range.max); + .with_range(column.width_range); sizing.add(size); } sizing @@ -401,13 +400,14 @@ impl<'a> TableBuilder<'a> { fn available_width(&self) -> f32 { self.ui.available_rect_before_wrap().width() - - if self.scroll_options.vscroll { - self.ui.spacing().scroll.bar_inner_margin - + self.ui.spacing().scroll.bar_width - + self.ui.spacing().scroll.bar_outer_margin - } else { - 0.0 - } + - (self.scroll_options.vscroll as i32 as f32) + * self.ui.spacing().scroll.allocated_width() + } + + /// Reset all column widths. + pub fn reset(&mut self) { + let state_id = self.ui.id().with("__table_state"); + TableState::reset(self.ui, state_id); } /// Create a header row which always stays visible and at the top @@ -428,18 +428,14 @@ impl<'a> TableBuilder<'a> { let state_id = ui.id().with("__table_state"); - let initial_widths = - to_sizing(&columns).to_lengths(available_width, ui.spacing().item_spacing.x); - let mut max_used_widths = vec![0.0; initial_widths.len()]; - let (had_state, state) = TableState::load(ui, initial_widths, state_id); - let is_first_frame = !had_state; - let first_frame_auto_size_columns = is_first_frame && columns.iter().any(|c| c.is_auto()); + let (is_sizing_pass, state) = + TableState::load(ui, state_id, resizable, &columns, available_width); + let mut max_used_widths = vec![0.0; columns.len()]; let table_top = ui.cursor().top(); ui.scope(|ui| { - if first_frame_auto_size_columns { - // Hide first-frame-jitters when auto-sizing. + if is_sizing_pass { ui.set_sizing_pass(); } let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense); @@ -468,7 +464,7 @@ impl<'a> TableBuilder<'a> { available_width, state, max_used_widths, - first_frame_auto_size_columns, + is_sizing_pass, resizable, striped, cell_layout, @@ -498,13 +494,10 @@ impl<'a> TableBuilder<'a> { let state_id = ui.id().with("__table_state"); - let initial_widths = - to_sizing(&columns).to_lengths(available_width, ui.spacing().item_spacing.x); - let max_used_widths = vec![0.0; initial_widths.len()]; - let (had_state, state) = TableState::load(ui, initial_widths, state_id); - let is_first_frame = !had_state; - let first_frame_auto_size_columns = is_first_frame && columns.iter().any(|c| c.is_auto()); + let (is_sizing_pass, state) = + TableState::load(ui, state_id, resizable, &columns, available_width); + let max_used_widths = vec![0.0; columns.len()]; let table_top = ui.cursor().top(); Table { @@ -515,7 +508,7 @@ impl<'a> TableBuilder<'a> { available_width, state, max_used_widths, - first_frame_auto_size_columns, + is_sizing_pass, resizable, striped, cell_layout, @@ -532,32 +525,84 @@ impl<'a> TableBuilder<'a> { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct TableState { column_widths: Vec, + + /// If known from previous frame + #[cfg_attr(feature = "serde", serde(skip))] + max_used_widths: Vec, } impl TableState { - /// Returns `true` if it did load. - fn load(ui: &egui::Ui, default_widths: Vec, state_id: egui::Id) -> (bool, Self) { + /// Return true if we should do a sizing pass. + fn load( + ui: &Ui, + state_id: egui::Id, + resizable: bool, + columns: &[Column], + available_width: f32, + ) -> (bool, Self) { let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO); ui.ctx().check_for_id_clash(state_id, rect, "Table"); - if let Some(state) = ui.data_mut(|d| d.get_persisted::(state_id)) { - // make sure that the stored widths aren't out-dated - if state.column_widths.len() == default_widths.len() { - return (true, state); + let state = ui + .data_mut(|d| d.get_persisted::(state_id)) + .filter(|state| { + // make sure that the stored widths aren't out-dated + state.column_widths.len() == columns.len() + }); + + let is_sizing_pass = + ui.is_sizing_pass() || state.is_none() && columns.iter().any(|c| c.is_auto()); + + let mut state = state.unwrap_or_else(|| { + let initial_widths = + to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x); + Self { + column_widths: initial_widths, + max_used_widths: Default::default(), + } + }); + + if !is_sizing_pass && state.max_used_widths.len() == columns.len() { + // Make sure any non-resizable `remainder` columns are updated + // to take up the remainder of the current available width. + // Also handles changing item spacing. + let mut sizing = crate::sizing::Sizing::default(); + for ((prev_width, max_used), column) in state + .column_widths + .iter() + .zip(&state.max_used_widths) + .zip(columns) + { + use crate::Size; + + let column_resizable = column.resizable.unwrap_or(resizable); + let size = if column_resizable { + // Resiable columns keep their width: + Size::exact(*prev_width) + } else { + match column.initial_width { + InitialColumnSize::Absolute(width) => Size::exact(width), + InitialColumnSize::Automatic(_) => Size::exact(*prev_width), + InitialColumnSize::Remainder => Size::remainder(), + } + .at_least(column.width_range.min.max(*max_used)) + .at_most(column.width_range.max) + }; + sizing.add(size); } + state.column_widths = sizing.to_lengths(available_width, ui.spacing().item_spacing.x); } - ( - false, - Self { - column_widths: default_widths, - }, - ) + (is_sizing_pass, state) } fn store(self, ui: &egui::Ui, state_id: egui::Id) { ui.data_mut(|d| d.insert_persisted(state_id, self)); } + + fn reset(ui: &egui::Ui, state_id: egui::Id) { + ui.data_mut(|d| d.remove::(state_id)); + } } // ---------------------------------------------------------------------------- @@ -576,7 +621,8 @@ pub struct Table<'a> { /// Accumulated maximum used widths for each column. max_used_widths: Vec, - first_frame_auto_size_columns: bool, + /// During the sizing pass we calculate the width of columns with [`Column::auto`]. + is_sizing_pass: bool, resizable: bool, striped: bool, cell_layout: egui::Layout, @@ -608,7 +654,7 @@ impl<'a> Table<'a> { mut available_width, mut state, mut max_used_widths, - first_frame_auto_size_columns, + is_sizing_pass, striped, cell_layout, scroll_options, @@ -651,9 +697,8 @@ impl<'a> Table<'a> { let clip_rect = ui.clip_rect(); - // Hide first-frame-jitters when auto-sizing. ui.scope(|ui| { - if first_frame_auto_size_columns { + if is_sizing_pass { ui.set_sizing_pass(); } @@ -701,16 +746,11 @@ impl<'a> Table<'a> { let column_is_resizable = column.resizable.unwrap_or(resizable); let width_range = column.width_range; - if !column.clip { - // Unless we clip we don't want to shrink below the - // size that was actually used: - *column_width = column_width.at_least(max_used_widths[i]); - } - *column_width = width_range.clamp(*column_width); - let is_last_column = i + 1 == columns.len(); - - if is_last_column && column.initial_width == InitialColumnSize::Remainder { + if is_last_column + && column.initial_width == InitialColumnSize::Remainder + && !ui.is_sizing_pass() + { // If the last column is 'remainder', then let it fill the remainder! let eps = 0.1; // just to avoid some rounding errors. *column_width = available_width - eps; @@ -721,11 +761,24 @@ impl<'a> Table<'a> { break; } + if ui.is_sizing_pass() { + if column.clip { + // If we clip, we don't need to be as wide as the max used width + *column_width = column_width.min(max_used_widths[i]); + } else { + *column_width = max_used_widths[i]; + } + } else if !column.clip { + // Unless we clip we don't want to shrink below the + // size that was actually used: + *column_width = column_width.at_least(max_used_widths[i]); + } + *column_width = width_range.clamp(*column_width); + x += *column_width + spacing_x; - if column.is_auto() && (first_frame_auto_size_columns || !column_is_resizable) { - *column_width = max_used_widths[i]; - *column_width = width_range.clamp(*column_width); + if column.is_auto() && (is_sizing_pass || !column_is_resizable) { + *column_width = width_range.clamp(max_used_widths[i]); } else if column_is_resizable { let column_resize_id = ui.id().with("resize_column").with(i); @@ -782,11 +835,13 @@ impl<'a> Table<'a> { }; ui.painter().line_segment([p0, p1], stroke); - }; + } available_width -= *column_width + spacing_x; } + state.max_used_widths = max_used_widths; + state.store(ui, state_id); } }