use std::sync::Arc;
use tessera_ui::{
ComputedData, CursorEventContent, Focus, InputHandlerInput, Px, PxPosition, State,
accesskit::{Action, Role},
winit::window::CursorIcon,
};
use super::{ACCESSIBILITY_STEP, SliderArgs, SliderController, SliderLayout};
pub(super) fn snap_fraction(value: f32, steps: usize) -> f32 {
if steps == 0 {
return value.clamp(0.0, 1.0);
}
let denom = steps as f32 + 1.0;
let step = 1.0 / denom;
(value / step).round().mul_add(step, 0.0).clamp(0.0, 1.0)
}
pub(super) fn cursor_within_component(
cursor_pos: Option<PxPosition>,
computed: &ComputedData,
) -> bool {
if let Some(pos) = cursor_pos {
let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
within_x && within_y
} else {
false
}
}
pub(super) fn cursor_progress(
cursor_pos: Option<PxPosition>,
layout: &SliderLayout,
) -> Option<f32> {
if layout.track_total_width.0 <= 0 {
return None;
}
cursor_pos.map(|pos| {
let cursor_x = pos.x.to_f32();
let half_handle = layout.handle_width.to_f32() / 2.0;
let start_x = layout.handle_gap.to_f32() + half_handle;
let fraction = (cursor_x - start_x) / layout.track_total_width.to_f32();
fraction.clamp(0.0, 1.0)
})
}
fn range_cursor_progress(
cursor_pos: Option<PxPosition>,
layout: &SliderLayout,
start_handle_width: Px,
end_handle_width: Px,
) -> Option<f32> {
let cursor_pos = cursor_pos?;
let component_width = layout.component_width.to_f32();
let gap = layout.handle_gap.to_f32();
let start_half = start_handle_width.to_f32() / 2.0;
let end_half = end_handle_width.to_f32() / 2.0;
let track_total = (component_width - start_half - end_half - gap * 2.0).max(0.0);
if track_total <= 0.0 {
return None;
}
let start_x = gap + start_half;
let fraction = (cursor_pos.x.to_f32() - start_x) / track_total;
Some(fraction.clamp(0.0, 1.0))
}
fn range_handle_center_x(
layout: &SliderLayout,
value: f32,
start_handle_width: Px,
end_handle_width: Px,
) -> f32 {
let component_width = layout.component_width.to_f32();
let gap = layout.handle_gap.to_f32();
let start_half = start_handle_width.to_f32() / 2.0;
let end_half = end_handle_width.to_f32() / 2.0;
let track_total = (component_width - start_half - end_half - gap * 2.0).max(0.0);
let start_x = gap + start_half;
let raw = start_x + value.clamp(0.0, 1.0) * track_total;
raw.clamp(start_x, (component_width - gap - end_half).max(start_x))
}
pub(super) fn handle_slider_state(
input: &mut InputHandlerInput,
state: State<SliderController>,
args: &SliderArgs,
layout: &SliderLayout,
) {
if args.disabled {
state.with_mut(|inner| {
inner.is_hovered = false;
inner.is_dragging = false;
});
return;
}
let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
state.with_mut(|inner| {
inner.is_hovered = is_in_component;
});
if is_in_component {
input.requests.cursor_icon = CursorIcon::Pointer;
}
if !is_in_component && !state.with(|s| s.is_dragging) {
return;
}
let mut new_value: Option<f32> = None;
handle_cursor_events(input, state, &mut new_value, layout, args.steps);
update_value_on_drag(input, state, &mut new_value, layout, args.steps);
notify_on_change(new_value, args);
}
fn handle_cursor_events(
input: &mut InputHandlerInput,
state: State<SliderController>,
new_value: &mut Option<f32>,
layout: &SliderLayout,
steps: usize,
) {
for event in input.cursor_events.iter() {
match &event.content {
CursorEventContent::Pressed(_) => {
state.with_mut(|inner| {
inner.focus.request_focus();
inner.is_dragging = true;
});
if let Some(v) = cursor_progress(input.cursor_position_rel, layout) {
*new_value = Some(snap_fraction(v, steps));
}
}
CursorEventContent::Released(_) => {
state.with_mut(|s| s.is_dragging = false);
}
_ => {}
}
}
}
fn update_value_on_drag(
input: &InputHandlerInput,
state: State<SliderController>,
new_value: &mut Option<f32>,
layout: &SliderLayout,
steps: usize,
) {
if state.with(|s| s.is_dragging)
&& let Some(v) = cursor_progress(input.cursor_position_rel, layout)
{
*new_value = Some(snap_fraction(v, steps));
}
}
fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
if let Some(v) = new_value
&& (v - args.value).abs() > f32::EPSILON
{
(args.on_change)(v);
}
}
pub(super) fn apply_slider_accessibility(
input: &mut InputHandlerInput<'_>,
args: &SliderArgs,
current_value: f32,
on_change: &Arc<dyn Fn(f32) + Send + Sync>,
) {
let mut builder = input.accessibility().role(Role::Slider);
if let Some(label) = args.accessibility_label.as_ref() {
builder = builder.label(label.clone());
}
if let Some(description) = args.accessibility_description.as_ref() {
builder = builder.description(description.clone());
}
builder = builder
.numeric_value(current_value as f64)
.numeric_range(0.0, 1.0);
if args.disabled {
builder = builder.disabled();
} else {
builder = builder
.focusable()
.action(Action::Increment)
.action(Action::Decrement);
}
builder.commit();
if args.disabled {
return;
}
let on_change = on_change.clone();
let steps = args.steps;
input.set_accessibility_action_handler(move |action| {
let delta = if steps == 0 {
ACCESSIBILITY_STEP
} else {
1.0 / (steps as f32 + 1.0)
};
let new_value = match action {
Action::Increment => Some(snap_fraction(current_value + delta, steps)),
Action::Decrement => Some(snap_fraction(current_value - delta, steps)),
_ => None,
};
if let Some(new_value) = new_value
&& (new_value - current_value).abs() > f32::EPSILON
{
on_change(new_value);
}
});
}
pub struct RangeSliderController {
pub(crate) is_hovered: bool,
pub(crate) is_dragging_start: bool,
pub(crate) is_dragging_end: bool,
pub(crate) focus_start: Focus,
pub(crate) focus_end: Focus,
}
impl Default for RangeSliderController {
fn default() -> Self {
Self::new()
}
}
impl RangeSliderController {
pub fn new() -> Self {
Self {
is_hovered: false,
is_dragging_start: false,
is_dragging_end: false,
focus_start: Focus::new(),
focus_end: Focus::new(),
}
}
}
pub(super) fn handle_range_slider_state(
input: &mut InputHandlerInput,
state: &State<RangeSliderController>,
args: &super::RangeSliderArgs,
layout: &SliderLayout,
start_handle_width: Px,
end_handle_width: Px,
) {
if args.disabled {
state.with_mut(|inner| {
inner.is_hovered = false;
inner.is_dragging_start = false;
inner.is_dragging_end = false;
});
return;
}
let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
state.with_mut(|inner| {
inner.is_hovered = is_in_component;
});
if is_in_component {
input.requests.cursor_icon = CursorIcon::Pointer;
}
let is_dragging = state.with(|s| s.is_dragging_start || s.is_dragging_end);
if !is_in_component && !is_dragging {
return;
}
let mut new_start: Option<f32> = None;
let mut new_end: Option<f32> = None;
for event in input.cursor_events.iter() {
match &event.content {
CursorEventContent::Pressed(_) => {
if let Some(progress) = range_cursor_progress(
input.cursor_position_rel,
layout,
start_handle_width,
end_handle_width,
) {
let progress = snap_fraction(progress, args.steps);
let start_value = args.value.0.clamp(0.0, 1.0);
let end_value = args.value.1.clamp(start_value, 1.0);
let cursor_x = input.cursor_position_rel.map(|pos| pos.x.to_f32());
let start_center_x = range_handle_center_x(
layout,
start_value,
start_handle_width,
end_handle_width,
);
let end_center_x = range_handle_center_x(
layout,
end_value,
start_handle_width,
end_handle_width,
);
let dist_start = cursor_x.map(|x| (x - start_center_x).abs());
let dist_end = cursor_x.map(|x| (x - end_center_x).abs());
let drag_start =
dist_start.unwrap_or(f32::INFINITY) <= dist_end.unwrap_or(f32::INFINITY);
state.with_mut(|inner| {
if drag_start {
inner.is_dragging_start = true;
inner.focus_start.request_focus();
} else {
inner.is_dragging_end = true;
inner.focus_end.request_focus();
}
});
if drag_start {
new_start = Some(progress);
} else {
new_end = Some(progress);
}
}
}
CursorEventContent::Released(_) => {
state.with_mut(|inner| {
inner.is_dragging_start = false;
inner.is_dragging_end = false;
});
}
_ => {}
}
}
if let Some(progress) = range_cursor_progress(
input.cursor_position_rel,
layout,
start_handle_width,
end_handle_width,
) {
let progress = snap_fraction(progress, args.steps);
state.with(|s| {
if s.is_dragging_start {
new_start = Some(progress.min(args.value.1)); } else if s.is_dragging_end {
new_end = Some(progress.max(args.value.0)); }
});
}
if let Some(ns) = new_start
&& (ns - args.value.0).abs() > f32::EPSILON
{
(args.on_change)((ns, args.value.1));
}
if let Some(ne) = new_end
&& (ne - args.value.1).abs() > f32::EPSILON
{
(args.on_change)((args.value.0, ne));
}
}
pub(super) fn apply_range_slider_accessibility(
input: &mut InputHandlerInput<'_>,
args: &super::RangeSliderArgs,
_current_start: f32,
_current_end: f32,
_on_change: &Arc<dyn Fn((f32, f32)) + Send + Sync>,
) {
let mut builder = input.accessibility().hidden();
if args.disabled {
builder = builder.disabled();
}
builder.commit();
}