use crate::layout::subpixel_snap_rect;
use crate::theme::MacTheme;
use egui::{CornerRadius, Key, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget};
use std::ops::RangeInclusive;
pub struct MacStepper<'a> {
value: &'a mut f64,
range: RangeInclusive<f64>,
step: f64,
theme: Option<&'a MacTheme>,
width: Option<f32>,
height: Option<f32>,
corner_radius: Option<f32>,
wraps: bool,
enabled: bool,
}
impl<'a> MacStepper<'a> {
pub fn new(value: &'a mut f64, range: RangeInclusive<f64>, step: f64) -> Self {
Self {
value,
range,
step,
theme: None,
width: None,
height: None,
corner_radius: None,
wraps: false,
enabled: true,
}
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
pub fn corner_radius(mut self, r: f32) -> Self {
self.corner_radius = Some(r);
self
}
pub fn wraps(mut self, wraps: bool) -> Self {
self.wraps = wraps;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn show(self, ui: &mut Ui) -> Response {
ui.add(self)
}
}
impl<'a> Widget for MacStepper<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let fallback_theme;
let theme = match self.theme {
Some(t) => t,
None => {
fallback_theme = MacTheme::default();
&fallback_theme
}
};
let metrics = theme.stepper();
let width = self.width.unwrap_or_else(|| metrics.width.points());
let height = self.height.unwrap_or_else(|| metrics.height.points());
let corner_radius = self
.corner_radius
.unwrap_or_else(|| metrics.corner_radius.points());
let divider_thickness = metrics.divider_thickness.points();
let arrow_size = metrics.arrow_size.points();
let arrow_stroke = metrics.arrow_stroke_width.points();
let (raw_rect, mut response) =
ui.allocate_exact_size(Vec2::new(width, height), Sense::click());
let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
let top_rect = Rect::from_min_max(rect.min, Pos2::new(rect.max.x, rect.center().y));
let bottom_rect = Rect::from_min_max(Pos2::new(rect.min.x, rect.center().y), rect.max);
let top_id = response.id.with("mac_stepper_up");
let bottom_id = response.id.with("mac_stepper_down");
let top_resp = ui.interact(
top_rect,
top_id,
if self.enabled {
Sense::click()
} else {
Sense::hover()
},
);
let bottom_resp = ui.interact(
bottom_rect,
bottom_id,
if self.enabled {
Sense::click()
} else {
Sense::hover()
},
);
let min_val = *self.range.start();
let max_val = *self.range.end();
if *self.value < min_val {
*self.value = min_val;
} else if *self.value > max_val {
*self.value = max_val;
}
let mut changed = false;
let clicked_top = top_resp.clicked();
let clicked_bottom = bottom_resp.clicked();
if clicked_top || clicked_bottom {
response.request_focus();
}
let eps = 1e-9 * self.step.abs().max(1.0);
if self.enabled
&& (clicked_top || (response.has_focus() && ui.input(|i| i.key_pressed(Key::ArrowUp))))
{
let next_val = *self.value + self.step;
if next_val <= max_val + eps {
*self.value = next_val.min(max_val);
} else if self.wraps {
*self.value = min_val;
} else {
*self.value = max_val;
}
changed = true;
}
if self.enabled
&& (clicked_bottom
|| (response.has_focus() && ui.input(|i| i.key_pressed(Key::ArrowDown))))
{
let next_val = *self.value - self.step;
if next_val >= min_val - eps {
*self.value = next_val.max(min_val);
} else if self.wraps {
*self.value = max_val;
} else {
*self.value = min_val;
}
changed = true;
}
if changed {
response.mark_changed();
}
let painter = ui.painter_at(rect);
if self.enabled && response.has_focus() {
let ring = theme.focus_ring();
let focus_rect = rect.expand(ring.offset.points());
let focus_radius = corner_radius + ring.corner_radius_delta.points();
painter.rect_stroke(
focus_rect,
CornerRadius::same(focus_radius as u8),
Stroke::new(ring.stroke_width.points(), theme.keyboard_focus_indicator()),
StrokeKind::Outside,
);
}
let bg_color = theme.control_background();
let border_stroke = if response.has_focus() {
Stroke::new(metrics.border_width.points(), theme.control_accent())
} else {
Stroke::new(metrics.border_width.points(), theme.separator())
};
painter.rect(
rect,
CornerRadius::same(corner_radius as u8),
bg_color,
border_stroke,
StrokeKind::Inside,
);
if self.enabled {
if top_resp.is_pointer_button_down_on() {
painter.rect_filled(
top_rect,
CornerRadius {
nw: corner_radius as u8,
ne: corner_radius as u8,
sw: 0,
se: 0,
},
theme.selected_control_inactive(),
);
} else if top_resp.hovered() {
painter.rect_filled(
top_rect,
CornerRadius {
nw: corner_radius as u8,
ne: corner_radius as u8,
sw: 0,
se: 0,
},
theme.control(),
);
}
if bottom_resp.is_pointer_button_down_on() {
painter.rect_filled(
bottom_rect,
CornerRadius {
nw: 0,
ne: 0,
sw: corner_radius as u8,
se: corner_radius as u8,
},
theme.selected_control_inactive(),
);
} else if bottom_resp.hovered() {
painter.rect_filled(
bottom_rect,
CornerRadius {
nw: 0,
ne: 0,
sw: corner_radius as u8,
se: corner_radius as u8,
},
theme.control(),
);
}
}
let divider_y = rect.center().y;
painter.line_segment(
[
Pos2::new(rect.left(), divider_y),
Pos2::new(rect.right(), divider_y),
],
Stroke::new(divider_thickness, theme.separator()),
);
let arrow_color = if !self.enabled {
theme.placeholder_text()
} else {
theme.label()
};
let arrow_stroke_style = Stroke::new(arrow_stroke, arrow_color);
let arrow_half_w = arrow_size * 0.5;
let arrow_delta = metrics.arrow_delta.points();
let up_cx = top_rect.center().x;
let up_cy = top_rect.center().y;
let up_tip = Pos2::new(up_cx, up_cy - arrow_delta);
let up_left = Pos2::new(up_cx - arrow_half_w, up_cy + arrow_delta);
let up_right = Pos2::new(up_cx + arrow_half_w, up_cy + arrow_delta);
painter.line_segment([up_left, up_tip], arrow_stroke_style);
painter.line_segment([up_tip, up_right], arrow_stroke_style);
let down_cx = bottom_rect.center().x;
let down_cy = bottom_rect.center().y;
let down_tip = Pos2::new(down_cx, down_cy + arrow_delta);
let down_left = Pos2::new(down_cx - arrow_half_w, down_cy - arrow_delta);
let down_right = Pos2::new(down_cx + arrow_half_w, down_cy - arrow_delta);
painter.line_segment([down_left, down_tip], arrow_stroke_style);
painter.line_segment([down_tip, down_right], arrow_stroke_style);
response
}
}