mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -04:00
Fade out the edges of ScrollAreas (#8018)
## Before: <img width="381" height="307" alt="image" src="https://github.com/user-attachments/assets/0528ae2a-44bf-4d9e-89a4-c3f4ab438eb2" /> It is very hard here to realize this is a scrollable area ## After <img width="383" height="310" alt="image" src="https://github.com/user-attachments/assets/9e0ee6de-8b96-4e5c-a505-f57977010990" /> The fade at the bottom tells the user they should try scrolling. You can turn if off with `style.spacing.scroll.fade.enabled`
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::Margin;
|
||||
use epaint::{Color32, Direction, Margin, Shape};
|
||||
|
||||
use crate::{
|
||||
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
|
||||
@@ -1019,13 +1019,17 @@ impl ScrollArea {
|
||||
.inner;
|
||||
|
||||
let (content_size, state) = prepared.end(ui);
|
||||
ScrollAreaOutput {
|
||||
let output = ScrollAreaOutput {
|
||||
inner,
|
||||
id,
|
||||
state,
|
||||
content_size,
|
||||
inner_rect,
|
||||
}
|
||||
};
|
||||
|
||||
paint_fade_areas(ui, &output);
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1504,3 +1508,89 @@ impl Prepared {
|
||||
(content_size, state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint fade-out gradients at the top and/or bottom of a scroll area to
|
||||
/// indicate that more content is available beyond the visible region.
|
||||
fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
|
||||
let crate::style::ScrollFadeStyle {
|
||||
enabled,
|
||||
size: fade_size,
|
||||
} = ui.spacing().scroll.fade;
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let bg = ui.stack().bg_color();
|
||||
|
||||
let offset = scroll_output.state.offset;
|
||||
let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
|
||||
|
||||
let paint_rect = scroll_output
|
||||
.inner_rect
|
||||
.intersect(ui.min_rect())
|
||||
.expand(ui.visuals().clip_rect_margin);
|
||||
|
||||
// Top fade: animate opacity based on how far we've scrolled down.
|
||||
if 0.0 < offset.y {
|
||||
let t = (offset.y / fade_size).clamp(0.0, 1.0);
|
||||
let bg_faded = bg.gamma_multiply(t);
|
||||
let rect = Rect::from_min_max(
|
||||
paint_rect.left_top(),
|
||||
pos2(paint_rect.right(), paint_rect.top() + fade_size),
|
||||
);
|
||||
ui.painter().add(Shape::gradient_rect(
|
||||
rect,
|
||||
Direction::TopDown,
|
||||
[bg_faded, Color32::TRANSPARENT],
|
||||
));
|
||||
}
|
||||
|
||||
// Bottom fade: animate opacity based on distance from the bottom.
|
||||
let distance_from_bottom = overflow.y - offset.y;
|
||||
if 0.0 < distance_from_bottom {
|
||||
let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0);
|
||||
let bg_faded = bg.gamma_multiply(t);
|
||||
let rect = Rect::from_min_max(
|
||||
pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
|
||||
paint_rect.right_bottom(),
|
||||
);
|
||||
ui.painter().add(Shape::gradient_rect(
|
||||
rect,
|
||||
Direction::BottomUp,
|
||||
[bg_faded, Color32::TRANSPARENT],
|
||||
));
|
||||
}
|
||||
|
||||
// Left fade: animate opacity based on how far we've scrolled right.
|
||||
|
||||
if 0.0 < offset.x {
|
||||
let t = (offset.x / fade_size).clamp(0.0, 1.0);
|
||||
let bg_faded = bg.gamma_multiply(t);
|
||||
let rect = Rect::from_min_max(
|
||||
paint_rect.left_top(),
|
||||
pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
|
||||
);
|
||||
ui.painter().add(Shape::gradient_rect(
|
||||
rect,
|
||||
Direction::LeftToRight,
|
||||
[bg_faded, Color32::TRANSPARENT],
|
||||
));
|
||||
}
|
||||
|
||||
// Right fade: animate opacity based on distance from the right edge.
|
||||
let distance_from_right = overflow.x - offset.x;
|
||||
if 0.0 < distance_from_right {
|
||||
let t = (distance_from_right / fade_size).clamp(0.0, 1.0);
|
||||
let bg_faded = bg.gamma_multiply(t);
|
||||
let rect = Rect::from_min_max(
|
||||
pos2(paint_rect.right() - fade_size, paint_rect.top()),
|
||||
paint_rect.right_bottom(),
|
||||
);
|
||||
ui.painter().add(Shape::gradient_rect(
|
||||
rect,
|
||||
Direction::RightToLeft,
|
||||
[bg_faded, Color32::TRANSPARENT],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
use crate::{
|
||||
Align,
|
||||
Align, Direction,
|
||||
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
|
||||
};
|
||||
const INFINITY: f32 = f32::INFINITY;
|
||||
@@ -87,36 +87,6 @@ impl Region {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Direction {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
TopDown,
|
||||
BottomUp,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
#[inline(always)]
|
||||
pub fn is_horizontal(self) -> bool {
|
||||
match self {
|
||||
Self::LeftToRight | Self::RightToLeft => true,
|
||||
Self::TopDown | Self::BottomUp => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_vertical(self) -> bool {
|
||||
match self {
|
||||
Self::LeftToRight | Self::RightToLeft => false,
|
||||
Self::TopDown | Self::BottomUp => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -449,7 +449,7 @@ pub use emath::{
|
||||
remap_clamp, vec2,
|
||||
};
|
||||
pub use epaint::{
|
||||
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
|
||||
ClippedPrimitive, ColorImage, CornerRadius, Direction, ImageData, Margin, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
|
||||
|
||||
@@ -586,6 +586,8 @@ pub struct ScrollStyle {
|
||||
/// This is only for floating scroll bars.
|
||||
/// Solid scroll bars are always opaque.
|
||||
pub interact_handle_opacity: f32,
|
||||
|
||||
pub fade: ScrollFadeStyle,
|
||||
}
|
||||
|
||||
impl Default for ScrollStyle {
|
||||
@@ -616,6 +618,8 @@ impl ScrollStyle {
|
||||
dormant_handle_opacity: 0.0,
|
||||
active_handle_opacity: 0.6,
|
||||
interact_handle_opacity: 1.0,
|
||||
|
||||
fade: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,6 +703,8 @@ impl ScrollStyle {
|
||||
dormant_handle_opacity,
|
||||
active_handle_opacity,
|
||||
interact_handle_opacity,
|
||||
|
||||
fade,
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@@ -772,6 +778,49 @@ impl ScrollStyle {
|
||||
ui.label("Inner margin");
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
fade.ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls if and how to fade out the sides of a [`crate::ScrollArea`]
|
||||
/// to indicate there is more there if you scroll.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct ScrollFadeStyle {
|
||||
/// Show fade areas?
|
||||
pub enabled: bool,
|
||||
|
||||
/// Size of the fade-area (height for vertical scrolling,
|
||||
/// width for horizontal scrolling).
|
||||
pub size: f32,
|
||||
}
|
||||
|
||||
impl Default for ScrollFadeStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
size: 20.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollFadeStyle {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self { enabled, size } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(enabled, "Fade edges");
|
||||
});
|
||||
|
||||
if *enabled {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(size).range(0.0..=64.0));
|
||||
ui.label("Fade size");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ impl UiStack {
|
||||
!self.info.frame.stroke.is_empty()
|
||||
}
|
||||
|
||||
/// The background color of this [`Ui`].
|
||||
/// The background color of this [`crate::Ui`].
|
||||
///
|
||||
/// This blend together all [`Frame::fill`] colors
|
||||
/// up to the root.
|
||||
|
||||
Reference in New Issue
Block a user