Horizontal scrolling (#663)

* First pass (missing rendering the bar)

* Render horizontal bars, and change Window scroll API

* emath: add impl Index + IndexMut for Align2

* Scrolling: fix subtle sizing bugs

* Add horizontal scrolling to color test

* try to wrap content before showing scrollbars, + add auto-shrink option

* Add hscroll to the misc demo window

* Fix for putting wrapping labels in an infinitely wide layout

* Add a egui_asserts to protect against nans in the layout engine

* Add line about horizontal scrolling to changelog

* Add example to docs of ScrollArea

* code cleanup
This commit is contained in:
Emil Ernerfeldt 2021-08-28 13:18:21 +02:00 committed by GitHub
parent e98ae2ea7a
commit 105b999cb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 464 additions and 194 deletions

View file

@ -8,6 +8,10 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
## Unreleased ## Unreleased
### Added ⭐
* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
## 0.14.2 - 2021-08-28 - Window resize fix ## 0.14.2 - 2021-08-28 - Window resize fix
### Fixed 🐛 ### Fixed 🐛

View file

@ -188,7 +188,9 @@ fn combo_box<R>(
ui.memory().toggle_popup(popup_id); ui.memory().toggle_popup(popup_id);
} }
let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| { 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 { InnerResponse {

View file

@ -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::*; use crate::*;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -7,48 +14,77 @@ pub(crate) struct State {
/// Positive offset means scrolling down/right /// Positive offset means scrolling down/right
offset: Vec2, offset: Vec2,
show_scroll: bool, show_scroll: [bool; 2],
/// Momentum, used for kinetic scrolling /// Momentum, used for kinetic scrolling
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub vel: Vec2, pub vel: Vec2,
/// Mouse offset relative to the top of the handle when started moving the handle. /// Mouse offset relative to the top of the handle when started moving the handle.
scroll_start_offset_from_top: Option<f32>, scroll_start_offset_from_top_left: [Option<f32>; 2],
} }
impl Default for State { impl Default for State {
fn default() -> Self { fn default() -> Self {
Self { Self {
offset: Vec2::ZERO, offset: Vec2::ZERO,
show_scroll: false, show_scroll: [false; 2],
vel: Vec2::ZERO, vel: Vec2::ZERO,
scroll_start_offset_from_top: None, scroll_start_offset_from_top_left: [None; 2],
} }
} }
} }
// TODO: rename VScroll /// Add vertical and/or horizontal scrolling to a contained [`Ui`].
/// Add vertical 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)] #[derive(Clone, Debug)]
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct ScrollArea { 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, always_show_scroll: bool,
id_source: Option<Id>, id_source: Option<Id>,
offset: Option<Vec2>, offset: Option<Vec2>,
/// If false, we ignore scroll events.
scrolling_enabled: bool, scrolling_enabled: bool,
} }
impl ScrollArea { impl ScrollArea {
/// Will make the area be as high as it is allowed to be (i.e. fill the [`Ui`] it is in) /// Create a horizontal scroll area.
pub fn auto_sized() -> Self { pub fn horizontal() -> Self {
Self::from_max_height(f32::INFINITY) Self::new([true, false])
} }
/// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding Ui /// Create a vertical scroll area.
pub fn from_max_height(max_height: f32) -> Self { 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 { Self {
max_height, has_bar,
auto_shrink: [true; 2],
max_size: Vec2::INFINITY,
always_show_scroll: false, always_show_scroll: false,
id_source: None, id_source: None,
offset: 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 `false` (default), the scroll bar will be hidden when not needed/
/// If `true`, the scroll bar will always be displayed even if 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 { pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
@ -78,22 +142,62 @@ impl ScrollArea {
self 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 /// Control the scrolling behavior
/// If `true` (default), the scroll area will respond to user scrolling /// If `true` (default), the scroll area will respond to user scrolling
/// If `false`, the scroll area will not 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 /// 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 { pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable; self.scrolling_enabled = enable;
self 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 { struct Prepared {
id: Id, id: Id,
state: State, 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, always_show_scroll: bool,
inner_rect: Rect, inner_rect: Rect,
content_ui: Ui, content_ui: Ui,
@ -106,7 +210,9 @@ struct Prepared {
impl ScrollArea { impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared { fn begin(self, ui: &mut Ui) -> Prepared {
let Self { let Self {
max_height, has_bar,
auto_shrink,
max_size,
always_show_scroll, always_show_scroll,
id_source, id_source,
offset, offset,
@ -123,38 +229,59 @@ impl ScrollArea {
state.offset = offset; 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 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 max_scroll_bar_width
} else { } 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 available_outer = ui.available_rect_before_wrap();
let outer_size = vec2( let outer_size = available_outer.size().at_most(max_size);
available_outer.width(),
available_outer.height().at_most(max_height),
);
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 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( let mut content_ui = ui.child_ui(
Rect::from_min_size( Rect::from_min_size(inner_rect.min - state.offset, inner_child_max_size),
inner_rect.min - state.offset,
vec2(inner_size.x, f32::INFINITY),
),
*ui.layout(), *ui.layout(),
); );
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin); 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 = 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); content_ui.set_clip_rect(content_clip_rect);
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
@ -162,7 +289,9 @@ impl ScrollArea {
Prepared { Prepared {
id, id,
state, state,
current_scroll_bar_width, has_bar,
auto_shrink,
current_bar_use,
always_show_scroll, always_show_scroll,
inner_rect, inner_rect,
content_ui, content_ui,
@ -186,7 +315,7 @@ impl ScrollArea {
/// let row_height = ui.fonts()[text_style].row_height(); /// 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 row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
/// let num_rows = 10_000; /// 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 { /// for row in row_range {
/// let text = format!("Row {}/{}", row + 1, num_rows); /// let text = format!("Row {}/{}", row + 1, num_rows);
/// ui.label(text); /// ui.label(text);
@ -241,8 +370,10 @@ impl Prepared {
id, id,
mut state, mut state,
inner_rect, inner_rect,
has_bar,
auto_shrink,
mut current_bar_use,
always_show_scroll, always_show_scroll,
mut current_scroll_bar_width,
content_ui, content_ui,
viewport: _, viewport: _,
scrolling_enabled, scrolling_enabled,
@ -251,52 +382,67 @@ impl Prepared {
let content_size = content_ui.min_size(); let content_size = content_ui.min_size();
// We take the scroll target so only this ScrollArea will use it. // 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 { 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 center_factor = align.to_factor();
let top = content_ui.min_rect().top(); let min = content_ui.min_rect().min[d];
let visible_range = top..=top + content_ui.clip_rect().height(); let visible_range = min..=min + content_ui.clip_rect().size()[d];
let offset_y = scroll_y - lerp(visible_range, center_factor); let offset = scroll - lerp(visible_range, center_factor);
let mut spacing = ui.spacing().item_spacing.y; let mut spacing = ui.spacing().item_spacing[d];
// Depending on the alignment we need to add or subtract the 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); spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
state.offset.y = offset_y + spacing; state.offset[d] = offset + spacing;
}
}
} }
let inner_rect = { let inner_rect = {
let width = if inner_rect.width().is_finite() { let mut inner_size = inner_rect.size();
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_rect = for d in 0..2 {
Rect::from_min_size(inner_rect.min, vec2(width, inner_rect.height())); 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: // 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() let max_x =
- current_scroll_bar_width ui.input().screen_rect().right() - current_bar_use.x - ui.spacing().item_spacing.x;
- ui.spacing().item_spacing.x;
inner_rect.max.x = inner_rect.max.x.at_most(max_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 inner_rect
}; };
let outer_rect = Rect::from_min_size( let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
inner_rect.min,
inner_rect.size() + vec2(current_scroll_bar_width, 0.0),
);
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): // Drag contents to scroll (for touch screens mostly):
let sense = if self.scrolling_enabled { let sense = if self.scrolling_enabled {
Sense::drag() Sense::drag()
@ -307,8 +453,14 @@ impl Prepared {
let input = ui.input(); let input = ui.input();
if content_response.dragged() { if content_response.dragged() {
state.offset.y -= input.pointer.delta().y; for d in 0..2 {
state.vel = input.pointer.velocity(); 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 { } else {
let stop_speed = 20.0; // Pixels per second. let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared. let friction_coeff = 1000.0; // Pixels per second squared.
@ -321,59 +473,91 @@ impl Prepared {
state.vel -= friction * state.vel.normalized(); state.vel -= friction * state.vel.normalized();
// Offset has an inverted coordinate system compared to // Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it // 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(); 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) { if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 {
if has_bar[d] {
let mut frame_state = ui.ctx().frame_state(); let mut frame_state = ui.ctx().frame_state();
let scroll_delta = frame_state.scroll_delta; let scroll_delta = frame_state.scroll_delta;
let scrolling_up = state.offset.y > 0.0 && scroll_delta.y > 0.0; let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
let scrolling_down = state.offset.y < max_offset && scroll_delta.y < 0.0; let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
if scrolling_up || scrolling_down { if scrolling_up || scrolling_down {
state.offset.y -= scroll_delta.y; state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it. // Clear scroll delta so no parent scroll will use it.
frame_state.scroll_delta = Vec2::ZERO; 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); 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: // Avoid frame delay; start showing scroll bar right away:
current_scroll_bar_width = max_scroll_bar_width * ui.ctx().animate_bool(id, true); 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);
}
for d in 0..2 {
let animation_t = current_bar_use[1 - d] / max_scroll_bar_width;
if animation_t == 0.0 {
continue;
} }
if current_scroll_bar_width > 0.0 {
let animation_t = current_scroll_bar_width / max_scroll_bar_width;
// margin between contents and scroll bar // margin between contents and scroll bar
let margin = animation_t * ui.spacing().item_spacing.x; let margin = animation_t * ui.spacing().item_spacing.x;
let left = inner_rect.right() + margin; let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1)
let right = outer_rect.right(); let max_cross = outer_rect.max[1 - d]; // right of vertical scroll (d == 1)
let top = inner_rect.top(); let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1)
let bottom = inner_rect.bottom(); let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1)
let outer_scroll_rect = Rect::from_min_max( let outer_scroll_rect = if d == 0 {
pos2(left, inner_rect.top()), Rect::from_min_max(
pos2(right, inner_rect.bottom()), 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 = 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( let handle_rect = if d == 0 {
pos2(left, from_content(state.offset.y)), Rect::from_min_max(
pos2(right, from_content(state.offset.y + inner_rect.height())), 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 { let sense = if self.scrolling_enabled {
Sense::click_and_drag() Sense::click_and_drag()
} else { } else {
@ -382,43 +566,57 @@ impl Prepared {
let response = ui.interact(outer_scroll_rect, interact_id, sense); let response = ui.interact(outer_scroll_rect, interact_id, sense);
if let Some(pointer_pos) = response.interact_pointer_pos() { if let Some(pointer_pos) = response.interact_pointer_pos() {
let scroll_start_offset_from_top = let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
state.scroll_start_offset_from_top.get_or_insert_with(|| { .get_or_insert_with(|| {
if handle_rect.contains(pointer_pos) { if handle_rect.contains(pointer_pos) {
pointer_pos.y - handle_rect.top() pointer_pos[d] - handle_rect.min[d]
} else { } 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. // 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) let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
.clamp(top, handle_top_pos_at_bottom); .clamp(min_main, handle_top_pos_at_bottom);
pointer_pos.y - new_handle_top_pos pointer_pos[d] - new_handle_top_pos
} }
}); });
let new_handle_top = pointer_pos.y - *scroll_start_offset_from_top; let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
state.offset.y = remap(new_handle_top, top..=bottom, 0.0..=content_size.y); state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]);
} else { } else {
state.scroll_start_offset_from_top = None; state.scroll_start_offset_from_top_left[d] = None;
} }
let unbounded_offset_y = state.offset.y; let unbounded_offset = state.offset[d];
state.offset.y = state.offset.y.max(0.0); state.offset[d] = state.offset[d].max(0.0);
state.offset.y = state.offset.y.min(max_offset); state.offset[d] = state.offset[d].min(max_offset[d]);
if state.offset.y != unbounded_offset_y { if state.offset[d] != unbounded_offset {
state.vel = Vec2::ZERO; state.vel[d] = 0.0;
} }
// Avoid frame-delay by calculating a new handle rect: // Avoid frame-delay by calculating a new handle rect:
let mut handle_rect = Rect::from_min_max( let mut handle_rect = if d == 0 {
pos2(left, from_content(state.offset.y)), Rect::from_min_max(
pos2(right, from_content(state.offset.y + inner_rect.height())), pos2(from_content(state.offset.x), min_cross),
); pos2(from_content(state.offset.x + inner_rect.width()), max_cross),
let min_handle_height = ui.spacing().scroll_bar_width; )
if handle_rect.size().y < min_handle_height { } 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 = Rect::from_center_size(
handle_rect.center(), 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( ui.advance_cursor_after_rect(outer_rect);
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));
if show_scroll_this_frame != state.show_scroll { if show_scroll_this_frame != state.show_scroll {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height()); state.offset = state.offset.min(content_size - inner_rect.size());
state.offset.y = state.offset.y.max(0.0); state.offset = state.offset.max(Vec2::ZERO);
state.show_scroll = show_scroll_this_frame; state.show_scroll = show_scroll_this_frame;
ui.memory().id_data.insert(id, state); 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 { fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 {
ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width
} }

View file

@ -28,7 +28,7 @@ pub struct Window<'open> {
area: Area, area: Area,
frame: Option<Frame>, frame: Option<Frame>,
resize: Resize, resize: Resize,
scroll: Option<ScrollArea>, scroll: ScrollArea,
collapsible: bool, collapsible: bool,
with_title_bar: bool, with_title_bar: bool,
} }
@ -51,7 +51,7 @@ impl<'open> Window<'open> {
.with_stroke(false) .with_stroke(false)
.min_size([96.0, 32.0]) .min_size([96.0, 32.0])
.default_size([340.0, 420.0]), // Default inner size of a window .default_size([340.0, 420.0]), // Default inner size of a window
scroll: None, scroll: ScrollArea::neither(),
collapsible: true, collapsible: true,
with_title_bar: 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. /// Text will not wrap, but will instead make your window width expand.
pub fn auto_sized(mut self) -> Self { pub fn auto_sized(mut self) -> Self {
self.resize = self.resize.auto_sized(); self.resize = self.resize.auto_sized();
self.scroll = None; self.scroll = ScrollArea::neither();
self self
} }
/// Enable/disable scrolling. `false` by default. /// Enable/disable horizontal/vertical scrolling. `false` by default.
pub fn scroll(mut self, scroll: bool) -> Self { pub fn scroll2(mut self, scroll: [bool; 2]) -> Self {
if scroll { self.scroll = self.scroll.scroll2(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;
}
self 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. /// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self { pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.area = self.area.drag_bounds(bounds); self.area = self.area.drag_bounds(bounds);
@ -352,7 +359,7 @@ impl<'open> Window<'open> {
ui.add_space(title_content_spacing); ui.add_space(title_content_spacing);
} }
if let Some(scroll) = scroll { if scroll.has_any_bar() {
scroll.show(ui, add_contents) scroll.show(ui, add_contents)
} else { } else {
add_contents(ui) add_contents(ui)

View file

@ -27,8 +27,9 @@ pub(crate) struct FrameState {
pub(crate) tooltip_rect: Option<(Id, Rect, usize)>, pub(crate) tooltip_rect: Option<(Id, Rect, usize)>,
/// Cleared by the first `ScrollArea` that makes use of it. /// Cleared by the first `ScrollArea` that makes use of it.
pub(crate) scroll_delta: Vec2, pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
pub(crate) scroll_target: Option<(f32, Align)>, /// horizontal, vertical
pub(crate) scroll_target: [Option<(f32, Align)>; 2],
} }
impl Default for FrameState { impl Default for FrameState {
@ -40,7 +41,7 @@ impl Default for FrameState {
used_by_panels: Rect::NAN, used_by_panels: Rect::NAN,
tooltip_rect: None, tooltip_rect: None,
scroll_delta: Vec2::ZERO, scroll_delta: Vec2::ZERO,
scroll_target: None, scroll_target: [None; 2],
} }
} }
} }
@ -63,7 +64,7 @@ impl FrameState {
*used_by_panels = Rect::NOTHING; *used_by_panels = Rect::NOTHING;
*tooltip_rect = None; *tooltip_rect = None;
*scroll_delta = input.scroll_delta; *scroll_delta = input.scroll_delta;
*scroll_target = None; *scroll_target = [None; 2];
} }
/// How much space is still available after panels has been added. /// How much space is still available after panels has been added.

View file

@ -1,4 +1,4 @@
use crate::{emath::*, Align}; use crate::{egui_assert, emath::*, Align};
use std::f32::INFINITY; use std::f32::INFINITY;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -84,6 +84,12 @@ impl Region {
self.max_rect.extend_with_y(y); self.max_rect.extend_with_y(y);
self.cursor.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 /// Given the cursor in the region, how much space is available
/// for the next widget? /// for the next widget?
fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect { 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, // NOTE: in normal top-down layout the cursor has moved below the current max_rect,
// but the available shouldn't be negative. // but the available shouldn't be negative.
@ -450,6 +459,8 @@ impl Layout {
avail.max.y = y; avail.max.y = y;
} }
egui_assert!(!avail.any_nan());
avail avail
} }
@ -458,6 +469,7 @@ impl Layout {
/// This is what you then pass to `advance_after_rects`. /// This is what you then pass to `advance_after_rects`.
/// Use `justify_and_align` to get the inner `widget_rect`. /// Use `justify_and_align` to get the inner `widget_rect`.
pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> 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); crate::egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
if self.main_wrap { if self.main_wrap {
@ -537,6 +549,7 @@ impl Layout {
} }
fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect { 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); crate::egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
let available_rect = self.available_rect_before_wrap_finite(region); 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())); 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 frame_rect
} }
@ -595,6 +611,7 @@ impl Layout {
let frame = self.next_frame_ignore_wrap(region, size); let frame = self.next_frame_ignore_wrap(region, size);
let rect = self.align_size_within_rect(size, frame); let rect = self.align_size_within_rect(size, frame);
crate::egui_assert!(!rect.any_nan()); 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.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY);
crate::egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY); crate::egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY);
rect rect
@ -639,6 +656,7 @@ impl Layout {
widget_rect: Rect, widget_rect: Rect,
item_spacing: Vec2, item_spacing: Vec2,
) { ) {
egui_assert!(!cursor.any_nan());
if self.main_wrap { if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) { if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary // make row/column larger if necessary

View file

@ -119,6 +119,7 @@ impl Placer {
/// This is what you then pass to `advance_after_rects`. /// This is what you then pass to `advance_after_rects`.
/// Use `justify_and_align` to get the inner `widget_rect`. /// Use `justify_and_align` to get the inner `widget_rect`.
pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect { pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect {
self.region.sanity_check();
if let Some(grid) = &self.grid { if let Some(grid) = &self.grid {
grid.next_cell(self.region.cursor, child_size) grid.next_cell(self.region.cursor, child_size)
} else { } else {
@ -169,6 +170,10 @@ impl Placer {
widget_rect: Rect, widget_rect: Rect,
item_spacing: Vec2, 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 { if let Some(grid) = &mut self.grid {
grid.advance(&mut self.region.cursor, frame_rect, widget_rect) grid.advance(&mut self.region.cursor, frame_rect, widget_rect)
} else { } 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.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. /// 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.min.x = region.max_rect.min.x;
region.cursor.max.x = region.max_rect.max.x; region.cursor.max.x = region.max_rect.max.x;
region.sanity_check();
} }
/// Set the maximum height of the ui. /// Set the maximum height of the ui.
@ -244,6 +253,8 @@ impl Placer {
region.cursor.min.y = region.max_rect.min.y; region.cursor.min.y = region.max_rect.min.y;
region.cursor.max.y = region.max_rect.max.y; region.cursor.max.y = region.max_rect.max.y;
region.sanity_check();
} }
/// Set the minimum width of the ui. /// Set the minimum width of the ui.

View file

@ -422,7 +422,7 @@ impl Response {
/// ``` /// ```
/// # use egui::Align; /// # use egui::Align;
/// # let mut ui = &mut egui::Ui::__test(); /// # 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 { /// for i in 0..1000 {
/// let response = ui.button(format!("Button {}", i)); /// let response = ui.button(format!("Button {}", i));
/// if response.clicked() { /// if response.clicked() {
@ -432,8 +432,11 @@ impl Response {
/// }); /// });
/// ``` /// ```
pub fn scroll_to_me(&self, align: Align) { 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()); 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. /// For accessibility.

View file

@ -696,6 +696,7 @@ impl Ui {
fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
let item_spacing = self.spacing().item_spacing; let item_spacing = self.spacing().item_spacing;
let frame_rect = self.placer.next_space(desired_size, 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); let widget_rect = self.placer.justify_and_align(frame_rect, desired_size);
self.placer self.placer
@ -714,6 +715,7 @@ impl Ui {
} }
pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id {
egui_assert!(!rect.any_nan());
let item_spacing = self.spacing().item_spacing; let item_spacing = self.spacing().item_spacing;
self.placer.advance_after_rects(rect, rect, item_spacing); self.placer.advance_after_rects(rect, rect, item_spacing);
@ -856,7 +858,7 @@ impl Ui {
/// ``` /// ```
/// # use egui::Align; /// # use egui::Align;
/// # let mut ui = &mut egui::Ui::__test(); /// # 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(); /// let scroll_bottom = ui.button("Scroll to bottom.").clicked();
/// for i in 0..1000 { /// for i in 0..1000 {
/// ui.label(format!("Item {}", i)); /// ui.label(format!("Item {}", i));
@ -868,8 +870,10 @@ impl Ui {
/// }); /// });
/// ``` /// ```
pub fn scroll_to_cursor(&mut self, align: Align) { pub fn scroll_to_cursor(&mut self, align: Align) {
let target_y = self.next_widget_position().y; let target = self.next_widget_position();
self.ctx().frame_state().scroll_target = Some((target_y, align)); for d in 0..2 {
self.ctx().frame_state().scroll_target[d] = Some((target[d], align));
}
} }
} }

View file

@ -314,16 +314,19 @@ impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let sense = self.sense; let sense = self.sense;
let max_width = ui.available_width();
if self.should_wrap(ui) if self.should_wrap(ui)
&& ui.layout().main_dir() == Direction::LeftToRight && ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap() && ui.layout().main_wrap()
&& max_width.is_finite()
{ {
// On a wrapping horizontal layout we want text to start after the previous widget, // 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: // then continue on the line below! This will take some extra work:
let cursor = ui.cursor(); let cursor = ui.cursor();
let max_width = ui.available_width();
let first_row_indentation = max_width - ui.available_size_before_wrap().x; 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 text_style = self.text_style_or_default(ui.style());
let galley = ui.fonts().layout_multiline_with_indentation_and_max_width( let galley = ui.fonts().layout_multiline_with_indentation_and_max_width(

View file

@ -42,7 +42,7 @@ impl epi::App for ColorTest {
); );
ui.separator(); 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())); self.ui(ui, &mut Some(frame.tex_allocator()));
}); });
}); });
@ -55,6 +55,8 @@ impl ColorTest {
ui: &mut Ui, ui: &mut Ui,
mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>, mut tex_allocator: &mut Option<&mut dyn epi::TextureAllocator>,
) { ) {
ui.set_max_width(680.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file!()); ui.add(crate::__egui_github_link_file!());
}); });

View file

@ -15,7 +15,7 @@ impl super::Demo for DancingStrings {
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.default_size(vec2(512.0, 256.0)) .default_size(vec2(512.0, 256.0))
.scroll(false) .vscroll(false)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }

View file

@ -159,7 +159,7 @@ impl DemoWindows {
ui.separator(); ui.separator();
ScrollArea::auto_sized().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS}; use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS};
ui.label("egui is an immediate mode GUI library written in Rust."); ui.label("egui is an immediate mode GUI library written in Rust.");

View file

@ -103,7 +103,7 @@ impl super::Demo for DragAndDropDemo {
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.default_size(vec2(256.0, 256.0)) .default_size(vec2(256.0, 256.0))
.scroll(false) .vscroll(false)
.resizable(false) .resizable(false)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }

View file

@ -84,7 +84,7 @@ impl super::View for FontBook {
ui.separator(); ui.separator();
egui::ScrollArea::auto_sized().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0); ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0);

View file

@ -34,13 +34,16 @@ impl Demo for MiscDemoWindow {
fn show(&mut self, ctx: &CtxRef, open: &mut bool) { fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.scroll(true) .vscroll(true)
.hscroll(true)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }
impl View for MiscDemoWindow { impl View for MiscDemoWindow {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
ui.set_min_width(250.0);
CollapsingHeader::new("Widgets") CollapsingHeader::new("Widgets")
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {

View file

@ -79,7 +79,7 @@ impl super::Demo for Painting {
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.default_size(vec2(512.0, 512.0)) .default_size(vec2(512.0, 512.0))
.scroll(false) .vscroll(false)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }

View file

@ -401,7 +401,7 @@ impl super::Demo for PlotDemo {
Window::new(self.name()) Window::new(self.name())
.open(open) .open(open)
.default_size(vec2(400.0, 400.0)) .default_size(vec2(400.0, 400.0))
.scroll(false) .vscroll(false)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }

View file

@ -77,7 +77,7 @@ fn huge_content_lines(ui: &mut egui::Ui) {
let text_style = TextStyle::Body; let text_style = TextStyle::Body;
let row_height = ui.fonts()[text_style].row_height(); let row_height = ui.fonts()[text_style].row_height();
let num_rows = 10_000; 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 { for row in row_range {
let text = format!("This is row {}/{}", row + 1, num_rows); let text = format!("This is row {}/{}", row + 1, num_rows);
ui.label(text); 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 row_height = ui.fonts()[text_style].row_height() + ui.spacing().item_spacing.y;
let num_rows = 10_000; 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); ui.set_height(row_height * num_rows as f32);
let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize; 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(); 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 { if go_to_scroll_offset {
scroll_area = scroll_area.scroll_offset(self.offset); scroll_area = scroll_area.scroll_offset(self.offset);
} }

View file

@ -400,7 +400,7 @@ impl super::Demo for WindowResizeTest {
Window::new("↔ resizable + scroll") Window::new("↔ resizable + scroll")
.open(open) .open(open)
.scroll(true) .vscroll(true)
.resizable(true) .resizable(true)
.default_height(300.0) .default_height(300.0)
.show(ctx, |ui| { .show(ctx, |ui| {
@ -413,14 +413,14 @@ impl super::Demo for WindowResizeTest {
Window::new("↔ resizable + embedded scroll") Window::new("↔ resizable + embedded scroll")
.open(open) .open(open)
.scroll(false) .vscroll(false)
.resizable(true) .resizable(true)
.default_height(300.0) .default_height(300.0)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.label("This window is resizable but has no built-in scroll area."); 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.label("However, we have a sub-region with a scroll bar:");
ui.separator(); 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);
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") Window::new("↔ resizable without scroll")
.open(open) .open(open)
.scroll(false) .vscroll(false)
.resizable(true) .resizable(true)
.show(ctx, |ui| { .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."); 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") Window::new("↔ resizable with TextEdit")
.open(open) .open(open)
.scroll(false) .vscroll(false)
.resizable(true) .resizable(true)
.default_height(300.0) .default_height(300.0)
.show(ctx, |ui| { .show(ctx, |ui| {
@ -450,7 +450,7 @@ impl super::Demo for WindowResizeTest {
Window::new("↔ freely resized") Window::new("↔ freely resized")
.open(open) .open(open)
.scroll(false) .vscroll(false)
.resizable(true) .resizable(true)
.default_size([250.0, 150.0]) .default_size([250.0, 150.0])
.show(ctx, |ui| { .show(ctx, |ui| {

View file

@ -6,7 +6,7 @@ pub struct WindowOptions {
closable: bool, closable: bool,
collapsible: bool, collapsible: bool,
resizable: bool, resizable: bool,
scroll: bool, scroll2: [bool; 2],
disabled_time: f64, disabled_time: f64,
anchored: bool, anchored: bool,
@ -21,8 +21,8 @@ impl Default for WindowOptions {
title_bar: true, title_bar: true,
closable: true, closable: true,
collapsible: true, collapsible: true,
resizable: false, resizable: true,
scroll: false, scroll2: [true; 2],
disabled_time: f64::NEG_INFINITY, disabled_time: f64::NEG_INFINITY,
anchored: false, anchored: false,
anchor: egui::Align2::RIGHT_TOP, anchor: egui::Align2::RIGHT_TOP,
@ -43,7 +43,7 @@ impl super::Demo for WindowOptions {
closable, closable,
collapsible, collapsible,
resizable, resizable,
scroll, scroll2,
disabled_time, disabled_time,
anchored, anchored,
anchor, anchor,
@ -61,7 +61,7 @@ impl super::Demo for WindowOptions {
.resizable(resizable) .resizable(resizable)
.collapsible(collapsible) .collapsible(collapsible)
.title_bar(title_bar) .title_bar(title_bar)
.scroll(scroll) .scroll2(scroll2)
.enabled(enabled); .enabled(enabled);
if closable { if closable {
window = window.open(open); window = window.open(open);
@ -81,7 +81,7 @@ impl super::View for WindowOptions {
closable, closable,
collapsible, collapsible,
resizable, resizable,
scroll, scroll2,
disabled_time: _, disabled_time: _,
anchored, anchored,
anchor, anchor,
@ -100,7 +100,8 @@ impl super::View for WindowOptions {
ui.checkbox(closable, "closable"); ui.checkbox(closable, "closable");
ui.checkbox(collapsible, "collapsible"); ui.checkbox(collapsible, "collapsible");
ui.checkbox(resizable, "resizable"); ui.checkbox(resizable, "resizable");
ui.checkbox(scroll, "scroll"); ui.checkbox(&mut scroll2[0], "hscroll");
ui.checkbox(&mut scroll2[1], "vscroll");
}); });
}); });
ui.group(|ui| { ui.group(|ui| {
@ -109,15 +110,15 @@ impl super::View for WindowOptions {
ui.set_enabled(*anchored); ui.set_enabled(*anchored);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("x:"); ui.label("x:");
ui.selectable_value(&mut anchor.0[0], egui::Align::LEFT, "Left"); ui.selectable_value(&mut anchor[0], egui::Align::LEFT, "Left");
ui.selectable_value(&mut anchor.0[0], egui::Align::Center, "Center"); ui.selectable_value(&mut anchor[0], egui::Align::Center, "Center");
ui.selectable_value(&mut anchor.0[0], egui::Align::RIGHT, "Right"); ui.selectable_value(&mut anchor[0], egui::Align::RIGHT, "Right");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("y:"); ui.label("y:");
ui.selectable_value(&mut anchor.0[1], egui::Align::TOP, "Top"); ui.selectable_value(&mut anchor[1], egui::Align::TOP, "Top");
ui.selectable_value(&mut anchor.0[1], egui::Align::Center, "Center"); ui.selectable_value(&mut anchor[1], egui::Align::Center, "Center");
ui.selectable_value(&mut anchor.0[1], egui::Align::BOTTOM, "Bottom"); ui.selectable_value(&mut anchor[1], egui::Align::BOTTOM, "Bottom");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Offset:"); ui.label("Offset:");

View file

@ -12,7 +12,7 @@ impl super::Demo for WindowWithPanels {
let window = egui::Window::new("Window with Panels") let window = egui::Window::new("Window with Panels")
.default_width(600.0) .default_width(600.0)
.default_height(400.0) .default_height(400.0)
.scroll(false) .vscroll(false)
.open(open); .open(open);
window.show(ctx, |ui| self.ui(ui)); window.show(ctx, |ui| self.ui(ui));
} }
@ -26,7 +26,7 @@ impl super::View for WindowWithPanels {
.resizable(true) .resizable(true)
.min_height(32.0) .min_height(32.0)
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
egui::ScrollArea::auto_sized().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("Expandable Upper Panel"); ui.heading("Expandable Upper Panel");
}); });
@ -42,7 +42,7 @@ impl super::View for WindowWithPanels {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("Left Panel"); 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()); 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.vertical_centered(|ui| {
ui.heading("Right Panel"); 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()); 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.vertical_centered(|ui| {
ui.heading("Central Panel"); 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()); ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
}); });
}); });

View file

@ -264,7 +264,7 @@ fn ui_resource(
ui.separator(); ui.separator();
egui::ScrollArea::auto_sized().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
if let Some(image) = image { if let Some(image) = image {
if let Some(texture_id) = tex_mngr.texture(frame, &response.url, 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); let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32);

View file

@ -323,14 +323,14 @@ impl EguiWindows {
egui::Window::new("🔧 Settings") egui::Window::new("🔧 Settings")
.open(settings) .open(settings)
.scroll(true) .vscroll(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ctx.settings_ui(ui); ctx.settings_ui(ui);
}); });
egui::Window::new("🔍 Inspection") egui::Window::new("🔍 Inspection")
.open(inspection) .open(inspection)
.scroll(true) .vscroll(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ctx.inspection_ui(ui); ctx.inspection_ui(ui);
}); });

View file

@ -34,14 +34,14 @@ impl EasyMarkEditor {
}); });
ui.separator(); ui.separator();
ui.columns(2, |columns| { ui.columns(2, |columns| {
ScrollArea::auto_sized() ScrollArea::vertical()
.id_source("source") .id_source("source")
.show(&mut columns[0], |ui| { .show(&mut columns[0], |ui| {
ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace)); ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace));
// let cursor = TextEdit::cursor(response.id); // let cursor = TextEdit::cursor(response.id);
// TODO: cmd-i, cmd-b, etc for italics, bold, .... // TODO: cmd-i, cmd-b, etc for italics, bold, ....
}); });
ScrollArea::auto_sized() ScrollArea::vertical()
.id_source("rendered") .id_source("rendered")
.show(&mut columns[1], |ui| { .show(&mut columns[1], |ui| {
crate::easy_mark::easy_mark(ui, &self.code); crate::easy_mark::easy_mark(ui, &self.code);

View file

@ -172,6 +172,22 @@ impl Align2 {
} }
} }
impl std::ops::Index<usize> for Align2 {
type Output = Align;
#[inline(always)]
fn index(&self, index: usize) -> &Align {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> 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 { pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect {
Align2::CENTER_CENTER.align_size_within_rect(size, frame) Align2::CENTER_CENTER.align_size_within_rect(size, frame)
} }