use teksilo_core::build_context::BuildContext;
use teksilo_core::styles::{ComboBoxStyle, ComboBoxStyleConfig, ComboBoxVariant};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{BorderRole, CornerRadius, SurfaceRole};
use crate::primitives::{FixedSize, HStack, IconWidget, Padding, RectWidget, Spacer, ZStack};
pub const COMBO_BOX_HEIGHT: f32 = 28.0;
pub const COMBO_BOX_PADDING_HORIZONTAL: f32 = 9.0;
pub const COMBO_BOX_ARROW_COLUMN_WIDTH: f32 = 23.0;
pub const COMBO_BOX_CORNER_RADIUS: f32 = 4.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComboBoxRecipe {
pub height: f32,
pub padding_horizontal: f32,
pub arrow_column_width: f32,
pub corner_radius: f32,
}
impl Default for ComboBoxRecipe {
fn default() -> Self {
Self {
height: COMBO_BOX_HEIGHT,
padding_horizontal: COMBO_BOX_PADDING_HORIZONTAL,
arrow_column_width: COMBO_BOX_ARROW_COLUMN_WIDTH,
corner_radius: COMBO_BOX_CORNER_RADIUS,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeComboBoxStyle {
pub recipe: ComboBoxRecipe,
}
impl RecipeComboBoxStyle {
pub fn new(recipe: ComboBoxRecipe) -> Self {
Self { recipe }
}
}
impl ComboBoxStyle for RecipeComboBoxStyle {
fn make_body(&self, cfg: &ComboBoxStyleConfig, ctx: &mut BuildContext) -> WidgetId {
let theme = ctx.theme();
let border_width = theme.shape.border_width;
let focus_ring_width = theme.shape.focus_ring_width;
let height = self.recipe.height;
let divider_height = height * 0.6;
let padding_h = self.recipe.padding_horizontal;
let corner_radius = self.recipe.corner_radius;
if matches!(cfg.variant, ComboBoxVariant::Plain) {
let row_id = build_inner_row(ctx, cfg.selected_label, border_width, divider_height);
let padded_id =
ctx.add(Padding::symmetric(padding_h * 0.5, padding_h).child_id(row_id));
return ctx.add(crate::primitives::MinSize::new(0.0, height).child_id(padded_id));
}
let variant = cfg.variant;
let bg_role = cfg.is_open.zip3(&cfg.is_hovered, &cfg.is_disabled).map(
move |(open, hovered, disabled)| {
if *disabled {
SurfaceRole::Disabled
} else if matches!(variant, ComboBoxVariant::Filled) {
SurfaceRole::Hover
} else if *open || *hovered {
SurfaceRole::Hover
} else {
SurfaceRole::Main
}
},
);
let border_role = cfg
.is_focused
.zip(&cfg.is_disabled)
.map(move |(focused, disabled)| {
if matches!(variant, ComboBoxVariant::Filled) {
BorderRole::Default
} else if *disabled {
BorderRole::Disabled
} else if *focused {
BorderRole::Focused
} else {
BorderRole::Default
}
});
let border_width_signal = cfg.is_focused.map(move |focused| match variant {
ComboBoxVariant::Filled => 0.0,
_ => {
if *focused {
focus_ring_width
} else {
border_width
}
}
});
let row_id = build_inner_row(ctx, cfg.selected_label, border_width, divider_height);
let padding_id = ctx.add(Padding::symmetric(padding_h * 0.5, padding_h).child_id(row_id));
let bg = RectWidget::new()
.background(bg_role)
.border_color(border_role)
.border_width(border_width_signal)
.corner_radius(CornerRadius::uniform(corner_radius));
let bg_id = ctx.add(bg);
let visual_id = ctx.add(ZStack::new().add_child(bg_id).add_child(padding_id));
ctx.add(crate::primitives::MinSize::new(0.0, height).child_id(visual_id))
}
}
fn build_inner_row(
ctx: &mut BuildContext,
selected_label: WidgetId,
border_width: f32,
divider_height: f32,
) -> WidgetId {
let divider_fill_id = ctx.add(RectWidget::new().background(BorderRole::Default));
let divider_id = ctx.add(
FixedSize::new()
.width(border_width)
.height(divider_height)
.child_id(divider_fill_id),
);
let chevron_color = ctx
.theme_signal()
.map(|t| t.colors.text_primary.with_alpha(0.5));
let chevron = IconWidget::chevron_down(12.0).color(chevron_color);
let chevron_id = ctx.add(chevron);
ctx.add(
HStack::new()
.spacing(8.0)
.add_child(selected_label)
.child(Spacer::new())
.add_child(divider_id)
.add_child(chevron_id),
)
}