use std::sync::Arc;
use tessera_ui::{
ComputedData, CursorEventContent, GestureState, PressKeyEventType, PxPosition, PxSize, State,
accesskit::{self, Action, Toggled},
tessera,
winit::window::CursorIcon,
};
use crate::{pos_misc::is_position_in_rect, theme::MaterialAlpha};
#[derive(Clone, Copy, Debug)]
pub struct PointerEventContext {
pub normalized_pos: [f32; 2],
pub size: PxSize,
}
type PressCallback = Arc<dyn Fn(PointerEventContext) + Send + Sync>;
#[derive(Clone)]
pub struct ClickableArgs {
pub on_click: Arc<dyn Fn() + Send + Sync>,
pub enabled: bool,
pub block_input: bool,
pub on_press: Option<PressCallback>,
pub on_release: Option<PressCallback>,
pub role: Option<accesskit::Role>,
pub label: Option<String>,
pub description: Option<String>,
pub interaction_state: Option<State<InteractionState>>,
}
impl ClickableArgs {
pub fn new(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
Self {
on_click,
enabled: true,
block_input: true,
on_press: None,
on_release: None,
role: None,
label: None,
description: None,
interaction_state: None,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn block_input(mut self, block_input: bool) -> Self {
self.block_input = block_input;
self
}
pub fn on_press(mut self, on_press: PressCallback) -> Self {
self.on_press = Some(on_press);
self
}
pub fn on_release(mut self, on_release: PressCallback) -> Self {
self.on_release = Some(on_release);
self
}
pub fn role(mut self, role: accesskit::Role) -> Self {
self.role = Some(role);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn interaction_state(mut self, state: State<InteractionState>) -> Self {
self.interaction_state = Some(state);
self
}
}
#[derive(Clone)]
pub struct ToggleableArgs {
pub value: bool,
pub on_value_change: Arc<dyn Fn(bool) + Send + Sync>,
pub enabled: bool,
pub role: Option<accesskit::Role>,
pub label: Option<String>,
pub description: Option<String>,
pub interaction_state: Option<State<InteractionState>>,
pub on_press: Option<PressCallback>,
pub on_release: Option<PressCallback>,
}
impl ToggleableArgs {
pub fn new(value: bool, on_value_change: Arc<dyn Fn(bool) + Send + Sync>) -> Self {
Self {
value,
on_value_change,
enabled: true,
role: None,
label: None,
description: None,
interaction_state: None,
on_press: None,
on_release: None,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn role(mut self, role: accesskit::Role) -> Self {
self.role = Some(role);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn interaction_state(mut self, state: State<InteractionState>) -> Self {
self.interaction_state = Some(state);
self
}
pub fn on_press(mut self, on_press: PressCallback) -> Self {
self.on_press = Some(on_press);
self
}
pub fn on_release(mut self, on_release: PressCallback) -> Self {
self.on_release = Some(on_release);
self
}
}
#[derive(Clone)]
pub struct SelectableArgs {
pub selected: bool,
pub on_click: Arc<dyn Fn() + Send + Sync>,
pub enabled: bool,
pub role: Option<accesskit::Role>,
pub label: Option<String>,
pub description: Option<String>,
pub interaction_state: Option<State<InteractionState>>,
pub on_press: Option<PressCallback>,
pub on_release: Option<PressCallback>,
}
impl SelectableArgs {
pub fn new(selected: bool, on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
Self {
selected,
on_click,
enabled: true,
role: None,
label: None,
description: None,
interaction_state: None,
on_press: None,
on_release: None,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn role(mut self, role: accesskit::Role) -> Self {
self.role = Some(role);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn interaction_state(mut self, state: State<InteractionState>) -> Self {
self.interaction_state = Some(state);
self
}
pub fn on_press(mut self, on_press: PressCallback) -> Self {
self.on_press = Some(on_press);
self
}
pub fn on_release(mut self, on_release: PressCallback) -> Self {
self.on_release = Some(on_release);
self
}
}
fn pointer_context(position: Option<PxPosition>, size: ComputedData) -> PointerEventContext {
let Some(position) = position else {
return PointerEventContext {
normalized_pos: [0.5, 0.5],
size: PxSize::new(size.width, size.height),
};
};
let width = size.width.to_f32().max(1.0);
let height = size.height.to_f32().max(1.0);
let x = (position.x.to_f32() / width).clamp(0.0, 1.0);
let y = (position.y.to_f32() / height).clamp(0.0, 1.0);
PointerEventContext {
normalized_pos: [x, y],
size: PxSize::new(size.width, size.height),
}
}
#[tessera]
pub(crate) fn modifier_clickable<F>(args: ClickableArgs, child: F)
where
F: FnOnce(),
{
let ClickableArgs {
on_click,
enabled,
block_input,
on_press,
on_release,
role,
label,
description,
interaction_state,
} = args;
child();
let role = role.unwrap_or(accesskit::Role::Button);
input_handler(move |mut input| {
let cursor_events: Vec<_> = input
.cursor_events
.iter()
.filter(|event| {
matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
| CursorEventContent::Released(PressKeyEventType::Left)
)
})
.cloned()
.collect();
let within_bounds = input
.cursor_position_rel
.map(|pos| {
is_position_in_rect(
pos,
PxPosition::ZERO,
input.computed_data.width,
input.computed_data.height,
)
})
.unwrap_or(false);
if enabled && within_bounds {
input.requests.cursor_icon = CursorIcon::Pointer;
}
let mut builder = input.accessibility().role(role);
if let Some(label) = label.as_ref() {
builder = builder.label(label.clone());
}
if let Some(description) = description.as_ref() {
builder = builder.description(description.clone());
}
builder = if enabled {
builder.action(Action::Click).focusable()
} else {
builder.disabled()
};
builder.commit();
if enabled {
let on_click_action = on_click.clone();
input.set_accessibility_action_handler(move |action| {
if action == Action::Click {
on_click_action();
}
});
}
let Some(interaction_state) = interaction_state else {
if !enabled {
return;
}
for event in cursor_events.iter() {
if within_bounds
&& event.gesture_state == GestureState::TapCandidate
&& matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
)
{
on_click();
}
}
if block_input && within_bounds {
input.block_all();
}
return;
};
if enabled {
interaction_state.with_mut(|s| s.set_hovered(within_bounds));
} else {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
return;
}
let context = pointer_context(input.cursor_position_rel, input.computed_data);
for event in cursor_events.iter() {
if within_bounds
&& matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
)
{
if let Some(on_press) = on_press.as_ref() {
on_press(context);
}
interaction_state.with_mut(|s| s.set_pressed(true));
}
if matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
) {
interaction_state.with_mut(|s| s.release());
if let Some(on_release) = on_release.as_ref() {
on_release(context);
}
}
if within_bounds
&& event.gesture_state == GestureState::TapCandidate
&& matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
)
{
on_click();
}
}
if !within_bounds {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
}
if block_input && within_bounds {
input.block_all();
}
});
}
#[tessera]
pub(crate) fn modifier_block_touch_propagation<F>(child: F)
where
F: FnOnce(),
{
child();
input_handler(move |mut input| {
let within_bounds = input
.cursor_position_rel
.map(|pos| {
is_position_in_rect(
pos,
PxPosition::ZERO,
input.computed_data.width,
input.computed_data.height,
)
})
.unwrap_or(false);
if within_bounds {
input.block_cursor();
}
});
}
#[tessera]
pub(crate) fn modifier_toggleable<F>(args: ToggleableArgs, child: F)
where
F: FnOnce(),
{
let ToggleableArgs {
value,
on_value_change,
enabled,
role,
label,
description,
interaction_state,
on_press,
on_release,
} = args;
child();
let role = role.unwrap_or(accesskit::Role::CheckBox);
input_handler(move |input| {
let cursor_events: Vec<_> = input
.cursor_events
.iter()
.filter(|event| {
matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
| CursorEventContent::Released(PressKeyEventType::Left)
)
})
.cloned()
.collect();
let within_bounds = input
.cursor_position_rel
.map(|pos| {
is_position_in_rect(
pos,
PxPosition::ZERO,
input.computed_data.width,
input.computed_data.height,
)
})
.unwrap_or(false);
if enabled && within_bounds {
input.requests.cursor_icon = CursorIcon::Pointer;
}
let mut builder = input.accessibility().role(role);
if let Some(label) = label.as_ref() {
builder = builder.label(label.clone());
}
if let Some(description) = description.as_ref() {
builder = builder.description(description.clone());
}
builder = builder.toggled(if value { Toggled::True } else { Toggled::False });
builder = if enabled {
builder.action(Action::Click).focusable()
} else {
builder.disabled()
};
builder.commit();
if enabled {
let on_value_change = on_value_change.clone();
input.set_accessibility_action_handler(move |action| {
if action == Action::Click {
on_value_change(!value);
}
});
}
let Some(interaction_state) = interaction_state else {
return;
};
if enabled {
interaction_state.with_mut(|s| s.set_hovered(within_bounds));
} else {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
return;
}
let context = pointer_context(input.cursor_position_rel, input.computed_data);
for event in cursor_events.iter() {
if within_bounds
&& matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
)
{
if let Some(on_press) = on_press.as_ref() {
on_press(context);
}
interaction_state.with_mut(|s| s.set_pressed(true));
}
if matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
) {
interaction_state.with_mut(|s| s.release());
if let Some(on_release) = on_release.as_ref() {
on_release(context);
}
}
if within_bounds
&& event.gesture_state == GestureState::TapCandidate
&& matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
)
{
on_value_change(!value);
}
}
if !within_bounds {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
}
});
}
#[tessera]
pub(crate) fn modifier_selectable<F>(args: SelectableArgs, child: F)
where
F: FnOnce(),
{
let SelectableArgs {
selected,
on_click,
enabled,
role,
label,
description,
interaction_state,
on_press,
on_release,
} = args;
child();
let role = role.unwrap_or(accesskit::Role::Button);
input_handler(move |input| {
let cursor_events: Vec<_> = input
.cursor_events
.iter()
.filter(|event| {
matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
| CursorEventContent::Released(PressKeyEventType::Left)
)
})
.cloned()
.collect();
let within_bounds = input
.cursor_position_rel
.map(|pos| {
is_position_in_rect(
pos,
PxPosition::ZERO,
input.computed_data.width,
input.computed_data.height,
)
})
.unwrap_or(false);
if enabled && within_bounds {
input.requests.cursor_icon = CursorIcon::Pointer;
}
let mut builder = input.accessibility().role(role);
if let Some(label) = label.as_ref() {
builder = builder.label(label.clone());
}
if let Some(description) = description.as_ref() {
builder = builder.description(description.clone());
}
builder = builder.toggled(if selected {
Toggled::True
} else {
Toggled::False
});
builder = if enabled {
builder.action(Action::Click).focusable()
} else {
builder.disabled()
};
builder.commit();
if enabled {
let on_click = on_click.clone();
input.set_accessibility_action_handler(move |action| {
if action == Action::Click {
on_click();
}
});
}
let Some(interaction_state) = interaction_state else {
return;
};
if enabled {
interaction_state.with_mut(|s| s.set_hovered(within_bounds));
} else {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
return;
}
let context = pointer_context(input.cursor_position_rel, input.computed_data);
for event in cursor_events.iter() {
if within_bounds
&& matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
)
{
if let Some(on_press) = on_press.as_ref() {
on_press(context);
}
interaction_state.with_mut(|s| s.set_pressed(true));
}
if matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
) {
interaction_state.with_mut(|s| s.release());
if let Some(on_release) = on_release.as_ref() {
on_release(context);
}
}
if within_bounds
&& event.gesture_state == GestureState::TapCandidate
&& matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
)
{
on_click();
}
}
if !within_bounds {
interaction_state.with_mut(|s| {
s.release();
s.set_hovered(false);
});
}
});
}
#[derive(Clone, Copy, Debug, Default)]
pub struct InteractionState {
is_hovered: bool,
is_focused: bool,
is_dragged: bool,
is_pressed: bool,
}
impl InteractionState {
pub fn new() -> Self {
Self::default()
}
pub fn release(&mut self) {
self.set_pressed(false);
}
pub fn set_hovered(&mut self, hovered: bool) {
self.is_hovered = hovered;
}
pub fn is_hovered(&self) -> bool {
self.is_hovered
}
pub fn set_focused(&mut self, focused: bool) {
self.is_focused = focused;
}
pub fn is_focused(&self) -> bool {
self.is_focused
}
pub fn set_dragged(&mut self, dragged: bool) {
self.is_dragged = dragged;
}
pub fn is_dragged(&self) -> bool {
self.is_dragged
}
pub fn set_pressed(&mut self, pressed: bool) {
self.is_pressed = pressed;
}
pub fn is_pressed(&self) -> bool {
self.is_pressed
}
pub fn state_layer_alpha(&self) -> f32 {
if self.is_dragged {
MaterialAlpha::DRAGGED
} else if self.is_pressed {
MaterialAlpha::PRESSED
} else if self.is_focused {
MaterialAlpha::FOCUSED
} else if self.is_hovered {
MaterialAlpha::HOVER
} else {
0.0
}
}
}