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