1use super::IndexMessage;
4use super::press::{self, Press};
5use crate::env::Env;
6use crate::event::Event;
7use crate::geometry::{Rect, Size};
8use crate::keymap::Key;
9use crate::style::CellStyle;
10use crate::text;
11use crate::theme::State;
12use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
13
14pub struct Segmented<Msg> {
26 options: Vec<String>,
27 selected: usize,
28 disabled: bool,
29 wrap: bool,
30 on_select: Option<IndexMessage<Msg>>,
31}
32
33impl<Msg> Segmented<Msg> {
34 #[must_use]
36 pub fn new(options: impl IntoIterator<Item = impl Into<String>>) -> Self {
37 Self {
38 options: options.into_iter().map(Into::into).collect(),
39 selected: 0,
40 disabled: false,
41 wrap: false,
42 on_select: None,
43 }
44 }
45
46 #[must_use]
48 pub fn selected(mut self, index: usize) -> Self {
49 self.selected = index;
50 self
51 }
52
53 #[must_use]
55 pub fn disabled(mut self, disabled: bool) -> Self {
56 self.disabled = disabled;
57 self
58 }
59
60 #[must_use]
63 pub fn wrap(mut self, wrap: bool) -> Self {
64 self.wrap = wrap;
65 self
66 }
67
68 #[must_use]
70 pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
71 self.on_select = Some(Box::new(message));
72 self
73 }
74
75 fn active(&self) -> bool {
76 !self.disabled && self.on_select.is_some() && !self.options.is_empty()
77 }
78
79 fn spans(&self, horizontal_padding: u16) -> Vec<(u16, u16)> {
81 let mut x = 0u16;
82 self.options
83 .iter()
84 .map(|label| {
85 let width = text::width(label).saturating_add(horizontal_padding.saturating_mul(2));
86 let span = (x, width);
87 x = x.saturating_add(width);
88 span
89 })
90 .collect()
91 }
92
93 fn choose(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
94 let index = index.min(self.options.len() - 1);
95 if index != self.selected
96 && let Some(message) = &self.on_select
97 {
98 cx.emit(message(index));
99 }
100 }
101}
102
103fn padding(env: &Env) -> u16 {
105 env.theme().style("segment", None, &[]).pair("padding").map_or(2, |(_, horizontal)| horizontal)
106}
107
108impl<Msg: 'static> Widget<Msg> for Segmented<Msg> {
109 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
110 let pad = padding(cx.env());
111 let width = self.spans(pad).last().map_or(0, |(x, width)| x.saturating_add(*width));
112 Size::new(width, 1).min(available)
113 }
114
115 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
116 let active = self.active();
117 let focused = active && cx.is_focus_visible();
118 let pointer = if active { cx.pointer() } else { None };
119 let pad = padding(cx.env());
120 for (index, (x, width)) in self.spans(pad).into_iter().enumerate() {
121 let rect = Rect::new(area.x + i32::from(x), area.y, width, 1).intersect(area);
122 if rect.is_empty() {
123 continue;
124 }
125 let chosen = index == self.selected;
126 let mut states = Vec::new();
127 if pointer.is_some_and(|(px, py)| rect.contains(px, py)) {
128 states.push(State::Hover);
129 }
130 if focused && chosen {
131 states.push(State::Focus);
132 }
133 if chosen {
134 states.push(State::Checked);
135 }
136 if self.disabled {
137 states.push(State::Disabled);
138 }
139 let segment_style = cx.style("segment", None, &states);
140 let style = segment_style.text();
141 cx.clear(rect, style.bg.unwrap_or_else(|| cx.color("raised")));
142 if let Some(color) = segment_style.color("pillar").filter(|_| pad >= 1) {
144 cx.pillar(rect.x, rect.y, color);
145 }
146 let budget = rect.width.saturating_sub(pad);
147 let label = text::truncate(&self.options[index], budget).into_owned();
148 cx.text(rect.x + i32::from(pad), rect.y, &label, CellStyle { bg: None, ..style }, budget);
149 }
150 if active {
151 cx.register_hit(area);
152 }
153 }
154
155 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
156 if !self.active() {
157 return false;
158 }
159 if let Event::Key(key) = event {
160 let last = self.options.len() - 1;
161 let target = if key.is_plain(Key::Left) {
162 Some(if self.wrap && self.selected == 0 { last } else { self.selected.saturating_sub(1) })
163 } else if key.is_plain(Key::Right) {
164 Some(if self.wrap && self.selected >= last { 0 } else { self.selected + 1 })
165 } else if key.is_plain(Key::Home) {
166 Some(0)
167 } else if key.is_plain(Key::End) {
168 Some(last)
169 } else {
170 None
171 };
172 if let Some(index) = target {
173 self.choose(cx, index);
174 return true;
175 }
176 }
177 match press::read(cx, event) {
178 Press::Ignored | Press::Key => false,
179 Press::Used => true,
180 Press::Click(x, _) => {
181 let offset = x - cx.area().x;
182 let pad = padding(cx.env());
183 if let Some(index) = self
184 .spans(pad)
185 .iter()
186 .position(|(start, width)| (i32::from(*start)..i32::from(start + width)).contains(&offset))
187 {
188 self.choose(cx, index);
189 }
190 true
191 }
192 }
193 }
194
195 fn focusable(&self) -> bool {
196 self.active()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::runtime::{App, Command, Harness};
204 use crate::widget::View;
205
206 struct Demo {
207 chosen: usize,
208 }
209
210 impl App for Demo {
211 type Msg = usize;
212 fn update(&mut self, index: usize) -> Command<usize> {
213 self.chosen = index;
214 Command::none()
215 }
216 fn view(&self, ui: &mut View<'_, usize>) {
217 ui.add(Segmented::new(["List", "Grid", "Tree"]).selected(self.chosen).on_select(|i| i)).id("view");
218 }
219 }
220
221 #[test]
222 fn chosen_segment_is_filled_and_changes_by_keys_and_clicks() {
223 let mut h = Harness::new(Demo { chosen: 0 }, 30, 1);
224 assert_eq!(h.screen(), " List Grid Tree\n");
225 let theme = h.env().theme();
226 assert!(h.bg(2, 0) != theme.color("raised") && h.bg(2, 0) != theme.color("accent"));
228 assert_eq!(h.bg(10, 0), theme.color("raised"));
229 h.press("tab").press("right");
230 assert_eq!(h.app().chosen, 1);
231 h.press("end");
232 assert_eq!(h.app().chosen, 2);
233 h.click_text("List");
234 assert_eq!(h.app().chosen, 0);
235 }
236
237 #[test]
238 fn a_wrapping_control_goes_round_its_ends_and_stops_otherwise() {
239 struct Wrapping {
240 chosen: usize,
241 wrap: bool,
242 }
243 impl App for Wrapping {
244 type Msg = usize;
245 fn update(&mut self, index: usize) -> Command<usize> {
246 self.chosen = index;
247 Command::none()
248 }
249 fn view(&self, ui: &mut View<'_, usize>) {
250 ui.add(Segmented::new(["List", "Grid", "Tree"]).selected(self.chosen).wrap(self.wrap).on_select(|i| i));
251 }
252 }
253 let mut h = Harness::new(Wrapping { chosen: 2, wrap: true }, 30, 1);
254 let raised = h.env().theme().color("raised");
255 h.press("tab").press("right");
256 assert_ne!(h.bg(2, 0), raised, "right on the last segment chooses the first");
258 assert_eq!(h.bg(18, 0), raised);
259 h.press("left");
260 assert_eq!(h.app().chosen, 2, "left on the first segment chooses the last");
261 h.press("end").press("right").press("home").press("home");
262 assert_eq!(h.app().chosen, 0, "home stops at the start");
263
264 let mut h = Harness::new(Wrapping { chosen: 2, wrap: false }, 30, 1);
265 h.press("tab").press("right");
266 assert_eq!(h.app().chosen, 2);
267 }
268
269 #[test]
270 fn every_cell_of_a_segment_including_its_padding_chooses_it() {
271 let mut h = Harness::new(Demo { chosen: 0 }, 30, 1);
272 for x in 8..16 {
273 h.click(x, 0);
274 assert_eq!(h.app().chosen, 1, "column {x} belongs to Grid");
275 h.click(0, 0);
276 assert_eq!(h.app().chosen, 0, "column 0 belongs to List");
277 }
278 }
279
280 #[test]
281 fn the_pillar_marks_the_hovered_segment_in_its_own_first_cell() {
282 let mut h = Harness::new(Demo { chosen: 0 }, 30, 1);
283 h.hover(12, 0);
284 assert_eq!(h.screen(), " List ▌ Grid Tree\n");
285 }
286}