Modernize application scrollbars

This commit is contained in:
2026-07-19 21:56:39 +02:00
parent ca7b864261
commit c56550f1ad
21 changed files with 178 additions and 32 deletions

View File

@@ -1,3 +1,4 @@
use iced::widget::scrollable;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{ use iced::widget::{
Container, button, checkbox, column, container, pick_list, row, text, text_editor, text_input, Container, button, checkbox, column, container, pick_list, row, text, text_editor, text_input,
@@ -55,6 +56,68 @@ pub fn tooltip_style(_theme: &Theme) -> container::Style {
} }
} }
/// Thin scrollbar geometry shared by every vertical application surface.
pub fn compact_scrollbar() -> scrollable::Scrollbar {
scrollable::Scrollbar::new()
.width(6)
.margin(1)
.scroller_width(3)
}
/// Muted overlay-like scrollbar chrome that gains contrast only while interacting.
pub fn scrollable_style(_theme: &Theme, status: scrollable::Status) -> scrollable::Style {
let rail = |opacity| scrollable::Rail {
background: None,
border: Border::default(),
scroller: scrollable::Scroller {
color: Color::from_rgba(0.72, 0.74, 0.80, opacity),
border: Border {
radius: 999.0.into(),
..Border::default()
},
},
};
let (horizontal_opacity, vertical_opacity) = match status {
scrollable::Status::Active => (0.24, 0.24),
scrollable::Status::Hovered {
is_horizontal_scrollbar_hovered,
is_vertical_scrollbar_hovered,
} => (
if is_horizontal_scrollbar_hovered {
0.55
} else {
0.24
},
if is_vertical_scrollbar_hovered {
0.55
} else {
0.24
},
),
scrollable::Status::Dragged {
is_horizontal_scrollbar_dragged,
is_vertical_scrollbar_dragged,
} => (
if is_horizontal_scrollbar_dragged {
0.75
} else {
0.24
},
if is_vertical_scrollbar_dragged {
0.75
} else {
0.24
},
),
};
scrollable::Style {
container: container::Style::default(),
vertical_rail: rail(vertical_opacity),
horizontal_rail: rail(horizontal_opacity),
gap: None,
}
}
pub fn field_style(_theme: &Theme, status: text_input::Status) -> text_input::Style { pub fn field_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
let border_color = match status { let border_color = match status {
text_input::Status::Focused => FOCUS_COLOR, text_input::Status::Focused => FOCUS_COLOR,
@@ -402,4 +465,27 @@ mod tests {
assert_eq!(style.border.width, 1.0); assert_eq!(style.border.width, 1.0);
assert_eq!(style.border.radius.top_left, 6.0); assert_eq!(style.border.radius.top_left, 6.0);
} }
#[test]
fn application_scrollbars_are_compact_muted_and_only_brighten_on_hover() {
assert_eq!(
compact_scrollbar(),
scrollable::Scrollbar::new()
.width(6)
.margin(1)
.scroller_width(3)
);
let theme = app_theme();
let active = scrollable_style(&theme, scrollable::Status::Active);
let hovered = scrollable_style(
&theme,
scrollable::Status::Hovered {
is_horizontal_scrollbar_hovered: false,
is_vertical_scrollbar_hovered: true,
},
);
assert!(active.vertical_rail.background.is_none());
assert!(active.vertical_rail.scroller.color.a < 0.5);
assert!(hovered.vertical_rail.scroller.color.a > active.vertical_rail.scroller.color.a);
}
} }

View File

@@ -335,8 +335,9 @@ fn table<'a>(surface: &'a InlineSurface, locale: UiLocale) -> Element<'a, Messag
} }
scrollable(iced::widget::Column::with_children(body).spacing(6)) scrollable(iced::widget::Column::with_children(body).spacing(6))
.direction(scrollable::Direction::Horizontal( .direction(scrollable::Direction::Horizontal(
scrollable::Scrollbar::default(), inputs::compact_scrollbar(),
)) ))
.style(inputs::scrollable_style)
.into() .into()
} }

View File

@@ -339,6 +339,8 @@ pub fn view<'a>(
.spacing(10) .spacing(10)
.width(Length::Fill), .width(Length::Fill),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.anchor_bottom() .anchor_bottom()
.height(Length::Fill); .height(Length::Fill);

View File

@@ -174,6 +174,8 @@ pub fn view<'a>(state: &'a DashboardState, locale: UiLocale) -> Element<'a, Mess
} }
scrollable(container(content.padding(24).width(Length::Fill))) scrollable(container(content.padding(24).width(Length::Fill)))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()

View File

@@ -200,6 +200,8 @@ pub fn view(state: &DocumentationState, locale: UiLocale) -> Element<'_, Message
}); });
} }
scrollable(container(content).padding(2)) scrollable(container(content).padding(2))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.id(scroll_id(state.kind)) .id(scroll_id(state.kind))
.height(Length::Fill) .height(Length::Fill)
.into() .into()

View File

@@ -170,6 +170,8 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
); );
} }
scrollable(container(pairs).padding(2)) scrollable(container(pairs).padding(2))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
}; };

View File

