maycoon_widgets/
switch.rs1use maycoon_core::app::context::AppContext;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout;
5use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, LengthPercentageAuto, StyleNode};
6use maycoon_core::signal::MaybeSignal;
7use maycoon_core::vgi::kurbo::{Circle, Point, Rect, RoundedRect, RoundedRectRadii, Stroke};
8use maycoon_core::vgi::{Brush, Scene};
9use maycoon_core::widget::{Widget, WidgetLayoutExt};
10use maycoon_core::window::{ElementState, MouseButton};
11use maycoon_theme::id::WidgetId;
12use maycoon_theme::theme::Theme;
13use nalgebra::Vector2;
14
15pub struct Switch {
28 layout: MaybeSignal<LayoutStyle>,
29 value: MaybeSignal<bool>,
30 on_change: MaybeSignal<Update>,
31}
32
33impl Switch {
34 #[inline(always)]
38 pub fn new(value: impl Into<MaybeSignal<bool>>) -> Self {
39 Self {
40 layout: LayoutStyle {
41 size: Vector2::new(Dimension::length(60.0), Dimension::length(30.0)),
42 margin: layout::Rect::<LengthPercentageAuto> {
43 left: LengthPercentageAuto::length(2.5),
44 right: LengthPercentageAuto::length(2.5),
45 top: LengthPercentageAuto::length(2.5),
46 bottom: LengthPercentageAuto::length(2.5),
47 },
48 ..Default::default()
49 }
50 .into(),
51 value: value.into(),
52 on_change: Update::empty().into(),
53 }
54 }
55
56 #[inline(always)]
58 pub fn with_value(mut self, value: impl Into<MaybeSignal<bool>>) -> Self {
59 self.value = value.into();
60 self
61 }
62
63 #[inline(always)]
65 pub fn with_on_change(mut self, update: impl Into<MaybeSignal<Update>>) -> Self {
66 self.on_change = update.into();
67 self
68 }
69}
70
71impl WidgetLayoutExt for Switch {
72 #[inline(always)]
73 fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
74 self.layout = layout_style.into();
75 }
76}
77
78impl Widget for Switch {
79 fn render(
80 &mut self,
81 scene: &mut dyn Scene,
82 theme: &mut dyn Theme,
83 layout_node: &LayoutNode,
84 _: &AppInfo,
85 _: AppContext,
86 ) {
87 let checked = *self.value.get();
88
89 let color = if let Some(style) = theme.of(self.widget_id()) {
90 if checked {
91 style.get_color("color_checked").unwrap()
92 } else {
93 style.get_color("color_unchecked").unwrap()
94 }
95 } else if checked {
96 theme.defaults().interactive().active()
97 } else {
98 theme.defaults().interactive().inactive()
99 };
100
101 scene.draw_rounded_rect(
102 &Brush::Solid(color),
103 None,
104 Some(&Stroke::new(5.0)),
105 &RoundedRect::from_rect(
106 Rect::new(
107 layout_node.layout.location.x as f64,
108 layout_node.layout.location.y as f64,
109 (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
110 (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
111 ),
112 RoundedRectRadii::from_single_radius(60.0),
113 ),
114 );
115
116 let offset = if checked { 15.0 } else { -15.0 };
117
118 scene.draw_circle(
119 &Brush::Solid(color),
120 None,
121 None,
122 &Circle::new(
123 Point::new(
124 (layout_node.layout.location.x + (layout_node.layout.size.width / 2.0)) as f64
125 + offset,
126 (layout_node.layout.location.y + (layout_node.layout.size.height / 2.0)) as f64,
127 ),
128 10.0,
129 ),
130 );
131 }
132
133 #[inline(always)]
134 fn layout_style(&self) -> StyleNode {
135 StyleNode {
136 style: self.layout.get().clone(),
137 children: Vec::new(),
138 }
139 }
140
141 fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
142 let mut update = Update::empty();
143
144 if let Some(cursor) = info.cursor_pos
145 && layout::intersects(cursor, &layout.layout)
146 {
147 for (_, btn, el) in &info.buttons {
148 if btn == &MouseButton::Left && *el == ElementState::Released {
149 update |= *self.on_change.get();
150 update |= Update::DRAW;
151
152 if let Some(sig) = self.value.as_signal() {
153 let checked = *sig.get();
154 sig.set(!checked);
155 }
156 }
157 }
158 }
159
160 update
161 }
162
163 #[inline(always)]
164 fn widget_id(&self) -> WidgetId {
165 WidgetId::new("maycoon-widgets", "Switch")
166 }
167}