Slider
A slider component for selecting numeric values within a specified range. Supports both single value and range selection modes, horizontal and vertical orientations, custom styling, and step intervals.
#Import
use gpui_kit::component::slider::{Slider, SliderState, SliderEvent, SliderValue};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Slider } from "slider.slint";use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme, font};#Usage
#Volume
struct VolumeControl {
volume_slider: Entity<SliderState>,
volume: f32,
}
impl VolumeControl {
fn new(cx: &mut Context<Self>) -> Self {
let volume_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.step(1.0)
.default_value(50.0)
});
let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| {
match event {
SliderEvent::Change(value) => {
this.volume = value.start();
this.apply_volume_change();
cx.notify();
}
}
});
Self {
volume_slider,
volume: 50.0,
}
}
fn apply_volume_change(&self) {
// Apply volume change to audio system
println!("Volume changed to: {}%", self.volume);
}
}
impl Render for VolumeControl {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
h_flex()
.items_center()
.gap_3()
.child("🔊")
.child(Slider::new(&self.volume_slider).flex_1())
.child(format!("{}%", self.volume as i32))
}
}Slider {
value <=> root.volume;
}
Text {
text: "\{round(root.volume)}%";
color: Theme.muted-foreground;
}// The Slint example's volume: one slider bound to a number, with the number
// printed beside it. The value is the caller's — the slider reports what a
// drag asked for and the application writes it back.
let volume = column![
row![
slider(Slot::Volume, app.volume).width(Length::Fill),
readout(app.volume, "%"),
]
.spacing(12.0)
.align_y(Alignment::Center),
]
.spacing(8.0);#Basic Slider
let slider_state = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.default_value(50.0)
.step(1.0)
});
Slider::new(&slider_state)VerticalLayout {
spacing: 10px;
alignment: start;
Slider {
value: 25;
}
Slider {
value: 50;
}
Slider {
value: 75;
}
}// The value is a plain number, so a slider backs any bounded quantity. Three
// sliders, each with the default 0..=100 range and step of one.
let basic = column![
slider(Slot::BasicTop, app.basic_top),
slider(Slot::BasicMiddle, app.basic_middle),
slider(Slot::BasicBottom, app.basic_bottom),
]
.spacing(10.0);#Slider with Event Handling
struct MyView {
slider_state: Entity<SliderState>,
current_value: f32,
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let slider_state = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.default_value(25.0)
.step(5.0)
});
let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| {
match event {
SliderEvent::Change(value) => {
this.current_value = value.start();
cx.notify();
}
}
});
Self {
slider_state,
current_value: 25.0,
}
}
}
impl Render for MyView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_2()
.child(Slider::new(&self.slider_state))
.child(format!("Value: {}", self.current_value))
}
}SlintNo Slint version of this example yet.
#Range Slider
let range_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.default_value(20.0..80.0) // Range from 20 to 80
.step(1.0)
});
Slider::new(&range_slider)SlintNo Slint version of this example yet.
#Vertical Slider
Slider::new(&slider_state)
.vertical()
.h(px(200.))SlintNo Slint version of this example yet.
#Custom Step Intervals
// Integer steps
let integer_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(10.0)
.step(1.0)
.default_value(5.0)
});
// Decimal steps
let decimal_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(1.0)
.step(0.01)
.default_value(0.5)
});Slider {
value: 20;
step: 10;
}// A step of ten, so a drag snaps to 0, 10, 20 … and the arrow keys move by
// the same interval.
let stepped = column![
slider(Slot::Stepped, app.stepped).step(10.0),
muted_line(&format!(
"Step 10 — the value is {}.",
number_input::shown(app.stepped as f64)
)),
]
.spacing(8.0);#Min/Max Configuration
// Temperature slider
let temp_slider = cx.new(|_| {
SliderState::new()
.min(-10.0)
.max(40.0)
.default_value(20.0)
.step(0.5)
});
// Percentage slider
let percent_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.default_value(75.0)
.step(5.0)
});Slider {
value: 21;
minimum: -10;
maximum: 40;
}// The bounds are the caller's: this one runs from -10 to 40, the temperature
// range the Slint group names.
let ranged = column![
slider(Slot::Ranged, app.ranged)
.minimum(-10.0)
.maximum(40.0),
muted_line(&format!(
"From -10 to 40 at step 1 — the value is {}.",
number_input::shown(app.ranged as f64)
)),
]
.spacing(8.0);#Disabled State
Slider::new(&slider_state)
.disabled(true)Slider {
value: 30;
enabled: false;
}// A control with no message cannot move, which is how iced spells disabled;
// `disabled(true)` also draws the muted track and thumb, which iced cannot
// fade as one subtree.
let disabled = column![
Slider::<Message>::new(30.0).disabled(true),
muted_line("Disabled: the fill and the thumb's edge take a lower weight, and the pointer and the keys do nothing."),
]
.spacing(8.0);#Custom Styling
Slider::new(&slider_state)
.bg(cx.theme().success)
.text_color(cx.theme().success_foreground)
.rounded(px(4.))VerticalLayout {
spacing: 12px;
alignment: start;
Slider {
value: 40;
}
Slider {
value: 65;
}
}// The page's own sample names a fill, a thumb and a corner. Here each part
// is named: the first slider tightens its track to a 2-pixel corner and fills
// it in success over a light success tint, and the second reverses the fill
// in danger and gives the thumb a solid face. Neither takes the primary the
// default sliders above use.
let styled = column![
slider(Slot::StyledTop, app.styled_top)
.track_color(|tokens| theme::alpha(tokens.success, 0.2))
.fill_color(|tokens| tokens.success)
.thumb_color(|tokens| tokens.background)
.thumb_border_color(|tokens| tokens.success)
.radius(2.0),
slider(Slot::StyledBottom, app.styled_bottom)
.track_color(|tokens| theme::alpha(tokens.danger, 0.15))
.fill_color(|tokens| tokens.danger)
.thumb_color(|tokens| tokens.danger)
.thumb_border_color(|tokens| tokens.background)
.reverse(true),
muted_line("Success and danger replace the primary on both track and thumb: the first tightens the track's corner, the second fills from the thumb to the end. Each part is a builder over the active tokens, so both themes keep the colours readable."),
]
.spacing(10.0);#Scale
There have 2 types of scale for the slider:
Linear(default)Logarithmic
The logarithmic scale is useful when the range of values is large and you want to give more precision to smaller values.
let log_slider = cx.new(|_| {
SliderState::new()
.min(1.0) // min must be greater than 0 for log scale
.max(1000.0)
.default_value(10.0)
.step(1.0)
.scale(SliderScale::Logarithmic)
});SlintNo Slint version of this example yet.
In this case:
- If slider at 25%, value will be approximately
5.62. - If slider at 50%, value will be approximately
31.62. - If slider at 75%, value will be approximately
177.83. - If slider at 100%, value will be
1000.0.
#Conversions
// From f32
let single_value: SliderValue = 42.0.into();
// From tuple
let range_value: SliderValue = (10.0, 90.0).into();
// From Range
let range_value: SliderValue = (10.0..90.0).into();SlintNo Slint version of this example yet.
#SliderEvent
| Event | Description |
|---|---|
Change(SliderValue) | Emitted continuously while the slider value is being changed |
Release(SliderValue) | Emitted once when the user releases the slider after interaction |
#Styling
The slider component implements Styled trait and supports:
- Background color for track and thumb
- Text color for thumb
- Border radius
- Size customization
#Color Picker
struct ColorPicker {
hue_slider: Entity<SliderState>,
saturation_slider: Entity<SliderState>,
lightness_slider: Entity<SliderState>,
alpha_slider: Entity<SliderState>,
current_color: Hsla,
}
impl ColorPicker {
fn new(cx: &mut Context<Self>) -> Self {
let hue_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(1.0)
.step(0.01)
.default_value(0.5)
});
let saturation_slider = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(1.0)
.step(0.01)
.default_value(1.0)
});
// Subscribe to all sliders to update color
let subscriptions = [&hue_slider, &saturation_slider /* ... */]
.iter()
.map(|slider| {
cx.subscribe(slider, |this, _, event: &SliderEvent, cx| {
match event {
SliderEvent::Change(_) => {
this.update_color(cx);
}
}
})
})
.collect::<Vec<_>>();
Self {
hue_slider,
saturation_slider,
// ... other fields
}
}
fn update_color(&mut self, cx: &mut Context<Self>) {
let h = self.hue_slider.read(cx).value().start();
let s = self.saturation_slider.read(cx).value().start();
// ... calculate color
self.current_color = hsla(h, s, l, a);
cx.notify();
}
}
impl Render for ColorPicker {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_4()
.child(
v_flex()
.gap_2()
.child("Hue")
.child(Slider::new(&self.hue_slider).vertical().h(px(120.)))
)
.child(
v_flex()
.gap_2()
.child("Saturation")
.child(Slider::new(&self.saturation_slider).vertical().h(px(120.)))
)
// ... other sliders
}
}SlintNo Slint version of this example yet.
#Price Range Filter
struct PriceFilter {
price_range: Entity<SliderState>,
min_price: f32,
max_price: f32,
}
impl PriceFilter {
fn new(cx: &mut Context<Self>) -> Self {
let price_range = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(1000.0)
.step(10.0)
.default_value(100.0..500.0) // Range slider
});
let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| {
match event {
SliderEvent::Change(value) => {
this.min_price = value.start();
this.max_price = value.end();
this.filter_products();
cx.notify();
}
}
});
Self {
price_range,
min_price: 100.0,
max_price: 500.0,
}
}
fn filter_products(&self) {
println!("Filtering products: ${} - ${}", self.min_price, self.max_price);
}
}
impl Render for PriceFilter {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_2()
.child("Price Range")
.child(Slider::new(&self.price_range))
.child(format!("${} - ${}", self.min_price as i32, self.max_price as i32))
}
}SlintNo Slint version of this example yet.
#Temperature Slider with Custom Styling
struct TemperatureControl {
temp_slider: Entity<SliderState>,
temperature: f32,
}
impl Render for TemperatureControl {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let temp_color = if self.temperature < 10.0 {
cx.theme().info // Cold - blue
} else if self.temperature > 25.0 {
cx.theme().destructive // Hot - red
} else {
cx.theme().success // Comfortable - green
};
v_flex()
.gap_3()
.child("Temperature Control")
.child(
Slider::new(&self.temp_slider)
.bg(temp_color)
.text_color(cx.theme().background)
.rounded(px(8.))
)
.child(format!("{}°C", self.temperature as i32))
}
}SlintNo Slint version of this example yet.
#Keyboard Shortcuts
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The four arrows are the control's own, read while the pointer is over it
// because iced reports no focus to a widget: `←`/`↓` and `→`/`↑` take the
// bounded step. `Home`, `End`, `Page Up` and `Page Down` are the
// application's, wired through the subscription above, and `Tab`/`Shift+Tab`
// are iced's focus traversal.
let keyboard = column![
slider(Slot::Keyboard, app.keyboard),
muted_line(&format!(
"← ↓ → ↑ step, Home and End jump, Page Up and Page Down move by ten — the value is {}.",
number_input::shown(app.keyboard as f64)
)),
muted_line("iced reports no focus to the application, so the subscription acts on this slider whatever is focused."),
]
.spacing(8.0);| Key | Action |
|---|---|
← / ↓ | Decrease value by step |
→ / ↑ | Increase value by step |
Page Down | Decrease by larger amount |
Page Up | Increase by larger amount |
Home | Set to minimum value |
End | Set to maximum value |
Tab | Move focus to next element |
Shift + Tab | Move focus to previous element |