Developing a Switch View from Scratch
This chapter will guide you through implementing a complete Switch view from scratch. Through this process, you will master all core aspects of view development: defining style enums, implementing the View trait, drawing, measuring, supporting atomic classes, and responding to mouse events.
Switch is an ideal learning case: it has "on/off" states, requires drawing a track and thumb, needs to respond to clicks, and can be customized with atomic classes for colors and sizes. After completing this chapter, you can follow the same pattern to develop your own views.
Final Effect Preview
use flor_lys::switch::switch;
let enabled = create_signal(false);
// Use atomic classes to customize style
switch(enabled).class("switch-track-blue switch-size-md");
// Use style builder to customize style
switch(enabled)
.style(|s| s
.track_color(Color::GREEN)
.thumb_color(Color::WHITE)
);
Step 1: Create File
Create a new file in your view library crate:
flor-lys/crates/flor-lys/src/switch.rs
And register it in lib.rs:
Step 2: Define Style Enum
Use #[derive(Resolver)] to define the style properties supported by Switch. The derive macro will automatically generate helper types like SwitchStyleKey, SwitchStyleResolver, and SwitchStyleComputed.
use flor::macros::Resolver;
use flor::types::Color;
/// Switch style enum
///
/// Each variant corresponds to a configurable style property.
/// #[derive(Resolver)] automatically generates for you:
/// - SwitchStyleKey — Property key enum
/// - SwitchStyleResolver — Resolver type alias
/// - SwitchStyleComputed — Computed style value struct
/// - SwitchStyleResolverExt — Chain method trait
#[derive(Clone, Debug, Resolver)]
pub enum SwitchStyle {
/// Track color (on state)
TrackColor(Color),
/// Track color (off state)
TrackOffColor(Color),
/// Thumb color
ThumbColor(Color),
/// Thumb color (hover state)
ThumbHoverColor(Color),
/// Switch width
Width(f32),
/// Switch height
Height(f32),
/// Corner radius
CornerRadius(f32),
/// Opacity
Opacity(f32),
}
Key Points:
- Each variant carries a concrete type, and the
Resolver macro generates corresponding chain methods for each variant.
- Variant names use
PascalCase, and generated method names automatically convert to snake_case (e.g., TrackColor → .track_color(...)).
- Types in variants are wrapped as
Option<T> in the Computed struct.
Step 3: Define View Struct
use flor::view::ViewId;
use flor::signal::RwSignal;
/// Switch view
///
/// Contains a bool signal to control on/off state.
/// All switches hold their own style resolver.
#[derive(Debug)]
pub struct Switch {
/// View unique ID, framework manages view tree through it
view_id: ViewId,
/// Current on/off state (reactively updated by create_updater)
enabled: bool,
/// On/off state signal reference (for writing)
enabled_signal: RwSignal<bool>,
/// Style resolver
style: SwitchStyleResolver,
}
Key Design:
enabled: bool is a snapshot of the current value, automatically kept in sync by the reactive dependency established through create_updater.
enabled_signal: RwSignal<bool> retains the signal reference for writing new values in on_button_down.
- This split is Flor's recommended pattern: read using the value, write using the signal; when external code modifies the signal,
create_updater automatically triggers on_update_state to update the enabled field.
Step 4: Implement View Trait
4.1 Must Implement Methods
use flor::view::{View, ControlState};
impl View for Switch {
fn view_id(&self) -> ViewId {
// Directly return the view_id in the struct, don't create a new ID here
self.view_id
}
fn tag(&self) -> &str {
// Debug identifier, just return the view name for now
"Switch"
}
}
4.2 on_update_state — Reactive Update
When external code updates styles or states through signals, the framework calls on_update_state. You need to downcast and update internal fields:
use std::any::Any;
impl View for Switch {
// ... view_id, tag ...
fn on_update_state(&mut self, state: Box<dyn Any>) {
// First handle enabled state update (triggered by create_updater)
if let Ok(value) = state.downcast::<bool>() {
self.enabled = *value;
return;
}
// Handle style update (SwitchStyleUpdate is auto-generated by Resolver macro)
if let Ok(update) = state.downcast::<SwitchStyleUpdate>() {
SwitchStyle::update_view(&mut self.style, *update);
}
}
}
Reactive Principle: The constructor uses create_updater to register a bool update callback. When external code modifies the enabled signal (e.g., enabled.set(true)), create_updater detects the change and automatically calls view_id.update_state(Box::new(new_value)), ultimately triggering on_update_state here. You don't need to manually pass signals to the view.
4.3 on_measure — Measuring
on_measure tells the layout system how much space this view needs. For Switch, size is primarily determined by style:
use flor::error::Error;
use flor::render::FlorRenderer;
use flor::taffy::{AvailableSpace, Size, Style};
impl View for Switch {
// ...
fn on_measure(
&mut self,
known_dimensions: Size<Option<f32>>,
_available_space: Size<AvailableSpace>,
_style: &Style,
control_state: ControlState,
_render: &mut FlorRenderer,
) -> Result<Size<f32>, Error> {
let computed = self.style.get_data_borrow(control_state);
// Default size
let width = computed.width.unwrap_or(44.0);
let height = computed.height.unwrap_or(24.0);
Ok(Size {
width: known_dimensions.width.unwrap_or(width),
height: known_dimensions.height.unwrap_or(height),
})
}
}
Key Points:
known_dimensions is the known size constraint passed down from the parent container. If the parent sets fixed width/height, you'll receive Some(...) here.
- Prioritize using
known_dimensions, only use the view's default value when not available.
self.style.get_data_borrow(control_state) gets the computed style for the current view state.
4.4 on_draw — Drawing
This is the most core method in view development. Switch needs to draw a track (background) and thumb (circle):
use flor::base::graphics::RenderContext;
use flor::view::resolver::ComputedLayout;
impl View for Switch {
// ...
fn on_draw(
&mut self,
render: &mut FlorRenderer,
control_state: ControlState,
abs_location: (f32, f32),
layout: ComputedLayout,
) -> Result<(), Error> {
let x = abs_location.0;
let y = abs_location.1;
let w = layout.size.width;
let h = layout.size.height;
let computed = self.style.get_data_borrow(control_state);
let is_on = self.enabled;
// Track color: select based on on/off state
let track_color = if is_on {
computed.track_color.unwrap_or_else(|| {
Color::from_hex_str("#3b82f6").unwrap_or_default()
})
} else {
computed.track_off_color.unwrap_or_else(|| {
Color::from_hex_str("#d1d5db").unwrap_or_default()
})
};
let corner_radius = computed.corner_radius.unwrap_or(h / 2.0);
let opacity = computed.opacity.unwrap_or(1.0);
// Draw track
let track_brush = render.create_solid_color_brush(
track_color.with_alpha((255.0 * opacity) as u8),
None,
)?;
render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?;
// Calculate thumb position
let thumb_size = h - 4.0; // Leave 2px on top and bottom
let thumb_x = if is_on {
x + w - thumb_size - 3.0
} else {
x + 3.0
};
let thumb_y = y + 2.0;
// Thumb color: use hover color when hovering
let thumb_color = if control_state == ControlState::Hover {
computed
.thumb_hover_color
.unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default())
} else {
computed
.thumb_color
.unwrap_or_else(|| Color::WHITE)
};
let thumb_brush = render.create_solid_color_brush(
thumb_color.with_alpha((255.0 * opacity) as u8),
None,
)?;
render.fill_quad(
thumb_x,
thumb_y,
thumb_size,
thumb_size,
&thumb_brush,
Some(thumb_size / 2.0), // Circle
None,
)?;
Ok(())
}
}
Drawing Key Points:
- Use the provided
abs_location (absolute coordinates) and layout.size (layout size), don't re-derive them.
- Read style values from
computed, use unwrap_or to provide default values.
- Draw different states based on
control_state (normal vs hover vs disabled).
- Corner radius value
h / 2.0 creates a "capsule" shape.
- Thumb uses
thumb_size / 2.0 as corner radius to create a circle.
4.5 on_update_class — Atomic Class Support
Allow users to set styles through .class("switch-track-blue"):
use flor::view::resolver::parse_color;
impl View for Switch {
// ...
fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> {
// Switch to the corresponding state style layer
self.style.switch_control_state(control_state);
let class = class.trim();
// 1. Track color (on state): switch-track-{color}
if let Some(rest) = class.strip_prefix("switch-track-") {
if let Some(color) = parse_color(rest) {
self.style.set_track_color(color);
return Ok(());
}
}
// 2. Track color (off state): switch-track-off-{color}
if let Some(rest) = class.strip_prefix("switch-track-off-") {
if let Some(color) = parse_color(rest) {
self.style.set_track_off_color(color);
return Ok(());
}
}
// 3. Thumb color: switch-thumb-{color}
if let Some(rest) = class.strip_prefix("switch-thumb-") {
if let Some(color) = parse_color(rest) {
self.style.set_thumb_color(color);
return Ok(());
}
}
// 4. Size: switch-size-{sm|md|lg}
match class {
"switch-size-sm" => {
self.style.set_width(36.0);
self.style.set_height(20.0);
return Ok(());
}
"switch-size-md" => {
self.style.set_width(44.0);
self.style.set_height(24.0);
return Ok(());
}
"switch-size-lg" => {
self.style.set_width(52.0);
self.style.set_height(28.0);
return Ok(());
}
_ => {}
}
Ok(())
}
}
Atomic Class Parsing Key Points:
- First use
switch_control_state(control_state) to switch to the current state layer, so subsequent set_* calls write to the correct state.
- Use
strip_prefix to match class name prefix, then use shared parsing methods like parse_color.
- After successful parsing, directly
return Ok(()), don't let subsequent rules accidentally match.
- Unrecognized class names are simply ignored,
on_update_class is called multiple times, each time handling only one class name.
Switch toggles state when clicked:
use flor::base::platform::{HandleResult, KeyState, MousePosition};
impl View for Switch {
// ...
fn on_button_down(
&mut self,
key_state: KeyState,
mouse_position: MousePosition,
) -> Result<HandleResult, Error> {
// Toggle switch state: write through signal, triggers create_updater callback
self.enabled_signal.set(!self.enabled);
Ok(HandleResult::Handled)
}
}
You could also use on_click here. The difference is on_click requires down and up to hit the same view, while on_button_down triggers immediately on press. For switches, on_button_down provides a more immediate experience.
Step 5: Constructor and Factory Function
use flor::signal::{RwSignal, create_updater};
use flor::view::resolver::LayoutResolver;
use flor::view::builder::ViewBuilder;
impl Switch {
/// Create switch view
pub fn new(enabled: RwSignal<bool>) -> Self {
// Create ViewId, and create LayoutResolver
let view_id = ViewId::new_with_layout(|view_id| {
LayoutResolver::new(view_id)
});
// Build reactive dependency: when enabled signal changes, automatically call on_update_state
let enabled_value = create_updater(
move || enabled.get(),
move |v: bool| view_id.update_state(Box::new(v)),
);
Self {
view_id,
enabled: enabled_value,
enabled_signal: enabled,
// computed_switch_style is auto-generated by #[derive(Resolver)] macro
style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style),
}
}
}
/// Factory function: create Switch
#[inline]
pub fn switch(enabled: RwSignal<bool>) -> Switch {
Switch::new(enabled)
}
Constructor Key Points:
ViewId::new_with_layout creates a LayoutResolver for the view.
create_updater is key: it receives two closures—the first reads the signal value (establishing reactive tracking), the second triggers on_update_state through view_id.update_state when the value changes. The return value is the signal's current value, stored as enabled: bool.
enabled_signal retains the signal reference for write operations in on_button_down.
computed_switch_style function is auto-generated by the #[derive(Resolver)] macro, you don't need to write it manually.
Step 6: Understanding compute Function (Macro Auto-generated)
The #[derive(Resolver)] macro automatically generates a computed_switch_style function by default, which maps the enum's raw style values to the Computed struct. You don't need to write it manually, the macro has already generated code equivalent to the following:
// Below is auto-generated by #[derive(Resolver)], here only shows the effect
pub fn computed_switch_style(
_unit_resolver: &UnitResolver,
variants: &ResolverComputeMap<SwitchStyleKey, SwitchStyle>,
) -> SwitchStyleComputed {
let mut computed = SwitchStyleComputed::default();
for (k, v) in variants.iter() {
match k {
SwitchStyleKey::TrackColor => {
if let SwitchStyle::TrackColor(val) = v {
computed.track_color = Some(val.clone());
}
}
SwitchStyleKey::TrackOffColor => {
if let SwitchStyle::TrackOffColor(val) = v {
computed.track_off_color = Some(val.clone());
}
}
// ... other variants same
_ => {}
}
}
computed
}
Key Points:
- Macro generated compute function name rule is
computed_{EnumName's snake_case} (e.g., LabelStyle → computed_label_style).
- It only does simple
clone mapping, doesn't handle unit conversion. Unit px conversion is handled internally by Resoled's get_data_borrow.
- If you don't need this function, you can disable it via
#[resolver(computed_fn = false)]. See Resolver Derive Macro documentation.
Complete File Overview
Below is the complete code for switch.rs:
use flor::base::graphics::RenderContext;
use flor::base::platform::{HandleResult, KeyState, MousePosition};
use flor::error::Error;
use flor::macros::Resolver;
use flor::render::FlorRenderer;
use flor::signal::{RwSignal, create_updater};
use flor::taffy::{AvailableSpace, Size, Style};
use flor::types::Color;
use flor::view::builder::ViewBuilder;
use flor::view::resolver::{ComputedLayout, LayoutResolver, parse_color};
use flor::view::{ControlState, View, ViewId};
use std::any::Any;
// ============================================================================
// SwitchStyle style enum
// ============================================================================
#[derive(Clone, Debug, Resolver)]
pub enum SwitchStyle {
TrackColor(Color),
TrackOffColor(Color),
ThumbColor(Color),
ThumbHoverColor(Color),
Width(f32),
Height(f32),
CornerRadius(f32),
Opacity(f32),
}
// ============================================================================
// Switch struct
// ============================================================================
#[derive(Debug)]
pub struct Switch {
view_id: ViewId,
/// Current state value (reactively updated by create_updater)
enabled: bool,
/// Signal reference (for writing)
enabled_signal: RwSignal<bool>,
style: SwitchStyleResolver,
}
// ============================================================================
// View trait implementation
// ============================================================================
impl View for Switch {
fn view_id(&self) -> ViewId {
self.view_id
}
fn tag(&self) -> &str {
"Switch"
}
fn on_update_state(&mut self, state: Box<dyn Any>) {
// Handle enabled state update (triggered by create_updater)
if let Ok(value) = state.downcast::<bool>() {
self.enabled = *value;
return;
}
// Handle style update
if let Ok(update) = state.downcast::<SwitchStyleUpdate>() {
SwitchStyle::update_view(&mut self.style, *update);
}
}
fn on_measure(
&mut self,
known_dimensions: Size<Option<f32>>,
_available_space: Size<AvailableSpace>,
_style: &Style,
control_state: ControlState,
_render: &mut FlorRenderer,
) -> Result<Size<f32>, Error> {
let computed = self.style.get_data_borrow(control_state);
let width = computed.width.unwrap_or(44.0);
let height = computed.height.unwrap_or(24.0);
Ok(Size {
width: known_dimensions.width.unwrap_or(width),
height: known_dimensions.height.unwrap_or(height),
})
}
fn on_draw(
&mut self,
render: &mut FlorRenderer,
control_state: ControlState,
abs_location: (f32, f32),
layout: ComputedLayout,
) -> Result<(), Error> {
let x = abs_location.0;
let y = abs_location.1;
let w = layout.size.width;
let h = layout.size.height;
let computed = self.style.get_data_borrow(control_state);
let is_on = self.enabled;
let track_color = if is_on {
computed
.track_color
.unwrap_or_else(|| Color::from_hex_str("#3b82f6").unwrap_or_default())
} else {
computed
.track_off_color
.unwrap_or_else(|| Color::from_hex_str("#d1d5db").unwrap_or_default())
};
let corner_radius = computed.corner_radius.unwrap_or(h / 2.0);
let opacity = computed.opacity.unwrap_or(1.0);
let track_brush = render.create_solid_color_brush(
track_color.with_alpha((255.0 * opacity) as u8),
None,
)?;
render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?;
let thumb_size = h - 4.0;
let thumb_x = if is_on {
x + w - thumb_size - 3.0
} else {
x + 3.0
};
let thumb_y = y + 2.0;
let thumb_color = if control_state == ControlState::Hover {
computed
.thumb_hover_color
.unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default())
} else {
computed.thumb_color.unwrap_or(Color::WHITE)
};
let thumb_brush = render.create_solid_color_brush(
thumb_color.with_alpha((255.0 * opacity) as u8),
None,
)?;
render.fill_quad(
thumb_x,
thumb_y,
thumb_size,
thumb_size,
&thumb_brush,
Some(thumb_size / 2.0),
None,
)?;
Ok(())
}
fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> {
self.style.switch_control_state(control_state);
let class = class.trim();
if let Some(rest) = class.strip_prefix("switch-track-") {
if let Some(color) = parse_color(rest) {
self.style.set_track_color(color);
return Ok(());
}
}
if let Some(rest) = class.strip_prefix("switch-track-off-") {
if let Some(color) = parse_color(rest) {
self.style.set_track_off_color(color);
return Ok(());
}
}
if let Some(rest) = class.strip_prefix("switch-thumb-") {
if let Some(color) = parse_color(rest) {
self.style.set_thumb_color(color);
return Ok(());
}
}
match class {
"switch-size-sm" => {
self.style.set_width(36.0);
self.style.set_height(20.0);
return Ok(());
}
"switch-size-md" => {
self.style.set_width(44.0);
self.style.set_height(24.0);
return Ok(());
}
"switch-size-lg" => {
self.style.set_width(52.0);
self.style.set_height(28.0);
return Ok(());
}
_ => {}
}
Ok(())
}
fn on_button_down(
&mut self,
_key_state: KeyState,
_mouse_position: MousePosition,
) -> Result<HandleResult, Error> {
self.enabled_signal.set(!self.enabled);
Ok(HandleResult::Handled)
}
}
// ============================================================================
// Constructor and factory function
// ============================================================================
impl Switch {
pub fn new(enabled: RwSignal<bool>) -> Self {
let view_id = ViewId::new_with_layout(|view_id| LayoutResolver::new(view_id));
let enabled_value = create_updater(
move || enabled.get(),
move |v: bool| view_id.update_state(Box::new(v)),
);
Self {
view_id,
enabled: enabled_value,
enabled_signal: enabled,
style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style),
}
}
}
#[inline]
pub fn switch(enabled: RwSignal<bool>) -> Switch {
Switch::new(enabled)
}
Usage Examples
Basic Usage
use flor::signal::create_signal;
use flor_lys::switch::switch;
let notifications = create_signal(false);
switch(notifications);
With Atomic Classes
switch(notifications)
.class("switch-track-blue switch-size-lg");
With Signal Linkage
let dark_mode = create_signal(false);
// Other views can read the dark_mode signal to respond
let bg_color = move || {
if dark_mode.get() {
Color::from_hex_str("#1a1a2e").unwrap()
} else {
Color::WHITE
}
};
let panel = div(views![
label("Dark Mode"),
switch(dark_mode).class("switch-track-purple"),
])
.class("flex items-center gap-3 p-4");
With Style Builder
switch(dark_mode)
.style(|s| s
.track_color(Color::GREEN)
.track_off_color(Color::GRAY)
.thumb_color(Color::WHITE)
);
View Development Checklist
Review the entire process, developing a Flor view requires completing these steps:
- Define Style Enum — Use
#[derive(Resolver)] to declare all configurable style properties.
- Define View Struct — Hold
view_id, state fields, and style resolver.
- Implement
View trait:
view_id() — Return the view_id in the struct.
tag() — Return the debug label name.
on_update_state() — Handle style updates passed through update_state.
on_measure() — Views with natural size need to implement this, return the size the view needs.
on_draw() — Core drawing method, read computed style and view state to draw.
on_update_class() — Parse atomic class strings, convert to style updates.
- Override mouse/keyboard event handling as needed (
on_button_down, on_click, etc.).
- Write Constructor — Create
ViewId, LayoutResolver, StyleResolver, use create_updater to build reactive dependency, computed_xxx function is auto-generated by macro.
- Write Factory Function — Provide concise API entry.
Advanced Topics
Adding Animation
If you want to add translation animation to the thumb, you can implement it in on_frame:
fn on_frame(&mut self, now: Instant) -> Result<Option<Duration>, Error> {
let target_x = if self.enabled { /* on position */ } else { /* off position */ };
// Calculate current transition position
// If position is still changing, return Some(Duration) to request next frame
// If already reached target, return None
}
Supporting Disabled State
In on_draw, check ControlState::Disabled, draw gray track and semi-transparent thumb. In on_button_down, check self.view_id.control_state() == ControlState::Disabled and ignore clicks.
Expanding Click Hot Area
If your thumb is too small to click easily, override on_hit_test to expand the hit area:
fn on_hit_test(&self, mouse_position: MousePosition, _key_state: KeyState) -> bool {
// Expand 4px on the layout rectangle basis
let padding = 4.0;
mouse_position.x >= -padding
&& mouse_position.x <= self.layout_width() + padding
&& mouse_position.y >= -padding
&& mouse_position.y <= self.layout_height() + padding
}