mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Add a ScrollAreaStyle, so the theme decides how a scroll area scrolls
`ScrollArea` read `ui.spacing().scroll` directly, so the only way to change how one scrolls was to mutate the ambient style around it — which is what `Popup` did for `overflow_margin`. It now resolves a `ScrollAreaStyle` once per pass, and the builder methods override that. This lets a theme turn on full bleed for the scroll areas it wants it for, so `PopupStyle::scroll_overflow_margin` and the ambient mutation both go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -637,7 +637,6 @@ impl<'a> Popup<'a> {
|
|||||||
// The theme decides how the popup is framed and how tightly its items sit together.
|
// The theme decides how the popup is framed and how tightly its items sit together.
|
||||||
let popup_style: PopupStyle = ui.widget_style(id, &classes);
|
let popup_style: PopupStyle = ui.widget_style(id, &classes);
|
||||||
ui.spacing_mut().item_spacing = popup_style.item_spacing;
|
ui.spacing_mut().item_spacing = popup_style.item_spacing;
|
||||||
ui.spacing_mut().scroll.overflow_margin = popup_style.scroll_overflow_margin;
|
|
||||||
|
|
||||||
let frame = frame.unwrap_or(popup_style.frame);
|
let frame = frame.unwrap_or(popup_style.frame);
|
||||||
frame.show(ui, content).inner
|
frame.show(ui, content).inner
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{
|
|||||||
AsIdSalt, Context, CursorIcon, Id, IdSalt, NumExt as _, Pos2, Rangef, Rect, Response, Sense,
|
AsIdSalt, Context, CursorIcon, Id, IdSalt, NumExt as _, Pos2, Rangef, Rect, Response, Sense,
|
||||||
Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state,
|
Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state,
|
||||||
pos2, remap, remap_clamp,
|
pos2, remap, remap_clamp,
|
||||||
|
widget_style::{Classes, HasClasses, ScrollAreaStyle, StyleArgs, WidgetState},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -361,7 +362,9 @@ pub struct ScrollArea {
|
|||||||
content_margin: Option<Margin>,
|
content_margin: Option<Margin>,
|
||||||
overflow_margin: Option<Margin>,
|
overflow_margin: Option<Margin>,
|
||||||
|
|
||||||
extend_into_parent_margin: bool,
|
extend_into_parent_margin: Option<bool>,
|
||||||
|
|
||||||
|
classes: Classes,
|
||||||
|
|
||||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||||
/// end position until user manually changes position. It will become true
|
/// end position until user manually changes position. It will become true
|
||||||
@@ -417,7 +420,8 @@ impl ScrollArea {
|
|||||||
wheel_scroll_multiplier: Vec2::splat(1.0),
|
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||||
content_margin: None,
|
content_margin: None,
|
||||||
overflow_margin: None,
|
overflow_margin: None,
|
||||||
extend_into_parent_margin: false,
|
extend_into_parent_margin: None,
|
||||||
|
classes: Classes::default(),
|
||||||
stick_to_end: Vec2b::FALSE,
|
stick_to_end: Vec2b::FALSE,
|
||||||
animated: true,
|
animated: true,
|
||||||
}
|
}
|
||||||
@@ -637,7 +641,7 @@ impl ScrollArea {
|
|||||||
/// The scroll bars will be either on top of this margin, or outside of it,
|
/// The scroll bars will be either on top of this margin, or outside of it,
|
||||||
/// depending on the value of [`crate::style::ScrollStyle::floating`].
|
/// depending on the value of [`crate::style::ScrollStyle::floating`].
|
||||||
///
|
///
|
||||||
/// Default: [`crate::style::ScrollStyle::content_margin`].
|
/// Default: [`crate::widget_style::ScrollAreaStyle::scroll`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
|
pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
|
||||||
self.content_margin = Some(margin.into());
|
self.content_margin = Some(margin.into());
|
||||||
@@ -650,7 +654,10 @@ impl ScrollArea {
|
|||||||
/// the scroll area would otherwise cut that off. Only the ends of the scroll range are
|
/// the scroll area would otherwise cut that off. Only the ends of the scroll range are
|
||||||
/// widened, where there is nothing that could scroll into view through the gap.
|
/// widened, where there is nothing that could scroll into view through the gap.
|
||||||
///
|
///
|
||||||
/// Default: [`crate::style::ScrollStyle::overflow_margin`].
|
/// This covers the sides that [`Self::extend_into_parent_margin`] cannot: a side with a widget
|
||||||
|
/// between the scroll area and the frame edge stays where it is, and still clips.
|
||||||
|
///
|
||||||
|
/// Default: [`crate::widget_style::ScrollAreaStyle::scroll`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn overflow_margin(mut self, margin: impl Into<Margin>) -> Self {
|
pub fn overflow_margin(mut self, margin: impl Into<Margin>) -> Self {
|
||||||
self.overflow_margin = Some(margin.into());
|
self.overflow_margin = Some(margin.into());
|
||||||
@@ -673,10 +680,10 @@ impl ScrollArea {
|
|||||||
/// _after_ the scroll area would be overlapped by it, so reserve the space for it
|
/// _after_ the scroll area would be overlapped by it, so reserve the space for it
|
||||||
/// first, e.g. with [`Layout::bottom_up`](crate::Layout::bottom_up).
|
/// first, e.g. with [`Layout::bottom_up`](crate::Layout::bottom_up).
|
||||||
///
|
///
|
||||||
/// Default: `false`.
|
/// Default: [`crate::widget_style::ScrollAreaStyle::extend_into_parent_margin`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn extend_into_parent_margin(mut self, extend: bool) -> Self {
|
pub fn extend_into_parent_margin(mut self, extend: bool) -> Self {
|
||||||
self.extend_into_parent_margin = extend;
|
self.extend_into_parent_margin = Some(extend);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,6 +746,9 @@ struct Prepared {
|
|||||||
/// See [`ScrollArea::extend_into_parent_margin`].
|
/// See [`ScrollArea::extend_into_parent_margin`].
|
||||||
claimed_margin: Margin,
|
claimed_margin: Margin,
|
||||||
|
|
||||||
|
/// The resolved style, with the builder overrides already folded in.
|
||||||
|
scroll_style: crate::style::ScrollStyle,
|
||||||
|
|
||||||
content_ui: Ui,
|
content_ui: Ui,
|
||||||
|
|
||||||
/// Relative coordinates: the offset and size of the view of the inner UI.
|
/// Relative coordinates: the offset and size of the view of the inner UI.
|
||||||
@@ -759,6 +769,16 @@ struct Prepared {
|
|||||||
animated: bool,
|
animated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasClasses for ScrollArea {
|
||||||
|
fn classes(&self) -> &Classes {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classes_mut(&mut self) -> &mut Classes {
|
||||||
|
&mut self.classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ScrollArea {
|
impl ScrollArea {
|
||||||
fn begin(self, ui: &mut Ui) -> Prepared {
|
fn begin(self, ui: &mut Ui) -> Prepared {
|
||||||
let Self {
|
let Self {
|
||||||
@@ -775,9 +795,10 @@ impl ScrollArea {
|
|||||||
on_drag_cursor,
|
on_drag_cursor,
|
||||||
scroll_source,
|
scroll_source,
|
||||||
wheel_scroll_multiplier,
|
wheel_scroll_multiplier,
|
||||||
content_margin: _, // Used elsewhere
|
content_margin,
|
||||||
overflow_margin,
|
overflow_margin,
|
||||||
extend_into_parent_margin,
|
extend_into_parent_margin,
|
||||||
|
classes,
|
||||||
stick_to_end,
|
stick_to_end,
|
||||||
animated,
|
animated,
|
||||||
} = self;
|
} = self;
|
||||||
@@ -807,7 +828,27 @@ impl ScrollArea {
|
|||||||
ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
|
ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
let scroll_style = ui.spacing().scroll;
|
// The theme decides how the scroll area looks; the builder methods override it.
|
||||||
|
let scroll_area_style: ScrollAreaStyle = ui.ctx().get_widget_style(&StyleArgs {
|
||||||
|
classes: &classes,
|
||||||
|
state: WidgetState::Inactive,
|
||||||
|
style: ui.style(),
|
||||||
|
stack: ui.stack(),
|
||||||
|
ctx: ui.ctx(),
|
||||||
|
});
|
||||||
|
let ScrollAreaStyle {
|
||||||
|
mut scroll,
|
||||||
|
extend_into_parent_margin: extend_by_default,
|
||||||
|
} = scroll_area_style;
|
||||||
|
if let Some(content_margin) = content_margin {
|
||||||
|
scroll.content_margin = content_margin;
|
||||||
|
}
|
||||||
|
if let Some(overflow_margin) = overflow_margin {
|
||||||
|
scroll.overflow_margin = overflow_margin;
|
||||||
|
}
|
||||||
|
let extend_into_parent_margin = extend_into_parent_margin.unwrap_or(extend_by_default);
|
||||||
|
|
||||||
|
let scroll_style = scroll;
|
||||||
let current_bar_use = if scroll_style.floating {
|
let current_bar_use = if scroll_style.floating {
|
||||||
show_bars.to_vec2().yx() * scroll_style.allocated_width()
|
show_bars.to_vec2().yx() * scroll_style.allocated_width()
|
||||||
} else {
|
} else {
|
||||||
@@ -874,8 +915,7 @@ impl ScrollArea {
|
|||||||
{
|
{
|
||||||
// Clip the content, but only when we really need to:
|
// Clip the content, but only when we really need to:
|
||||||
let mut content_clip_rect = ui.clip_rect();
|
let mut content_clip_rect = ui.clip_rect();
|
||||||
let overflow_margin =
|
let overflow_margin = scroll_style.overflow_margin;
|
||||||
overflow_margin.unwrap_or_else(|| ui.spacing().scroll.overflow_margin);
|
|
||||||
let overflow_min = Vec2::new(overflow_margin.leftf(), overflow_margin.topf());
|
let overflow_min = Vec2::new(overflow_margin.leftf(), overflow_margin.topf());
|
||||||
let overflow_max = Vec2::new(overflow_margin.rightf(), overflow_margin.bottomf());
|
let overflow_max = Vec2::new(overflow_margin.rightf(), overflow_margin.bottomf());
|
||||||
|
|
||||||
@@ -1021,6 +1061,7 @@ impl ScrollArea {
|
|||||||
scroll_bar_rect,
|
scroll_bar_rect,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
claimed_margin,
|
claimed_margin,
|
||||||
|
scroll_style,
|
||||||
content_ui,
|
content_ui,
|
||||||
viewport,
|
viewport,
|
||||||
scroll_source,
|
scroll_source,
|
||||||
@@ -1109,16 +1150,13 @@ impl ScrollArea {
|
|||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
|
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
|
||||||
) -> ScrollAreaOutput<R> {
|
) -> ScrollAreaOutput<R> {
|
||||||
let content_margin = self.content_margin;
|
|
||||||
|
|
||||||
let mut prepared = self.begin(ui);
|
let mut prepared = self.begin(ui);
|
||||||
let id = prepared.id;
|
let id = prepared.id;
|
||||||
let inner_rect = prepared.inner_rect;
|
let inner_rect = prepared.inner_rect;
|
||||||
|
|
||||||
// The margin we took over from the parent frame is applied to the contents instead,
|
// The margin we took over from the parent frame is applied to the contents instead,
|
||||||
// so they keep the same padding:
|
// so they keep the same padding:
|
||||||
let margin = content_margin.unwrap_or_else(|| ui.spacing().scroll.content_margin)
|
let margin = prepared.scroll_style.content_margin + prepared.claimed_margin;
|
||||||
+ prepared.claimed_margin;
|
|
||||||
|
|
||||||
let inner = crate::Frame::NONE
|
let inner = crate::Frame::NONE
|
||||||
.inner_margin(margin)
|
.inner_margin(margin)
|
||||||
@@ -1152,6 +1190,7 @@ impl Prepared {
|
|||||||
scroll_bar_visibility,
|
scroll_bar_visibility,
|
||||||
scroll_bar_rect,
|
scroll_bar_rect,
|
||||||
claimed_margin,
|
claimed_margin,
|
||||||
|
scroll_style,
|
||||||
content_ui,
|
content_ui,
|
||||||
viewport: _,
|
viewport: _,
|
||||||
scroll_source,
|
scroll_source,
|
||||||
@@ -1277,7 +1316,7 @@ 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 limit_rect = if ui.spacing().scroll.floating {
|
let limit_rect = if scroll_style.floating {
|
||||||
outer_rect
|
outer_rect
|
||||||
} else {
|
} else {
|
||||||
inner_rect
|
inner_rect
|
||||||
@@ -1350,13 +1389,18 @@ impl Prepared {
|
|||||||
show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
|
show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll_style = ui.spacing().scroll;
|
|
||||||
|
|
||||||
// We paint into the margin of the parent frame, but we allocate only the rect we would
|
// We paint into the margin of the parent frame, but we allocate only the rect we would
|
||||||
// have used without it. The frame then adds that margin back around what we painted.
|
// have used without it. The frame then adds that margin back around what we painted.
|
||||||
ui.advance_cursor_after_rect(shrink_at_least_to_nothing(outer_rect, claimed_margin));
|
ui.advance_cursor_after_rect(shrink_at_least_to_nothing(outer_rect, claimed_margin));
|
||||||
|
|
||||||
paint_fade_areas_impl(ui, inner_rect, claimed_margin, content_size, state.offset);
|
paint_fade_areas_impl(
|
||||||
|
ui,
|
||||||
|
scroll_style.fade,
|
||||||
|
inner_rect,
|
||||||
|
claimed_margin,
|
||||||
|
content_size,
|
||||||
|
state.offset,
|
||||||
|
);
|
||||||
|
|
||||||
// Paint the bars:
|
// Paint the bars:
|
||||||
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
|
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
|
||||||
@@ -1648,6 +1692,7 @@ impl Prepared {
|
|||||||
/// indicate that more content is available beyond the visible region.
|
/// indicate that more content is available beyond the visible region.
|
||||||
fn paint_fade_areas_impl(
|
fn paint_fade_areas_impl(
|
||||||
ui: &Ui,
|
ui: &Ui,
|
||||||
|
fade: crate::style::ScrollFadeStyle,
|
||||||
inner_rect: Rect,
|
inner_rect: Rect,
|
||||||
corner_inset: Margin,
|
corner_inset: Margin,
|
||||||
content_size: Vec2,
|
content_size: Vec2,
|
||||||
@@ -1656,7 +1701,7 @@ fn paint_fade_areas_impl(
|
|||||||
let crate::style::ScrollFadeStyle {
|
let crate::style::ScrollFadeStyle {
|
||||||
strength,
|
strength,
|
||||||
size: fade_size,
|
size: fade_size,
|
||||||
} = ui.spacing().scroll.fade;
|
} = fade;
|
||||||
|
|
||||||
if strength <= 0.0 {
|
if strength <= 0.0 {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use crate::{
|
|||||||
theme::StyleProvider,
|
theme::StyleProvider,
|
||||||
widget_style::{
|
widget_style::{
|
||||||
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, LayoutStyle,
|
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, LayoutStyle,
|
||||||
PopupStyle, READ_ONLY_CLASS, SELECTED_CLASS, SeparatorStyle, StyleArgs, TextEditStyle,
|
PopupStyle, READ_ONLY_CLASS, SELECTED_CLASS, ScrollAreaStyle, SeparatorStyle, StyleArgs,
|
||||||
TextVisuals, WidgetState,
|
TextEditStyle, TextVisuals, WidgetState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +106,15 @@ impl StyleProvider<PopupStyle> for DefaultStyle {
|
|||||||
PopupStyle {
|
PopupStyle {
|
||||||
frame: Frame::popup(style),
|
frame: Frame::popup(style),
|
||||||
item_spacing: style.spacing.item_spacing,
|
item_spacing: style.spacing.item_spacing,
|
||||||
scroll_overflow_margin: style.spacing.scroll.overflow_margin,
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<ScrollAreaStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ScrollAreaStyle {
|
||||||
|
ScrollAreaStyle {
|
||||||
|
scroll: modifiers.style.spacing.scroll,
|
||||||
|
extend_into_parent_margin: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use crate::{
|
|||||||
theme::{StyleProvider, default_style::DefaultStyle},
|
theme::{StyleProvider, default_style::DefaultStyle},
|
||||||
util::IdTypeMap,
|
util::IdTypeMap,
|
||||||
widget_style::{
|
widget_style::{
|
||||||
BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, PopupStyle, SeparatorStyle,
|
BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, PopupStyle, ScrollAreaStyle,
|
||||||
TextEditStyle, WidgetStyle,
|
SeparatorStyle, TextEditStyle, WidgetStyle,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +62,11 @@ impl Default for Themes {
|
|||||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<ScrollAreaStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
themes.insert_temp::<ThemeWrap<LabelStyle>>(
|
themes.insert_temp::<ThemeWrap<LabelStyle>>(
|
||||||
Id::NULL,
|
Id::NULL,
|
||||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use emath::Vec2;
|
|||||||
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Context, Frame, Margin, Response, Style, TextStyle, UiStack,
|
Context, Frame, Response, Style, TextStyle, UiStack,
|
||||||
style::{WidgetVisuals, Widgets},
|
style::{WidgetVisuals, Widgets},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,17 +109,24 @@ pub struct PopupStyle {
|
|||||||
/// A menu wants its items flush against each other, while a tooltip wants them spaced out
|
/// A menu wants its items flush against each other, while a tooltip wants them spaced out
|
||||||
/// like any other content.
|
/// like any other content.
|
||||||
pub item_spacing: Vec2,
|
pub item_spacing: Vec2,
|
||||||
|
|
||||||
/// How far the contents of a [`crate::ScrollArea`] inside the popup may paint outside it.
|
|
||||||
///
|
|
||||||
/// A [`crate::ScrollArea`] clips to its viewport, which would cut off anything a widget paints
|
|
||||||
/// outside its own box — an item whose fill bleeds past it, say. This is how much room such a
|
|
||||||
/// widget gets, applied via [`crate::style::ScrollStyle::overflow_margin`].
|
|
||||||
pub scroll_overflow_margin: Margin,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetStyle for PopupStyle {}
|
impl WidgetStyle for PopupStyle {}
|
||||||
|
|
||||||
|
/// Dedicated style for a [`crate::ScrollArea`]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScrollAreaStyle {
|
||||||
|
/// How the scroll bars look, and how much room the contents get.
|
||||||
|
pub scroll: crate::style::ScrollStyle,
|
||||||
|
|
||||||
|
/// Grow into the inner margin of the closest parent [`Frame`].
|
||||||
|
///
|
||||||
|
/// See [`crate::ScrollArea::extend_into_parent_margin`], which overrides this.
|
||||||
|
pub extend_into_parent_margin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for ScrollAreaStyle {}
|
||||||
|
|
||||||
/// Dedicated text edit style
|
/// Dedicated text edit style
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextEditStyle {
|
pub struct TextEditStyle {
|
||||||
|
|||||||
Reference in New Issue
Block a user