mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49: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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b9ad01a55950f96a3ae9e48a2c026143d11ffee62bff4f83b4529cd884ce11f0
|
||||
size 169683
|
||||
oid sha256:65776e4d9e31e7163117166915a0cfaaece98b1dd69695533655dee889ac5597
|
||||
size 169277
|
||||
|
||||
@@ -27,7 +27,7 @@ impl crate::Demo for StripDemo {
|
||||
impl crate::View for StripDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let dark_mode = ui.visuals().dark_mode;
|
||||
let faded_color = ui.visuals().window_fill();
|
||||
let faded_color = ui.stack().bg_color();
|
||||
let faded_color = |color: Color32| -> Color32 {
|
||||
use egui::Rgba;
|
||||
let t = if dark_mode { 0.95 } else { 0.8 };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6
|
||||
size 114409
|
||||
oid sha256:583003c59f40515a5de435ee1eea2ee6fceae409c7881456401b004c6409896a
|
||||
size 114575
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:627114dcbda4f3d2255d34926ed0b77c679d248ed1d822d723479b1e6652c67a
|
||||
size 249258
|
||||
oid sha256:8815473873602211a95e0a077dc77560de154a0a1a2f9c1418d90746670075f7
|
||||
size 248630
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c
|
||||
size 172609
|
||||
oid sha256:2881255fa694b713a3c3049f30143ee60a20b68105fb724fbba188f81d34e572
|
||||
size 171778
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef
|
||||
size 76022
|
||||
oid sha256:63fb9d15956efa69818cfd5398b4dfaa796dfbe1df3fb171b8ae413a6f641c9f
|
||||
size 76049
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89986132c5d2a3ccccf0a16cb2b0be07b7c5512838cc10ec9067022e7a238515
|
||||
size 63918
|
||||
oid sha256:4ac837d03e3e8959196942d030a8511d00cdd0d5ac9bfe761d80b59d32e73581
|
||||
size 63967
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1
|
||||
size 483497
|
||||
oid sha256:4107ba4569b3e37ce6720595cfc4de60aec11dcfdb835720442b244e121209fc
|
||||
size 482862
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eb8737af84c3d3b0c054b7e2a8bcb04685243d84cb13b72a1372dc40dbfd14fb
|
||||
size 7267
|
||||
oid sha256:6f6d516cc3d1439256a19f72153468125bac1647e2e211e841579545570faef7
|
||||
size 7366
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f1651bb1b9bbaa3c65ecd07c39c57527f4beb4c607581a5b2596a49dcf4c5db3
|
||||
size 7996
|
||||
oid sha256:2e05d28e5541eb5926b4758e611f5c340311269fcd8b63055ff3a6793abbb140
|
||||
size 8279
|
||||
|
||||
27
crates/epaint/src/direction.rs
Normal file
27
crates/epaint/src/direction.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
/// A cardinal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ mod brush;
|
||||
pub mod color;
|
||||
mod corner_radius;
|
||||
mod corner_radius_f32;
|
||||
mod direction;
|
||||
pub mod image;
|
||||
mod margin;
|
||||
mod margin_f32;
|
||||
@@ -50,6 +51,7 @@ pub use self::{
|
||||
color::ColorMode,
|
||||
corner_radius::CornerRadius,
|
||||
corner_radius_f32::CornerRadiusF32,
|
||||
direction::Direction,
|
||||
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
|
||||
margin::Margin,
|
||||
margin_f32::*,
|
||||
|
||||
@@ -23,6 +23,18 @@ pub struct Vertex {
|
||||
pub color: Color32, // 32 bit
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
/// An untextured vertex
|
||||
#[inline]
|
||||
pub fn untextured(pos: Pos2, color: Color32) -> Self {
|
||||
Self {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg(all(feature = "unity", not(feature = "_override_unity")))]
|
||||
@@ -159,11 +171,7 @@ impl Mesh {
|
||||
self.texture_id == TextureId::default(),
|
||||
"Mesh has an assigned texture"
|
||||
);
|
||||
self.vertices.push(Vertex {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
});
|
||||
self.vertices.push(Vertex::untextured(pos, color));
|
||||
}
|
||||
|
||||
/// Add a triangle.
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use emath::{Align2, Pos2, Rangef, Rect, TSTransform, Vec2, pos2};
|
||||
|
||||
use crate::{
|
||||
Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId,
|
||||
Color32, CornerRadius, Direction, Mesh, Stroke, StrokeKind, TextureId, Vertex,
|
||||
stroke::PathStroke,
|
||||
text::{FontId, FontsView, Galley},
|
||||
};
|
||||
@@ -297,6 +297,32 @@ impl Shape {
|
||||
Self::Rect(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
|
||||
}
|
||||
|
||||
/// Paints a gradient rectangle that transitions from `color_from` to `color_to`
|
||||
/// along the given `direction`.
|
||||
///
|
||||
/// For example, [`Direction::TopDown`] paints `color_from` at the top edge fading
|
||||
/// to `color_to` at the bottom edge.
|
||||
#[inline]
|
||||
pub fn gradient_rect(rect: Rect, direction: Direction, [from, to]: [Color32; 2]) -> Self {
|
||||
let (left_top, right_top, left_bottom, right_bottom) = match direction {
|
||||
Direction::TopDown => (from, from, to, to),
|
||||
Direction::BottomUp => (to, to, from, from),
|
||||
Direction::LeftToRight => (from, to, from, to),
|
||||
Direction::RightToLeft => (to, from, to, from),
|
||||
};
|
||||
|
||||
Self::from(Mesh {
|
||||
indices: vec![0, 1, 2, 2, 1, 3],
|
||||
vertices: vec![
|
||||
Vertex::untextured(rect.left_top(), left_top),
|
||||
Vertex::untextured(rect.right_top(), right_top),
|
||||
Vertex::untextured(rect.left_bottom(), left_bottom),
|
||||
Vertex::untextured(rect.right_bottom(), right_bottom),
|
||||
],
|
||||
texture_id: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
pub fn text(
|
||||
fonts: &mut FontsView<'_>,
|
||||
|
||||
@@ -10,8 +10,8 @@ use emath::{GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2, pos2, remap,
|
||||
use crate::{
|
||||
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
|
||||
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
|
||||
StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, color::ColorMode, emath,
|
||||
stroke::PathStroke, texture_atlas::PreparedDisc,
|
||||
StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
|
||||
texture_atlas::PreparedDisc,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -809,11 +809,8 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32
|
||||
} else {
|
||||
out.reserve_triangles(n as usize);
|
||||
let idx = out.vertices.len() as u32;
|
||||
out.vertices.extend(path.iter().map(|p| Vertex {
|
||||
pos: p.pos,
|
||||
uv: WHITE_UV,
|
||||
color: fill_color,
|
||||
}));
|
||||
out.vertices
|
||||
.extend(path.iter().map(|p| Vertex::untextured(p.pos, fill_color)));
|
||||
for i in 2..n {
|
||||
out.add_triangle(idx, idx + i - 1, idx + i);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a
|
||||
size 1510
|
||||
oid sha256:5dfd9b576e0ab4b47a0f8c8acfc2664f92faa88cc7fb088409bff359fa1dfadd
|
||||
size 1861
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea
|
||||
size 1501
|
||||
oid sha256:4529530bb46af68260ca910a91f888e8b296790be2b976b450cec884799f53b4
|
||||
size 1953
|
||||
|
||||
Reference in New Issue
Block a user