1use omp_core::Str;
2
3use super::radio::pill;
4use crate::{
5 component::{Component, EventCtx, Flow, Hit, HitTag, PaintCtx, Slot, next_slot},
6 context::UiContext,
7 frame::Rect,
8 input::{Key, Mouse, UiEvent},
9 props::{Prop, PropValue, Props},
10 rich::cell_width,
11};
12
13#[derive(Default)]
14struct ButtonState {
15 armed: bool,
16}
17
18pub struct Button {
20 props: Props,
21 slot: Slot,
22 state: ButtonState,
23 label: Str,
24}
25
26impl Button {
27 pub fn new() -> Self {
29 Self {
30 props: Props::new(),
31 slot: next_slot(),
32 state: ButtonState::default(),
33 label: Str::default(),
34 }
35 }
36
37 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
39 self.props.set(prop, value);
40 self
41 }
42
43 pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
45 self.props.set(prop, value);
46 self
47 }
48
49 pub fn child(mut self, label: impl Into<Str>) -> Self {
51 let label = label.into();
52 if self.label.is_empty() {
53 self.label = label;
54 } else {
55 self.label = Str::from(format!("{}{}", self.label, label));
56 }
57 self
58 }
59
60 fn label(&self) -> &str {
61 if !self.label.is_empty() {
62 &self.label
63 } else if let Some(label) = self.props.str_of(Prop::Label) {
64 label
65 } else if let Some(id) = self.props.id() {
66 id
67 } else {
68 "ok"
69 }
70 }
71
72 fn press(&mut self) -> Flow {
73 if self.props.flag(Prop::Confirm) && !self.state.armed {
74 self.state.armed = true;
75 return Flow::Consumed;
76 }
77 self.state.armed = false;
78 if self.props.flag(Prop::Cancel) {
79 Flow::Event(UiEvent::Cancel)
80 } else if self.props.flag(Prop::Submit) {
81 Flow::Event(UiEvent::Submit)
82 } else if let Some(id) = self.props.id() {
83 Flow::Event(UiEvent::Pressed(id.clone()))
84 } else {
85 Flow::Event(UiEvent::Submit)
86 }
87 }
88}
89
90impl Default for Button {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl Component for Button {
97 fn props(&self) -> &Props {
98 &self.props
99 }
100
101 fn props_mut(&mut self) -> &mut Props {
102 &mut self.props
103 }
104
105 fn slot(&self) -> Slot {
106 self.slot
107 }
108
109 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
110 let width = cell_width(self.label()).saturating_add(4);
111 (width, width)
112 }
113
114 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
115 1
116 }
117
118 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
119 if rect.y >= pc.clip {
120 return;
121 }
122 pc.hits
123 .push(Hit { rect, slot: self.slot, tag: HitTag::Press });
124 let focused = pc.focus == Some(self.slot);
125 let hovered = matches!(pc.hover, Some((slot, _)) if slot == self.slot);
126 let accent = self.props.flag(Prop::Accent) || self.props.flag(Prop::Submit);
127 let (text, background, foreground) = if self.state.armed {
128 ("sure?", pc.ctx.theme.warn, pc.ctx.theme.contrast)
129 } else if accent {
130 (self.label(), pc.ctx.theme.accent, pc.ctx.theme.contrast)
131 } else {
132 (self.label(), pc.ctx.theme.surface, pc.ctx.theme.fg)
133 };
134 pill(
135 pc.frame,
136 rect.x,
137 rect.y,
138 text,
139 background,
140 foreground,
141 pc.ctx.charset.pill_caps(),
142 focused || hovered,
143 );
144 }
145
146 fn focusable(&self) -> bool {
147 true
148 }
149
150 fn key(&mut self, _ec: &mut EventCtx<'_>, key: Key) -> Flow {
151 match key {
152 Key::Enter | Key::Space => self.press(),
153 _ => {
154 self.state.armed = false;
155 Flow::Skip
156 },
157 }
158 }
159
160 fn mouse(
161 &mut self,
162 _ec: &mut EventCtx<'_>,
163 tag: HitTag,
164 _at: (u16, u16),
165 _rect: Rect,
166 mouse: Mouse,
167 ) -> Flow {
168 match mouse {
169 Mouse::Click if tag == HitTag::Press => self.press(),
170 Mouse::Click
171 | Mouse::RightClick
172 | Mouse::MiddleClick
173 | Mouse::Move
174 | Mouse::Drag
175 | Mouse::Release
176 | Mouse::WheelUp
177 | Mouse::WheelDown
178 | Mouse::WheelLeft
179 | Mouse::WheelRight => Flow::Skip,
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::{Frame, Size, test_support::frame_row_text};
188
189 fn event_ctx(ctx: &UiContext) -> EventCtx<'_> {
190 EventCtx::new(ctx, 16, 1)
191 }
192
193 #[test]
194 fn enter_emits_matching_application_event() {
195 let ctx = UiContext::default();
196 let mut submit = Button::new().with(Prop::Submit, true).child("Go");
197 assert_eq!(submit.key(&mut event_ctx(&ctx), Key::Enter), Flow::Event(UiEvent::Submit));
198 let mut plain = Button::new().with(Prop::Id, "again").child("Again");
199 assert_eq!(
200 plain.key(&mut event_ctx(&ctx), Key::Enter),
201 Flow::Event(UiEvent::Pressed(Str::from("again")))
202 );
203 }
204
205 #[test]
206 fn confirm_arms_before_emitting() {
207 let ctx = UiContext::default();
208 let mut button = Button::new().with(Prop::Confirm, true);
209 assert_eq!(button.key(&mut event_ctx(&ctx), Key::Enter), Flow::Consumed);
210 assert_eq!(button.key(&mut event_ctx(&ctx), Key::Enter), Flow::Event(UiEvent::Submit));
211 }
212
213 #[test]
214 fn paint_draws_label_and_press_hit() {
215 let mut button = Button::new().child("Continue");
216 let ctx = UiContext::default();
217 let mut frame = Frame::new(Size::new(24, 1));
218 let mut hits = Vec::new();
219 let slot = button.slot();
220 let mut wakes = Vec::new();
221 let mut pc = PaintCtx::new(&mut frame, &ctx, &mut hits, &mut wakes);
222 pc.focus = Some(slot);
223 button.paint(&mut pc, Rect::new(0, 0, 24, 1));
224 assert!(frame_row_text(&frame, 0).contains("Continue"));
225 assert_eq!(hits[0].tag, HitTag::Press);
226 }
227}