use teksilo_canvas::{Canvas, Path, Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::focus::FocusOrigin;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{CheckboxState, CheckboxStyle, CheckboxStyleConfig, CheckboxVariant};
use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{Color, CornerRadius};
pub const CHECKBOX_BOX_VISUAL_SIZE: f32 = 19.0;
pub const CHECKBOX_BOX_HIT_AREA: f32 = 24.0;
pub const CHECKBOX_LABEL_GAP: f32 = 6.0;
pub const CHECKBOX_CORNER_RADIUS: f32 = 3.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CheckboxRecipe {
pub box_visual_size: f32,
pub box_hit_area: f32,
pub label_gap: f32,
pub corner_radius: f32,
}
impl Default for CheckboxRecipe {
fn default() -> Self {
Self {
box_visual_size: CHECKBOX_BOX_VISUAL_SIZE,
box_hit_area: CHECKBOX_BOX_HIT_AREA,
label_gap: CHECKBOX_LABEL_GAP,
corner_radius: CHECKBOX_CORNER_RADIUS,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeCheckboxStyle {
pub recipe: CheckboxRecipe,
}
impl RecipeCheckboxStyle {
pub fn new(recipe: CheckboxRecipe) -> Self {
Self { recipe }
}
}
impl CheckboxStyle for RecipeCheckboxStyle {
fn make_body(&self, cfg: &CheckboxStyleConfig, ctx: &mut BuildContext) -> WidgetId {
let focus_origin = cfg
.is_focused
.zip(&cfg.is_hovered)
.map(|(focused, hovered)| {
if *focused {
Some(if *hovered {
FocusOrigin::Pointer
} else {
FocusOrigin::Keyboard
})
} else {
None
}
});
ctx.add(CheckboxBody {
state: cfg.state.clone(),
is_hovered: cfg.is_hovered.clone(),
is_pressed: cfg.is_pressed.clone(),
is_disabled: cfg.is_disabled.clone(),
focus_origin,
variant: cfg.variant,
recipe: self.recipe,
})
}
}
struct CheckboxBody {
state: Signal<CheckboxState>,
is_hovered: Signal<bool>,
is_pressed: Signal<bool>,
is_disabled: Signal<bool>,
focus_origin: Signal<Option<FocusOrigin>>,
variant: CheckboxVariant,
recipe: CheckboxRecipe,
}
impl std::fmt::Debug for CheckboxBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckboxBody")
.field("variant", &self.variant)
.finish()
}
}
impl Widget for CheckboxBody {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let id = ctx.self_id();
let registry = ctx.binding_registry();
self.state.bind_to(id, registry, BindingLevel::RepaintOnly);
self.is_hovered
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.is_pressed
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.is_disabled
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.focus_origin
.bind_to(id, registry, BindingLevel::RepaintOnly);
vec![]
}
fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
Size::new(self.recipe.box_visual_size, self.recipe.box_visual_size).into()
}
fn place_children(
&self,
_bounds: Rect,
_proposal: SizeProposal,
_children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let colors = &ctx.theme.colors;
let state = self.state.get();
let is_filled = matches!(state, CheckboxState::Checked | CheckboxState::Indeterminate);
let disabled = self.is_disabled.get();
let pressed = self.is_pressed.get();
let hovered = self.is_hovered.get();
let focused = self.focus_origin.get().is_some();
let bg = if disabled {
if is_filled {
colors.accent_disabled
} else {
Color::TRANSPARENT
}
} else if is_filled {
if pressed {
colors.accent_pressed
} else if hovered {
colors.accent_hover
} else {
colors.accent
}
} else {
Color::TRANSPARENT
};
let border_color = if focused {
colors.border_focused
} else if disabled {
colors.accent_disabled
} else if is_filled {
Color::TRANSPARENT
} else if hovered {
colors.border_strong
} else {
colors.border
};
let border_width = if focused {
ctx.theme.shape.focus_ring_width
} else {
ctx.theme.shape.border_width
};
let corner = match self.variant {
CheckboxVariant::Square => CornerRadius::uniform(self.recipe.corner_radius),
CheckboxVariant::Rounded => CornerRadius::uniform(self.recipe.corner_radius * 2.0),
CheckboxVariant::Circle => CornerRadius::uniform(self.recipe.box_visual_size / 2.0),
};
canvas.fill_rounded_rect(bounds, corner, bg);
if border_width > 0.0 && border_color != Color::TRANSPARENT {
canvas.stroke_rounded_rect(bounds, corner, border_color, border_width);
}
if matches!(state, CheckboxState::Unchecked) {
return;
}
let glyph_color = if disabled {
colors.text_disabled
} else {
colors.text_on_accent
};
let glyph_size = self.recipe.box_visual_size * 0.75;
let glyph_rect = Rect::new(
bounds.x + (bounds.width - glyph_size) / 2.0,
bounds.y + (bounds.height - glyph_size) / 2.0,
glyph_size,
glyph_size,
);
let stroke_w = (glyph_size * 0.18).max(1.5);
let mut path = Path::new();
match state {
CheckboxState::Checked => {
path.move_to(Point::new(
glyph_rect.x + glyph_size * 0.20,
glyph_rect.y + glyph_size * 0.55,
));
path.line_to(Point::new(
glyph_rect.x + glyph_size * 0.43,
glyph_rect.y + glyph_size * 0.78,
));
path.line_to(Point::new(
glyph_rect.x + glyph_size * 0.80,
glyph_rect.y + glyph_size * 0.28,
));
}
CheckboxState::Indeterminate => {
path.move_to(Point::new(
glyph_rect.x + glyph_size * 0.20,
glyph_rect.y + glyph_size * 0.50,
));
path.line_to(Point::new(
glyph_rect.x + glyph_size * 0.80,
glyph_rect.y + glyph_size * 0.50,
));
}
CheckboxState::Unchecked => unreachable!(),
}
canvas.stroke_path(&path, glyph_color, stroke_w);
}
fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
}
}