@@ -458,11 +458,15 @@ pub fn diff_view(
} else { } else {
sections.push(muted_text(t(locale, "git.noDiff"))); sections.push(muted_text(t(locale, "git.noDiff")));
} }
container(scrollable( container(
scrollable(
iced::widget::Column::with_children(sections) iced::widget::Column::with_children(sections)
.spacing(12) .spacing(12)
.padding(16), .padding(16),
)) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style),
)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
@@ -480,6 +484,8 @@ fn code_card(label: String, contents: String, wrapping: Wrapping) -> Element<'st
.font(Font::MONOSPACE) .font(Font::MONOSPACE)
.wrapping(wrapping) .wrapping(wrapping)
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Fill), .height(Length::Fill),
] ]
.spacing(8) .spacing(8)

View File

@@ -259,6 +259,8 @@ pub fn view<'a>(state: &'a ImportEditorState, locale: UiLocale) -> Element<'a, M
.padding(16) .padding(16)
.width(Length::Fill), .width(Length::Fill),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
} }

View File

@@ -540,7 +540,9 @@ pub fn view<'a>(
.spacing(12) .spacing(12)
.padding(16) .padding(16)
.width(Length::Fill), .width(Length::Fill),
); )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style);
container(body) container(body)
.width(Length::Fill) .width(Length::Fill)

View File

@@ -777,6 +777,8 @@ pub fn view(state: &MenuEditorState, locale: UiLocale) -> Element<'_, Message> {
centered_status(locale, "menuEditor.empty") centered_status(locale, "menuEditor.empty")
} else { } else {
scrollable(tree_view(state, locale)) scrollable(tree_view(state, locale))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
} }

View File

@@ -136,7 +136,11 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
content = content.push(message_card(t(locale, "metadataDiff.idle"))); content = content.push(message_card(t(locale, "metadataDiff.idle")));
} }
container(scrollable(content)) container(
scrollable(content)
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style),
)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(24) .padding(24)

View File

@@ -1196,7 +1196,10 @@ pub fn view(
let content = column![ let content = column![
title, title,
Space::with_height(12.0), Space::with_height(12.0),
scrollable(column(rows).spacing(8)).height(Length::Fixed(320.0)), scrollable(column(rows).spacing(8))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Fixed(320.0)),
Space::with_height(16.0), Space::with_height(16.0),
buttons, buttons,
] ]

View File

@@ -256,6 +256,8 @@ pub fn view(
.spacing(4) .spacing(4)
.padding(8), .padding(8),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.into() .into()
} }
} }
@@ -285,6 +287,8 @@ pub fn view(
.spacing(2) .spacing(2)
.padding(8), .padding(8),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.into() .into()
} }
} }
@@ -347,6 +351,8 @@ pub fn view(
.spacing(4) .spacing(4)
.padding(8), .padding(8),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.into() .into()
} }
} }
@@ -388,6 +394,8 @@ pub fn view(
.spacing(2) .spacing(2)
.padding(8), .padding(8),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.into() .into()
} }
} }

View File

@@ -969,6 +969,8 @@ pub fn view<'a>(
.spacing(8) .spacing(8)
.width(Length::Fill), .width(Length::Fill),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Shrink); .height(Length::Shrink);
// ── Full layout: top pane (shrink), editor (fill), footer (shrink) ── // ── Full layout: top pane (shrink), editor (fill), footer (shrink) ──

View File

@@ -260,6 +260,8 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
.width(Length::Fill), .width(Length::Fill),
); );
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill)) let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Shrink); .height(Length::Shrink);
// Full layout: top pane (shrink), content (fill), validation + footer (shrink) // Full layout: top pane (shrink), content (fill), validation + footer (shrink)

View File

@@ -421,6 +421,8 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
.padding(16) .padding(16)
.width(Length::Fill), .width(Length::Fill),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill); .height(Length::Fill);

View File

@@ -1254,7 +1254,11 @@ pub fn view(
.padding(12); .padding(12);
// layout.allium: sidebar width is resizable, passed as parameter // layout.allium: sidebar width is resizable, passed as parameter
container(scrollable(content)) container(
scrollable(content)
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style),
)
.width(Length::Fixed(width)) .width(Length::Fixed(width))
.height(Length::Fill) .height(Length::Fill)
.style(sidebar_style) .style(sidebar_style)

View File

@@ -131,7 +131,11 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
} }
} }
container(scrollable(content)) container(
scrollable(content)
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style),
)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(24) .padding(24)

View File

@@ -287,6 +287,8 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
)] )]
.padding(16), .padding(16),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
@@ -457,6 +459,8 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
.padding(16) .padding(16)
.width(Length::Fill), .width(Length::Fill),
) )
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()

View File

@@ -224,6 +224,8 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
.width(Length::Fill), .width(Length::Fill),
); );
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill)) let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style)
.height(Length::Shrink); .height(Length::Shrink);
// Full layout: top pane (shrink), content (fill), validation + footer (shrink) // Full layout: top pane (shrink), content (fill), validation + footer (shrink)

View File

@@ -59,7 +59,11 @@ pub fn view<'a>(state: &'a TranslationValidationState, locale: UiLocale) -> Elem
content = content.push(message_card(t(locale, "translationValidation.idle"))); content = content.push(message_card(t(locale, "translationValidation.idle")));
} }
container(scrollable(content)) container(
scrollable(content)
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.style(inputs::scrollable_style),
)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(24) .padding(24)