use std::sync::Arc;
use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, MeasurementError,
Modifier, Px, PxPosition, State,
accesskit::Role,
focus_state::Focus,
layout::{LayoutInput, LayoutOutput, LayoutSpec},
remember, tessera,
winit::window::CursorIcon,
};
use crate::{
fluid_glass::{FluidGlassArgs, GlassBorder, fluid_glass},
modifier::{ModifierExt as _, SemanticsArgs},
shape_def::Shape,
};
const ACCESSIBILITY_STEP: f32 = 0.05;
pub struct GlassSliderController {
is_dragging: bool,
focus: Focus,
}
impl GlassSliderController {
pub fn new() -> Self {
Self {
is_dragging: false,
focus: Focus::new(),
}
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn set_dragging(&mut self, dragging: bool) {
self.is_dragging = dragging;
}
pub fn request_focus(&mut self) {
self.focus.request_focus();
}
pub fn clear_focus(&mut self) {
self.focus.unfocus();
}
pub fn is_focused(&self) -> bool {
self.focus.is_focused()
}
}
impl Default for GlassSliderController {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Setters)]
pub struct GlassSliderArgs {
pub value: f32,
pub modifier: Modifier,
#[setters(skip)]
pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
pub track_height: Dp,
pub track_tint_color: Color,
pub progress_tint_color: Color,
pub blur_radius: Dp,
pub track_border_width: Dp,
pub disabled: bool,
#[setters(strip_option, into)]
pub accessibility_label: Option<String>,
#[setters(strip_option, into)]
pub accessibility_description: Option<String>,
}
impl GlassSliderArgs {
pub fn on_change<F>(mut self, on_change: F) -> Self
where
F: Fn(f32) + Send + Sync + 'static,
{
self.on_change = Arc::new(on_change);
self
}
pub fn on_change_shared(mut self, on_change: Arc<dyn Fn(f32) + Send + Sync>) -> Self {
self.on_change = on_change;
self
}
}
impl Default for GlassSliderArgs {
fn default() -> Self {
Self {
value: 0.0,
modifier: default_slider_modifier(),
on_change: Arc::new(|_| {}),
track_height: Dp(12.0),
track_tint_color: Color::new(0.3, 0.3, 0.3, 0.15),
progress_tint_color: Color::new(0.5, 0.7, 1.0, 0.25),
blur_radius: Dp(0.0),
track_border_width: Dp(1.0),
disabled: false,
accessibility_label: None,
accessibility_description: None,
}
}
}
fn default_slider_modifier() -> Modifier {
Modifier::new().width(Dp(200.0))
}
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
}
}
fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
}
fn process_cursor_events(
controller: State<GlassSliderController>,
input: &tessera_ui::InputHandlerInput,
width_f: f32,
) -> Option<f32> {
let mut new_value: Option<f32> = None;
for event in input.cursor_events.iter() {
match &event.content {
CursorEventContent::Pressed(_) => {
controller.with_mut(|c| {
c.request_focus();
c.set_dragging(true);
});
if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
new_value = Some(v);
}
}
CursorEventContent::Released(_) => {
controller.with_mut(|c| c.set_dragging(false));
}
_ => {}
}
}
if controller.with(|c| c.is_dragging())
&& let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
{
new_value = Some(v);
}
new_value
}
#[tessera]
pub fn glass_slider(args: impl Into<GlassSliderArgs>) {
let args: GlassSliderArgs = args.into();
let controller = remember(GlassSliderController::new);
glass_slider_with_controller(args, controller);
}
#[tessera]
fn glass_slider_progress_fill(value: f32, tint_color: Color, blur_radius: Dp) {
fluid_glass(
FluidGlassArgs::default()
.tint_color(tint_color)
.blur_radius(blur_radius)
.shape(Shape::capsule())
.refraction_amount(0.0),
|| {},
);
let clamped = value.clamp(0.0, 1.0);
layout(GlassSliderFillLayout { value: clamped });
}
#[derive(Clone, PartialEq)]
struct GlassSliderFillLayout {
value: f32,
}
impl LayoutSpec for GlassSliderFillLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let available_width = match input.parent_constraint().width() {
DimensionValue::Fixed(px) => px,
DimensionValue::Wrap { max, .. } => max.unwrap_or(Px(0)),
DimensionValue::Fill { max, .. } => max.expect(
"Seems that you are trying to fill an infinite width, which is not allowed",
),
};
let available_height = match input.parent_constraint().height() {
DimensionValue::Fixed(px) => px,
DimensionValue::Wrap { max, .. } => max.unwrap_or(Px(0)),
DimensionValue::Fill { max, .. } => max.expect(
"Seems that you are trying to fill an infinite height, which is not allowed",
),
};
let width_px = Px((available_width.to_f32() * self.value).round() as i32);
let child_id = input
.children_ids()
.first()
.copied()
.expect("progress fill child should exist");
let child_constraint = Constraint::new(
DimensionValue::Fixed(width_px),
DimensionValue::Fixed(available_height),
);
input.measure_child(child_id, &child_constraint)?;
output.place_child(child_id, PxPosition::new(Px(0), Px(0)));
Ok(ComputedData {
width: width_px,
height: available_height,
})
}
}
#[tessera]
pub fn glass_slider_with_controller(
args: impl Into<GlassSliderArgs>,
controller: State<GlassSliderController>,
) {
let args: GlassSliderArgs = args.into();
let mut modifier = args.modifier;
let mut semantics = SemanticsArgs::new().role(Role::Slider);
if let Some(label) = args.accessibility_label.clone() {
semantics = semantics.label(label);
}
if let Some(description) = args.accessibility_description.clone() {
semantics = semantics.description(description);
}
semantics = semantics
.numeric_range(0.0, 1.0)
.numeric_value(args.value as f64)
.numeric_value_step(ACCESSIBILITY_STEP as f64);
semantics = if args.disabled {
semantics.disabled(true)
} else {
semantics.focusable(true)
};
modifier = modifier.semantics(semantics);
modifier.run(move || glass_slider_inner(args, controller));
}
#[tessera]
fn glass_slider_inner(args: GlassSliderArgs, controller: State<GlassSliderController>) {
fluid_glass(
FluidGlassArgs::default()
.modifier(Modifier::new().fill_max_size())
.tint_color(args.track_tint_color)
.blur_radius(args.blur_radius)
.shape(Shape::capsule())
.border(GlassBorder::new(args.track_border_width.into()))
.padding(args.track_border_width),
move || {
glass_slider_progress_fill(args.value, args.progress_tint_color, args.blur_radius);
},
);
let on_change = args.on_change.clone();
let args_for_handler = args.clone();
input_handler(move |input| {
if !args_for_handler.disabled {
let is_in_component =
cursor_within_component(input.cursor_position_rel, &input.computed_data);
if is_in_component {
input.requests.cursor_icon = CursorIcon::Pointer;
}
if is_in_component || controller.with(|c| c.is_dragging()) {
let width_f = input.computed_data.width.0 as f32;
if let Some(v) = process_cursor_events(controller, &input, width_f)
&& (v - args_for_handler.value).abs() > f32::EPSILON
{
on_change(v);
}
}
}
});
let mut semantics = SemanticsArgs::new().role(Role::Slider);
if let Some(label) = args.accessibility_label.clone() {
semantics = semantics.label(label);
}
if let Some(description) = args.accessibility_description.clone() {
semantics = semantics.description(description);
}
semantics = semantics
.numeric_range(0.0, 1.0)
.numeric_value(args.value as f64)
.numeric_value_step(ACCESSIBILITY_STEP as f64);
semantics = if args.disabled {
semantics.disabled(true)
} else {
semantics.focusable(true)
};
let _modifier = Modifier::new().semantics(semantics);
let track_height = args.track_height.to_px();
let fallback_width = Dp(200.0).to_px();
layout(GlassSliderLayout {
track_height,
fallback_width,
});
}
#[derive(Clone, Copy, PartialEq)]
struct GlassSliderLayout {
track_height: Px,
fallback_width: Px,
}
impl LayoutSpec for GlassSliderLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let width_dim = input.parent_constraint().width();
let self_width = match width_dim {
DimensionValue::Fixed(px) => px,
DimensionValue::Wrap { max, .. } => max.unwrap_or(self.fallback_width),
DimensionValue::Fill { max, .. } => max.expect(
"Seems that you are trying to fill an infinite width, which is not allowed",
),
};
let self_height = self.track_height;
let track_id = input.children_ids()[0];
let track_constraint = Constraint::new(
DimensionValue::Fixed(self_width),
DimensionValue::Fixed(self_height),
);
input.measure_child(track_id, &track_constraint)?;
output.place_child(track_id, PxPosition::new(Px(0), Px(0)));
Ok(ComputedData {
width: self_width,
height: self_height,
})
}
}