use floem_reactive::{create_effect, SignalGet, SignalUpdate};
use floem_winit::keyboard::{Key, NamedKey};
use peniko::kurbo::{Point, Size};
use peniko::Brush;
use crate::{
event::EventPropagation,
id::ViewId,
prop, prop_extractor,
style::{self, Foreground, Style},
style_class,
unit::PxPct,
view::View,
views::Decorators,
Renderer,
};
#[derive(Debug, Clone, PartialEq)]
pub enum ToggleHandleBehavior {
Follow,
Snap,
}
impl style::StylePropValue for ToggleHandleBehavior {}
prop!(pub ToggleButtonInset: PxPct {} = PxPct::Px(0.));
prop!(pub ToggleButtonCircleRad: PxPct {} = PxPct::Pct(95.));
prop!(pub ToggleButtonBehavior: ToggleHandleBehavior {} = ToggleHandleBehavior::Snap);
prop_extractor! {
ToggleStyle {
foreground: Foreground,
inset: ToggleButtonInset,
circle_rad: ToggleButtonCircleRad,
switch_behavior: ToggleButtonBehavior
}
}
style_class!(pub ToggleButtonClass);
#[derive(PartialEq, Eq)]
enum ToggleState {
Nothing,
Held,
Drag,
}
pub struct ToggleButton {
id: ViewId,
state: bool,
ontoggle: Option<Box<dyn Fn(bool)>>,
position: f32,
held: ToggleState,
width: f32,
radius: f32,
style: ToggleStyle,
}
pub fn toggle_button(state: impl Fn() -> bool + 'static) -> ToggleButton {
ToggleButton::new(state)
}
impl View for ToggleButton {
fn id(&self) -> ViewId {
self.id
}
fn debug_name(&self) -> std::borrow::Cow<'static, str> {
"Toggle Button".into()
}
fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
if let Ok(state) = state.downcast::<bool>() {
if self.held == ToggleState::Nothing {
self.update_restrict_position(true);
}
self.state = *state;
self.id.request_layout();
}
}
fn event_before_children(
&mut self,
cx: &mut crate::context::EventCx,
event: &crate::event::Event,
) -> EventPropagation {
match event {
crate::event::Event::PointerDown(_event) => {
cx.update_active(self.id);
self.held = ToggleState::Held;
}
crate::event::Event::PointerUp(_event) => {
self.id.request_layout();
if self.held == ToggleState::Held {
if self.position > self.width / 2. {
self.position = 0.;
} else {
self.position = self.width;
}
}
if self.held == ToggleState::Held {
if self.state && self.position < self.width / 2. {
self.state = false;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(false);
}
} else if !self.state && self.position > self.width / 2. {
self.state = true;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(true);
}
}
}
self.held = ToggleState::Nothing;
}
crate::event::Event::PointerMove(event) => {
if self.held == ToggleState::Held || self.held == ToggleState::Drag {
self.held = ToggleState::Drag;
match self.style.switch_behavior() {
ToggleHandleBehavior::Follow => {
self.position = event.pos.x as f32;
if self.position > self.width / 2. && !self.state {
self.state = true;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(true);
}
} else if self.position < self.width / 2. && self.state {
self.state = false;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(false);
}
}
self.id.request_layout();
}
ToggleHandleBehavior::Snap => {
if event.pos.x as f32 > self.width / 2. && !self.state {
self.position = self.width;
self.id.request_layout();
self.state = true;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(true);
}
} else if (event.pos.x as f32) < self.width / 2. && self.state {
self.position = 0.;
self.id.request_layout();
self.state = false;
if let Some(ontoggle) = &self.ontoggle {
ontoggle(false);
}
}
}
}
}
}
crate::event::Event::FocusLost => {
self.held = ToggleState::Nothing;
}
crate::event::Event::KeyDown(event) => {
if event.key.logical_key == Key::Named(NamedKey::Enter) {
if let Some(ontoggle) = &self.ontoggle {
ontoggle(!self.state);
}
}
}
_ => {}
};
EventPropagation::Continue
}
fn compute_layout(
&mut self,
_cx: &mut crate::context::ComputeLayoutCx,
) -> Option<peniko::kurbo::Rect> {
let layout = self.id.get_layout().unwrap_or_default();
let size = layout.size;
self.width = size.width;
let circle_radius = match self.style.circle_rad() {
PxPct::Px(px) => px as f32,
PxPct::Pct(pct) => size.width.min(size.height) / 2. * (pct as f32 / 100.),
};
self.radius = circle_radius;
self.update_restrict_position(false);
None
}
fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
if self.style.read(cx) {
cx.app_state_mut().request_paint(self.id);
}
}
fn paint(&mut self, cx: &mut crate::context::PaintCx) {
let layout = self.id.get_layout().unwrap_or_default();
let size = Size::new(layout.size.width as f64, layout.size.height as f64);
let circle_point = Point::new(self.position as f64, size.to_rect().center().y);
let circle = crate::kurbo::Circle::new(circle_point, self.radius as f64);
if let Some(color) = self.style.foreground() {
cx.fill(&circle, &color, 0.);
}
}
}
impl ToggleButton {
fn update_restrict_position(&mut self, end_pos: bool) {
let inset = match self.style.inset() {
PxPct::Px(px) => px as f32,
PxPct::Pct(pct) => (self.width * (pct as f32 / 100.)).min(self.width / 2.),
};
if self.held == ToggleState::Nothing || end_pos {
self.position = if self.state { self.width } else { 0. };
}
self.position = self
.position
.max(self.radius + inset)
.min(self.width - self.radius - inset);
}
pub fn new(state: impl Fn() -> bool + 'static) -> Self {
let id = ViewId::new();
create_effect(move |_| {
let state = state();
id.update_state(state);
});
Self {
id,
state: false,
ontoggle: None,
position: 0.0,
held: ToggleState::Nothing,
width: 0.,
radius: 0.,
style: Default::default(),
}
.class(ToggleButtonClass)
.keyboard_navigable()
}
pub fn new_rw(state: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static) -> Self {
Self::new(move || state.get()).on_toggle(move |ns| state.set(ns))
}
pub fn on_toggle(mut self, ontoggle: impl Fn(bool) + 'static) -> Self {
self.ontoggle = Some(Box::new(ontoggle));
self
}
pub fn toggle_style(
self,
style: impl Fn(ToggleButtonCustomStyle) -> ToggleButtonCustomStyle + 'static,
) -> Self {
self.style(move |s| s.apply_custom(style(Default::default())))
}
}
#[derive(Debug, Default, Clone)]
pub struct ToggleButtonCustomStyle(Style);
impl From<ToggleButtonCustomStyle> for Style {
fn from(value: ToggleButtonCustomStyle) -> Self {
value.0
}
}
impl ToggleButtonCustomStyle {
pub fn new() -> Self {
Self(Style::new())
}
pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
self = Self(self.0.set(Foreground, Some(color.into())));
self
}
pub fn accent_color(mut self, color: impl Into<Brush>) -> Self {
self = Self(self.0.background(color));
self
}
pub fn handle_inset(mut self, inset: impl Into<PxPct>) -> Self {
self = Self(self.0.set(ToggleButtonInset, inset));
self
}
pub fn circle_rad(mut self, rad: impl Into<PxPct>) -> Self {
self = Self(self.0.set(ToggleButtonCircleRad, rad));
self
}
pub fn behavior(mut self, switch: ToggleHandleBehavior) -> Self {
self = Self(self.0.set(ToggleButtonBehavior, switch));
self
}
}