Floating scroll bars (#3539)

* Move scroll bar spacing settings to a `struct ScrollSpacing`

* Add a demo for changing scroll bar appearance

* Add setting for ScrollBarVisibility in demo

* Add `#[inline]` to a `ScrollArea` builder methods

* Refactor how scroll bar show/hide is computed

* Add support for floating scroll bars

* Tweak color and opacity of the scroll handle

* Allow allocating a fixed size even for floating scroll bars

* Add three pre-sets of scroll bars: solid, thin, floating

* Use floating scroll bars as the default

* Fix id-clash with bidir scroll areas

* Improve demo

* Fix doclink

* Remove reset button from demo

* Fix doclinks

* Fix visual artifact with thin rounded rectangles

* Fix doclink

* typos
This commit is contained in:
Emil Ernerfeldt 2023-11-09 18:41:58 +01:00 committed by GitHub
parent f218825d94
commit 41f9df5cb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 668 additions and 149 deletions

View File

@ -25,7 +25,7 @@ struct ValueAnim {
} }
impl AnimationManager { impl AnimationManager {
/// See `Context::animate_bool` for documentation /// See [`crate::Context::animate_bool`] for documentation
pub fn animate_bool( pub fn animate_bool(
&mut self, &mut self,
input: &InputState, input: &InputState,

View File

@ -1,8 +1,3 @@
//! 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)] #![allow(clippy::needless_range_loop)]
use crate::*; use crate::*;
@ -20,6 +15,9 @@ pub struct State {
/// The content were to large to fit large frame. /// The content were to large to fit large frame.
content_is_too_large: [bool; 2], content_is_too_large: [bool; 2],
/// Did the user interact (hover or drag) the scroll bars last frame?
scroll_bar_interaction: [bool; 2],
/// Momentum, used for kinetic scrolling /// Momentum, used for kinetic scrolling
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
vel: Vec2, vel: Vec2,
@ -39,6 +37,7 @@ impl Default for State {
offset: Vec2::ZERO, offset: Vec2::ZERO,
show_scroll: [false; 2], show_scroll: [false; 2],
content_is_too_large: [false; 2], content_is_too_large: [false; 2],
scroll_bar_interaction: [false; 2],
vel: Vec2::ZERO, vel: Vec2::ZERO,
scroll_start_offset_from_top_left: [None; 2], scroll_start_offset_from_top_left: [None; 2],
scroll_stuck_to_end: [true; 2], scroll_stuck_to_end: [true; 2],
@ -80,15 +79,61 @@ pub struct ScrollAreaOutput<R> {
} }
/// Indicate whether the horizontal and vertical scroll bars must be always visible, hidden or visible when needed. /// Indicate whether the horizontal and vertical scroll bars must be always visible, hidden or visible when needed.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ScrollBarVisibility { pub enum ScrollBarVisibility {
AlwaysVisible, /// Hide scroll bar even if they are needed.
VisibleWhenNeeded, ///
/// You can still scroll, with the scroll-wheel
/// and by dragging the contents, but there is no
/// visual indication of how far you have scrolled.
AlwaysHidden, AlwaysHidden,
/// Show scroll bars only when the content size exceeds the container,
/// i.e. when there is any need to scroll.
///
/// This is the default.
VisibleWhenNeeded,
/// Always show the scroll bar, even if the contents fit in the container
/// and there is no need to scroll.
AlwaysVisible,
}
impl Default for ScrollBarVisibility {
#[inline]
fn default() -> Self {
Self::VisibleWhenNeeded
}
}
impl ScrollBarVisibility {
pub const ALL: [Self; 3] = [
Self::AlwaysHidden,
Self::VisibleWhenNeeded,
Self::AlwaysVisible,
];
} }
/// Add vertical and/or horizontal scrolling to a contained [`Ui`]. /// Add vertical and/or horizontal scrolling to a contained [`Ui`].
/// ///
/// By default, scroll bars only show up when needed, i.e. when the contents
/// is larger than the container.
/// This is controlled by [`Self::scroll_bar_visibility`].
///
/// There are two flavors of scroll areas: solid and floating.
/// Solid scroll bars use up space, reducing the amount of space available
/// to the contents. Floating scroll bars float on top of the contents, covering it.
/// You can change the scroll style by changing the [`crate::style::Spacing::scroll`].
///
/// ### Coordinate system
/// * 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.
///
/// If the floating scroll bars settings is turned on then `inner == outer`.
///
/// ## Example
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// egui::ScrollArea::vertical().show(ui, |ui| { /// egui::ScrollArea::vertical().show(ui, |ui| {
@ -101,8 +146,9 @@ pub enum ScrollBarVisibility {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct ScrollArea { pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling? /// Do we have horizontal/vertical scrolling enabled?
has_bar: [bool; 2], scroll_enabled: [bool; 2],
auto_shrink: [bool; 2], auto_shrink: [bool; 2],
max_size: Vec2, max_size: Vec2,
min_scrolled_size: Vec2, min_scrolled_size: Vec2,
@ -123,35 +169,39 @@ pub struct ScrollArea {
impl ScrollArea { impl ScrollArea {
/// Create a horizontal scroll area. /// Create a horizontal scroll area.
#[inline]
pub fn horizontal() -> Self { pub fn horizontal() -> Self {
Self::new([true, false]) Self::new([true, false])
} }
/// Create a vertical scroll area. /// Create a vertical scroll area.
#[inline]
pub fn vertical() -> Self { pub fn vertical() -> Self {
Self::new([false, true]) Self::new([false, true])
} }
/// Create a bi-directional (horizontal and vertical) scroll area. /// Create a bi-directional (horizontal and vertical) scroll area.
#[inline]
pub fn both() -> Self { pub fn both() -> Self {
Self::new([true, true]) Self::new([true, true])
} }
/// Create a scroll area where both direction of scrolling is disabled. /// Create a scroll area where both direction of scrolling is disabled.
/// It's unclear why you would want to do this. /// It's unclear why you would want to do this.
#[inline]
pub fn neither() -> Self { pub fn neither() -> Self {
Self::new([false, false]) Self::new([false, false])
} }
/// Create a scroll area where you decide which axis has scrolling enabled. /// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling. /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
pub fn new(has_bar: [bool; 2]) -> Self { pub fn new(scroll_enabled: [bool; 2]) -> Self {
Self { Self {
has_bar, scroll_enabled,
auto_shrink: [true; 2], auto_shrink: [true; 2],
max_size: Vec2::INFINITY, max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0), min_scrolled_size: Vec2::splat(64.0),
scroll_bar_visibility: ScrollBarVisibility::VisibleWhenNeeded, scroll_bar_visibility: Default::default(),
id_source: None, id_source: None,
offset_x: None, offset_x: None,
offset_y: None, offset_y: None,
@ -166,6 +216,7 @@ impl ScrollArea {
/// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
/// ///
/// See also [`Self::auto_shrink`]. /// See also [`Self::auto_shrink`].
#[inline]
pub fn max_width(mut self, max_width: f32) -> Self { pub fn max_width(mut self, max_width: f32) -> Self {
self.max_size.x = max_width; self.max_size.x = max_width;
self self
@ -176,6 +227,7 @@ impl ScrollArea {
/// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
/// ///
/// See also [`Self::auto_shrink`]. /// See also [`Self::auto_shrink`].
#[inline]
pub fn max_height(mut self, max_height: f32) -> Self { pub fn max_height(mut self, max_height: f32) -> Self {
self.max_size.y = max_height; self.max_size.y = max_height;
self self
@ -187,6 +239,7 @@ impl ScrollArea {
/// (and so we don't require scroll bars). /// (and so we don't require scroll bars).
/// ///
/// Default: `64.0`. /// Default: `64.0`.
#[inline]
pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self { pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
self.min_scrolled_size.x = min_scrolled_width; self.min_scrolled_size.x = min_scrolled_width;
self self
@ -198,6 +251,7 @@ impl ScrollArea {
/// (and so we don't require scroll bars). /// (and so we don't require scroll bars).
/// ///
/// Default: `64.0`. /// Default: `64.0`.
#[inline]
pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self { pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
self.min_scrolled_size.y = min_scrolled_height; self.min_scrolled_size.y = min_scrolled_height;
self self
@ -206,12 +260,14 @@ impl ScrollArea {
/// Set the visibility of both horizontal and vertical scroll bars. /// Set the visibility of both horizontal and vertical scroll bars.
/// ///
/// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed. /// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed.
#[inline]
pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self { pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
self.scroll_bar_visibility = scroll_bar_visibility; self.scroll_bar_visibility = scroll_bar_visibility;
self self
} }
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`. /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
#[inline]
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self { pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
self.id_source = Some(Id::new(id_source)); self.id_source = Some(Id::new(id_source));
self self
@ -224,6 +280,7 @@ impl ScrollArea {
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`], /// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
#[inline]
pub fn scroll_offset(mut self, offset: Vec2) -> Self { pub fn scroll_offset(mut self, offset: Vec2) -> Self {
self.offset_x = Some(offset.x); self.offset_x = Some(offset.x);
self.offset_y = Some(offset.y); self.offset_y = Some(offset.y);
@ -236,6 +293,7 @@ impl ScrollArea {
/// ///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
#[inline]
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self { pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
self.offset_y = Some(offset); self.offset_y = Some(offset);
self self
@ -247,26 +305,30 @@ impl ScrollArea {
/// ///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me) /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
#[inline]
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self { pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
self.offset_x = Some(offset); self.offset_x = Some(offset);
self self
} }
/// Turn on/off scrolling on the horizontal axis. /// Turn on/off scrolling on the horizontal axis.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self { pub fn hscroll(mut self, hscroll: bool) -> Self {
self.has_bar[0] = hscroll; self.scroll_enabled[0] = hscroll;
self self
} }
/// Turn on/off scrolling on the vertical axis. /// Turn on/off scrolling on the vertical axis.
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self { pub fn vscroll(mut self, vscroll: bool) -> Self {
self.has_bar[1] = vscroll; self.scroll_enabled[1] = vscroll;
self self
} }
/// Turn on/off scrolling on the horizontal/vertical axes. /// Turn on/off scrolling on the horizontal/vertical axes.
pub fn scroll2(mut self, has_bar: [bool; 2]) -> Self { #[inline]
self.has_bar = has_bar; pub fn scroll2(mut self, scroll_enabled: [bool; 2]) -> Self {
self.scroll_enabled = scroll_enabled;
self self
} }
@ -279,6 +341,7 @@ impl ScrollArea {
/// is typing text in a [`TextEdit`] widget contained within the scroll area. /// is typing text in a [`TextEdit`] widget contained within the scroll area.
/// ///
/// This controls both scrolling directions. /// This controls both scrolling directions.
#[inline]
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
@ -291,6 +354,7 @@ impl ScrollArea {
/// If `true`, the [`ScrollArea`] will sense drags. /// If `true`, the [`ScrollArea`] will sense drags.
/// ///
/// Default: `true`. /// Default: `true`.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll; self.drag_to_scroll = drag_to_scroll;
self self
@ -302,13 +366,15 @@ impl ScrollArea {
/// * If `false`, egui will add blank space inside the scroll area. /// * If `false`, egui will add blank space inside the scroll area.
/// ///
/// Default: `[true; 2]`. /// Default: `[true; 2]`.
#[inline]
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self { pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
self.auto_shrink = auto_shrink; self.auto_shrink = auto_shrink;
self self
} }
pub(crate) fn has_any_bar(&self) -> bool { /// Is any scrolling enabled?
self.has_bar[0] || self.has_bar[1] pub(crate) fn is_any_scroll_enabled(&self) -> bool {
self.scroll_enabled[0] || self.scroll_enabled[1]
} }
/// The scroll handle will stick to the rightmost position even while the content size /// The scroll handle will stick to the rightmost position even while the content size
@ -317,6 +383,7 @@ impl ScrollArea {
/// it will remain focused on whatever content viewport the user left it on. If the scroll /// it will remain focused on whatever content viewport the user left it on. If the scroll
/// handle is dragged all the way to the right it will again become stuck and remain there /// handle is dragged all the way to the right it will again become stuck and remain there
/// until manually pulled from the end position. /// until manually pulled from the end position.
#[inline]
pub fn stick_to_right(mut self, stick: bool) -> Self { pub fn stick_to_right(mut self, stick: bool) -> Self {
self.stick_to_end[0] = stick; self.stick_to_end[0] = stick;
self self
@ -328,6 +395,7 @@ impl ScrollArea {
/// it will remain focused on whatever content viewport the user left it on. If the scroll /// it will remain focused on whatever content viewport the user left it on. If the scroll
/// handle is dragged to the bottom it will again become stuck and remain there until manually /// handle is dragged to the bottom it will again become stuck and remain there until manually
/// pulled from the end position. /// pulled from the end position.
#[inline]
pub fn stick_to_bottom(mut self, stick: bool) -> Self { pub fn stick_to_bottom(mut self, stick: bool) -> Self {
self.stick_to_end[1] = stick; self.stick_to_end[1] = stick;
self self
@ -337,11 +405,24 @@ impl ScrollArea {
struct Prepared { struct Prepared {
id: Id, id: Id,
state: State, state: State,
has_bar: [bool; 2],
auto_shrink: [bool; 2], auto_shrink: [bool; 2],
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
scroll_enabled: [bool; 2],
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
show_bars_factor: Vec2,
/// How much horizontal and vertical space are used up by the /// How much horizontal and vertical space are used up by the
/// width of the vertical bar, and the height of the horizontal bar? /// width of the vertical bar, and the height of the horizontal bar?
///
/// This is always zero for floating scroll bars.
///
/// Note that this is a `yx` swizzling of [`Self::show_bars_factor`]
/// times the maximum bar with.
/// That's because horizontal scroll uses up vertical space,
/// and vice versa.
current_bar_use: Vec2, current_bar_use: Vec2,
scroll_bar_visibility: ScrollBarVisibility, scroll_bar_visibility: ScrollBarVisibility,
@ -362,7 +443,7 @@ struct Prepared {
impl ScrollArea { impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared { fn begin(self, ui: &mut Ui) -> Prepared {
let Self { let Self {
has_bar, scroll_enabled,
auto_shrink, auto_shrink,
max_size, max_size,
min_scrolled_size, min_scrolled_size,
@ -379,7 +460,7 @@ impl ScrollArea {
let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area")); let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_source); let id = ui.make_persistent_id(id_source);
ui.ctx().check_for_id_clash( ctx.check_for_id_clash(
id, id,
Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO), Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
"ScrollArea", "ScrollArea",
@ -389,25 +470,18 @@ impl ScrollArea {
state.offset.x = offset_x.unwrap_or(state.offset.x); state.offset.x = offset_x.unwrap_or(state.offset.x);
state.offset.y = offset_y.unwrap_or(state.offset.y); state.offset.y = offset_y.unwrap_or(state.offset.y);
let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); let show_bars: [bool; 2] = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => [false; 2],
let current_hscroll_bar_height = if !has_bar[0] { ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
0.0 ScrollBarVisibility::AlwaysVisible => scroll_enabled,
} else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible {
max_scroll_bar_width
} else {
max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), state.show_scroll[0])
}; };
let current_vscroll_bar_width = if !has_bar[1] { let show_bars_factor = Vec2::new(
0.0 ctx.animate_bool(id.with("h"), show_bars[0]),
} else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible { ctx.animate_bool(id.with("v"), show_bars[1]),
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 current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
let available_outer = ui.available_rect_before_wrap(); let available_outer = ui.available_rect_before_wrap();
@ -421,7 +495,7 @@ impl ScrollArea {
// one shouldn't collapse into nothingness. // one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097 // See https://github.com/emilk/egui/issues/1097
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]); inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
} }
} }
@ -438,7 +512,7 @@ impl ScrollArea {
} else { } else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it! // Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
content_max_size[d] = f32::INFINITY; content_max_size[d] = f32::INFINITY;
} }
} }
@ -452,7 +526,7 @@ impl ScrollArea {
let clip_rect_margin = ui.visuals().clip_rect_margin; let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect(); let mut content_clip_rect = ui.clip_rect();
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
if state.content_is_too_large[d] { if state.content_is_too_large[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
@ -479,7 +553,7 @@ impl ScrollArea {
if content_response.dragged() { if content_response.dragged() {
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
ui.input(|input| { ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d]; state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d]; state.vel[d] = input.pointer.velocity()[d];
@ -502,7 +576,7 @@ impl ScrollArea {
// 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 -= state.vel * dt; state.offset -= state.vel * dt;
ui.ctx().request_repaint(); ctx.request_repaint();
} }
} }
} }
@ -510,8 +584,9 @@ impl ScrollArea {
Prepared { Prepared {
id, id,
state, state,
has_bar,
auto_shrink, auto_shrink,
scroll_enabled,
show_bars_factor,
current_bar_use, current_bar_use,
scroll_bar_visibility, scroll_bar_visibility,
inner_rect, inner_rect,
@ -621,9 +696,10 @@ impl Prepared {
id, id,
mut state, mut state,
inner_rect, inner_rect,
has_bar,
auto_shrink, auto_shrink,
mut current_bar_use, scroll_enabled,
mut show_bars_factor,
current_bar_use,
scroll_bar_visibility, scroll_bar_visibility,
content_ui, content_ui,
viewport: _, viewport: _,
@ -634,7 +710,7 @@ impl Prepared {
let content_size = content_ui.min_size(); let content_size = content_ui.min_size();
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
// 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 let scroll_target = content_ui
.ctx() .ctx()
@ -680,7 +756,7 @@ impl Prepared {
let mut inner_size = inner_rect.size(); let mut inner_size = inner_rect.size();
for d in 0..2 { for d in 0..2 {
inner_size[d] = match (has_bar[d], auto_shrink[d]) { inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it). (false, true) => content_size[d], // Follow the content (expand/contract to fit it).
@ -694,14 +770,15 @@ impl Prepared {
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = [ let content_is_too_large = [
content_size.x > inner_rect.width(), scroll_enabled[0] && inner_rect.width() < content_size.x,
content_size.y > inner_rect.height(), scroll_enabled[1] && inner_rect.height() < content_size.y,
]; ];
let max_offset = content_size - inner_rect.size(); let max_offset = content_size - inner_rect.size();
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
if scrolling_enabled && is_hovering_outer_rect {
for d in 0..2 { for d in 0..2 {
if has_bar[d] { if scroll_enabled[d] {
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta); let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
@ -718,39 +795,69 @@ impl Prepared {
} }
let show_scroll_this_frame = match scroll_bar_visibility { let show_scroll_this_frame = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysVisible => [true, true], ScrollBarVisibility::AlwaysHidden => [false, false],
ScrollBarVisibility::VisibleWhenNeeded => { ScrollBarVisibility::VisibleWhenNeeded => {
[content_is_too_large[0], content_is_too_large[1]] [content_is_too_large[0], content_is_too_large[1]]
} }
ScrollBarVisibility::AlwaysHidden => [false, false], ScrollBarVisibility::AlwaysVisible => scroll_enabled,
}; };
let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui);
// Avoid frame delay; start showing scroll bar right away: // Avoid frame delay; start showing scroll bar right away:
if show_scroll_this_frame[0] && current_bar_use.y <= 0.0 { if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
current_bar_use.y = max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), true); show_bars_factor.x = ui.ctx().animate_bool(id.with("h"), true);
} }
if show_scroll_this_frame[1] && current_bar_use.x <= 0.0 { if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
current_bar_use.x = max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), true); show_bars_factor.y = ui.ctx().animate_bool(id.with("v"), true);
} }
let scroll_style = ui.spacing().scroll;
// Paint the bars:
for d in 0..2 { for d in 0..2 {
let animation_t = current_bar_use[1 - d] / max_scroll_bar_width; let show_factor = show_bars_factor[d];
if show_factor == 0.0 {
if animation_t == 0.0 { state.scroll_bar_interaction[d] = false;
continue; continue;
} }
// margin on either side of the scroll bar // left/right of a horizontal scroll (d==1)
let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin; // top/bottom of vertical scroll (d == 1)
let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin; let main_range = Rangef::new(inner_rect.min[d], inner_rect.max[d]);
let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1)
let mut max_cross = outer_rect.max[1 - d] - outer_margin; // 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)
if ui.clip_rect().max[1 - d] < max_cross + outer_margin { // Margin on either side of the scroll bar:
let inner_margin = show_factor * scroll_style.bar_inner_margin;
let outer_margin = show_factor * scroll_style.bar_outer_margin;
// top/bottom of a horizontal scroll (d==0).
// left/rigth of a vertical scroll (d==1).
let mut cross = if scroll_style.floating {
let max_bar_rect = if d == 0 {
outer_rect.with_min_y(outer_rect.max.y - scroll_style.allocated_width())
} else {
outer_rect.with_min_x(outer_rect.max.x - scroll_style.allocated_width())
};
let is_hovering_bar_area = is_hovering_outer_rect
&& ui.rect_contains_pointer(max_bar_rect)
|| state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
.ctx()
.animate_bool(id.with((d, "bar_hover")), is_hovering_bar_area);
let width = show_factor
* lerp(
scroll_style.floating_width..=scroll_style.bar_width,
is_hovering_bar_area_t,
);
let max_cross = outer_rect.max[1 - d] - outer_margin;
let min_cross = max_cross - width;
Rangef::new(min_cross, max_cross)
} else {
let min_cross = inner_rect.max[1 - d] + inner_margin;
let max_cross = outer_rect.max[1 - d] - outer_margin;
Rangef::new(min_cross, max_cross)
};
if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
// Move the scrollbar so it is visible. This is needed in some cases. // Move the scrollbar so it is visible. This is needed in some cases.
// For instance: // For instance:
// * When we have a vertical-only scroll area in a top level panel, // * When we have a vertical-only scroll area in a top level panel,
@ -760,20 +867,20 @@ impl Prepared {
// is outside the clip rectangle. // is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it. // clip_rect_margin is quite a hack. It would be nice to get rid of it.
let width = max_cross - min_cross; let width = cross.max - cross.min;
max_cross = ui.clip_rect().max[1 - d] - outer_margin; cross.max = ui.clip_rect().max[1 - d] - outer_margin;
min_cross = max_cross - width; cross.min = cross.max - width;
} }
let outer_scroll_rect = if d == 0 { let outer_scroll_rect = if d == 0 {
Rect::from_min_max( Rect::from_min_max(
pos2(inner_rect.left(), min_cross), pos2(inner_rect.left(), cross.min),
pos2(inner_rect.right(), max_cross), pos2(inner_rect.right(), cross.max),
) )
} else { } else {
Rect::from_min_max( Rect::from_min_max(
pos2(min_cross, inner_rect.top()), pos2(cross.min, inner_rect.top()),
pos2(max_cross, inner_rect.bottom()), pos2(cross.max, inner_rect.bottom()),
) )
}; };
@ -782,19 +889,18 @@ impl Prepared {
state.offset[d] = content_size[d] - inner_rect.size()[d]; state.offset[d] = content_size[d] - inner_rect.size()[d];
} }
let from_content = let from_content = |content| remap_clamp(content, 0.0..=content_size[d], main_range);
|content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main);
let handle_rect = if d == 0 { let handle_rect = if d == 0 {
Rect::from_min_max( Rect::from_min_max(
pos2(from_content(state.offset.x), min_cross), pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), max_cross), pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
) )
} else { } else {
Rect::from_min_max( Rect::from_min_max(
pos2(min_cross, from_content(state.offset.y)), pos2(cross.min, from_content(state.offset.y)),
pos2( pos2(
max_cross, cross.max,
from_content(state.offset.y + inner_rect.height()), from_content(state.offset.y + inner_rect.height()),
), ),
) )
@ -808,22 +914,24 @@ impl Prepared {
}; };
let response = ui.interact(outer_scroll_rect, interact_id, sense); let response = ui.interact(outer_scroll_rect, interact_id, sense);
state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
if let Some(pointer_pos) = response.interact_pointer_pos() { if let Some(pointer_pos) = response.interact_pointer_pos() {
let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d] let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
.get_or_insert_with(|| { .get_or_insert_with(|| {
if handle_rect.contains(pointer_pos) { if handle_rect.contains(pointer_pos) {
pointer_pos[d] - handle_rect.min[d] pointer_pos[d] - handle_rect.min[d]
} else { } else {
let handle_top_pos_at_bottom = max_main - handle_rect.size()[d]; let handle_top_pos_at_bottom = main_range.max - 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[d] - handle_rect.size()[d] / 2.0) let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
.clamp(min_main, handle_top_pos_at_bottom); .clamp(main_range.min, handle_top_pos_at_bottom);
pointer_pos[d] - new_handle_top_pos pointer_pos[d] - new_handle_top_pos
} }
}); });
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left; 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]); state.offset[d] = remap(new_handle_top, main_range, 0.0..=content_size[d]);
// some manual action taken, scroll not stuck // some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false; state.scroll_stuck_to_end[d] = false;
@ -843,19 +951,19 @@ impl Prepared {
// Avoid frame-delay by calculating a new handle rect: // Avoid frame-delay by calculating a new handle rect:
let mut handle_rect = if d == 0 { let mut handle_rect = if d == 0 {
Rect::from_min_max( Rect::from_min_max(
pos2(from_content(state.offset.x), min_cross), pos2(from_content(state.offset.x), cross.min),
pos2(from_content(state.offset.x + inner_rect.width()), max_cross), pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
) )
} else { } else {
Rect::from_min_max( Rect::from_min_max(
pos2(min_cross, from_content(state.offset.y)), pos2(cross.min, from_content(state.offset.y)),
pos2( pos2(
max_cross, cross.max,
from_content(state.offset.y + inner_rect.height()), from_content(state.offset.y + inner_rect.height()),
), ),
) )
}; };
let min_handle_size = ui.spacing().scroll_handle_min_length; let min_handle_size = scroll_style.handle_min_length;
if handle_rect.size()[d] < min_handle_size { 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(),
@ -868,21 +976,76 @@ impl Prepared {
} }
let visuals = if scrolling_enabled { let visuals = if scrolling_enabled {
ui.style().interact(&response) // Pick visuals based on interaction with the handle.
// Remember that the response is for the whole scroll bar!
let is_hovering_handle = response.hovered()
&& ui.input(|i| {
i.pointer
.latest_pos()
.map_or(false, |p| handle_rect.contains(p))
});
let visuals = ui.visuals();
if response.is_pointer_button_down_on() {
&visuals.widgets.active
} else if is_hovering_handle {
&visuals.widgets.hovered
} else {
&visuals.widgets.inactive
}
} else { } else {
&ui.style().visuals.widgets.inactive &ui.visuals().widgets.inactive
}; };
let handle_opacity = if scroll_style.floating {
if response.hovered() || response.dragged() {
scroll_style.interact_handle_opacity
} else {
let is_hovering_outer_rect_t = ui.ctx().animate_bool(
id.with((d, "is_hovering_outer_rect")),
is_hovering_outer_rect,
);
lerp(
scroll_style.dormant_handle_opacity
..=scroll_style.active_handle_opacity,
is_hovering_outer_rect_t,
)
}
} else {
1.0
};
let background_opacity = if scroll_style.floating {
if response.hovered() || response.dragged() {
scroll_style.interact_background_opacity
} else if is_hovering_outer_rect {
scroll_style.active_background_opacity
} else {
scroll_style.dormant_background_opacity
}
} else {
1.0
};
let handle_color = if scroll_style.foreground_color {
visuals.fg_stroke.color
} else {
visuals.bg_fill
};
// Background:
ui.painter().add(epaint::Shape::rect_filled( ui.painter().add(epaint::Shape::rect_filled(
outer_scroll_rect, outer_scroll_rect,
visuals.rounding, visuals.rounding,
ui.visuals().extreme_bg_color, ui.visuals()
.extreme_bg_color
.gamma_multiply(background_opacity),
)); ));
// Handle:
ui.painter().add(epaint::Shape::rect_filled( ui.painter().add(epaint::Shape::rect_filled(
handle_rect, handle_rect,
visuals.rounding, visuals.rounding,
visuals.bg_fill, handle_color.gamma_multiply(handle_opacity),
)); ));
} }
} }
@ -904,9 +1067,9 @@ impl Prepared {
// has appropriate effect. // has appropriate effect.
state.scroll_stuck_to_end = [ state.scroll_stuck_to_end = [
(state.offset[0] == available_offset[0]) (state.offset[0] == available_offset[0])
|| (self.stick_to_end[0] && available_offset[0] < 0.), || (self.stick_to_end[0] && available_offset[0] < 0.0),
(state.offset[1] == available_offset[1]) (state.offset[1] == available_offset[1])
|| (self.stick_to_end[1] && available_offset[1] < 0.), || (self.stick_to_end[1] && available_offset[1] < 0.0),
]; ];
state.show_scroll = show_scroll_this_frame; state.show_scroll = show_scroll_this_frame;
@ -917,10 +1080,3 @@ impl Prepared {
(content_size, state) (content_size, 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().scroll_bar_inner_margin
+ ui.spacing().scroll_bar_width
+ ui.spacing().scroll_bar_outer_margin
}

View File

@ -419,7 +419,7 @@ impl<'open> Window<'open> {
ui.add_space(title_content_spacing); ui.add_space(title_content_spacing);
} }
if scroll.has_any_bar() { if scroll.is_any_scroll_enabled() {
scroll.show(ui, add_contents).inner scroll.show(ui, add_contents).inner
} else { } else {
add_contents(ui) add_contents(ui)

View File

@ -2,11 +2,13 @@
#![allow(clippy::if_same_then_else)] #![allow(clippy::if_same_then_else)]
use std::collections::BTreeMap;
use epaint::{Rounding, Shadow, Stroke};
use crate::{ use crate::{
ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Response, RichText, WidgetText, ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Response, RichText, WidgetText,
}; };
use epaint::{Rounding, Shadow, Stroke};
use std::collections::BTreeMap;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -303,16 +305,8 @@ pub struct Spacing {
/// Height of a combo-box before showing scroll bars. /// Height of a combo-box before showing scroll bars.
pub combo_height: f32, pub combo_height: f32,
pub scroll_bar_width: f32, /// Controls the spacing of a [`crate::ScrollArea`].
pub scroll: ScrollStyle,
/// Make sure the scroll handle is at least this big
pub scroll_handle_min_length: f32,
/// Margin between contents and scroll bar.
pub scroll_bar_inner_margin: f32,
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
pub scroll_bar_outer_margin: f32,
} }
impl Spacing { impl Spacing {
@ -333,6 +327,277 @@ impl Spacing {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Controls the spacing and visuals of a [`crate::ScrollArea`].
///
/// There are three presets to chose from:
/// * [`Self::solid`]
/// * [`Self::thin`]
/// * [`Self::floating`]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollStyle {
/// If `true`, scroll bars float above the content, partially covering it.
///
/// If `false`, the scroll bars allocate space, shrinking the area
/// available to the contents.
///
/// This also changes the colors of the scroll-handle to make
/// it more promiment.
pub floating: bool,
/// The width of the scroll bars at it largest.
pub bar_width: f32,
/// Make sure the scroll handle is at least this big
pub handle_min_length: f32,
/// Margin between contents and scroll bar.
pub bar_inner_margin: f32,
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
/// Only makes sense for non-floating scroll bars.
pub bar_outer_margin: f32,
/// The thin width of floating scroll bars that the user is NOT hovering.
///
/// When the user hovers the scroll bars they expand to [`Self::bar_width`].
pub floating_width: f32,
/// How much space i allocated for a floating scroll bar?
///
/// Normally this is zero, but you could set this to something small
/// like 4.0 and set [`Self::dormant_handle_opacity`] and
/// [`Self::dormant_background_opacity`] to e.g. 0.5
/// so as to always show a thin scroll bar.
pub floating_allocated_width: f32,
/// If true, use colors with more contrast. Good for floating scroll bars.
pub foreground_color: bool,
/// The opaqueness of the background when the user is neither scrolling
/// nor hovering the scroll area.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub dormant_background_opacity: f32,
/// The opaqueness of the background when the user is hovering
/// the scroll area, but not the scroll bar.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub active_background_opacity: f32,
/// The opaqueness of the background when the user is hovering
/// over the scroll bars.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub interact_background_opacity: f32,
/// The opaqueness of the handle when the user is neither scrolling
/// nor hovering the scroll area.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub dormant_handle_opacity: f32,
/// The opaqueness of the handle when the user is hovering
/// the scroll area, but not the scroll bar.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub active_handle_opacity: f32,
/// The opaqueness of the handle when the user is hovering
/// over the scroll bars.
///
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub interact_handle_opacity: f32,
}
impl Default for ScrollStyle {
fn default() -> Self {
Self::floating()
}
}
impl ScrollStyle {
/// Solid scroll bars that always use up space
pub fn solid() -> Self {
Self {
floating: false,
bar_width: 6.0,
handle_min_length: 12.0,
bar_inner_margin: 4.0,
bar_outer_margin: 0.0,
floating_width: 2.0,
floating_allocated_width: 0.0,
foreground_color: false,
dormant_background_opacity: 0.0,
active_background_opacity: 0.4,
interact_background_opacity: 0.7,
dormant_handle_opacity: 0.0,
active_handle_opacity: 0.6,
interact_handle_opacity: 1.0,
}
}
/// Thin scroll bars that expand on hover
pub fn thin() -> Self {
Self {
floating: true,
bar_width: 12.0,
floating_allocated_width: 6.0,
foreground_color: false,
dormant_background_opacity: 1.0,
dormant_handle_opacity: 1.0,
active_background_opacity: 1.0,
active_handle_opacity: 1.0,
// Be tranlucent when expanded so we can see the content
interact_background_opacity: 0.6,
interact_handle_opacity: 0.6,
..Self::solid()
}
}
/// No scroll bars until you hover the scroll area,
/// at which time they appear faintly, and then expand
/// when you hover the scroll bars.
pub fn floating() -> Self {
Self {
floating: true,
bar_width: 12.0,
foreground_color: true,
floating_allocated_width: 0.0,
dormant_background_opacity: 0.0,
dormant_handle_opacity: 0.0,
..Self::solid()
}
}
/// Width of a solid vertical scrollbar, or height of a horizontal scroll bar, when it is at its widest.
pub fn allocated_width(&self) -> f32 {
if self.floating {
self.floating_allocated_width
} else {
self.bar_inner_margin + self.bar_width + self.bar_outer_margin
}
}
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.label("Presets:");
ui.selectable_value(self, Self::solid(), "Solid");
ui.selectable_value(self, Self::thin(), "Thin");
ui.selectable_value(self, Self::floating(), "Floating");
});
ui.collapsing("Details", |ui| {
self.details_ui(ui);
});
}
pub fn details_ui(&mut self, ui: &mut Ui) {
let Self {
floating,
bar_width,
handle_min_length,
bar_inner_margin,
bar_outer_margin,
floating_width,
floating_allocated_width,
foreground_color,
dormant_background_opacity,
active_background_opacity,
interact_background_opacity,
dormant_handle_opacity,
active_handle_opacity,
interact_handle_opacity,
} = self;
ui.horizontal(|ui| {
ui.label("Type:");
ui.selectable_value(floating, false, "Solid");
ui.selectable_value(floating, true, "Floating");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_width).clamp_range(0.0..=32.0));
ui.label("Full bar width");
});
if *floating {
ui.horizontal(|ui| {
ui.add(DragValue::new(floating_width).clamp_range(0.0..=32.0));
ui.label("Thin bar width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(floating_allocated_width).clamp_range(0.0..=32.0));
ui.label("Allocated width");
});
}
ui.horizontal(|ui| {
ui.add(DragValue::new(handle_min_length).clamp_range(0.0..=32.0));
ui.label("Minimum handle length");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_outer_margin).clamp_range(0.0..=32.0));
ui.label("Outer margin");
});
ui.horizontal(|ui| {
ui.label("Color:");
ui.selectable_value(foreground_color, false, "Background");
ui.selectable_value(foreground_color, true, "Foreground");
});
if *floating {
crate::Grid::new("opacity").show(ui, |ui| {
fn opacity_ui(ui: &mut Ui, opacity: &mut f32) {
ui.add(DragValue::new(opacity).speed(0.01).clamp_range(0.0..=1.0));
}
ui.label("Opacity");
ui.label("Dormant");
ui.label("Active");
ui.label("Interacting");
ui.end_row();
ui.label("Background:");
opacity_ui(ui, dormant_background_opacity);
opacity_ui(ui, active_background_opacity);
opacity_ui(ui, interact_background_opacity);
ui.end_row();
ui.label("Handle:");
opacity_ui(ui, dormant_handle_opacity);
opacity_ui(ui, active_handle_opacity);
opacity_ui(ui, interact_handle_opacity);
ui.end_row();
});
} else {
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_inner_margin).clamp_range(0.0..=32.0));
ui.label("Inner margin");
});
}
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Margin { pub struct Margin {
@ -807,10 +1072,7 @@ impl Default for Spacing {
icon_spacing: 4.0, icon_spacing: 4.0,
tooltip_width: 600.0, tooltip_width: 600.0,
combo_height: 200.0, combo_height: 200.0,
scroll_bar_width: 8.0, scroll: Default::default(),
scroll_handle_min_length: 12.0,
scroll_bar_inner_margin: 4.0,
scroll_bar_outer_margin: 0.0,
indent_ends_with_horizontal_line: false, indent_ends_with_horizontal_line: false,
} }
} }
@ -1146,10 +1408,7 @@ impl Spacing {
tooltip_width, tooltip_width,
indent_ends_with_horizontal_line, indent_ends_with_horizontal_line,
combo_height, combo_height,
scroll_bar_width, scroll,
scroll_handle_min_length,
scroll_bar_inner_margin,
scroll_bar_outer_margin,
} = self; } = self;
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing")); ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
@ -1176,21 +1435,9 @@ impl Spacing {
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
ui.label("TextEdit width"); ui.label("TextEdit width");
}); });
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0)); ui.collapsing("Scroll Area", |ui| {
ui.label("Scroll-bar width"); scroll.ui(ui);
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0));
ui.label("Scroll-bar handle min length");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
ui.label("Scroll-bar inner margin");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(scroll_bar_outer_margin).clamp_range(0.0..=32.0));
ui.label("Scroll-bar outer margin");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {

View File

@ -371,7 +371,7 @@ impl BoxPainting {
ui.painter().rect( ui.painter().rect(
rect, rect,
self.rounding, self.rounding,
Color32::from_gray(64), ui.visuals().text_color().gamma_multiply(0.5),
Stroke::new(self.stroke_width, Color32::WHITE), Stroke::new(self.stroke_width, Color32::WHITE),
); );
} }

View File

@ -1,8 +1,9 @@
use egui::*; use egui::{scroll_area::ScrollBarVisibility, *};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
enum ScrollDemo { enum ScrollDemo {
ScrollAppearance,
ScrollTo, ScrollTo,
ManyLines, ManyLines,
LargeCanvas, LargeCanvas,
@ -12,7 +13,7 @@ enum ScrollDemo {
impl Default for ScrollDemo { impl Default for ScrollDemo {
fn default() -> Self { fn default() -> Self {
Self::ScrollTo Self::ScrollAppearance
} }
} }
@ -20,6 +21,7 @@ impl Default for ScrollDemo {
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
pub struct Scrolling { pub struct Scrolling {
appearance: ScrollAppearance,
demo: ScrollDemo, demo: ScrollDemo,
scroll_to: ScrollTo, scroll_to: ScrollTo,
scroll_stick_to: ScrollStickTo, scroll_stick_to: ScrollStickTo,
@ -33,7 +35,9 @@ impl super::Demo for Scrolling {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) { fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name()) egui::Window::new(self.name())
.open(open) .open(open)
.resizable(false) .resizable(true)
.hscroll(false)
.vscroll(false)
.show(ctx, |ui| { .show(ctx, |ui| {
use super::View as _; use super::View as _;
self.ui(ui); self.ui(ui);
@ -44,6 +48,7 @@ impl super::Demo for Scrolling {
impl super::View for Scrolling { impl super::View for Scrolling {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollAppearance, "Appearance");
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to"); ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to");
ui.selectable_value( ui.selectable_value(
&mut self.demo, &mut self.demo,
@ -60,6 +65,9 @@ impl super::View for Scrolling {
}); });
ui.separator(); ui.separator();
match self.demo { match self.demo {
ScrollDemo::ScrollAppearance => {
self.appearance.ui(ui);
}
ScrollDemo::ScrollTo => { ScrollDemo::ScrollTo => {
self.scroll_to.ui(ui); self.scroll_to.ui(ui);
} }
@ -84,6 +92,75 @@ impl super::View for Scrolling {
} }
} }
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
#[derive(PartialEq)]
struct ScrollAppearance {
num_lorem_ipsums: usize,
visibility: ScrollBarVisibility,
}
impl Default for ScrollAppearance {
fn default() -> Self {
Self {
num_lorem_ipsums: 2,
visibility: ScrollBarVisibility::default(),
}
}
}
impl ScrollAppearance {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
num_lorem_ipsums,
visibility,
} = self;
let mut style: Style = (*ui.ctx().style()).clone();
style.spacing.scroll.ui(ui);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label("ScrollBarVisibility:");
for option in ScrollBarVisibility::ALL {
ui.selectable_value(visibility, option, format!("{option:?}"));
}
});
ui.weak("When to show scroll bars; resize the window to see the effect.");
ui.add_space(8.0);
ui.ctx().set_style(style.clone());
ui.set_style(style);
ui.separator();
ui.add(
egui::Slider::new(num_lorem_ipsums, 1..=100)
.text("Content length")
.logarithmic(true),
);
ui.separator();
ScrollArea::vertical()
.auto_shrink([false; 2])
.scroll_bar_visibility(*visibility)
.show(ui, |ui| {
ui.with_layout(
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|ui| {
for _ in 0..*num_lorem_ipsums {
ui.label(crate::LOREM_IPSUM_LONG);
}
},
);
});
}
}
fn huge_content_lines(ui: &mut egui::Ui) { fn huge_content_lines(ui: &mut egui::Ui) {
ui.label( ui.label(
"A lot of rows, but only the visible ones are laid out, so performance is still good:", "A lot of rows, but only the visible ones are laid out, so performance is still good:",

View File

@ -366,9 +366,9 @@ impl<'a> TableBuilder<'a> {
fn available_width(&self) -> f32 { fn available_width(&self) -> f32 {
self.ui.available_rect_before_wrap().width() self.ui.available_rect_before_wrap().width()
- if self.scroll_options.vscroll { - if self.scroll_options.vscroll {
self.ui.spacing().scroll_bar_inner_margin self.ui.spacing().scroll.bar_inner_margin
+ self.ui.spacing().scroll_bar_width + self.ui.spacing().scroll.bar_width
+ self.ui.spacing().scroll_bar_outer_margin + self.ui.spacing().scroll.bar_outer_margin
} else { } else {
0.0 0.0
} }

View File

@ -150,6 +150,34 @@ impl Rect {
rect rect
} }
#[inline]
#[must_use]
pub fn with_min_x(mut self, min_x: f32) -> Self {
self.min.x = min_x;
self
}
#[inline]
#[must_use]
pub fn with_min_y(mut self, min_y: f32) -> Self {
self.min.y = min_y;
self
}
#[inline]
#[must_use]
pub fn with_max_x(mut self, max_x: f32) -> Self {
self.max.x = max_x;
self
}
#[inline]
#[must_use]
pub fn with_max_y(mut self, max_y: f32) -> Self {
self.max.y = max_y;
self
}
/// Expand by this much in each direction, keeping the center /// Expand by this much in each direction, keeping the center
#[must_use] #[must_use]
pub fn expand(self, amnt: f32) -> Self { pub fn expand(self, amnt: f32) -> Self {

View File

@ -274,6 +274,16 @@ impl Vec2 {
self.x.max(self.y) self.x.max(self.y)
} }
/// Swizzle the axes.
#[inline]
#[must_use]
pub fn yx(self) -> Vec2 {
Vec2 {
x: self.y,
y: self.x,
}
}
#[must_use] #[must_use]
#[inline] #[inline]
pub fn clamp(self, min: Self, max: Self) -> Self { pub fn clamp(self, min: Self, max: Self) -> Self {

View File

@ -535,6 +535,7 @@ pub mod path {
add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0); add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0);
add_circle_quadrant(path, pos2(min.x + r.nw, min.y + r.nw), r.nw, 2.0); add_circle_quadrant(path, pos2(min.x + r.nw, min.y + r.nw), r.nw, 2.0);
add_circle_quadrant(path, pos2(max.x - r.ne, min.y + r.ne), r.ne, 3.0); add_circle_quadrant(path, pos2(max.x - r.ne, min.y + r.ne), r.ne, 3.0);
path.dedup(); // We get duplicates for thin rectangles, producing visual artifats
} }
} }