use std::sync::Arc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, Key, MouseButton};
use crate::geometry::{Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::{paint_subtree, Widget};
use crate::widgets::label::{Label, LabelAlign};
fn format_value(value: f64, decimals: usize) -> String {
format!("{:.prec$}", value, prec = decimals)
}
const WIDGET_H: f64 = 24.0;
const ARROW_MARGIN: f64 = 8.0;
const DRAG_THRESHOLD: f64 = 3.0;
pub struct DragValue {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
value: f64,
min: f64,
max: f64,
speed: f64,
step: f64,
decimals: usize,
font: Arc<Font>,
font_size: f64,
dragging: bool,
mouse_pressed: bool,
press_x: f64,
drag_start_x: f64,
drag_start_value: f64,
focused: bool,
editing: bool,
edit_text: String,
edit_cursor: usize,
hovered: bool,
on_change: Option<Box<dyn FnMut(f64)>>,
value_label: Label,
}
impl DragValue {
pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
let clamped = value.clamp(min, max);
let initial_text = format_value(clamped, 2);
let value_label = Label::new(initial_text, Arc::clone(&font))
.with_font_size(13.0)
.with_align(LabelAlign::Center);
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
value: clamped,
min,
max,
speed: 1.0,
step: 0.0,
decimals: 2,
font,
font_size: 13.0,
dragging: false,
mouse_pressed: false,
press_x: 0.0,
drag_start_x: 0.0,
drag_start_value: 0.0,
focused: false,
editing: false,
edit_text: String::new(),
edit_cursor: 0,
hovered: false,
on_change: None,
value_label,
}
}
pub fn with_font_size(mut self, s: f64) -> Self {
self.font_size = s;
self.value_label.set_font_size(s);
self
}
pub fn with_step(mut self, step: f64) -> Self {
self.step = step;
self
}
pub fn with_speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
pub fn with_decimals(mut self, d: usize) -> Self {
self.decimals = d;
self.sync_label();
self
}
pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
self.on_change = Some(Box::new(cb));
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
pub fn with_min_size(mut self, s: Size) -> Self {
self.base.min_size = s;
self
}
pub fn with_max_size(mut self, s: Size) -> Self {
self.base.max_size = s;
self
}
pub fn value(&self) -> f64 {
self.value
}
fn format_value(&self) -> String {
format_value(self.value, self.decimals)
}
fn sync_label(&mut self) {
self.value_label.set_text(self.format_value());
}
fn apply_step_and_clamp(&self, raw: f64) -> f64 {
let snapped = if self.step > 0.0 {
(raw / self.step).round() * self.step
} else {
raw
};
snapped.clamp(self.min, self.max)
}
fn update_from_drag(&mut self, current_x: f64) {
let delta = (current_x - self.drag_start_x) * self.speed;
let raw = self.drag_start_value + delta;
self.value = self.apply_step_and_clamp(raw);
self.sync_label();
let v = self.value;
if let Some(cb) = self.on_change.as_mut() {
cb(v);
}
}
fn enter_edit_mode(&mut self) {
self.editing = true;
self.edit_text = self.format_value();
self.edit_cursor = self.edit_text.chars().count();
}
fn commit_edit(&mut self) {
self.editing = false;
if let Ok(raw) = self.edit_text.trim().parse::<f64>() {
self.value = self.apply_step_and_clamp(raw);
}
self.sync_label();
let v = self.value;
if let Some(cb) = self.on_change.as_mut() {
cb(v);
}
}
fn cancel_edit(&mut self) {
self.editing = false;
self.sync_label();
}
fn cursor_byte_offset(&self, char_idx: usize) -> usize {
self.edit_text
.char_indices()
.nth(char_idx)
.map(|(b, _)| b)
.unwrap_or(self.edit_text.len())
}
}
impl Widget for DragValue {
fn type_name(&self) -> &'static str {
"DragValue"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn is_focusable(&self) -> bool {
true
}
fn margin(&self) -> Insets {
self.base.margin
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.base.min_size
}
fn max_size(&self) -> Size {
self.base.max_size
}
fn layout(&mut self, available: Size) -> Size {
Size::new(available.width, WIDGET_H)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let v = ctx.visuals();
let w = self.bounds.width;
let h = self.bounds.height;
let a = v.accent;
if self.editing {
let bg = Color::rgba(a.r, a.g, a.b, 0.10);
ctx.set_fill_color(bg);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
ctx.fill();
ctx.set_stroke_color(Color::rgba(a.r, a.g, a.b, 0.80));
ctx.set_line_width(1.5);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
ctx.stroke();
self.value_label.set_text(self.edit_text.clone());
let avail_w = (w - 8.0).max(1.0);
let lsz = self.value_label.layout(Size::new(avail_w, h));
let lx = (w - lsz.width) * 0.5;
let ly = (h - lsz.height) * 0.5;
self.value_label
.set_bounds(Rect::new(0.0, 0.0, lsz.width, lsz.height));
ctx.save();
ctx.translate(lx, ly);
paint_subtree(&mut self.value_label, ctx);
ctx.restore();
let prefix: String = self.edit_text.chars().take(self.edit_cursor).collect();
ctx.set_font(Arc::clone(&self.font));
ctx.set_font_size(self.font_size);
let prefix_w = ctx.measure_text(&prefix).map(|m| m.width).unwrap_or(0.0);
let text_x = lx
+ (lsz.width
- ctx
.measure_text(&self.edit_text)
.map(|m| m.width)
.unwrap_or(lsz.width))
* 0.5;
let cursor_x = text_x + prefix_w;
ctx.set_fill_color(Color::rgba(
v.text_color.r,
v.text_color.g,
v.text_color.b,
0.85,
));
ctx.begin_path();
ctx.rect(cursor_x, ly + 2.0, 1.5, lsz.height - 4.0);
ctx.fill();
} else {
let bg = if self.dragging {
Color::rgba(a.r, a.g, a.b, 0.22)
} else if self.hovered {
Color::rgba(a.r, a.g, a.b, 0.14)
} else {
Color::rgba(a.r, a.g, a.b, 0.08)
};
let border = Color::rgba(a.r, a.g, a.b, 0.35);
let arrow = Color::rgba(a.r, a.g, a.b, 0.45);
ctx.set_fill_color(bg);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
ctx.fill();
ctx.set_stroke_color(border);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
ctx.stroke();
let mid = h * 0.5;
let tri_half = 4.0;
let tri_w = 6.0;
ctx.set_fill_color(arrow);
ctx.begin_path();
ctx.move_to(ARROW_MARGIN, mid);
ctx.line_to(ARROW_MARGIN + tri_w, mid - tri_half);
ctx.line_to(ARROW_MARGIN + tri_w, mid + tri_half);
ctx.close_path();
ctx.fill();
ctx.begin_path();
ctx.move_to(w - ARROW_MARGIN, mid);
ctx.line_to(w - ARROW_MARGIN - tri_w, mid - tri_half);
ctx.line_to(w - ARROW_MARGIN - tri_w, mid + tri_half);
ctx.close_path();
ctx.fill();
let avail_w = (w - (ARROW_MARGIN + tri_w + 4.0) * 2.0).max(1.0);
let lsz = self.value_label.layout(Size::new(avail_w, h));
let lx = (w - lsz.width) * 0.5;
let ly = (h - lsz.height) * 0.5;
self.value_label
.set_bounds(Rect::new(0.0, 0.0, lsz.width, lsz.height));
ctx.save();
ctx.translate(lx, ly);
paint_subtree(&mut self.value_label, ctx);
ctx.restore();
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::KeyDown { key, .. } if self.editing => {
match key {
Key::Char(c) => {
if c.is_ascii_digit() || *c == '.' || (*c == '-' && self.edit_cursor == 0) {
let byte = self.cursor_byte_offset(self.edit_cursor);
self.edit_text.insert(byte, *c);
self.edit_cursor += 1;
}
}
Key::Backspace => {
if self.edit_cursor > 0 {
self.edit_cursor -= 1;
let byte = self.cursor_byte_offset(self.edit_cursor);
self.edit_text.remove(byte);
}
}
Key::Delete => {
let n = self.edit_text.chars().count();
if self.edit_cursor < n {
let byte = self.cursor_byte_offset(self.edit_cursor);
self.edit_text.remove(byte);
}
}
Key::ArrowLeft => {
if self.edit_cursor > 0 {
self.edit_cursor -= 1;
}
}
Key::ArrowRight => {
let n = self.edit_text.chars().count();
if self.edit_cursor < n {
self.edit_cursor += 1;
}
}
Key::Enter => {
self.commit_edit();
}
Key::Escape => {
self.cancel_edit();
}
_ => {}
}
crate::animation::request_draw();
EventResult::Consumed
}
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
if self.mouse_pressed && !self.editing {
let dx = (pos.x - self.press_x).abs();
if !self.dragging && dx >= DRAG_THRESHOLD {
self.dragging = true;
self.drag_start_x = self.press_x;
self.drag_start_value = self.value;
}
if self.dragging {
self.update_from_drag(pos.x);
crate::animation::request_draw();
return EventResult::Consumed;
}
}
if was != self.hovered {
crate::animation::request_draw();
return EventResult::Consumed;
}
EventResult::Ignored
}
Event::MouseDown {
button: MouseButton::Left,
pos,
..
} => {
if self.editing {
return EventResult::Consumed;
}
self.mouse_pressed = true;
self.dragging = false;
self.press_x = pos.x;
EventResult::Consumed
}
Event::MouseUp {
button: MouseButton::Left,
..
} => {
let was_drag = self.dragging;
let was_pressed = self.mouse_pressed;
self.dragging = false;
self.mouse_pressed = false;
if was_pressed && !was_drag && !self.editing {
self.enter_edit_mode();
crate::animation::request_draw();
} else if was_drag {
crate::animation::request_draw();
}
EventResult::Consumed
}
Event::FocusGained => {
self.focused = true;
crate::animation::request_draw();
EventResult::Ignored
}
Event::FocusLost => {
let was_focused = self.focused;
let was_editing = self.editing;
self.focused = false;
if self.editing {
self.commit_edit();
}
self.dragging = false;
self.mouse_pressed = false;
if was_focused || was_editing {
crate::animation::request_draw();
}
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
}