Skip to content

Commit

Permalink
Add toggle button (#154)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jrmoulton authored Nov 8, 2023
1 parent a32abd4 commit 0d9e05f
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 10 deletions.
7 changes: 4 additions & 3 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -1052,6 +1052,7 @@ define_builtin_props!(
Cursor cursor nocb: Option<CursorStyle> {} = None,
TextColor color nocb: Option<Color> { inherited } = None,
Background background nocb: Option<Color> {} = None,
Foreground foreground nocb: Option<Color> {} = None,
BoxShadowProp box_shadow nocb: Option<BoxShadow> {} = None,
FontSize font_size nocb: Option<f32> { inherited } = None,
FontFamily font_family nocb: Option<String> { inherited } = None,
Expand Down
16 changes: 13 additions & 3 deletions src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
);
Expand All @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion src/views/clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/views/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.),
}
}
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 11 additions & 1 deletion src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
235 changes: 235 additions & 0 deletions src/widgets/toggle_button.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn Fn(bool)>>,
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<dyn std::any::Any>) {
if let Ok(state) = state.downcast::<bool>() {
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<kurbo::Rect> {
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
}
}

0 comments on commit 0d9e05f

Please sign in to comment.