Modernize application scrollbars
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) ──
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user