use std::error::Error;
use std::fmt::Display;
use std::ops::Not;
use figures::units::Lp;
use figures::{Point, Rect, Round, ScreenScale, Size};
use kludgine::shapes::{PathBuilder, Shape, StrokeOptions};
use crate::context::{GraphicsContext, LayoutContext};
use crate::styles::components::{LineHeight, OutlineColor, TextColor, WidgetAccentColor};
use crate::styles::Dimension;
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Value};
use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance};
use crate::widgets::button::ButtonKind;
use crate::ConstraintLimit;
pub struct Checkbox {
pub state: Dynamic<CheckboxState>,
pub kind: Value<ButtonKind>,
label: WidgetInstance,
}
impl Checkbox {
pub fn new(state: impl IntoDynamic<CheckboxState>, label: impl MakeWidget) -> Self {
Self {
state: state.into_dynamic(),
kind: Value::Constant(ButtonKind::Transparent),
label: label.make_widget(),
}
}
#[must_use]
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
self.kind = kind.into_value();
self
}
}
impl MakeWidgetWithTag for Checkbox {
fn make_with_tag(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_tag(id)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CheckboxState {
Indeterminant,
Unchecked,
Checked,
}
impl From<bool> for CheckboxState {
fn from(value: bool) -> Self {
if value {
Self::Checked
} else {
Self::Unchecked
}
}
}
impl From<CheckboxState> for Option<bool> {
fn from(value: CheckboxState) -> Self {
match value {
CheckboxState::Indeterminant => None,
CheckboxState::Unchecked => Some(false),
CheckboxState::Checked => Some(true),
}
}
}
impl From<Option<bool>> for CheckboxState {
fn from(value: Option<bool>) -> Self {
match value {
Some(true) => CheckboxState::Checked,
Some(false) => CheckboxState::Unchecked,
None => CheckboxState::Indeterminant,
}
}
}
impl TryFrom<CheckboxState> for bool {
type Error = CheckboxToBoolError;
fn try_from(value: CheckboxState) -> Result<Self, Self::Error> {
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<CheckboxState> for Dynamic<bool> {
fn into_dynamic(self) -> Dynamic<CheckboxState> {
self.linked(
|bool| CheckboxState::from(*bool),
|tri_state: &CheckboxState| bool::try_from(*tri_state).ok(),
)
}
}
impl IntoDynamic<CheckboxState> for Dynamic<Option<bool>> {
fn into_dynamic(self) -> Dynamic<CheckboxState> {
self.linked(
|bool| CheckboxState::from(*bool),
|tri_state: &CheckboxState| bool::try_from(*tri_state).ok(),
)
}
}
#[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<CheckboxState>,
}
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 stroke_options =
StrokeOptions::px_wide(Lp::points(2).into_px(context.gfx.scale()).round());
let half_line = stroke_options.line_width / 2;
let checkbox_rect = Rect::new(
Point::new(
half_line,
(context.gfx.region().size.height - checkbox_size) / 2 + half_line,
),
Size::squared(checkbox_size - stroke_options.line_width),
);
match self.value.get_tracking_redraw(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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<figures::units::UPx> {
let checkbox_size = context.get(&CheckboxSize).into_upx(context.gfx.scale());
Size::squared(checkbox_size)
}
}
pub trait Checkable: IntoDynamic<CheckboxState> + Sized {
fn into_checkbox(self, label: impl MakeWidget) -> Checkbox {
Checkbox::new(self.into_dynamic(), label)
}
fn to_checkbox(&self, label: impl MakeWidget) -> Checkbox
where
Self: Clone,
{
self.clone().into_checkbox(label)
}
}
impl<T> Checkable for T where T: IntoDynamic<CheckboxState> {}
define_components! {
Checkbox {
CheckboxSize(Dimension, "size", @LineHeight)
}
}