use teksilo_canvas::{Canvas, 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::{SegmentSlots, SegmentedControlStyle, SegmentedControlStyleConfig};
use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::CornerRadius;
pub const SEGMENTED_CONTROL_HEIGHT: f32 = 24.0;
pub const SEGMENTED_CONTROL_PADDING_HORIZONTAL: f32 = 12.0;
pub const SEGMENTED_CONTROL_PADDING_VERTICAL: f32 = 6.0;
pub const SEGMENTED_CONTROL_CORNER_RADIUS: f32 = 3.0;
pub const SEGMENTED_CONTROL_BORDER_WIDTH: f32 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SegmentedControlRecipe {
pub height: f32,
pub padding_horizontal: f32,
pub padding_vertical: f32,
pub corner_radius: f32,
pub border_width: f32,
}
impl Default for SegmentedControlRecipe {
fn default() -> Self {
Self {
height: SEGMENTED_CONTROL_HEIGHT,
padding_horizontal: SEGMENTED_CONTROL_PADDING_HORIZONTAL,
padding_vertical: SEGMENTED_CONTROL_PADDING_VERTICAL,
corner_radius: SEGMENTED_CONTROL_CORNER_RADIUS,
border_width: SEGMENTED_CONTROL_BORDER_WIDTH,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeSegmentedControlStyle {
pub recipe: SegmentedControlRecipe,
}
impl RecipeSegmentedControlStyle {
pub fn new(recipe: SegmentedControlRecipe) -> Self {
Self { recipe }
}
}
impl SegmentedControlStyle for RecipeSegmentedControlStyle {
fn make_body(&self, cfg: &SegmentedControlStyleConfig, ctx: &mut BuildContext) -> WidgetId {
ctx.add(SegmentedControlChrome {
slots: cfg.slots.clone(),
selected: cfg.selected.clone(),
hovered_segment: cfg.hovered_segment.clone(),
focus_origin: cfg.focus_origin.clone(),
is_enabled: cfg.is_enabled.clone(),
recipe: self.recipe,
})
}
}
struct SegmentedControlChrome {
slots: SegmentSlots,
selected: Signal<usize>,
hovered_segment: Signal<Option<usize>>,
focus_origin: Signal<Option<FocusOrigin>>,
is_enabled: Signal<bool>,
recipe: SegmentedControlRecipe,
}
impl std::fmt::Debug for SegmentedControlChrome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SegmentedControlChrome")
.field("slots", &self.slots.len())
.finish()
}
}
impl Widget for SegmentedControlChrome {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let id = ctx.self_id();
let registry = ctx.binding_registry();
self.selected
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.hovered_segment
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.focus_origin
.bind_to(id, registry, BindingLevel::RepaintOnly);
self.is_enabled
.bind_to(id, registry, BindingLevel::RepaintOnly);
vec![]
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
Size::new(
proposal.width.unwrap_or(0.0),
proposal.height.unwrap_or(0.0),
)
.into()
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let colors = &ctx.theme.colors;
let shape = &ctx.theme.shape;
let bw = self.recipe.border_width;
let frame_cr = CornerRadius::uniform(self.recipe.corner_radius);
let selected = self.selected.get();
let hovered = self.hovered_segment.get();
let focus_origin = self.focus_origin.get();
let focused = focus_origin.is_some();
let keyboard_focused = focus_origin == Some(FocusOrigin::Keyboard);
let is_enabled = self.is_enabled.get();
self.slots.with(|geometry| {
if geometry.segments.is_empty() {
return;
}
let frame_border = if !is_enabled {
colors.border
} else {
colors.border_strong
};
canvas.stroke_rounded_rect(geometry.frame, frame_cr, frame_border, bw);
let selected_slot = geometry.order.iter().position(|&s| s == selected);
let hovered_slot = hovered.and_then(|h| geometry.order.iter().position(|&s| s == h));
if is_enabled
&& let Some(slot) = hovered_slot
&& Some(slot) != selected_slot
&& let Some(rect) = geometry.segments.get(slot)
{
canvas.fill_rounded_rect(*rect, frame_cr, colors.surface_hover);
}
if let Some(slot) = selected_slot
&& let Some(base) = geometry.segments.get(slot)
{
let sel = Rect::new(
base.x - bw,
base.y - bw,
base.width + bw * 2.0,
base.height + bw * 2.0,
);
let (sel_bg, sel_border) = if !is_enabled {
(colors.surface_selected_inactive, colors.border)
} else if focused {
(colors.accent, colors.accent)
} else {
(colors.surface_selected_inactive, colors.border_strong)
};
canvas.fill_rounded_rect(sel, frame_cr, sel_bg);
canvas.stroke_rounded_rect(sel, frame_cr, sel_border, bw);
}
if let Some(overflow) = geometry.overflow {
canvas.fill_rect(
Rect::new(overflow.x, overflow.y, bw, overflow.height),
frame_border,
);
}
});
if keyboard_focused {
let half_stroke = shape.focus_ring_width * 0.5;
let ring_rect = Rect::new(
bounds.x + half_stroke,
bounds.y + half_stroke,
(bounds.width - half_stroke * 2.0).max(0.0),
(bounds.height - half_stroke * 2.0).max(0.0),
);
let ring_radius = self.recipe.corner_radius + shape.focus_ring_offset + half_stroke;
canvas.stroke_rounded_rect(
ring_rect,
CornerRadius::uniform(ring_radius),
colors.focus_ring,
shape.focus_ring_width,
);
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_hidden();
}
}