egui Drag Values: Speed, Range & Formatting | Rust GUI Ep 20
Video: egui Drag Values: Speed, Range & Formatting | Rust GUI Ep 20 by Taught by Celeste AI - AI Coding Coach
egui::DragValue::new(&mut value).speed(0.5).range(0.0..=360.0).suffix("°")— a compact numeric input you drag horizontally to modify.
DragValue is the lesser-known sibling of Slider. A small text-like widget showing a number; click and drag it left or right to change the value. Compact, fast to interact with, and great inside grids where horizontal space is limited.
We use it today for a transform editor: position (x, y), rotation, and scale of a rectangle drawn on a canvas. Plus, we wire pointer drag and scroll on the canvas itself, so the rectangle can be moved and rotated by direct manipulation.
What we are building
Two-pane layout. Left sidebar with a 4-row grid of DragValue controls (X, Y, Rotation, Scale) plus a color picker. Central canvas showing a rotatable, scalable rectangle. The canvas itself responds to drag and scroll: left-click drag moves the rectangle, right-click drag rotates it, scroll wheel zooms.
The DragValues and the canvas interactions read and write the same state. Move the rectangle by dragging on the canvas — the X/Y values in the sidebar update. Click into the X field, type a number — the rectangle teleports.
DragValue::new
ui.add(
egui::DragValue::new(&mut self.x)
.speed(1.0)
.range(0.0..=400.0),
);
Builder pattern, just like Slider. Three configurations to know:
.speed(per_pixel)— how much the value changes per pixel of horizontal drag.1.0means each pixel of cursor movement adds 1 unit. Smaller values for fine control (0.01for floats), larger for coarser (5.0for big integers)..range(start..=end)— clamp the value to a range. Without this, dragging keeps changing the value forever..suffix("°")/.prefix("$")— units or prefix.
DragValue also supports .fixed_decimals(n), .min_decimals(n), .max_decimals(n) for floating-point formatting.
Slider vs DragValue
Both edit a numeric field. The trade-off:
- Slider — a visible track with a draggable handle. You see the range. Good for "where am I in this range?" feedback. Takes more horizontal space.
- DragValue — just the number. Drag horizontally on the number itself to change it. More compact. No range visualisation. Good for grids and dense forms.
For tight UIs (transform editors, color components, settings tables), DragValue is often better. For controls where the user benefits from seeing min/max (volume, opacity, brightness), Slider is better.
Drag interaction on the canvas
let (canvas, response) = ui.allocate_exact_size(
size, egui::Sense::click_and_drag(),
);
if response.dragged_by(egui::PointerButton::Primary) {
let delta = response.drag_delta();
self.x = (self.x + delta.x as f64).clamp(0.0, 400.0);
self.y = (self.y + delta.y as f64).clamp(0.0, 300.0);
}
Custom interaction starts with Sense::click_and_drag() when allocating the rectangle. That tells egui to track press, drag, and release events on this region.
response.dragged_by(button) returns true while the user is currently dragging this region with the given mouse button.
response.drag_delta() returns the movement since the last frame — a Vec2 containing the cursor's displacement. We add it to self.x and self.y to track the drag.
The whole interaction is a few lines: enable sensing, check if dragging, read the delta, apply to state. The next frame's render reflects the new state.
Right-click for rotation
if response.dragged_by(egui::PointerButton::Secondary) {
let delta = response.drag_delta();
self.rotation = (self.rotation + delta.x as f64 * 0.5).clamp(0.0, 360.0);
}
Same pattern, different button. Right-click drag (Secondary) on horizontal axis adds to rotation. Multiplying by 0.5 makes the rotation feel less jumpy than 1°-per-pixel.
You can extend this to arbitrary multi-button gestures. Check Primary, Secondary, Middle, the modifier keys via ui.input(|i| i.modifiers). The interaction logic is yours; egui just supplies the events.
Scroll wheel for scale
if response.hovered() {
let scroll = ui.input(|i| i.smooth_scroll_delta.y);
if scroll != 0.0 {
self.scale = (self.scale + scroll as f64 * 0.002).clamp(0.1, 3.0);
}
}
Scroll detection is slightly different. smooth_scroll_delta.y is the vertical scroll amount this frame, accumulated and smoothed across hardware scroll events. Multiply by a small factor (0.002) for a comfortable zoom feel.
response.hovered() is the gate — only react when the cursor is over the canvas, not when the user scrolls anywhere else.
Drawing the rotated rectangle
let corners = [
[-half_w, -half_h],
[half_w, -half_h],
[half_w, half_h],
[-half_w, half_h],
];
let rotated: Vec<egui::Pos2> = corners
.iter()
.map(|[dx, dy]| {
let rx = dx * angle.cos() - dy * angle.sin();
let ry = dx * angle.sin() + dy * angle.cos();
egui::pos2(cx + rx, cy + ry)
})
.collect();
painter.add(egui::Shape::convex_polygon(
rotated.clone(), color, egui::Stroke::NONE,
));
Standard 2D rotation math. Each corner is offset from the centre, rotated by the rotation angle (converted to radians), then translated to the centre's screen position. The four rotated points become a polygon.
If you find yourself rotating things often, factor this into a helper: rotate_point(p, center, angle) returns the rotated Pos2. Easy to test, reusable.
The f64 choice
pub struct MyApp {
x: f64,
y: f64,
rotation: f64,
scale: f64,
color: [f32; 3],
}
x, y, rotation, scale are f64 — DragValue works with both f32 and f64, and using doubles avoids accumulated rounding error from many drag operations. color is [f32; 3] because the color picker requires it.
In real apps, mix and match based on what each field needs. f32 is fine for things drawn directly on screen; f64 is better for transformations and accumulated state.
Running it
cargo run. Drag any of the X / Y / Rotation / Scale numbers in the sidebar — the rectangle moves accordingly. Or grab the rectangle on the canvas with the left mouse button and move it directly. Right-click drag to rotate. Scroll wheel to zoom.
The two interaction methods (drag values + canvas drag) both write to the same state, so they stay in sync. Drag the rectangle to (300, 200) on the canvas, then look at the sidebar — it shows X=300, Y=200.
Common mistakes
Forgetting Sense::click_and_drag(). Without it, the canvas is a passive widget and response.dragged_by always returns false.
Drag delta as absolute position. drag_delta() is the movement this frame, not the cumulative position. Apply it as a delta (add to existing state).
Not clamping after drag. Without .clamp(0.0, 400.0), the rectangle drags off the canvas.
Reading scroll without hovered() check. The user could be scrolling somewhere else; reacting to global scroll is surprising.
What's next
Next episode: keyboard input. Arrow keys, modifier keys, character events — the ctx.input(|i| i.key_pressed(...)) pattern. We will build a small game-like demo where a sprite moves in response to keys.
Recap
egui::DragValue::new(&mut field).speed(...).range(...).suffix(...) for compact numeric input. Sense::click_and_drag() plus response.drag_delta() for canvas interaction. Combine state-bound widgets with direct manipulation; both write the same fields, both stay in sync.
Next episode: keyboard input. See you in the next one.