1
0
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:
Emil Ernerfeldt
2026-03-25 12:53:00 +01:00
committed by GitHub
parent 845b8c2f09
commit 0b0c561a81
22 changed files with 241 additions and 72 deletions

View File

@@ -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],
));
}
}

View File

@@ -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".
///
/// ```

View File

@@ -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},

View File

@@ -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");
});
}
}
}

View File

@@ -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.