Skip to content

Commit

Permalink
Enhance checkbox customizability (#728)
Browse files Browse the repository at this point in the history
* Enhance checkbox customizability

The `Checkbox` component now offers a greater degree of customizability
over the appearance of the `checked` state. Previously, the checkmark
was not customizable. Now, users may provide their own SVGs if they
wish. No existing APIs were broken, but a few new ones were added,
namely there's now a `custom_` prefixed static method for each existing
method. Also, two new convenience functions, `custom_checkbox` and
`custom_labeled_checkboxes` are available. The default checkbox SVG is
now made available to users if they want to use it elsewhere, for
example in a different component (previously it was a function-local
`const`, now it's a `pub const`.

The `checkbox` example in the `widget-gallery` has been enhanced with
two additional examples showcasing some of the new behavior.
  • Loading branch information
anishsinha-io authored Jan 9, 2025
1 parent 2aa0e5a commit be07520
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 10 deletions.
32 changes: 31 additions & 1 deletion examples/widget-gallery/src/checkbox.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
use floem::{
peniko::Color,
reactive::{RwSignal, SignalGet},
views::{checkbox, labeled_checkbox, Checkbox, Decorators},
views::{
checkbox, custom_checkbox, custom_labeled_checkbox, labeled_checkbox, Checkbox,
CheckboxClass, Decorators,
},
IntoView,
};

use crate::form::{form, form_item};

// Source: https://www.svgrepo.com/svg/509804/check | License: MIT
const CUSTOM_CHECK_SVG: &str = r##"
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.6097 5.20743C21.0475 5.54416 21.1294 6.17201 20.7926 6.60976L10.7926 19.6098C10.6172 19.8378 10.352 19.9793 10.0648 19.9979C9.77765 20.0166 9.49637 19.9106 9.29289 19.7072L4.29289 14.7072C3.90237 14.3166 3.90237 13.6835 4.29289 13.2929C4.68342 12.9024 5.31658 12.9024 5.70711 13.2929L9.90178 17.4876L19.2074 5.39034C19.5441 4.95258 20.172 4.87069 20.6097 5.20743Z" fill="#000000"/>
</svg>
"##;

// Source: https://www.svgrepo.com/svg/505349/cross | License: MIT
const CROSS_SVG: &str = r##"
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 5L5 19M5.00001 5L19 19" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
"##;

pub fn checkbox_view() -> impl IntoView {
let width = 160.0;
let is_checked = RwSignal::new(true);
Expand All @@ -29,6 +47,18 @@ pub fn checkbox_view() -> impl IntoView {
labeled_checkbox(move || is_checked.get(), || "Check me!").disabled(|| true)
},
),
form_item("Custom Checkbox 1:".to_string(), width, move || {
custom_checkbox(move || is_checked.get(), CUSTOM_CHECK_SVG)
.style(|s| s.margin(5.0).color(Color::GREEN))
}),
form_item("Custom Checkbox 2:".to_string(), width, move || {
custom_labeled_checkbox(move || is_checked.get(), move || "Custom Label", CROSS_SVG)
.style(|s| {
s.margin(5.0)
.margin_left(0.)
.class(CheckboxClass, |s| s.color(Color::RED))
})
}),
)
})
}
95 changes: 86 additions & 9 deletions src/views/checkbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ style_class!(
pub LabeledCheckboxClass
);

