Build Image Filters with egui Sliders | Rust GUI Tutorial #14
Video: Build Image Filters with egui Sliders | Rust GUI Tutorial #14 by Taught by Celeste AI - AI Coding Coach
egui::Slider::new(&mut self.value, range)— continuous numeric input with a draggable handle.
When the user needs to set a numeric value within a range — brightness, volume, opacity, font size — radio buttons and dropdowns are wrong. You want a slider: a draggable handle along a track, with smooth values and immediate feedback. egui's Slider does exactly this in one line.
What we are building
An image-filter control panel. Four sliders for Brightness, Contrast, Saturation, Blur — each with a range and a units suffix. A "Reset All" button that puts everything back to defaults. A central panel with a colored preview rectangle whose color is computed from the current slider values.
The script (slider section)
ui.label("Brightness:");
ui.add(
egui::Slider::new(&mut self.brightness, -100.0..=100.0)
.suffix(" %"),
);
ui.label("Contrast:");
ui.add(
egui::Slider::new(&mut self.contrast, 0.0..=200.0)
.suffix(" %"),
);
ui.label("Saturation:");
ui.add(
egui::Slider::new(&mut self.saturation, 0.0..=200.0)
.suffix(" %"),
);
ui.label("Blur:");
ui.add(
egui::Slider::new(&mut self.blur, 0.0..=20.0)
.suffix(" px"),
);
Each slider is Slider::new(&mut value, range).suffix(" %") wrapped in ui.add(). Drag the handle, the bound f32 updates, the next frame's central panel re-renders with the new value.
Slider::new(&mut value, range)
egui::Slider::new(&mut self.brightness, -100.0..=100.0)
Two arguments:
- A mutable reference to the value the slider edits. Same bound-widget pattern as text edits and checkboxes.
- A range as
Rust RangeInclusive<f32>(the..=syntax). The slider clamps the value to this range — drag past the end and it pegs at the bound.
The slider works for any numeric type that implements egui's Numeric trait — f32, f64, i32, u32, etc. The range type matches the value type.
Builder methods worth knowing
.suffix(" %")— text appended after the value. Units of measure..prefix("$")— text before the value..text("Brightness")— label to the right of the slider (alternative to the separateui.label(...))..step_by(5.0)— snap to multiples of 5. Useful for integer-feeling controls..logarithmic(true)— non-linear distribution; useful for ranges like 1Hz to 20kHz where a linear slider would be unusable..smart_aim(true)— egui's default behaviour that snaps to "round" values when the user drags slowly..clamp_to_range(true)— also default; turn off if you want to allow values outside the range when typed numerically..show_value(false)— hide the numeric readout next to the handle.
For most use cases, Slider::new plus .suffix is enough.
Computing the preview color
fn preview_color(&self) -> egui::Color32 {
let base_r = 100.0_f32;
let base_g = 149.0_f32;
let base_b = 237.0_f32;
let r = base_r + self.brightness;
// ... contrast ...
// ... saturation ...
// ... clamp ...
egui::Color32::from_rgb(r, g, b)
}
A simple piece of color math turns the four slider values into a single output color. The math is illustrative — real image filters apply per-pixel — but it is enough to make the preview rectangle feel responsive.
The point isn't the math; it is that state in, frame out is the whole render path. Move a slider, the next frame's preview color changes. No subscription, no recomputation triggers; it is just code that runs every frame.
Painting the preview
let color = self.preview_color();
let size = egui::vec2(400.0, 300.0);
let (rect, _response) = ui.allocate_exact_size(
size,
egui::Sense::hover(),
);
ui.painter().rect_filled(rect, 0.0, color);
Same custom-paint pattern as Episode 12's color swatches. Reserve a rectangle of the right size, paint into it. The rectangle's fill color is computed from the slider state.
Reset All
if ui.button("Reset All").clicked() {
*self = MyApp::default();
}
A single line resets every slider to its default. *self = MyApp::default() replaces the entire struct. Because all four sliders bind to fields of MyApp, replacing the struct resets all four at once.
This is a Rust pattern that pairs well with egui: when "reset" means "back to defaults," and Default is what you want, replacing *self is concise and correct.
Running it
cargo run. The left panel has four sliders. Drag any of them — the preview rectangle updates immediately. The status line at the bottom shows the current values.
Drag Brightness all the way left — preview goes black. All the way right — bright. Saturation to 0 — preview becomes grayscale. Click "Reset All" — every slider snaps back.
When to use a slider vs. other widgets
- Slider — continuous numeric value within a known range. Brightness, opacity, volume, time, percentages.
- Drag value (Episode 20) — numeric input without a visible track. Better for unbounded or large-range values.
- Spinner / number input — when typing the exact value matters more than dragging.
- Radio buttons / combobox — for discrete choices, not continuous values.
The rule: if dragging feels right, use a slider. If typing feels right, use a drag value or text input.
Common mistakes
Forgetting ..= (inclusive range). 0.0..100.0 is exclusive — the slider can never quite reach 100. Use 0.0..=100.0.
Using mismatched types. Slider::new(&mut self.x, 0..100) where self.x: f32 — the range needs to match the value type.
No suffix for percentages. A slider showing "75" is ambiguous; "75 %" is clear. Always provide units.
Slider for a value that should be discrete. If the variable can only be 1, 2, or 3, use radio buttons. Sliders set up a continuous expectation.
What's next
Next episode: progress bar. A simulated download manager with egui::ProgressBar, animated progress, and a status line that updates as the download "completes."
Recap
egui::Slider::new(&mut value, range).suffix(" %") for a continuous numeric input. Drag the handle, the bound field updates, the next frame's render reflects it. Pair with ui.painter() for live previews. Use .logarithmic(true) for wide-range values, .step_by for snapping.
Next episode: progress bar. See you in the next one.