//! A tri-state, labelable checkbox widget. use std::error::Error; use std::fmt::Display; use std::ops::Not; use kludgine::figures::units::{Lp, Px}; use kludgine::figures::{Point, Rect, ScreenScale, Size}; use kludgine::shapes::{PathBuilder, Shape, StrokeOptions}; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{LineHeight, OutlineColor, TextColor, WidgetAccentColor}; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::widget::{MakeWidget, MakeWidgetWithId, Widget, WidgetInstance}; use crate::widgets::button::ButtonKind; use crate::ConstraintLimit; /// A labeled-widget that supports three states: Checked, Unchecked, and /// Indeterminant pub struct Checkbox { /// The state (value) of the checkbox. pub state: Dynamic, /// The button kind to use as the basis for this checkbox. Checkboxes /// default to [`ButtonKind::Transparent`]. pub kind: Value, label: WidgetInstance, } impl Checkbox { /// Returns a new checkbox that updates `state` when clicked. `label` is /// drawn next to the checkbox and is also clickable to toggle the checkbox. /// /// `state` can also be a `Dynamic` if there is no need to represent /// an indeterminant state. pub fn new(state: impl IntoDynamic, label: impl MakeWidget) -> Self { Self { state: state.into_dynamic(), kind: Value::Constant(ButtonKind::Transparent), label: label.make_widget(), } } /// Updates the button kind to use as the basis for this checkbox, and /// returns self. /// /// Checkboxes default to [`ButtonKind::Transparent`]. #[must_use] pub fn kind(mut self, kind: impl IntoValue) -> Self { self.kind = kind.into_value(); self } } impl MakeWidgetWithId for Checkbox { fn make_with_id(self, id: crate::widget::WidgetTag) -> WidgetInstance { CheckboxOrnament { value: self.state.create_reader(), } .and(self.label) .into_columns() .into_button() .on_click(move |()| { let mut value = self.state.lock(); *value = !*value; }) .kind(self.kind) .make_with_id(id) } } /// The state/value of a [`Checkbox`]. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CheckboxState { /// The checkbox should display showing that it is neither checked or /// unchecked. /// /// This state is used to represent concepts such as: /// /// - States that are neither true/false, or on/off. /// - States that are partially true or partially on. Indeterminant, /// The checkbox should display in an unchecked/off/false state. Unchecked, /// The checkbox should display in an checked/on/true state. Checked, } impl From for CheckboxState { fn from(value: bool) -> Self { if value { Self::Checked } else { Self::Unchecked } } } impl From for Option { fn from(value: CheckboxState) -> Self { match value { CheckboxState::Indeterminant => None, CheckboxState::Unchecked => Some(false), CheckboxState::Checked => Some(true), } } } impl From> for CheckboxState { fn from(value: Option) -> Self { match value { Some(true) => CheckboxState::Checked, Some(false) => CheckboxState::Unchecked, None => CheckboxState::Indeterminant, } } } impl TryFrom for bool { type Error = CheckboxToBoolError; fn try_from(value: CheckboxState) -> Result { match value { CheckboxState::Checked => Ok(true), CheckboxState::Unchecked => Ok(false), CheckboxState::Indeterminant => Err(CheckboxToBoolError), } } } impl Not for CheckboxState { type Output = Self; fn not(self) -> Self::Output { match self { Self::Indeterminant | Self::Unchecked => Self::Checked, Self::Checked => Self::Unchecked, } } } impl IntoDynamic for Dynamic { fn into_dynamic(self) -> Dynamic { self.linked( |bool| CheckboxState::from(*bool), |tri_state: &CheckboxState| bool::try_from(*tri_state).ok(), ) } } impl IntoDynamic for Dynamic> { fn into_dynamic(self) -> Dynamic { self.linked( |bool| CheckboxState::from(*bool), |tri_state: &CheckboxState| bool::try_from(*tri_state).ok(), ) } } /// An [`CheckboxState::Indeterminant`] was encountered when converting to a /// `bool`. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct CheckboxToBoolError; impl Display for CheckboxToBoolError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("CheckboxState was Indeterminant") } } impl Error for CheckboxToBoolError {} #[derive(Debug)] struct CheckboxOrnament { value: DynamicReader, } impl Widget for CheckboxOrnament { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let checkbox_size = context .gfx .region() .size .width .min(context.gfx.region().size.height); let checkbox_rect = Rect::new( Point::new( Px::ZERO, (context.gfx.region().size.height - checkbox_size) / 2, ), Size::squared(checkbox_size), ); let stroke_options = StrokeOptions::lp_wide(Lp::points(2)).into_px(context.gfx.scale()); match self.value.get_tracking_refresh(context) { state @ (CheckboxState::Checked | CheckboxState::Indeterminant) => { let color = context.get(&WidgetAccentColor); context .gfx .draw_shape(&Shape::filled_rect(checkbox_rect, color)); let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale())); let text_color = context.get(&TextColor); let center = icon_area.origin + icon_area.size / 2; if matches!(state, CheckboxState::Checked) { context.gfx.draw_shape( &PathBuilder::new(Point::new(icon_area.origin.x, center.y)) .line_to(Point::new( icon_area.origin.x + icon_area.size.width / 4, icon_area.origin.y + icon_area.size.height * 3 / 4, )) .line_to(Point::new( icon_area.origin.x + icon_area.size.width, icon_area.origin.y, )) .build() .stroke(stroke_options.colored(text_color)), ); } else { context.gfx.draw_shape( &PathBuilder::new(Point::new(icon_area.origin.x, center.y)) .line_to(Point::new( icon_area.origin.x + icon_area.size.width, center.y, )) .build() .stroke(stroke_options.colored(text_color)), ); } } CheckboxState::Unchecked => { let color = context.get(&OutlineColor); context.gfx.draw_shape(&Shape::stroked_rect( checkbox_rect, stroke_options.colored(color), )); } } } fn layout( &mut self, _available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let checkbox_size = context.get(&LineHeight).into_upx(context.gfx.scale()); // TODO create a component? Size::squared(checkbox_size) } } /// A value that can be used as a checkbox. pub trait Checkable: IntoDynamic + Sized { /// Returns a new checkbox using `self` as the value and `label`. fn into_checkbox(self, label: impl MakeWidget) -> Checkbox { Checkbox::new(self.into_dynamic(), label) } } impl Checkable for T where T: IntoDynamic {}