diff --git a/CHANGELOG.md b/CHANGELOG.md index 974db0ea..e37dd71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ## Unreleased +### Added ⭐ +* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in). + + ## 0.14.2 - 2021-08-28 - Window resize fix ### Fixed 🐛 diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index 6d7b07c0..868e341e 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -188,7 +188,9 @@ fn combo_box( ui.memory().toggle_popup(popup_id); } let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| { - ScrollArea::from_max_height(ui.spacing().combo_height).show(ui, menu_contents) + ScrollArea::vertical() + .max_height(ui.spacing().combo_height) + .show(ui, menu_contents) }); InnerResponse { diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 71f88f58..a74f8c5b 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -1,3 +1,10 @@ +//! Coordinate system names: +//! * content: size of contents (generally large; that's why we want scroll bars) +//! * outer: size of scroll area including scroll bar(s) +//! * inner: excluding scroll bar(s). The area we clip the contents to. + +#![allow(clippy::needless_range_loop)] + use crate::*; #[derive(Clone, Copy, Debug)] @@ -7,48 +14,77 @@ pub(crate) struct State { /// Positive offset means scrolling down/right offset: Vec2, - show_scroll: bool, + show_scroll: [bool; 2], /// Momentum, used for kinetic scrolling #[cfg_attr(feature = "persistence", serde(skip))] pub vel: Vec2, + /// Mouse offset relative to the top of the handle when started moving the handle. - scroll_start_offset_from_top: Option, + scroll_start_offset_from_top_left: [Option; 2], } impl Default for State { fn default() -> Self { Self { offset: Vec2::ZERO, - show_scroll: false, + show_scroll: [false; 2], vel: Vec2::ZERO, - scroll_start_offset_from_top: None, + scroll_start_offset_from_top_left: [None; 2], } } } -// TODO: rename VScroll -/// Add vertical scrolling to a contained [`Ui`]. +/// Add vertical and/or horizontal scrolling to a contained [`Ui`]. +/// +/// ``` +/// # let ui = &mut egui::Ui::__test(); +/// egui::ScrollArea::vertical().show(ui, |ui| { +/// // Add a lot of widgets here. +/// }); #[derive(Clone, Debug)] #[must_use = "You should call .show()"] pub struct ScrollArea { - max_height: f32, + /// Do we have horizontal/vertical scrolling? + has_bar: [bool; 2], + auto_shrink: [bool; 2], + max_size: Vec2, always_show_scroll: bool, id_source: Option, offset: Option, + /// If false, we ignore scroll events. scrolling_enabled: bool, } impl ScrollArea { - /// Will make the area be as high as it is allowed to be (i.e. fill the [`Ui`] it is in) - pub fn auto_sized() -> Self { - Self::from_max_height(f32::INFINITY) + /// Create a horizontal scroll area. + pub fn horizontal() -> Self { + Self::new([true, false]) } - /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding Ui - pub fn from_max_height(max_height: f32) -> Self { + /// Create a vertical scroll area. + pub fn vertical() -> Self { + Self::new([false, true]) + } + + /// Create a bi-directional (horizontal and vertical) scroll area. + pub fn both() -> Self { + Self::new([true, true]) + } + + /// Create a scroll area where both direction of scrolling is disabled. + /// It's unclear why you would want to do this. + pub fn neither() -> Self { + Self::new([false, false]) + } + + /// Create a scroll area where you decide which axis has scrolling enabled. + /// For instance, `ScrollAre::new([true, false])` enable horizontal scrolling. + pub fn new(has_bar: [bool; 2]) -> Self { Self { - max_height, + has_bar, + auto_shrink: [true; 2], + max_size: Vec2::INFINITY, always_show_scroll: false, id_source: None, offset: None, @@ -56,6 +92,34 @@ impl ScrollArea { } } + /// Will make the area be as high as it is allowed to be (i.e. fill the [`Ui`] it is in) + #[deprecated = "Use pub ScrollArea::vertical() instead"] + pub fn auto_sized() -> Self { + Self::vertical() + } + + /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding Ui + #[deprecated = "Use pub ScrollArea::vertical().max_height(…) instead"] + pub fn from_max_height(max_height: f32) -> Self { + Self::vertical().max_height(max_height) + } + + /// The desired width of the outer frame of the scroll area. + /// + /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding `Ui` (default). + pub fn max_width(mut self, max_width: f32) -> Self { + self.max_size.x = max_width; + self + } + + /// The desired height of the outer frame of the scroll area. + /// + /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding `Ui` (default). + pub fn max_height(mut self, max_height: f32) -> Self { + self.max_size.y = max_height; + self + } + /// If `false` (default), the scroll bar will be hidden when not needed/ /// If `true`, the scroll bar will always be displayed even if not needed. pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self { @@ -78,22 +142,62 @@ impl ScrollArea { self } + /// Turn on/off scrolling on the horizontal axis. + pub fn hscroll(mut self, hscroll: bool) -> Self { + self.has_bar[0] = hscroll; + self + } + + /// Turn on/off scrolling on the vertical axis. + pub fn vscroll(mut self, vscroll: bool) -> Self { + self.has_bar[1] = vscroll; + self + } + + /// Turn on/off scrolling on the horizontal/vertical axes. + pub fn scroll2(mut self, has_bar: [bool; 2]) -> Self { + self.has_bar = has_bar; + self + } + /// Control the scrolling behavior /// If `true` (default), the scroll area will respond to user scrolling /// If `false`, the scroll area will not respond to user scrolling /// /// This can be used, for example, to optionally freeze scrolling while the user - /// is inputing text in a `TextEdit` widget contained within the scroll area + /// is inputing text in a `TextEdit` widget contained within the scroll area. + /// + /// This controls both scrolling directions. pub fn enable_scrolling(mut self, enable: bool) -> Self { self.scrolling_enabled = enable; self } + + /// For each enabled axis, should the containing area shrink + /// if the content is small? + /// + /// If true, egui will add blank space outside the scroll area. + /// If false, egui will add blank space inside the scroll area. + /// + /// Default: `[true; 2]`. + pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self { + self.auto_shrink = auto_shrink; + self + } + + pub(crate) fn has_any_bar(&self) -> bool { + self.has_bar[0] || self.has_bar[1] + } } struct Prepared { id: Id, state: State, - current_scroll_bar_width: f32, + has_bar: [bool; 2], + auto_shrink: [bool; 2], + /// How much horizontal and vertical space are used up by the + /// width of the vertical bar, and the height of the horizontal bar? + current_bar_use: Vec2, always_show_scroll: bool, inner_rect: Rect, content_ui: Ui, @@ -106,7 +210,9 @@ struct Prepared { impl ScrollArea { fn begin(self, ui: &mut Ui) -> Prepared { let Self { - max_height, + has_bar, + auto_shrink, + max_size, always_show_scroll, id_source, offset, @@ -123,38 +229,59 @@ impl ScrollArea { state.offset = offset; } - // content: size of contents (generally large; that's why we want scroll bars) - // outer: size of scroll area including scroll bar(s) - // inner: excluding scroll bar(s). The area we clip the contents to. - let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - let current_scroll_bar_width = if always_show_scroll { + let current_hscroll_bar_height = if !has_bar[0] { + 0.0 + } else if always_show_scroll { max_scroll_bar_width } else { - max_scroll_bar_width * ui.ctx().animate_bool(id, state.show_scroll) + max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), state.show_scroll[0]) }; + let current_vscroll_bar_width = if !has_bar[1] { + 0.0 + } else if always_show_scroll { + max_scroll_bar_width + } else { + max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), state.show_scroll[1]) + }; + + let current_bar_use = vec2(current_vscroll_bar_width, current_hscroll_bar_height); + let available_outer = ui.available_rect_before_wrap(); - let outer_size = vec2( - available_outer.width(), - available_outer.height().at_most(max_height), - ); + let outer_size = available_outer.size().at_most(max_size); - let inner_size = outer_size - vec2(current_scroll_bar_width, 0.0); + let inner_size = outer_size - current_bar_use; let inner_rect = Rect::from_min_size(available_outer.min, inner_size); + let mut inner_child_max_size = inner_size; + + if true { + // Tell the inner Ui to *try* to fit the content without needing to scroll, + // i.e. better to wrap text than showing a horizontal scrollbar! + } else { + // Tell the inner Ui to use as much space as possible, we can scroll to see it! + for d in 0..2 { + if has_bar[d] { + inner_child_max_size[d] = f32::INFINITY; + } + } + } + let mut content_ui = ui.child_ui( - Rect::from_min_size( - inner_rect.min - state.offset, - vec2(inner_size.x, f32::INFINITY), - ), + Rect::from_min_size(inner_rect.min - state.offset, inner_child_max_size), *ui.layout(), ); let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin); content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); - content_clip_rect.max.x = ui.clip_rect().max.x - current_scroll_bar_width; // Nice handling of forced resizing beyond the possible + // Nice handling of forced resizing beyond the possible: + for d in 0..2 { + if !has_bar[d] { + content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d]; + } + } content_ui.set_clip_rect(content_clip_rect); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); @@ -162,7 +289,9 @@ impl ScrollArea { Prepared { id, state, - current_scroll_bar_width, + has_bar, + auto_shrink, + current_bar_use, always_show_scroll, inner_rect, content_ui, @@ -186,7 +315,7 @@ impl ScrollArea { /// let row_height = ui.fonts()[text_style].row_height(); /// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels. /// let num_rows = 10_000; - /// egui::ScrollArea::auto_sized().show_rows(ui, row_height, num_rows, |ui, row_range| { + /// egui::ScrollArea::vertical().show_rows(ui, row_height, num_rows, |ui, row_range| { /// for row in row_range { /// let text = format!("Row {}/{}", row + 1, num_rows); /// ui.label(text); @@ -241,8 +370,10 @@ impl Prepared { id, mut state, inner_rect, + has_bar, + auto_shrink, + mut current_bar_use, always_show_scroll, - mut current_scroll_bar_width, content_ui, viewport: _, scrolling_enabled, @@ -251,52 +382,67 @@ impl Prepared { let content_size = content_ui.min_size(); // We take the scroll target so only this ScrollArea will use it. - let scroll_target = content_ui.ctx().frame_state().scroll_target.take(); - if let Some((scroll_y, align)) = scroll_target { - let center_factor = align.to_factor(); - let top = content_ui.min_rect().top(); - let visible_range = top..=top + content_ui.clip_rect().height(); - let offset_y = scroll_y - lerp(visible_range, center_factor); + for d in 0..2 { + if has_bar[d] { + let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take(); + if let Some((scroll, align)) = scroll_target { + let center_factor = align.to_factor(); - let mut spacing = ui.spacing().item_spacing.y; + let min = content_ui.min_rect().min[d]; + let visible_range = min..=min + content_ui.clip_rect().size()[d]; + let offset = scroll - lerp(visible_range, center_factor); - // Depending on the alignment we need to add or subtract the spacing - spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); + let mut spacing = ui.spacing().item_spacing[d]; - state.offset.y = offset_y + spacing; + // Depending on the alignment we need to add or subtract the spacing + spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); + + state.offset[d] = offset + spacing; + } + } } let inner_rect = { - let width = if inner_rect.width().is_finite() { - inner_rect.width().max(content_size.x) // Expand width to fit content - } else { - // ScrollArea is in an infinitely wide parent - content_size.x - }; + let mut inner_size = inner_rect.size(); - let mut inner_rect = - Rect::from_min_size(inner_rect.min, vec2(width, inner_rect.height())); + for d in 0..2 { + inner_size[d] = if has_bar[d] { + if auto_shrink[d] { + inner_size[d].min(content_size[d]) // shrink scroll area if content is small + } else { + inner_size[d] // let scroll area be larger than content; fill with blank space + } + } else if inner_size[d].is_finite() { + inner_size[d].max(content_size[d]) // Expand to fit content + } else { + content_size[d] // ScrollArea is in an infinitely sized parent; take size of parent + }; + } + + let mut inner_rect = Rect::from_min_size(inner_rect.min, inner_size); // The window that egui sits in can't be expanded by egui, so we need to respect it: - let max_x = ui.input().screen_rect().right() - - current_scroll_bar_width - - ui.spacing().item_spacing.x; + let max_x = + ui.input().screen_rect().right() - current_bar_use.x - ui.spacing().item_spacing.x; inner_rect.max.x = inner_rect.max.x.at_most(max_x); - // TODO: when we support it, we should maybe auto-enable - // horizontal scrolling if this limit is reached + + let max_y = + ui.input().screen_rect().bottom() - current_bar_use.y - ui.spacing().item_spacing.y; + inner_rect.max.y = inner_rect.max.y.at_most(max_y); + // TODO: maybe auto-enable horizontal/vertical scrolling if this limit is reached inner_rect }; - let outer_rect = Rect::from_min_size( - inner_rect.min, - inner_rect.size() + vec2(current_scroll_bar_width, 0.0), - ); + let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); - let content_is_too_small = content_size.y > inner_rect.height(); + let content_is_too_small = [ + content_size.x > inner_rect.width(), + content_size.y > inner_rect.height(), + ]; - if content_is_too_small { + if content_is_too_small[0] || content_is_too_small[1] { // Drag contents to scroll (for touch screens mostly): let sense = if self.scrolling_enabled { Sense::drag() @@ -307,8 +453,14 @@ impl Prepared { let input = ui.input(); if content_response.dragged() { - state.offset.y -= input.pointer.delta().y; - state.vel = input.pointer.velocity(); + for d in 0..2 { + if has_bar[d] { + state.offset[d] -= input.pointer.delta()[d]; + state.vel[d] = input.pointer.velocity()[d]; + } else { + state.vel[d] = 0.0; + } + } } else { let stop_speed = 20.0; // Pixels per second. let friction_coeff = 1000.0; // Pixels per second squared. @@ -321,59 +473,91 @@ impl Prepared { state.vel -= friction * state.vel.normalized(); // Offset has an inverted coordinate system compared to // the velocity, so we subtract it instead of adding it - state.offset.y -= state.vel.y * dt; + state.offset -= state.vel * dt; ui.ctx().request_repaint(); } } } - let max_offset = content_size.y - inner_rect.height(); + let max_offset = content_size - inner_rect.size(); if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { - let mut frame_state = ui.ctx().frame_state(); - let scroll_delta = frame_state.scroll_delta; + for d in 0..2 { + if has_bar[d] { + let mut frame_state = ui.ctx().frame_state(); + let scroll_delta = frame_state.scroll_delta; - let scrolling_up = state.offset.y > 0.0 && scroll_delta.y > 0.0; - let scrolling_down = state.offset.y < max_offset && scroll_delta.y < 0.0; + let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; + let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0; - if scrolling_up || scrolling_down { - state.offset.y -= scroll_delta.y; - // Clear scroll delta so no parent scroll will use it. - frame_state.scroll_delta = Vec2::ZERO; + if scrolling_up || scrolling_down { + state.offset[d] -= scroll_delta[d]; + // Clear scroll delta so no parent scroll will use it. + frame_state.scroll_delta[d] = 0.0; + } + } } } - let show_scroll_this_frame = content_is_too_small || always_show_scroll; + let show_scroll_this_frame = [ + content_is_too_small[0] || always_show_scroll, + content_is_too_small[1] || always_show_scroll, + ]; let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - if show_scroll_this_frame && current_scroll_bar_width <= 0.0 { - // Avoid frame delay; start showing scroll bar right away: - current_scroll_bar_width = max_scroll_bar_width * ui.ctx().animate_bool(id, true); + // Avoid frame delay; start showing scroll bar right away: + if show_scroll_this_frame[0] && current_bar_use.y <= 0.0 { + current_bar_use.y = max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), true); + } + if show_scroll_this_frame[1] && current_bar_use.x <= 0.0 { + current_bar_use.x = max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), true); } - if current_scroll_bar_width > 0.0 { - let animation_t = current_scroll_bar_width / max_scroll_bar_width; + for d in 0..2 { + let animation_t = current_bar_use[1 - d] / max_scroll_bar_width; + + if animation_t == 0.0 { + continue; + } + // margin between contents and scroll bar let margin = animation_t * ui.spacing().item_spacing.x; - let left = inner_rect.right() + margin; - let right = outer_rect.right(); - let top = inner_rect.top(); - let bottom = inner_rect.bottom(); + let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1) + let max_cross = outer_rect.max[1 - d]; // right of vertical scroll (d == 1) + let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1) + let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1) - let outer_scroll_rect = Rect::from_min_max( - pos2(left, inner_rect.top()), - pos2(right, inner_rect.bottom()), - ); + let outer_scroll_rect = if d == 0 { + Rect::from_min_max( + pos2(inner_rect.left(), min_cross), + pos2(inner_rect.right(), max_cross), + ) + } else { + Rect::from_min_max( + pos2(min_cross, inner_rect.top()), + pos2(max_cross, inner_rect.bottom()), + ) + }; let from_content = - |content_y| remap_clamp(content_y, 0.0..=content_size.y, top..=bottom); + |content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main); - let handle_rect = Rect::from_min_max( - pos2(left, from_content(state.offset.y)), - pos2(right, from_content(state.offset.y + inner_rect.height())), - ); + let handle_rect = if d == 0 { + Rect::from_min_max( + pos2(from_content(state.offset.x), min_cross), + pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + ) + } else { + Rect::from_min_max( + pos2(min_cross, from_content(state.offset.y)), + pos2( + max_cross, + from_content(state.offset.y + inner_rect.height()), + ), + ) + }; - let interact_id = id.with("vertical"); + let interact_id = id.with(d); let sense = if self.scrolling_enabled { Sense::click_and_drag() } else { @@ -382,43 +566,57 @@ impl Prepared { let response = ui.interact(outer_scroll_rect, interact_id, sense); if let Some(pointer_pos) = response.interact_pointer_pos() { - let scroll_start_offset_from_top = - state.scroll_start_offset_from_top.get_or_insert_with(|| { + let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d] + .get_or_insert_with(|| { if handle_rect.contains(pointer_pos) { - pointer_pos.y - handle_rect.top() + pointer_pos[d] - handle_rect.min[d] } else { - let handle_top_pos_at_bottom = bottom - handle_rect.height(); + let handle_top_pos_at_bottom = max_main - handle_rect.size()[d]; // Calculate the new handle top position, centering the handle on the mouse. - let new_handle_top_pos = (pointer_pos.y - handle_rect.height() / 2.0) - .clamp(top, handle_top_pos_at_bottom); - pointer_pos.y - new_handle_top_pos + let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0) + .clamp(min_main, handle_top_pos_at_bottom); + pointer_pos[d] - new_handle_top_pos } }); - let new_handle_top = pointer_pos.y - *scroll_start_offset_from_top; - state.offset.y = remap(new_handle_top, top..=bottom, 0.0..=content_size.y); + let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left; + state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]); } else { - state.scroll_start_offset_from_top = None; + state.scroll_start_offset_from_top_left[d] = None; } - let unbounded_offset_y = state.offset.y; - state.offset.y = state.offset.y.max(0.0); - state.offset.y = state.offset.y.min(max_offset); + let unbounded_offset = state.offset[d]; + state.offset[d] = state.offset[d].max(0.0); + state.offset[d] = state.offset[d].min(max_offset[d]); - if state.offset.y != unbounded_offset_y { - state.vel = Vec2::ZERO; + if state.offset[d] != unbounded_offset { + state.vel[d] = 0.0; } // Avoid frame-delay by calculating a new handle rect: - let mut handle_rect = Rect::from_min_max( - pos2(left, from_content(state.offset.y)), - pos2(right, from_content(state.offset.y + inner_rect.height())), - ); - let min_handle_height = ui.spacing().scroll_bar_width; - if handle_rect.size().y < min_handle_height { + let mut handle_rect = if d == 0 { + Rect::from_min_max( + pos2(from_content(state.offset.x), min_cross), + pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + ) + } else { + Rect::from_min_max( + pos2(min_cross, from_content(state.offset.y)), + pos2( + max_cross, + from_content(state.offset.y + inner_rect.height()), + ), + ) + }; + let min_handle_size = ui.spacing().scroll_bar_width; + if handle_rect.size()[d] < min_handle_size { handle_rect = Rect::from_center_size( handle_rect.center(), - vec2(handle_rect.size().x, min_handle_height), + if d == 0 { + vec2(min_handle_size, handle_rect.size().y) + } else { + vec2(handle_rect.size().x, min_handle_size) + }, ); } @@ -441,24 +639,21 @@ impl Prepared { )); } - let size = vec2( - outer_rect.size().x, - outer_rect.size().y.min(content_size.y), // shrink if content is so small that we don't need scroll bars - ); - ui.advance_cursor_after_rect(Rect::from_min_size(outer_rect.min, size)); + ui.advance_cursor_after_rect(outer_rect); if show_scroll_this_frame != state.show_scroll { ui.ctx().request_repaint(); } - state.offset.y = state.offset.y.min(content_size.y - inner_rect.height()); - state.offset.y = state.offset.y.max(0.0); + state.offset = state.offset.min(content_size - inner_rect.size()); + state.offset = state.offset.max(Vec2::ZERO); state.show_scroll = show_scroll_this_frame; ui.memory().id_data.insert(id, state); } } +/// Width of a vertical scrollbar, or height of a horizontal scroll bar fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 { ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width } diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 77b49dc6..374a07ca 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -28,7 +28,7 @@ pub struct Window<'open> { area: Area, frame: Option, resize: Resize, - scroll: Option, + scroll: ScrollArea, collapsible: bool, with_title_bar: bool, } @@ -51,7 +51,7 @@ impl<'open> Window<'open> { .with_stroke(false) .min_size([96.0, 32.0]) .default_size([340.0, 420.0]), // Default inner size of a window - scroll: None, + scroll: ScrollArea::neither(), collapsible: true, with_title_bar: true, } @@ -203,26 +203,33 @@ impl<'open> Window<'open> { /// Text will not wrap, but will instead make your window width expand. pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); - self.scroll = None; + self.scroll = ScrollArea::neither(); self } - /// Enable/disable scrolling. `false` by default. - pub fn scroll(mut self, scroll: bool) -> Self { - if scroll { - if self.scroll.is_none() { - self.scroll = Some(ScrollArea::auto_sized()); - } - crate::egui_assert!( - self.scroll.is_some(), - "Window::scroll called multiple times" - ); - } else { - self.scroll = None; - } + /// Enable/disable horizontal/vertical scrolling. `false` by default. + pub fn scroll2(mut self, scroll: [bool; 2]) -> Self { + self.scroll = self.scroll.scroll2(scroll); self } + /// Enable/disable horizontal scrolling. `false` by default. + pub fn hscroll(mut self, hscroll: bool) -> Self { + self.scroll = self.scroll.hscroll(hscroll); + self + } + + /// Enable/disable vertical scrolling. `false` by default. + pub fn vscroll(mut self, vscroll: bool) -> Self { + self.scroll = self.scroll.vscroll(vscroll); + self + } + + #[deprecated = "Use .vscroll(…) instead"] + pub fn scroll(self, scroll: bool) -> Self { + self.vscroll(scroll) + } + /// Constrain the area up to which the window can be dragged. pub fn drag_bounds(mut self, bounds: Rect) -> Self { self.area = self.area.drag_bounds(bounds); @@ -352,7 +359,7 @@ impl<'open> Window<'open> { ui.add_space(title_content_spacing); } - if let Some(scroll) = scroll { + if scroll.has_any_bar() { scroll.show(ui, add_contents) } else { add_contents(ui) diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index 579ed16c..dc08674a 100644 --- a/egui/src/frame_state.rs +++ b/egui/src/frame_state.rs @@ -27,8 +27,9 @@ pub(crate) struct FrameState { pub(crate) tooltip_rect: Option<(Id, Rect, usize)>, /// Cleared by the first `ScrollArea` that makes use of it. - pub(crate) scroll_delta: Vec2, - pub(crate) scroll_target: Option<(f32, Align)>, + pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ? + /// horizontal, vertical + pub(crate) scroll_target: [Option<(f32, Align)>; 2], } impl Default for FrameState { @@ -40,7 +41,7 @@ impl Default for FrameState { used_by_panels: Rect::NAN, tooltip_rect: None, scroll_delta: Vec2::ZERO, - scroll_target: None, + scroll_target: [None; 2], } } } @@ -63,7 +64,7 @@ impl FrameState { *used_by_panels = Rect::NOTHING; *tooltip_rect = None; *scroll_delta = input.scroll_delta; - *scroll_target = None; + *scroll_target = [None; 2]; } /// How much space is still available after panels has been added. diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 1e75a36f..33728cdf 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -1,4 +1,4 @@ -use crate::{emath::*, Align}; +use crate::{egui_assert, emath::*, Align}; use std::f32::INFINITY; // ---------------------------------------------------------------------------- @@ -84,6 +84,12 @@ impl Region { self.max_rect.extend_with_y(y); self.cursor.extend_with_y(y); } + + pub fn sanity_check(&self) { + egui_assert!(!self.min_rect.any_nan()); + egui_assert!(!self.max_rect.any_nan()); + egui_assert!(!self.cursor.any_nan()); + } } // ---------------------------------------------------------------------------- @@ -398,6 +404,9 @@ impl Layout { /// Given the cursor in the region, how much space is available /// for the next widget? fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect { + egui_assert!(!cursor.any_nan()); + egui_assert!(!max_rect.any_nan()); + // NOTE: in normal top-down layout the cursor has moved below the current max_rect, // but the available shouldn't be negative. @@ -450,6 +459,8 @@ impl Layout { avail.max.y = y; } + egui_assert!(!avail.any_nan()); + avail } @@ -458,6 +469,7 @@ impl Layout { /// This is what you then pass to `advance_after_rects`. /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect { + region.sanity_check(); crate::egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); if self.main_wrap { @@ -537,6 +549,7 @@ impl Layout { } fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect { + region.sanity_check(); crate::egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); let available_rect = self.available_rect_before_wrap_finite(region); @@ -570,6 +583,9 @@ impl Layout { frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top())); } + egui_assert!(!frame_rect.any_nan()); + egui_assert!(!frame_rect.is_negative()); + frame_rect } @@ -595,6 +611,7 @@ impl Layout { let frame = self.next_frame_ignore_wrap(region, size); let rect = self.align_size_within_rect(size, frame); crate::egui_assert!(!rect.any_nan()); + crate::egui_assert!(!rect.is_negative()); crate::egui_assert!((rect.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY); crate::egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY); rect @@ -639,6 +656,7 @@ impl Layout { widget_rect: Rect, item_spacing: Vec2, ) { + egui_assert!(!cursor.any_nan()); if self.main_wrap { if cursor.intersects(frame_rect.shrink(1.0)) { // make row/column larger if necessary diff --git a/egui/src/placer.rs b/egui/src/placer.rs index d58bf845..2a32aa6b 100644 --- a/egui/src/placer.rs +++ b/egui/src/placer.rs @@ -119,6 +119,7 @@ impl Placer { /// This is what you then pass to `advance_after_rects`. /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect { + self.region.sanity_check(); if let Some(grid) = &self.grid { grid.next_cell(self.region.cursor, child_size) } else { @@ -169,6 +170,10 @@ impl Placer { widget_rect: Rect, item_spacing: Vec2, ) { + egui_assert!(!frame_rect.any_nan()); + egui_assert!(!widget_rect.any_nan()); + self.region.sanity_check(); + if let Some(grid) = &mut self.grid { grid.advance(&mut self.region.cursor, frame_rect, widget_rect) } else { @@ -181,6 +186,8 @@ impl Placer { } self.expand_to_include_rect(frame_rect); // e.g. for centered layouts: pretend we used whole frame + + self.region.sanity_check(); } /// Move to the next row in a grid layout or wrapping layout. @@ -231,6 +238,8 @@ impl Placer { region.cursor.min.x = region.max_rect.min.x; region.cursor.max.x = region.max_rect.max.x; + + region.sanity_check(); } /// Set the maximum height of the ui. @@ -244,6 +253,8 @@ impl Placer { region.cursor.min.y = region.max_rect.min.y; region.cursor.max.y = region.max_rect.max.y; + + region.sanity_check(); } /// Set the minimum width of the ui. diff --git a/egui/src/response.rs b/egui/src/response.rs index b7641592..65fdd28a 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -422,7 +422,7 @@ impl Response { /// ``` /// # use egui::Align; /// # let mut ui = &mut egui::Ui::__test(); - /// egui::ScrollArea::auto_sized().show(ui, |ui| { + /// egui::ScrollArea::vertical().show(ui, |ui| { /// for i in 0..1000 { /// let response = ui.button(format!("Button {}", i)); /// if response.clicked() { @@ -432,8 +432,11 @@ impl Response { /// }); /// ``` pub fn scroll_to_me(&self, align: Align) { + let scroll_target = lerp(self.rect.x_range(), align.to_factor()); + self.ctx.frame_state().scroll_target[0] = Some((scroll_target, align)); + let scroll_target = lerp(self.rect.y_range(), align.to_factor()); - self.ctx.frame_state().scroll_target = Some((scroll_target, align)); + self.ctx.frame_state().scroll_target[1] = Some((scroll_target, align)); } /// For accessibility. diff --git a/egui/src/ui.rs b/egui/src/ui.rs index e8d233ba..172e4c78 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -696,6 +696,7 @@ impl Ui { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); + egui_assert!(!frame_rect.any_nan()); let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer @@ -714,6 +715,7 @@ impl Ui { } pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { + egui_assert!(!rect.any_nan()); let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); @@ -856,7 +858,7 @@ impl Ui { /// ``` /// # use egui::Align; /// # let mut ui = &mut egui::Ui::__test(); - /// egui::ScrollArea::auto_sized().show(ui, |ui| { + /// egui::ScrollArea::vertical().show(ui, |ui| { /// let scroll_bottom = ui.button("Scroll to bottom.").clicked(); /// for i in 0..1000 { /// ui.label(format!("Item {}", i)); @@ -868,8 +870,10 @@ impl Ui { /// }); /// ``` pub fn scroll_to_cursor(&mut self, align: Align) { - let target_y = self.next_widget_position().y; - self.ctx().frame_state().scroll_target = Some((target_y, align)); + let target = self.next_widget_position(); + for d in 0..2 { + self.ctx().frame_state().scroll_target[d] = Some((target[d], align)); + } } } diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index 749f228d..59a9ae0e 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -314,16 +314,19 @@ impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { let sense = self.sense; + let max_width = ui.available_width(); + if self.should_wrap(ui) && ui.layout().main_dir() == Direction::LeftToRight && ui.layout().main_wrap() + && max_width.is_finite() { // On a wrapping horizontal layout we want text to start after the previous widget, // then continue on the line below! This will take some extra work: let cursor = ui.cursor(); - let max_width = ui.available_width(); let first_row_indentation = max_width - ui.available_size_before_wrap().x; + egui_assert!(first_row_indentation.is_finite()); let text_style = self.text_style_or_default(ui.style()); let galley = ui.fonts().layout_multiline_with_indentation_and_max_width( diff --git a/egui_demo_lib/src/apps/color_test.rs b/egui_demo_lib/src/apps/color_test.rs index cf2f8103..c3ed9e9a 100644 --- a/egui_demo_lib/src/apps/color_test.rs +++ b/egui_demo_lib/src/apps/color_test.rs @@ -42,7 +42,7 @@ impl epi::App for ColorTest { ); ui.separator(); } - ScrollArea::auto_sized().show(ui, |ui| { + ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| { self.ui(ui, &mut Some(frame.tex_allocator())); }); }); @@ -55,6 +55,8 @@ impl ColorTest { ui: &mut Ui, mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>, ) { + ui.set_max_width(680.0); + ui.vertical_centered(|ui| { ui.add(crate::__egui_github_link_file!()); }); diff --git a/egui_demo_lib/src/apps/demo/dancing_strings.rs b/egui_demo_lib/src/apps/demo/dancing_strings.rs index d96ecb1e..94beb42d 100644 --- a/egui_demo_lib/src/apps/demo/dancing_strings.rs +++ b/egui_demo_lib/src/apps/demo/dancing_strings.rs @@ -15,7 +15,7 @@ impl super::Demo for DancingStrings { Window::new(self.name()) .open(open) .default_size(vec2(512.0, 256.0)) - .scroll(false) + .vscroll(false) .show(ctx, |ui| self.ui(ui)); } } diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index 83ab40e2..eb64a543 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -159,7 +159,7 @@ impl DemoWindows { ui.separator(); - ScrollArea::auto_sized().show(ui, |ui| { + ScrollArea::vertical().show(ui, |ui| { use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS}; ui.label("egui is an immediate mode GUI library written in Rust."); diff --git a/egui_demo_lib/src/apps/demo/drag_and_drop.rs b/egui_demo_lib/src/apps/demo/drag_and_drop.rs index 6c247b56..f881384f 100644 --- a/egui_demo_lib/src/apps/demo/drag_and_drop.rs +++ b/egui_demo_lib/src/apps/demo/drag_and_drop.rs @@ -103,7 +103,7 @@ impl super::Demo for DragAndDropDemo { Window::new(self.name()) .open(open) .default_size(vec2(256.0, 256.0)) - .scroll(false) + .vscroll(false) .resizable(false) .show(ctx, |ui| self.ui(ui)); } diff --git a/egui_demo_lib/src/apps/demo/font_book.rs b/egui_demo_lib/src/apps/demo/font_book.rs index 0601b4ed..aa42a948 100644 --- a/egui_demo_lib/src/apps/demo/font_book.rs +++ b/egui_demo_lib/src/apps/demo/font_book.rs @@ -84,7 +84,7 @@ impl super::View for FontBook { ui.separator(); - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0); diff --git a/egui_demo_lib/src/apps/demo/misc_demo_window.rs b/egui_demo_lib/src/apps/demo/misc_demo_window.rs index 4d762948..a3477d3f 100644 --- a/egui_demo_lib/src/apps/demo/misc_demo_window.rs +++ b/egui_demo_lib/src/apps/demo/misc_demo_window.rs @@ -34,13 +34,16 @@ impl Demo for MiscDemoWindow { fn show(&mut self, ctx: &CtxRef, open: &mut bool) { Window::new(self.name()) .open(open) - .scroll(true) + .vscroll(true) + .hscroll(true) .show(ctx, |ui| self.ui(ui)); } } impl View for MiscDemoWindow { fn ui(&mut self, ui: &mut Ui) { + ui.set_min_width(250.0); + CollapsingHeader::new("Widgets") .default_open(true) .show(ui, |ui| { diff --git a/egui_demo_lib/src/apps/demo/painting.rs b/egui_demo_lib/src/apps/demo/painting.rs index 2e4d2072..f16d18dd 100644 --- a/egui_demo_lib/src/apps/demo/painting.rs +++ b/egui_demo_lib/src/apps/demo/painting.rs @@ -79,7 +79,7 @@ impl super::Demo for Painting { Window::new(self.name()) .open(open) .default_size(vec2(512.0, 512.0)) - .scroll(false) + .vscroll(false) .show(ctx, |ui| self.ui(ui)); } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 1f98b350..85f01919 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -401,7 +401,7 @@ impl super::Demo for PlotDemo { Window::new(self.name()) .open(open) .default_size(vec2(400.0, 400.0)) - .scroll(false) + .vscroll(false) .show(ctx, |ui| self.ui(ui)); } } diff --git a/egui_demo_lib/src/apps/demo/scrolling.rs b/egui_demo_lib/src/apps/demo/scrolling.rs index 216a3e36..2b388085 100644 --- a/egui_demo_lib/src/apps/demo/scrolling.rs +++ b/egui_demo_lib/src/apps/demo/scrolling.rs @@ -77,7 +77,7 @@ fn huge_content_lines(ui: &mut egui::Ui) { let text_style = TextStyle::Body; let row_height = ui.fonts()[text_style].row_height(); let num_rows = 10_000; - ScrollArea::auto_sized().show_rows(ui, row_height, num_rows, |ui, row_range| { + ScrollArea::vertical().show_rows(ui, row_height, num_rows, |ui, row_range| { for row in row_range { let text = format!("This is row {}/{}", row + 1, num_rows); ui.label(text); @@ -94,7 +94,7 @@ fn huge_content_painter(ui: &mut egui::Ui) { let row_height = ui.fonts()[text_style].row_height() + ui.spacing().item_spacing.y; let num_rows = 10_000; - ScrollArea::auto_sized().show_viewport(ui, |ui, viewport| { + ScrollArea::vertical().show_viewport(ui, |ui, viewport| { ui.set_height(row_height * num_rows as f32); let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize; @@ -184,7 +184,7 @@ impl super::View for ScrollTo { scroll_bottom |= ui.button("Scroll to bottom").clicked(); }); - let mut scroll_area = ScrollArea::from_max_height(200.0); + let mut scroll_area = ScrollArea::vertical().max_height(200.0); if go_to_scroll_offset { scroll_area = scroll_area.scroll_offset(self.offset); } diff --git a/egui_demo_lib/src/apps/demo/tests.rs b/egui_demo_lib/src/apps/demo/tests.rs index 05054ce0..4552c872 100644 --- a/egui_demo_lib/src/apps/demo/tests.rs +++ b/egui_demo_lib/src/apps/demo/tests.rs @@ -400,7 +400,7 @@ impl super::Demo for WindowResizeTest { Window::new("↔ resizable + scroll") .open(open) - .scroll(true) + .vscroll(true) .resizable(true) .default_height(300.0) .show(ctx, |ui| { @@ -413,14 +413,14 @@ impl super::Demo for WindowResizeTest { Window::new("↔ resizable + embedded scroll") .open(open) - .scroll(false) + .vscroll(false) .resizable(true) .default_height(300.0) .show(ctx, |ui| { ui.label("This window is resizable but has no built-in scroll area."); ui.label("However, we have a sub-region with a scroll bar:"); ui.separator(); - ScrollArea::auto_sized().show(ui, |ui| { + ScrollArea::vertical().show(ui, |ui| { ui.code(crate::LOREM_IPSUM_LONG); ui.code(crate::LOREM_IPSUM_LONG); }); @@ -429,7 +429,7 @@ impl super::Demo for WindowResizeTest { Window::new("↔ resizable without scroll") .open(open) - .scroll(false) + .vscroll(false) .resizable(true) .show(ctx, |ui| { ui.label("This window is resizable but has no scroll area. This means it can only be resized to a size where all the contents is visible."); @@ -440,7 +440,7 @@ impl super::Demo for WindowResizeTest { Window::new("↔ resizable with TextEdit") .open(open) - .scroll(false) + .vscroll(false) .resizable(true) .default_height(300.0) .show(ctx, |ui| { @@ -450,7 +450,7 @@ impl super::Demo for WindowResizeTest { Window::new("↔ freely resized") .open(open) - .scroll(false) + .vscroll(false) .resizable(true) .default_size([250.0, 150.0]) .show(ctx, |ui| { diff --git a/egui_demo_lib/src/apps/demo/window_options.rs b/egui_demo_lib/src/apps/demo/window_options.rs index d3321e57..b0b3b60c 100644 --- a/egui_demo_lib/src/apps/demo/window_options.rs +++ b/egui_demo_lib/src/apps/demo/window_options.rs @@ -6,7 +6,7 @@ pub struct WindowOptions { closable: bool, collapsible: bool, resizable: bool, - scroll: bool, + scroll2: [bool; 2], disabled_time: f64, anchored: bool, @@ -21,8 +21,8 @@ impl Default for WindowOptions { title_bar: true, closable: true, collapsible: true, - resizable: false, - scroll: false, + resizable: true, + scroll2: [true; 2], disabled_time: f64::NEG_INFINITY, anchored: false, anchor: egui::Align2::RIGHT_TOP, @@ -43,7 +43,7 @@ impl super::Demo for WindowOptions { closable, collapsible, resizable, - scroll, + scroll2, disabled_time, anchored, anchor, @@ -61,7 +61,7 @@ impl super::Demo for WindowOptions { .resizable(resizable) .collapsible(collapsible) .title_bar(title_bar) - .scroll(scroll) + .scroll2(scroll2) .enabled(enabled); if closable { window = window.open(open); @@ -81,7 +81,7 @@ impl super::View for WindowOptions { closable, collapsible, resizable, - scroll, + scroll2, disabled_time: _, anchored, anchor, @@ -100,7 +100,8 @@ impl super::View for WindowOptions { ui.checkbox(closable, "closable"); ui.checkbox(collapsible, "collapsible"); ui.checkbox(resizable, "resizable"); - ui.checkbox(scroll, "scroll"); + ui.checkbox(&mut scroll2[0], "hscroll"); + ui.checkbox(&mut scroll2[1], "vscroll"); }); }); ui.group(|ui| { @@ -109,15 +110,15 @@ impl super::View for WindowOptions { ui.set_enabled(*anchored); ui.horizontal(|ui| { ui.label("x:"); - ui.selectable_value(&mut anchor.0[0], egui::Align::LEFT, "Left"); - ui.selectable_value(&mut anchor.0[0], egui::Align::Center, "Center"); - ui.selectable_value(&mut anchor.0[0], egui::Align::RIGHT, "Right"); + ui.selectable_value(&mut anchor[0], egui::Align::LEFT, "Left"); + ui.selectable_value(&mut anchor[0], egui::Align::Center, "Center"); + ui.selectable_value(&mut anchor[0], egui::Align::RIGHT, "Right"); }); ui.horizontal(|ui| { ui.label("y:"); - ui.selectable_value(&mut anchor.0[1], egui::Align::TOP, "Top"); - ui.selectable_value(&mut anchor.0[1], egui::Align::Center, "Center"); - ui.selectable_value(&mut anchor.0[1], egui::Align::BOTTOM, "Bottom"); + ui.selectable_value(&mut anchor[1], egui::Align::TOP, "Top"); + ui.selectable_value(&mut anchor[1], egui::Align::Center, "Center"); + ui.selectable_value(&mut anchor[1], egui::Align::BOTTOM, "Bottom"); }); ui.horizontal(|ui| { ui.label("Offset:"); diff --git a/egui_demo_lib/src/apps/demo/window_with_panels.rs b/egui_demo_lib/src/apps/demo/window_with_panels.rs index 28dc126a..397f042e 100644 --- a/egui_demo_lib/src/apps/demo/window_with_panels.rs +++ b/egui_demo_lib/src/apps/demo/window_with_panels.rs @@ -12,7 +12,7 @@ impl super::Demo for WindowWithPanels { let window = egui::Window::new("Window with Panels") .default_width(600.0) .default_height(400.0) - .scroll(false) + .vscroll(false) .open(open); window.show(ctx, |ui| self.ui(ui)); } @@ -26,7 +26,7 @@ impl super::View for WindowWithPanels { .resizable(true) .min_height(32.0) .show_inside(ui, |ui| { - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Expandable Upper Panel"); }); @@ -42,7 +42,7 @@ impl super::View for WindowWithPanels { ui.vertical_centered(|ui| { ui.heading("Left Panel"); }); - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak()); }); }); @@ -55,7 +55,7 @@ impl super::View for WindowWithPanels { ui.vertical_centered(|ui| { ui.heading("Right Panel"); }); - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak()); }); }); @@ -73,7 +73,7 @@ impl super::View for WindowWithPanels { ui.vertical_centered(|ui| { ui.heading("Central Panel"); }); - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak()); }); }); diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index 7f048834..250d3260 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -264,7 +264,7 @@ fn ui_resource( ui.separator(); - egui::ScrollArea::auto_sized().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { if let Some(image) = image { if let Some(texture_id) = tex_mngr.texture(frame, &response.url, image) { let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32); diff --git a/egui_demo_lib/src/backend_panel.rs b/egui_demo_lib/src/backend_panel.rs index 26c955b4..a73ec70d 100644 --- a/egui_demo_lib/src/backend_panel.rs +++ b/egui_demo_lib/src/backend_panel.rs @@ -323,14 +323,14 @@ impl EguiWindows { egui::Window::new("🔧 Settings") .open(settings) - .scroll(true) + .vscroll(true) .show(ctx, |ui| { ctx.settings_ui(ui); }); egui::Window::new("🔍 Inspection") .open(inspection) - .scroll(true) + .vscroll(true) .show(ctx, |ui| { ctx.inspection_ui(ui); }); diff --git a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 3b6105f0..8558d249 100644 --- a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -34,14 +34,14 @@ impl EasyMarkEditor { }); ui.separator(); ui.columns(2, |columns| { - ScrollArea::auto_sized() + ScrollArea::vertical() .id_source("source") .show(&mut columns[0], |ui| { ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace)); // let cursor = TextEdit::cursor(response.id); // TODO: cmd-i, cmd-b, etc for italics, bold, .... }); - ScrollArea::auto_sized() + ScrollArea::vertical() .id_source("rendered") .show(&mut columns[1], |ui| { crate::easy_mark::easy_mark(ui, &self.code); diff --git a/emath/src/align.rs b/emath/src/align.rs index 0a31f65d..5cc86fd0 100644 --- a/emath/src/align.rs +++ b/emath/src/align.rs @@ -172,6 +172,22 @@ impl Align2 { } } +impl std::ops::Index for Align2 { + type Output = Align; + + #[inline(always)] + fn index(&self, index: usize) -> &Align { + &self.0[index] + } +} + +impl std::ops::IndexMut for Align2 { + #[inline(always)] + fn index_mut(&mut self, index: usize) -> &mut Align { + &mut self.0[index] + } +} + pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect { Align2::CENTER_CENTER.align_size_within_rect(size, frame) }