fn checkbox_svg(checked: impl SignalGet<bool> + 'static) -> impl IntoView {
const CHECKBOX_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 16 16"><polygon points="5.19,11.83 0.18,7.44 1.82,5.56 4.81,8.17 10,1.25 12,2.75" /></svg>"#;
let svg_str = move || if checked.get() { CHECKBOX_SVG } else { "" }.to_string();
svg(CHECKBOX_SVG)
.update_value(svg_str)
/// The default checkbox SVG
pub const DEFAULT_CHECKBOX_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 16 16"><polygon points="5.19,11.83 0.18,7.44 1.82,5.56 4.81,8.17 10,1.25 12,2.75" /></svg>"#;

fn checkbox_svg(
checked: impl SignalGet<bool> + 'static,
check_svg: impl Into<String> + Clone + 'static,
) -> impl IntoView {
let check = check_svg.clone();
let update_svg = move || {
if checked.get() {
check_svg.clone().into()
} else {
"".to_string()
}
};
svg(check)
.update_value(update_svg)
.class(CheckboxClass)
.keyboard_navigable()
}
Expand All @@ -48,11 +60,24 @@ impl Checkbox {
///
/// You can add an `on_update` handler to the returned `ValueContainer` to handle changes.
#[allow(clippy::new_ret_no_self)]
#[inline]
pub fn new(checked: impl Fn() -> bool + 'static) -> ValueContainer<bool> {
Self::new_custom(checked, DEFAULT_CHECKBOX_SVG)
}

/// Creates a new checkbox with a closure that determines its checked state and accepts a custom SVG
///
/// The semantics are the same as [`Checkbox::new`].
///
/// You can add an `on_update` handler to the returned `ValueContainer` to handle changes.
pub fn new_custom(
checked: impl Fn() -> bool + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> ValueContainer<bool> {
let (inbound_signal, outbound_signal) = create_value_container_signals(checked);

value_container(
checkbox_svg(inbound_signal.read_only()).on_click_stop(move |_| {
checkbox_svg(inbound_signal.read_only(), custom_check).on_click_stop(move |_| {
let checked = inbound_signal.get_untracked();
outbound_signal.set(!checked);
}),
Expand All @@ -64,10 +89,21 @@ impl Checkbox {
///
/// This method is ideal when you need a checkbox that not only reflects a signal's state but also updates it.
/// Clicking the checkbox will toggle its state and update the signal accordingly.
#[inline]
pub fn new_rw(
checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
) -> impl IntoView {
checkbox_svg(checked).on_click_stop(move |_| {
Self::new_rw_custom(checked, DEFAULT_CHECKBOX_SVG)
}

/// Creates a new checkbox with a signal that provides and updates its checked state and accepts a custom SVG for the symbol.
///
/// The semantics are the same as [`Checkbox::new_rw`].
pub fn new_rw_custom(
checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> impl IntoView {
checkbox_svg(checked, custom_check).on_click_stop(move |_| {
checked.update(|val| *val = !*val);
})
}
Expand All @@ -76,15 +112,27 @@ impl Checkbox {
///
/// This method is useful when you want a labeled checkbox whose state is determined by a closure.
/// The label is also provided by a closure, allowing for dynamic updates.
#[inline]
pub fn labeled<S: Display + 'static>(
checked: impl Fn() -> bool + 'static,
label: impl Fn() -> S + 'static,
) -> ValueContainer<bool> {
Self::custom_labeled(checked, label, DEFAULT_CHECKBOX_SVG)
}

/// Creates a new labeled checkbox with a closure that determines its checked state and accepts a custom SVG for the symbol.
///
/// The semantics are the same as [`Checkbox::labeled`].
pub fn custom_labeled<S: Display + 'static>(
checked: impl Fn() -> bool + 'static,
label: impl Fn() -> S + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> ValueContainer<bool> {
let (inbound_signal, outbound_signal) = create_value_container_signals(checked);

value_container(
h_stack((
checkbox_svg(inbound_signal.read_only()),
checkbox_svg(inbound_signal.read_only(), custom_check),
views::label(label),
))
.class(LabeledCheckboxClass)
Expand All @@ -101,11 +149,23 @@ impl Checkbox {
///
/// This method is ideal when you need a labeled checkbox that not only reflects a signal's state but also updates it.
/// Clicking the checkbox will toggle its state and update the signal accordingly.
#[inline]
pub fn labeled_rw<S: Display + 'static>(
checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
label: impl Fn() -> S + 'static,
) -> impl IntoView {
h_stack((checkbox_svg(checked), views::label(label)))
Self::custom_labeled_rw(checked, label, DEFAULT_CHECKBOX_SVG)
}

/// Creates a new labeled checkbox with a signal that provides and updates its checked state and accepts a custom SVG.
///
/// The semantics are the same as [`Checkbox::labeled_rw`].
pub fn custom_labeled_rw<S: Display + 'static>(
checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
label: impl Fn() -> S + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> impl IntoView {
h_stack((checkbox_svg(checked, custom_check), views::label(label)))
.class(LabeledCheckboxClass)
.style(|s| s.items_center().justify_center())
.on_click_stop(move |_| {
Expand All @@ -119,10 +179,27 @@ pub fn checkbox(checked: impl Fn() -> bool + 'static) -> ValueContainer<bool> {
Checkbox::new(checked)
}

/// Renders a checkbox using a `checked` signal and custom SVG. See also [`Checkbox::new_rw`] and
pub fn custom_checkbox(
checked: impl Fn() -> bool + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> ValueContainer<bool> {
Checkbox::new_custom(checked, custom_check)
}

/// Renders a checkbox using the provided checked signal. See also [`Checkbox::labeled`] and [`Checkbox::labeled_rw`].
pub fn labeled_checkbox<S: Display + 'static>(
checked: impl Fn() -> bool + 'static,
label: impl Fn() -> S + 'static,
) -> ValueContainer<bool> {
Checkbox::labeled(checked, label)
}

/// Renders a checkbox using the a `checked` signal and a custom SVG. See also [`Checkbox::custom_labeled_rw`]
pub fn custom_labeled_checkbox<S: Display + 'static>(
checked: impl Fn() -> bool + 'static,
label: impl Fn() -> S + 'static,
custom_check: impl Into<String> + Clone + 'static,
) -> ValueContainer<bool> {
Checkbox::custom_labeled(checked, label, custom_check)
}

0 comments on commit be07520

Please sign in to comment.