From 0d9e05f3798ffa1269b35c752794596753f09b48 Mon Sep 17 00:00:00 2001 From: Jared Moulton Date: Wed, 8 Nov 2023 11:07:46 -0700 Subject: [PATCH] Add toggle button (#154) * Add toggle button * Fix up toggle button * Add stlyes for button size / inset * Add follow behavior style and fix logic * Rename behavior enum * Switch toggle button to background, add foreground/pct border rad * Don't repaint background --- src/style.rs | 7 +- src/view.rs | 16 ++- src/views/clip.rs | 7 +- src/views/scroll.rs | 11 +- src/widgets/mod.rs | 12 +- src/widgets/toggle_button.rs | 235 +++++++++++++++++++++++++++++++++++ 6 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 src/widgets/toggle_button.rs diff --git a/src/style.rs b/src/style.rs index df4f472f..a6a55ae8 100644 --- a/src/style.rs +++ b/src/style.rs @@ -459,8 +459,8 @@ macro_rules! prop_extracter { #[allow(dead_code)] $vis fn read_explicit( &mut self, - style: &Style, - fallback: &Style, + style: &$crate::style::Style, + fallback: &$crate::style::Style, now: &std::time::Instant, request_transition: &mut bool ) -> bool { @@ -1032,7 +1032,7 @@ define_builtin_props!( BorderTop border_top: Px {} = Px(0.0), BorderRight border_right: Px {} = Px(0.0), BorderBottom border_bottom: Px {} = Px(0.0), - BorderRadius border_radius: Px {} = Px(0.0), + BorderRadius border_radius: PxPct {} = PxPct::Px(0.0), OutlineColor outline_color: Color {} = Color::TRANSPARENT, Outline outline: Px {} = Px(0.0), BorderColor border_color: Color {} = Color::BLACK, @@ -1052,6 +1052,7 @@ define_builtin_props!( Cursor cursor nocb: Option {} = None, TextColor color nocb: Option { inherited } = None, Background background nocb: Option {} = None, + Foreground foreground nocb: Option {} = None, BoxShadowProp box_shadow nocb: Option {} = None, FontSize font_size nocb: Option { inherited } = None, FontFamily font_family nocb: Option { inherited } = None, diff --git a/src/view.rs b/src/view.rs index 458819ef..559843a5 100644 --- a/src/view.rs +++ b/src/view.rs @@ -248,7 +248,10 @@ pub(crate) fn paint_bg( style: &ViewStyleProps, size: Size, ) { - let radius = style.border_radius().0; + let radius = match style.border_radius() { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => size.min_side() * (pct / 100.), + }; if radius > 0.0 { let rect = size.to_rect(); let width = rect.width(); @@ -306,8 +309,12 @@ pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size } let half = outline / 2.0; let rect = size.to_rect().inflate(half, half); + let border_radius = match style.border_radius() { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => size.min_side() * (pct / 100.), + }; cx.stroke( - &rect.to_rounded_rect(style.border_radius().0 + half), + &rect.to_rounded_rect(border_radius + half), style.outline_color(), outline, ); @@ -323,7 +330,10 @@ pub(crate) fn paint_border(cx: &mut PaintCx, style: &ViewStyleProps, size: Size) if left == top && top == right && right == bottom && bottom == left && left > 0.0 { let half = left / 2.0; let rect = size.to_rect().inflate(-half, -half); - let radius = style.border_radius().0; + let radius = match style.border_radius() { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => size.min_side() * (pct / 100.), + }; if radius > 0.0 { cx.stroke(&rect.to_rounded_rect(radius), border_color, left); } else { diff --git a/src/views/clip.rs b/src/views/clip.rs index 3763d544..a8270353 100644 --- a/src/views/clip.rs +++ b/src/views/clip.rs @@ -41,11 +41,16 @@ impl View for Clip { fn paint(&mut self, cx: &mut crate::context::PaintCx) { cx.save(); let style = cx.get_builtin_style(self.id); - let radius = style.border_radius().0; + let border_radius = style.border_radius(); let size = cx .get_layout(self.id) .map(|layout| Size::new(layout.size.width as f64, layout.size.height as f64)) .unwrap_or_default(); + + let radius = match border_radius { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => size.min_side() * (pct / 100.), + }; if radius > 0.0 { let rect = size.to_rect().to_rounded_rect(radius); cx.clip(&rect); diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 6592d0d0..075388e0 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -330,7 +330,10 @@ impl Scroll { (rect.y1 - rect.y0) / 2. } } else { - style.border_radius().0 + match style.border_radius() { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => rect.size().min_side() * (pct / 100.), + } } }; @@ -389,6 +392,7 @@ impl Scroll { let content_size = self.child_size; let scroll_offset = self.child_viewport.origin().to_vec2(); + // dbg!(viewport_size.height, content_size.height); if viewport_size.height >= content_size.height { return None; } @@ -813,7 +817,10 @@ impl View for Scroll { fn paint(&mut self, cx: &mut crate::context::PaintCx) { cx.save(); let style = cx.get_computed_style(self.id); - let radius = style.get(BorderRadius).0; + let radius = match style.get(BorderRadius) { + crate::unit::PxPct::Px(px) => px, + crate::unit::PxPct::Pct(pct) => self.actual_rect.size().min_side() * (pct / 100.), + }; if radius > 0.0 { let rect = self.actual_rect.to_rounded_rect(radius); cx.clip(&rect); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4d1d8036..046f731b 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -8,12 +8,16 @@ use std::rc::Rc; pub use checkbox::*; +mod toggle_button; +pub use toggle_button::*; + mod button; pub use button::*; use peniko::Color; use crate::{ style::{Background, Style, Transition}, + unit::UnitExt, views::scroll, }; @@ -103,6 +107,7 @@ pub(crate) fn default_theme() -> Theme { }) .apply(focus_style.clone()); + const FONT_SIZE: f32 = 13.0; let theme = Style::new() .class(FocusClass, |_| focus_style) .class(LabeledCheckboxClass, |_| labeled_checkbox_style) @@ -119,7 +124,12 @@ pub(crate) fn default_theme() -> Theme { .class(scroll::Track, |s| { s.hover(|s| s.background(Color::rgba8(166, 166, 166, 30))) }) - .font_size(13.0) + .class(ToggleButtonClass, |s| { + s.height(FONT_SIZE * 1.5) + .aspect_ratio(2.) + .border_radius(100.pct()) + }) + .font_size(FONT_SIZE) .color(Color::BLACK); Theme { diff --git a/src/widgets/toggle_button.rs b/src/widgets/toggle_button.rs new file mode 100644 index 00000000..0dbd6671 --- /dev/null +++ b/src/widgets/toggle_button.rs @@ -0,0 +1,235 @@ +use floem_reactive::create_effect; +use floem_renderer::Renderer; +use kurbo::{Point, Size}; +use winit::keyboard::{Key, NamedKey}; + +use crate::{ + id, prop, prop_extracter, + style::{self, Background, BorderRadius, Foreground}, + style_class, + unit::PxPct, + view::View, + views::Decorators, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ToggleButtonBehavior { + Follow, + Switch, +} + +impl style::StylePropValue for ToggleButtonBehavior {} + +prop!(pub ToggleButtonInset: PxPct {} = PxPct::Px(0.)); +prop!(pub ToggleButtonCircleRad: PxPct {} = PxPct::Pct(95.)); +prop!(pub ToggleButtonSwitch: ToggleButtonBehavior {} = ToggleButtonBehavior::Switch); + +prop_extracter! { + ToggleStyle { + foreground: Foreground, + background: Background, + border_radius: BorderRadius, + inset: ToggleButtonInset, + circle_rad: ToggleButtonCircleRad, + switch_behavior: ToggleButtonSwitch + } +} +style_class!(pub ToggleButtonClass); + +#[derive(PartialEq, Eq)] +enum ToggleState { + Nothing, + Held, + Drag, +} + +pub struct ToggleButton { + id: id::Id, + state: bool, + ontoggle: Option>, + position: f32, + held: ToggleState, + width: f32, + radius: f32, + style: ToggleStyle, +} +pub fn toggle_button(state: impl Fn() -> bool + 'static) -> ToggleButton { + let id = crate::id::Id::next(); + create_effect(move |_| { + let state = state(); + id.update_state(state, false); + }); + + ToggleButton { + id, + state: false, + ontoggle: None, + position: 0.0, + held: ToggleState::Nothing, + width: 0., + radius: 0., + style: Default::default(), + } + .class(ToggleButtonClass) + .keyboard_navigatable() +} + +impl View for ToggleButton { + fn id(&self) -> crate::id::Id { + self.id + } + + fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box) { + if let Ok(state) = state.downcast::() { + if self.held == ToggleState::Nothing { + self.update_restrict_position(true); + } + self.state = *state; + cx.request_layout(self.id()); + } + } + + fn event( + &mut self, + cx: &mut crate::context::EventCx, + _id_path: Option<&[crate::id::Id]>, + event: crate::event::Event, + ) -> bool { + match event { + crate::event::Event::PointerDown(_event) => { + cx.update_active(self.id); + self.held = ToggleState::Held; + } + crate::event::Event::PointerUp(_event) => { + cx.app_state_mut().request_layout(self.id()); + + // if held and pointer up. toggle the position (toggle state drag alrady changed the position) + if self.held == ToggleState::Held { + if self.position > self.width / 2. { + self.position = 0.; + } else { + self.position = self.width; + } + } + // set the state based on the position of the slider + 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() { + ToggleButtonBehavior::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); + } + } + cx.app_state_mut().request_layout(self.id()); + } + ToggleButtonBehavior::Switch => { + if event.pos.x as f32 > self.width / 2. && !self.state { + self.position = self.width; + cx.app_state_mut().request_layout(self.id()); + 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.held = ToggleState::Nothing; + cx.app_state_mut().request_layout(self.id()); + 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); + } + } + } + _ => {} + }; + false + } + + fn compute_layout(&mut self, cx: &mut crate::context::LayoutCx) -> Option { + let layout = cx.get_layout(self.id()).unwrap(); + 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(&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 = cx.get_layout(self.id).unwrap(); + 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 on_toggle(mut self, ontoggle: impl Fn(bool) + 'static) -> Self { + self.ontoggle = Some(Box::new(ontoggle)); + self + } +}