Scrollable
A comprehensive scrollable container component that provides custom scrollbars, scroll tracking, and virtualization capabilities. Supports both vertical and horizontal scrolling with customizable appearance and behavior.
#Import
use gpui_kit::component::{
scroll::{ScrollableElement, ScrollbarAxis, ScrollbarMode},
StyledExt as _,
};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { ScrollAxis, Scrollable } from "scrollable.slint";use iced::widget::{column, container, mouse_area, row, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Usage
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The three axes in one box: a column of rows taller than a fixed box, a row
// of cards wider than one, and a grid that overflows both ways. The vertical
// area reports its viewport, which is the offset line below the box.
let usage = column![
row![
sample(
"Vertical",
Scrollable::new(rows(1, 20))
.width(Length::Fixed(220.0))
.height(Length::Fixed(180.0))
.on_scroll(|viewport| {
let offset = viewport.absolute_offset();
Message::UsageViewport(offset.x, offset.y)
}),
),
sample(
"Horizontal",
Scrollable::new(cards(10))
.axis(Axis::Horizontal)
.width(Length::Fixed(220.0))
.height(Length::Fixed(120.0)),
),
sample(
"Both",
Scrollable::new(grid(8, 8))
.axis(Axis::Both)
.width(Length::Fixed(260.0))
.height(Length::Fixed(180.0)),
),
]
.spacing(theme::SPACING_LG),
text(format!(
"vertical area at x {:.0}, y {:.0}",
app.usage_x, app.usage_y
))
.size(theme::FONT_SIZE_XS)
.style(|theme: &Theme| text::Style {
color: Some(Tokens::of(theme).muted_foreground),
}),
]
.spacing(theme::SPACING_MD);#Basic Scrollable Container
The simplest way to make any element scrollable is using the overflow_scrollbar() method from ScrollableElement trait.
This method is almost like the overflow_scroll() method, but it adds scrollbars.
overflow_scrollbar()- Adds scrollbars for both axes as needed.overflow_x_scrollbar()- Adds horizontal scrollbar as needed.overflow_y_scrollbar()- Adds vertical scrollbar as needed.
use gpui_kit::{div, Axis};
use gpui_kit::component::ScrollableElement;
div()
.id("scrollable-container")
.size_full()
.child("Your content here")
.overflow_scrollbar()Scrollable {
height: 180px;
VerticalLayout {
padding: 8px;
spacing: 8px;
for i in 20: Text {
text: "Row \{i + 1}";
color: Theme.foreground;
}
}
}#Vertical Scrolling
v_flex()
.id("scrollable-container")
.overflow_y_scrollbar()
.gap_2()
.p_4()
.child("Scrollable Content")
.children((0..100).map(|i| {
div()
.h(px(40.))
.w_full()
.bg(cx.theme().secondary)
.child(format!("Item {}", i))
}))Scrollable {
height: 180px;
VerticalLayout {
padding: 12px;
spacing: 10px;
for i in 12: Rectangle {
height: 44px;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.muted.with-alpha(0.35);
HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
alignment: center;
Text {
text: "Commit \{i + 1}";
color: Theme.foreground;
font-size: Theme.font-size-sm;
vertical-alignment: center;
}
}
}
}
}#Horizontal Scrolling
h_flex()
.id("scrollable-container")
.overflow_x_scrollbar()
.gap_2()
.p_4()
.children((0..50).map(|i| {
div()
.min_w(px(120.))
.h(px(80.))
.bg(cx.theme().accent)
.child(format!("Card {}", i))
}))Scrollable {
axis: ScrollAxis.horizontal;
height: 120px;
HorizontalLayout {
padding: 8px;
spacing: 8px;
for i in 10: Rectangle {
width: 120px;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.muted.with-alpha(0.35);
Text {
text: "Card \{i + 1}";
color: Theme.foreground;
font-size: Theme.font-size-sm;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}#Both Directions
div()
.id("scrollable-container")
.size_full()
.overflow_scrollbar()
.child(
div()
.w(px(2000.)) // Wide content
.h(px(2000.)) // Tall content
.bg(cx.theme().background)
.child("Large content area")
)Scrollable {
axis: ScrollAxis.both;
height: 200px;
GridLayout {
padding: 8px;
spacing: 8px;
// One loop with the row worked out from the index: a nested
// `for` needs its own braces and reads worse here.
for i in 64: Rectangle {
row: (i - mod(i, 8)) / 8;
col: mod(i, 8);
width: 96px;
height: 56px;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.muted.with-alpha(0.35);
Text {
text: "\{(i - mod(i, 8)) / 8 + 1},\{mod(i, 8) + 1}";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}#File Browser with Scrolling
pub struct FileBrowser {
files: Vec<String>,
}
impl Render for FileBrowser {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.border_1()
.border_color(cx.theme().border)
.size_full()
.child(
v_flex()
.gap_1()
.p_2()
.overflow_y_scrollbar()
.children(self.files.iter().map(|file| {
div()
.h(px(32.))
.w_full()
.px_2()
.flex()
.items_center()
.hover(|style| style.bg(cx.theme().secondary_hover))
.child(file.clone())
}))
)
}
}SlintNo Slint version of this example yet.
#Chat Messages with Auto-scroll
pub struct ChatView {
messages: Vec<String>,
scroll_handle: ScrollHandle,
should_auto_scroll: bool,
}
impl ChatView {
fn add_message(&mut self, message: String) {
self.messages.push(message);
if self.should_auto_scroll {
// Scroll to bottom for new messages
let max_offset = self.scroll_handle.max_offset();
self.scroll_handle.set_offset(point(px(0.), max_offset.y));
}
}
}SlintNo Slint version of this example yet.
#Data Table with Virtual Scrolling
pub struct DataTable {
data: Vec<Vec<String>>,
scroll_handle: VirtualListScrollHandle,
}
impl Render for DataTable {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
VirtualList::new(
self.scroll_handle.clone(),
self.data.len(),
|_ix, _window, _cx| size(px(800.), px(32.)), // Fixed row height
|ix, bounds, _selected, _window, cx| {
h_flex()
.size(bounds.size)
.border_b_1()
.border_color(cx.theme().border)
.children(self.data[ix].iter().map(|cell| {
div()
.flex_1()
.px_2()
.flex()
.items_center()
.child(cell.clone())
}))
.into_any_element()
},
)
}
}SlintNo Slint version of this example yet.
#Custom Scrollbars
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The bar itself, where iced allows it to be arranged: a thicker bar, a rail
// embedded with a gap between it and the content, and a thumb drawn from a
// different token. There is no manual scrollbar widget to attach the way GPUI
// has one — iced's region owns its bar — so this is the whole custom surface.
let custom = row![
sample(
"Thicker bar",
Scrollable::new(rows(1, 20))
.scrollbar_width(18.0)
.scroller_width(12.0)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
sample(
"Embedded track",
Scrollable::new(rows(1, 20))
.scrollbar_spacing(6.0)
.track(Ink::Border)
.thumb(Ink::Primary)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
sample(
"Foreground thumb",
Scrollable::new(rows(1, 20))
.thumb(Ink::Foreground)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
]
.spacing(theme::SPACING_LG);#Manual Scrollbar Creation
For more control, you can create scrollbars manually:
use gpui_kit::component::scroll::{ScrollableElement};
pub struct ScrollableView {
scroll_handle: ScrollHandle,
}
impl Render for ScrollableView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.relative()
.size_full()
.child(
div()
.id("content")
.track_scroll(&self.scroll_handle)
.overflow_scroll()
.size_full()
.child("Your scrollable content")
)
.vertical_scrollbar(&self.scroll_handle)
}
}SlintNo Slint version of this example yet.
#Virtualization
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// iced 0.14 has no windowed list, so every one of these rows is a real
// element that is built and drawn; nothing here is virtual. What the
// component does expose is the viewport, and `on_scroll` reads the visible
// range out of it. A caller that needs windowing builds it on that number —
// this port does not fake an API for it.
let first = (app.window_offset / ROW_PITCH).floor().max(0.0) as usize + 1;
let visible = (WINDOW_HEIGHT / ROW_PITCH).ceil() as usize;
let last = (first + visible - 1).min(WINDOW_ROWS);
let virtualization = column![
text(format!(
"rows {first}–{last} of {WINDOW_ROWS} are on screen; all {WINDOW_ROWS} are built"
))
.size(theme::FONT_SIZE_XS)
.style(|theme: &Theme| text::Style {
color: Some(Tokens::of(theme).muted_foreground),
}),
Scrollable::new(rows(1, WINDOW_ROWS))
.height(Length::Fixed(WINDOW_HEIGHT))
.on_scroll(|viewport| Message::WindowViewport(viewport.absolute_offset().y)),
]
.spacing(theme::SPACING_SM);#VirtualList for Large Datasets
For rendering large lists efficiently, use VirtualList:
use gpui_kit::component::{VirtualList, VirtualListScrollHandle};
pub struct LargeListView {
items: Vec<String>,
scroll_handle: VirtualListScrollHandle,
}
impl Render for LargeListView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let item_count = self.items.len();
VirtualList::new(
self.scroll_handle.clone(),
item_count,
|ix, window, cx| {
// Item sizes - can be different for each item
size(px(300.), px(40.))
},
|ix, bounds, selected, window, cx| {
// Render each item
div()
.size(bounds.size)
.bg(if selected {
cx.theme().accent
} else {
cx.theme().background
})
.child(format!("Item {}: {}", ix, self.items[ix]))
.into_any_element()
},
)
}
}SlintNo Slint version of this example yet.
#Scrolling to Specific Items
impl LargeListView {
fn scroll_to_item(&mut self, index: usize) {
self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top);
}
fn scroll_to_item_centered(&mut self, index: usize) {
self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center);
}
}SlintNo Slint version of this example yet.
#Variable Item Sizes
VirtualList::new(
scroll_handle,
items.len(),
|ix, window, cx| {
// Different heights based on content
let height = if items[ix].len() > 50 {
px(80.) // Tall items for long content
} else {
px(40.) // Normal height
};
size(px(300.), height)
},
|ix, bounds, selected, window, cx| {
// Render logic
},
)SlintNo Slint version of this example yet.
#Theme Customization
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The bar reads the theme's tokens when it is drawn, so these are choices of
// *which* token, not of colour: a track from the border, a thumb from the
// foreground, a thumb from the primary, and the two show modes iced can
// express. iced has no per-component theme JSON to write, which is what the
// GPUI page configures instead.
let themed = column![
row![
sample(
"Muted thumb",
Scrollable::new(rows(1, 20))
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
sample(
"Track and foreground",
Scrollable::new(rows(1, 20))
.track(Ink::Border)
.thumb(Ink::Foreground)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
sample(
"Primary thumb",
Scrollable::new(rows(1, 20))
.thumb(Ink::Primary)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
]
.spacing(theme::SPACING_LG),
row![
sample(
"On hover",
Scrollable::new(rows(1, 20))
.visibility(Visibility::Hover)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
sample(
"Hidden",
Scrollable::new(rows(1, 20))
.visibility(Visibility::Hidden)
.width(Length::Fixed(200.0))
.height(Length::Fixed(160.0)),
),
]
.spacing(theme::SPACING_LG),
]
.spacing(theme::SPACING_MD);#Scrollbar Appearance
Customize scrollbar appearance through theme configuration:
// In your theme JSON
{
"scrollbar.background": "#ffffff20",
"scrollbar.thumb.background": "#00000060",
"scrollbar.thumb.hover.background": "#000000"
}SlintNo Slint version of this example yet.
#Scrollbar Show Modes
Control when scrollbars are visible:
use gpui_kit::component::{Theme, scroll::ScrollbarMode};
// In theme initialization
Theme::set_scrollbar_mode(ScrollbarMode::Scrolling, cx); // Only while scrolling
Theme::set_scrollbar_mode(ScrollbarMode::Hover, cx); // On hover
Theme::set_scrollbar_mode(ScrollbarMode::Always, cx); // Always visibleSlintNo Slint version of this example yet.
#System Integration
Sync scrollbar behavior with system preferences:
// Automatically sync with system settings
Theme::sync_scrollbar_appearance(cx);SlintNo Slint version of this example yet.