Another project
1use serde::Serialize;
2
3use crate::frame::FrameCtx;
4use crate::hit_test::Interaction;
5use crate::layout::LayoutRect;
6use crate::strings::StringKey;
7use crate::theme::{
8 Border, Color, ElevationLevel, Radius, Spacing, Step12, StrokeWidth, Theme, TypographyRole,
9};
10
11use bone_types::IconId;
12
13use super::paint::{GlyphMark, IconTint, LabelText, WidgetPaint};
14
15#[derive(Copy, Clone, Debug, PartialEq)]
16pub enum IndicatorMark {
17 Check,
18 Glyph(GlyphMark),
19}
20
21#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
22pub struct SurfaceVisuals {
23 pub fill: Color,
24 pub border: Option<Border>,
25 pub radius: Radius,
26 pub elevation: Option<ElevationLevel>,
27}
28
29#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
30pub struct TextVisuals {
31 pub color: Color,
32 pub role: TypographyRole,
33}
34
35#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
36pub struct FieldVisuals {
37 pub surface: SurfaceVisuals,
38 pub text: TextVisuals,
39 pub placeholder: Color,
40 pub caret: Color,
41 pub selection: Color,
42}
43
44pub fn push_focus_ring(
45 ctx: &FrameCtx<'_>,
46 paint: &mut Vec<WidgetPaint>,
47 rect: LayoutRect,
48 radius: Radius,
49 live_focused: bool,
50) {
51 if live_focused && ctx.focus.focus_visible() {
52 paint.push(WidgetPaint::FocusRing {
53 rect,
54 color: ctx.theme().colors.focus_ring(),
55 radius,
56 thickness: Spacing::px(StrokeWidth::HAIRLINE.value_px() * 2.0),
57 });
58 }
59}
60
61#[must_use]
62pub fn indicator_fill(
63 theme: &Theme,
64 active: bool,
65 disabled: bool,
66 interaction: Interaction,
67) -> Color {
68 let neutral = theme.colors.neutral;
69 let accent = theme.colors.accent;
70 let pressed = interaction.pressed();
71 let hovered = interaction.hover();
72 if disabled {
73 neutral.step(Step12::SUBTLE_BG)
74 } else if active {
75 if pressed || hovered {
76 accent.step(Step12::HOVER_SOLID)
77 } else {
78 accent.step(Step12::SOLID)
79 }
80 } else if pressed {
81 neutral.step(Step12::SELECTED_BG)
82 } else if hovered {
83 neutral.step(Step12::HOVER_BG)
84 } else {
85 neutral.step(Step12::ELEMENT_BG)
86 }
87}
88
89#[must_use]
90pub fn indicator_border(theme: &Theme, active: bool, hovered: bool) -> Option<Border> {
91 (!active).then(|| Border {
92 width: StrokeWidth::HAIRLINE,
93 color: theme.colors.neutral.step(if hovered {
94 Step12::HOVER_BORDER
95 } else {
96 Step12::BORDER
97 }),
98 })
99}
100
101#[must_use]
102pub fn indicator_label_color(
103 theme: &Theme,
104 surface_fill: Color,
105 active: bool,
106 disabled: bool,
107) -> Color {
108 if disabled {
109 theme.colors.text_disabled()
110 } else if active {
111 theme.colors.contrast_text(surface_fill)
112 } else {
113 theme.colors.text_primary()
114 }
115}
116
117#[derive(Copy, Clone, Debug, PartialEq)]
118pub struct Indicator {
119 pub rect: LayoutRect,
120 pub label: StringKey,
121 pub mark: Option<IndicatorMark>,
122 pub active: bool,
123 pub disabled: bool,
124 pub radius: Radius,
125}
126
127pub fn push_indicator(
128 ctx: &FrameCtx<'_>,
129 paint: &mut Vec<WidgetPaint>,
130 indicator: Indicator,
131 interaction: Interaction,
132 live_focused: bool,
133) {
134 let Indicator {
135 rect,
136 label,
137 mark,
138 active,
139 disabled,
140 radius,
141 } = indicator;
142 let fill = indicator_fill(ctx.theme(), active, disabled, interaction);
143 let border = indicator_border(ctx.theme(), active, interaction.hover());
144 paint.push(WidgetPaint::Surface {
145 rect,
146 fill,
147 border,
148 radius,
149 elevation: None,
150 });
151 paint.push(WidgetPaint::Label {
152 rect,
153 text: LabelText::Key(label),
154 color: indicator_label_color(ctx.theme(), fill, active, disabled),
155 role: ctx.theme().typography.label,
156 });
157 if let Some(mark) = mark {
158 let color = ctx.theme().colors.contrast_text(fill);
159 paint.push(match mark {
160 IndicatorMark::Check => WidgetPaint::Icon {
161 rect,
162 icon: IconId::Check,
163 tint: IconTint::Solid(color),
164 },
165 IndicatorMark::Glyph(kind) => WidgetPaint::Mark { rect, kind, color },
166 });
167 }
168 push_focus_ring(ctx, paint, rect, radius, live_focused);
169}