diff --git a/crates/re_ui/examples/re_ui_example/right_panel.rs b/crates/re_ui/examples/re_ui_example/right_panel.rs index cbc89d910eb7..ce77be79e4d3 100644 --- a/crates/re_ui/examples/re_ui_example/right_panel.rs +++ b/crates/re_ui/examples/re_ui_example/right_panel.rs @@ -186,47 +186,51 @@ impl RightPanel { list_item2::PropertyContent::new("PropertyContent features:") .value_text("bunch of properties"), |re_ui, ui| { - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Bool").value_bool(self.boolean), - ); - - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Bool (editable)") - .value_bool_mut(&mut self.boolean), - ); + // By using an inner scope, we allow the nested properties to not align themselves + // to the parent property, which in this particular case looks better. + list_item2::list_item_scope(ui, "inner_scope", None, |ui| { + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Bool").value_bool(self.boolean), + ); - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Text").value_text(&self.text), - ); + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Bool (editable)") + .value_bool_mut(&mut self.boolean), + ); - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Text (editable)") - .value_text_mut(&mut self.text), - ); + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Text").value_text(&self.text), + ); - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Color") - .with_icon(&re_ui::icons::SPACE_VIEW_TEXT) - .action_button(&re_ui::icons::ADD, || { - re_log::warn!("Add button clicked"); - }) - .value_color(&self.color), - ); + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Text (editable)") + .value_text_mut(&mut self.text), + ); - re_ui.list_item2().show_hierarchical( - ui, - list_item2::PropertyContent::new("Color (editable)") - .with_icon(&re_ui::icons::SPACE_VIEW_TEXT) - .action_button(&re_ui::icons::ADD, || { - re_log::warn!("Add button clicked"); - }) - .value_color_mut(&mut self.color), - ); + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Color") + .with_icon(&re_ui::icons::SPACE_VIEW_TEXT) + .action_button(&re_ui::icons::ADD, || { + re_log::warn!("Add button clicked"); + }) + .value_color(&self.color), + ); + + re_ui.list_item2().show_hierarchical( + ui, + list_item2::PropertyContent::new("Color (editable)") + .with_icon(&re_ui::icons::SPACE_VIEW_TEXT) + .action_button(&re_ui::icons::ADD, || { + re_log::warn!("Add button clicked"); + }) + .value_color_mut(&mut self.color), + ); + }); }, ); @@ -261,8 +265,6 @@ impl RightPanel { egui::Color32::LIGHT_RED, "CustomContent delegates to a closure", ); - - None }), ) }, diff --git a/crates/re_ui/src/list_item2/label_content.rs b/crates/re_ui/src/list_item2/label_content.rs index 58139d67f9cd..abc869aa3c09 100644 --- a/crates/re_ui/src/list_item2/label_content.rs +++ b/crates/re_ui/src/list_item2/label_content.rs @@ -126,12 +126,7 @@ impl<'a> LabelContent<'a> { } impl ListItemContent for LabelContent<'_> { - fn ui( - self: Box, - re_ui: &ReUi, - ui: &mut Ui, - context: &ContentContext<'_>, - ) -> Option { + fn ui(self: Box, re_ui: &ReUi, ui: &mut Ui, context: &ContentContext<'_>) { let Self { mut text, subdued, @@ -226,8 +221,6 @@ impl ListItemContent for LabelContent<'_> { .min; ui.painter().galley(text_pos, galley, visuals.text_color()); - - button_response } fn desired_width(&self, _re_ui: &ReUi, ui: &Ui) -> DesiredWidth { diff --git a/crates/re_ui/src/list_item2/list_item.rs b/crates/re_ui/src/list_item2/list_item.rs index 0d237b8ddcd9..b5627c6951ee 100644 --- a/crates/re_ui/src/list_item2/list_item.rs +++ b/crates/re_ui/src/list_item2/list_item.rs @@ -277,14 +277,14 @@ impl<'a> ListItem<'a> { if collapse_openness.is_some() { content_rect.min.x += extra_indent + collapse_extra; } + let content_ctx = ContentContext { rect: content_rect, bg_rect, response: &style_response, list_item: &self, - state: &state, }; - let content_response = content.ui(re_ui, ui, &content_ctx); + content.ui(re_ui, ui, &content_ctx); // Draw background on interaction. if drag_target { @@ -293,7 +293,7 @@ impl<'a> ListItem<'a> { Shape::rect_stroke(bg_rect, 0.0, (1.0, ui.visuals().selection.bg_fill)), ); } else { - let bg_fill = if content_response.map_or(false, |r| r.hovered()) { + let bg_fill = if !response.hovered() && ui.rect_contains_pointer(bg_rect) { // if some part of the content is active and hovered, our background should // become dimmer Some(visuals.bg_fill) diff --git a/crates/re_ui/src/list_item2/mod.rs b/crates/re_ui/src/list_item2/mod.rs index 7a4ea2c94970..e48be5d155b7 100644 --- a/crates/re_ui/src/list_item2/mod.rs +++ b/crates/re_ui/src/list_item2/mod.rs @@ -31,9 +31,6 @@ pub struct ContentContext<'a> { /// The current list item. pub list_item: &'a ListItem<'a>, - - /// The frame-over-frame state for this list item. - pub state: &'a State, } #[derive(Debug, Clone, Copy)] @@ -65,12 +62,7 @@ pub trait ListItemContent { /// If the content has some interactive elements, it should return its response. In particular, /// if the response is hovered, the list item will show a dimmer background highlight. //TODO(ab): could the return type be just a bool meaning "inner interactive widget was hovered"? - fn ui( - self: Box, - re_ui: &crate::ReUi, - ui: &mut egui::Ui, - context: &ContentContext<'_>, - ) -> Option; + fn ui(self: Box, re_ui: &crate::ReUi, ui: &mut egui::Ui, context: &ContentContext<'_>); /// The desired width of the content. fn desired_width(&self, _re_ui: &crate::ReUi, _ui: &egui::Ui) -> DesiredWidth { diff --git a/crates/re_ui/src/list_item2/other_contents.rs b/crates/re_ui/src/list_item2/other_contents.rs index cff6e6ada983..be3f14bd4588 100644 --- a/crates/re_ui/src/list_item2/other_contents.rs +++ b/crates/re_ui/src/list_item2/other_contents.rs @@ -11,34 +11,25 @@ impl ListItemContent for EmptyContent { _re_ui: &crate::ReUi, _ui: &mut egui::Ui, _context: &ContentContext<'_>, - ) -> Option { - None + ) { } } /// [`ListItemContent`] that delegates to a closure. #[allow(clippy::type_complexity)] -pub struct CustomContent { - ui: Box) -> Option>, +pub struct CustomContent<'a> { + ui: Box) + 'a>, } -impl CustomContent { - pub fn new( - ui: impl FnOnce(&crate::ReUi, &mut egui::Ui, &ContentContext<'_>) -> Option - + 'static, - ) -> Self { +impl<'a> CustomContent<'a> { + pub fn new(ui: impl FnOnce(&crate::ReUi, &mut egui::Ui, &ContentContext<'_>) + 'a) -> Self { Self { ui: Box::new(ui) } } } -impl ListItemContent for CustomContent { - fn ui( - self: Box, - re_ui: &crate::ReUi, - ui: &mut egui::Ui, - context: &ContentContext<'_>, - ) -> Option { - (self.ui)(re_ui, ui, context) +impl ListItemContent for CustomContent<'_> { + fn ui(self: Box, re_ui: &crate::ReUi, ui: &mut egui::Ui, context: &ContentContext<'_>) { + (self.ui)(re_ui, ui, context); } } @@ -64,17 +55,10 @@ impl DebugContent { } impl ListItemContent for DebugContent { - fn ui( - self: Box, - _re_ui: &crate::ReUi, - ui: &mut egui::Ui, - context: &ContentContext<'_>, - ) -> Option { + fn ui(self: Box, _re_ui: &crate::ReUi, ui: &mut egui::Ui, context: &ContentContext<'_>) { ui.ctx() .debug_painter() .debug_rect(context.rect, egui::Color32::DARK_GREEN, self.label); - - None } fn desired_width(&self, _re_ui: &ReUi, _ui: &Ui) -> DesiredWidth { diff --git a/crates/re_ui/src/list_item2/property_content.rs b/crates/re_ui/src/list_item2/property_content.rs index 1ff14f0d0822..38d270130063 100644 --- a/crates/re_ui/src/list_item2/property_content.rs +++ b/crates/re_ui/src/list_item2/property_content.rs @@ -1,15 +1,14 @@ -use crate::list_item2::{ContentContext, ListItemContent}; +use crate::list_item2::{ContentContext, DesiredWidth, ListItemContent}; use crate::{Icon, ReUi}; use eframe::emath::{Align, Align2}; use eframe::epaint::text::TextWrapping; -use egui::{NumExt, Response, Ui}; +use egui::{NumExt, Ui}; /// Closure to draw an icon left of the label. type IconFn<'a> = dyn FnOnce(&ReUi, &mut egui::Ui, egui::Rect, egui::style::WidgetVisuals) + 'a; /// Closure to draw the right column of the property. -type PropertyValueFn<'a> = - dyn FnOnce(&ReUi, &mut egui::Ui, egui::style::WidgetVisuals) -> Option + 'a; +type PropertyValueFn<'a> = dyn FnOnce(&ReUi, &mut egui::Ui, egui::style::WidgetVisuals) + 'a; struct PropertyActionButton<'a> { icon: &'static crate::icons::Icon, @@ -31,6 +30,8 @@ pub struct PropertyContent<'a> { } impl<'a> PropertyContent<'a> { + const COLUMN_SPACING: f32 = 12.0; + pub fn new(label: impl Into) -> Self { Self { label: label.into(), @@ -97,7 +98,7 @@ impl<'a> PropertyContent<'a> { #[inline] pub fn value_fn(mut self, value_fn: F) -> Self where - F: FnOnce(&ReUi, &mut egui::Ui, egui::style::WidgetVisuals) -> Option + 'a, + F: FnOnce(&ReUi, &mut egui::Ui, egui::style::WidgetVisuals) + 'a, { self.value_fn = Some(Box::new(value_fn)); self @@ -111,7 +112,7 @@ impl<'a> PropertyContent<'a> { #[inline] pub fn value_bool(self, mut b: bool) -> Self { self.value_fn(move |_, ui: &mut Ui, _| { - Some(ui.add_enabled(false, crate::toggle_switch(15.0, &mut b))) + ui.add_enabled(false, crate::toggle_switch(15.0, &mut b)); }) } @@ -122,20 +123,24 @@ impl<'a> PropertyContent<'a> { ui.visuals_mut().widgets.hovered.expansion = 0.0; ui.visuals_mut().widgets.active.expansion = 0.0; - Some(ui.add(crate::toggle_switch(15.0, b))) + ui.add(crate::toggle_switch(15.0, b)); }) } /// Show a static text in the value column. #[inline] pub fn value_text(self, text: impl Into + 'a) -> Self { - self.value_fn(move |_, ui, _| Some(ui.label(text.into()))) + self.value_fn(move |_, ui, _| { + ui.label(text.into()); + }) } /// Show an editable text in the value column. #[inline] pub fn value_text_mut(self, text: &'a mut String) -> Self { - self.value_fn(|_, ui, _| Some(ui.text_edit_singleline(text))) + self.value_fn(|_, ui, _| { + ui.text_edit_singleline(text); + }) } /// Show a read-only color in the value column. @@ -146,7 +151,6 @@ impl<'a> PropertyContent<'a> { let color = egui::Color32::from_rgba_unmultiplied(*r, *g, *b, *a); let response = egui::color_picker::show_color(ui, color, ui.spacing().interact_size); response.on_hover_text(format!("Color #{r:02x}{g:02x}{b:02x}{a:02x}")); - None }) } @@ -156,18 +160,13 @@ impl<'a> PropertyContent<'a> { self.value_fn(|_, ui: &mut egui::Ui, _| { ui.visuals_mut().widgets.hovered.expansion = 0.0; ui.visuals_mut().widgets.active.expansion = 0.0; - Some(ui.color_edit_button_srgba_unmultiplied(color)) + ui.color_edit_button_srgba_unmultiplied(color); }) } } impl ListItemContent for PropertyContent<'_> { - fn ui( - self: Box, - re_ui: &ReUi, - ui: &mut Ui, - context: &ContentContext<'_>, - ) -> Option { + fn ui(self: Box, re_ui: &ReUi, ui: &mut Ui, context: &ContentContext<'_>) { let Self { label, icon_fn, @@ -176,35 +175,67 @@ impl ListItemContent for PropertyContent<'_> { action_buttons, } = *self; - // We always reserve space for the action button(s), even if there are none. - let action_button_rect = egui::Rect::from_center_size( - context.rect.right_center() - egui::vec2(ReUi::small_icon_size().x / 2., 0.0), - ReUi::small_icon_size() + egui::vec2(1.0, 1.0), // padding is needed for the buttons - ); - - let content_width = - (context.rect.width() - action_button_rect.width() - ReUi::text_to_icon_padding()) - .at_least(0.0); - - //TODO(ab): adaptable columns - let column_width = ((content_width - ReUi::text_to_icon_padding()) / 2.).at_least(0.0); + // │ │ + // │◀─────────────────────────────background_x_range─────────────────────────────▶│ + // │ │ + // │ ◀───────────state.left_column_width────────────▶│┌──COLUMN_SPACING │ + // │ ▼ │ + // │ ◀──────────────CONTENT────┼──────────────────────────▶ │ + // │ ┌ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ┬ ┬────────┬─┬─────────────┬─┬─────────────┬─┬─────────┐ │ + // │ │ │ │ │││ │ │ │ │ + // │ │ │ │ │ │ │ │ │ │ │ │ │ + // │ INDENT ▼ │ ICON │ │ LABEL │││ VALUE │ │ BTN │ │ + // │ │ │ │ │ │ │ │ │ │ │ │ │ + // │ │ │ │ │││ │ │ │ │ + // │ └ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ┴ ┴────────┴─┴─────────────┴─┴─────────────┴─┴─────────┘ │ + // │ ▲ ▲ ▲ │ ▲ │ + // │ └──state.left_x │ └───────────────────────────────┤ │ + // │ │ ▲ │ │ + // │ content_left_x──┘ mid_point_x───┘ text_to_icon_padding │ + // │ │ + // + // content_indent = content_left_x - state.left_x + // left_column_width = content_indent + icon_extra + label_width + COLUMN_SPACING/2 + + let state = super::StateStack::top(ui.ctx()); + + let content_left_x = context.rect.left(); + // Total indent left of the content rect. This is part of the left column width. + let content_indent = content_left_x - state.left_x; + let mid_point_x = state.left_x + + state + .left_column_width + .unwrap_or_else(|| content_indent + (context.rect.width() / 2.).at_least(0.0)); + + let icon_extra = if icon_fn.is_some() { + ReUi::small_icon_size().x + ReUi::text_to_icon_padding() + } else { + 0.0 + }; let icon_rect = egui::Rect::from_center_size( context.rect.left_center() + egui::vec2(ReUi::small_icon_size().x / 2., 0.0), ReUi::small_icon_size(), ); - let mut label_rect = egui::Rect::from_min_size( - context.rect.left_top(), - egui::vec2(column_width, context.rect.height()), + // TODO(#6179): don't reserve space for action button if none are ever used in the current + // scope. + let action_button_rect = egui::Rect::from_center_size( + context.rect.right_center() - egui::vec2(ReUi::small_icon_size().x / 2., 0.0), + ReUi::small_icon_size() + egui::vec2(1.0, 1.0), // padding is needed for the buttons + ); + + let action_button_extra = action_button_rect.width() + ReUi::text_to_icon_padding(); + + let label_rect = egui::Rect::from_x_y_ranges( + (content_left_x + icon_extra)..=(mid_point_x - Self::COLUMN_SPACING / 2.0), + context.rect.y_range(), ); - if icon_fn.is_some() { - label_rect.min.x += icon_rect.width() + ReUi::text_to_icon_padding(); - } - let value_rect = egui::Rect::from_min_size( - context.rect.left_top() + egui::vec2(column_width + ReUi::text_to_icon_padding(), 0.0), - egui::vec2(column_width, context.rect.height()), + let value_rect = egui::Rect::from_x_y_ranges( + (mid_point_x + Self::COLUMN_SPACING / 2.0) + ..=(context.rect.right() - action_button_extra), + context.rect.y_range(), ); let visuals = ui @@ -216,25 +247,25 @@ impl ListItemContent for PropertyContent<'_> { icon_fn(re_ui, ui, icon_rect, visuals); } - let button_response = if let Some(action_button) = action_buttons { - let mut child_ui = ui.child_ui( - action_button_rect.expand(2.0), - egui::Layout::centered_and_justified(egui::Direction::LeftToRight), - ); - let button_response = re_ui.small_icon_button(&mut child_ui, action_button.icon); - if button_response.clicked() { - (action_button.on_click)(); - } - Some(button_response) - } else { - None - }; - - // Draw label + // Prepare the label galley. We first go for an un-truncated version to register our desired + // column width. If it doesn't fit the available space, we recreate it with truncation. let mut layout_job = label.into_layout_job(ui.style(), egui::FontSelection::Default, Align::LEFT); - layout_job.wrap = TextWrapping::truncate_at_width(label_rect.width()); - let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + let desired_galley = ui.fonts(|fonts| fonts.layout_job(layout_job.clone())); + let desired_width = + (content_indent + icon_extra + desired_galley.size().x + Self::COLUMN_SPACING / 2.0) + .ceil(); + + super::StateStack::top_mut(ui.ctx(), |state| { + state.register_desired_left_column_width(desired_width); + }); + + let galley = if desired_galley.size().x <= label_rect.width() { + desired_galley + } else { + layout_job.wrap = TextWrapping::truncate_at_width(label_rect.width()); + ui.fonts(|fonts| fonts.layout_job(layout_job)) + }; // this happens here to avoid cloning the text context.response.widget_info(|| { @@ -245,6 +276,7 @@ impl ListItemContent for PropertyContent<'_> { ) }); + // Label ready to draw. let text_pos = Align2::LEFT_CENTER .align_size_within_rect(galley.size(), label_rect) .min; @@ -256,23 +288,29 @@ impl ListItemContent for PropertyContent<'_> { .collapse_openness .map_or(true, |o| o == 0.0) || !summary_only; - let value_response = if let Some(value_fn) = value_fn { + if let Some(value_fn) = value_fn { if should_show_value { let mut child_ui = ui.child_ui(value_rect, egui::Layout::left_to_right(egui::Align::Center)); - value_fn(re_ui, &mut child_ui, visuals) - } else { - None + value_fn(re_ui, &mut child_ui, visuals); } - } else { - None - }; + } - // Make a union of all (possibly) interactive elements - match (value_response, button_response) { - (Some(a), Some(b)) => Some(a | b), - (Some(a), None) | (None, Some(a)) => Some(a), - (None, None) => None, + // Draw action button + if let Some(action_button) = action_buttons { + let mut child_ui = ui.child_ui( + action_button_rect.expand(2.0), + egui::Layout::centered_and_justified(egui::Direction::LeftToRight), + ); + let button_response = re_ui.small_icon_button(&mut child_ui, action_button.icon); + if button_response.clicked() { + (action_button.on_click)(); + } } } + + fn desired_width(&self, _re_ui: &ReUi, _ui: &Ui) -> DesiredWidth { + // really no point having a two-column widget collapsed to 0 width + super::DesiredWidth::AtLeast(200.0) + } } diff --git a/crates/re_ui/src/list_item2/scope.rs b/crates/re_ui/src/list_item2/scope.rs index 84b084056572..9619928b5371 100644 --- a/crates/re_ui/src/list_item2/scope.rs +++ b/crates/re_ui/src/list_item2/scope.rs @@ -1,8 +1,7 @@ +use egui::NumExt; use once_cell::sync::Lazy; -// TODO(ab): the state is currently very boring, its main purpose is to support the upcoming two- -// column ListItemContent. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct State { /// X coordinate span to use for hover/selection highlight. /// @@ -13,18 +12,54 @@ pub struct State { // be generalized to some `full_span_scope` mechanism to be used by all full-span widgets beyond // `ListItem`. pub(crate) background_x_range: egui::Rangef, - // TODO(ab): record the use of right action button in all PropertyContent such as to not reserve - // right gutter space if none have it. + + /// Left-most X coordinate for the scope. + /// + /// This is the reference point for tracking column width. This is set by [`list_item_scope`] + /// based on `ui.max_rect()`. + pub(crate) left_x: f32, + + /// Column width to be used this frame. + /// + /// The column width has `left_x` as reference, so it includes: + /// - All the indentation on the left side of the list item. + /// - Any extra indentation added by the list item itself. + /// - The list item's collapsing triangle, if any. + /// + /// The effective left column width for a given [`super::ListItemContent`] implementation can be + /// calculated as `left_column_width - (context.rect.left() - left_x)`. + pub(crate) left_column_width: Option, + + /// Maximum desired column width, to be updated this frame. + /// + /// The semantics are exactly the same as for `left_column_width`. + max_desired_left_column_width: f32, + /**/ + // TODO(#6179): record the use of right action button in all PropertyContent such as to not + // unnecessarily reserve right gutter space if none have it. } impl Default for State { fn default() -> Self { Self { background_x_range: egui::Rangef::NOTHING, + left_x: f32::NEG_INFINITY, + left_column_width: None, + max_desired_left_column_width: f32::NEG_INFINITY, } } } +impl State { + /// Register the desired width of the left column. + /// + /// All [`super::ListItemContent`] implementation that attempt to align on the two-column system should + /// call this function once in their [`super::ListItemContent::ui`] method. + pub(crate) fn register_desired_left_column_width(&mut self, desired_width: f32) { + self.max_desired_left_column_width = self.max_desired_left_column_width.max(desired_width); + } +} + /// Stack of [`State`]s. /// /// The stack is stored in `egui`'s memory and its API directly wraps the relevant calls. @@ -50,7 +85,7 @@ impl StateStack { }) } - /// Returns the current [`State`] to be used by `[ListItem]`s. + /// Returns the current [`State`] to be used by [`super::ListItemContent`] implementation. /// /// For ergonomic reasons, this function will fail by returning a default state if the stack is /// empty. This is an error condition that should be addressed by wrapping `ListItem` code in a @@ -69,8 +104,30 @@ impl StateStack { }) } - fn peek(ctx: &egui::Context) -> Option<&State> { - ctx.data(|reader| reader.get_temp(*STATE_STACK_ID)) + /// Provides mutable access to the current [`State`]. + /// + /// The closure is called with a mutable reference to the current state, if any. If the stack is + /// empty, the closure is not called and a warning is logged. + pub(crate) fn top_mut(ctx: &egui::Context, state_writer: impl FnOnce(&mut State)) { + ctx.data_mut(|writer| { + let stack: &mut StateStack = writer.get_temp_mut_or_default(*STATE_STACK_ID); + let state = stack.0.last_mut(); + if let Some(state) = state { + state_writer(state); + } else { + re_log::warn_once!( + "Failed to mutable access empty ListItem state stack. Wrap in a \ + `list_item_scope`." + ); + } + }); + } + + fn peek(ctx: &egui::Context) -> Option { + ctx.data_mut(|writer| { + let stack: &mut StateStack = writer.get_temp_mut_or_default(*STATE_STACK_ID); + stack.0.last().cloned() + }) } } @@ -96,8 +153,6 @@ impl StateStack { /// align the columns of two `ListItem`s subgroups, for which a single, global alignment would /// be detrimental. This may happen in deeply nested UI code. /// -/// TODO(#6156): the background X range stuff is to be split off and generalised for all full-span -/// widgets. pub fn list_item_scope( ui: &mut egui::Ui, id: impl Into, @@ -117,6 +172,8 @@ pub fn list_item_scope( let mut state = state.unwrap_or_default(); // determine the background x range to use + // TODO(#6156): the background X range stuff is to be split off and generalised for all full-span + // widgets. state.background_x_range = if let Some(background_x_range) = background_x_range { background_x_range } else if let Some(parent_state) = StateStack::peek(ui.ctx()) { @@ -125,6 +182,21 @@ pub fn list_item_scope( ui.clip_rect().x_range() }; + // Set up the state for this scope. + state.left_x = ui.max_rect().left(); + state.left_column_width = if state.max_desired_left_column_width > 0.0 { + Some( + // TODO(ab): this heuristics can certainly be improved, to be done with more hindsight + // from real-world usage. + state + .max_desired_left_column_width + .at_most(0.7 * ui.max_rect().width()), + ) + } else { + None + }; + state.max_desired_left_column_width = f32::NEG_INFINITY; + // push, run, pop StateStack::push(ui.ctx(), state.clone()); let result = content(ui);