1use std::time::Duration;
9
10use crate::event::{Event, MouseButton, MouseKind};
11use crate::geometry::{Rect, Size, clamp_u16};
12use crate::keymap::{Key, Modifiers};
13use crate::motion::{Easing, Tween, steps};
14use crate::style::CellStyle;
15use crate::text;
16use crate::theme::State;
17use crate::widget::{EventCx, MeasureCx, Node, PaintCx};
18
19use super::cells;
20use super::row;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Section {
27 title: String,
28 icon: Option<String>,
29 detail: Option<String>,
30}
31
32impl Section {
33 #[must_use]
35 pub fn new(title: impl Into<String>) -> Self {
36 Self { title: title.into(), icon: None, detail: None }
37 }
38
39 #[must_use]
41 pub fn icon(mut self, key: impl Into<String>) -> Self {
42 self.icon = Some(key.into());
43 self
44 }
45
46 #[must_use]
48 pub fn detail(mut self, detail: impl Into<String>) -> Self {
49 self.detail = Some(detail.into());
50 self
51 }
52}
53
54impl From<&str> for Section {
55 fn from(title: &str) -> Self {
56 Self::new(title)
57 }
58}
59
60impl From<String> for Section {
61 fn from(title: String) -> Self {
62 Self::new(title)
63 }
64}
65
66pub(crate) type ToggleSection<Msg> = Box<dyn Fn(usize, bool) -> Msg>;
68
69pub(crate) type MoveSection<Msg> = Box<dyn Fn(usize, usize) -> Msg>;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub(crate) enum Flow {
75 Natural,
77 Share,
79}
80
81const DRAG_THRESHOLD: i32 = 1;
83
84pub(crate) struct Sections<Msg> {
85 pub(crate) sections: Vec<Section>,
86 pub(crate) open: Vec<bool>,
87 pub(crate) bodies: Vec<Node<Msg>>,
88 pub(crate) on_toggle: Option<ToggleSection<Msg>>,
89 pub(crate) on_move: Option<MoveSection<Msg>>,
90 pub(crate) single: bool,
91}
92
93#[derive(Debug, Default)]
94struct SectionsMemory {
95 cursor: usize,
96 titles: Vec<Rect>,
98 blocks: Vec<(i32, i32)>,
100 pressed: Option<(usize, i32)>,
101 drag: Option<Drag>,
102 reveal: Vec<(String, Tween)>,
104}
105
106#[derive(Debug, Clone, Copy)]
107struct Drag {
108 from: usize,
109 pointer: i32,
110 target: usize,
111}
112
113enum Entry {
115 Section(usize),
116 Drop,
117}
118
119impl<Msg: 'static> Sections<Msg> {
120 pub(crate) fn new(sections: Vec<Section>) -> Self {
121 Self { sections, open: Vec::new(), bodies: Vec::new(), on_toggle: None, on_move: None, single: false }
122 }
123
124 fn is_open(&self, index: usize) -> bool {
125 self.open.get(index).copied().unwrap_or(false)
126 }
127
128 fn body(&self, index: usize) -> Option<&Node<Msg>> {
129 self.bodies.get(index)
130 }
131
132 fn gap(env: &crate::env::Env) -> u16 {
134 env.theme().style("section", None, &[]).cells("gap").unwrap_or(1)
135 }
136
137 fn natural(&self, cx: &mut MeasureCx<'_>, index: usize, width: u16) -> u16 {
139 let padding = crate::style::WidgetStyle::new(cx.env().theme().style("section-body", None, &[]), 0.0).padding();
140 let inner = Size::new(width.saturating_sub(padding.horizontal()), u16::MAX);
141 let content = self.body(index).map_or(0, |body| cx.measure_child(body, inner).height);
142 content.saturating_add(padding.vertical())
143 }
144
145 pub(crate) fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
146 if self.sections.is_empty() {
147 return Size::default();
148 }
149 let count = u16::try_from(self.sections.len()).unwrap_or(u16::MAX);
150 let mut height = count.saturating_add(Self::gap(cx.env()).saturating_mul(count - 1));
151 let mut width = 0;
152 for (index, section) in self.sections.iter().enumerate() {
153 let detail = section.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
154 let icon = if section.icon.is_some() { 2 } else { 0 };
155 width = width.max(cells::sum([text::width(§ion.title), detail, icon, 7]));
156 if self.is_open(index) {
157 height = height.saturating_add(self.natural(cx, index, available.width));
158 if let Some(body) = self.body(index) {
159 width = width.max(cx.measure_child(body, available).width + 4);
160 }
161 }
162 }
163 Size::new(width, height).min(available)
164 }
165
166 fn progress(&self, cx: &mut PaintCx<'_>, index: usize, flow: Flow) -> f32 {
169 let target = if self.is_open(index) { 1.0 } else { 0.0 };
170 if cx.reduced_motion() {
171 return target;
172 }
173 let now = cx.now();
174 let duration = cx.env().theme().motion().enter * 2;
175 let title = self.sections[index].title.clone();
176 let memory = cx.memory::<SectionsMemory>();
177 let tween = match memory.reveal.iter_mut().find(|(key, _)| *key == title) {
178 Some((_, tween)) => tween,
179 None => {
180 memory.reveal.push((title, Tween::settled(target)));
181 &mut memory.reveal.last_mut().expect("a tween was just pushed").1
182 }
183 };
184 if (tween.target() - target).abs() > f32::EPSILON {
185 if flow == Flow::Natural && target < 0.5 {
186 *tween = Tween::settled(0.0);
187 } else {
188 tween.retarget(target, now, duration, Easing::EaseOut);
189 }
190 }
191 let (value, running) = (tween.value(now), tween.is_running(now));
192 if running {
193 cx.request_frame_in(Duration::from_millis(16));
194 }
195 value
196 }
197
198 pub(crate) fn paint(&self, cx: &mut PaintCx<'_>, area: Rect, flow: Flow) {
199 let count = self.sections.len();
200 let gap = Self::gap(cx.env());
201 let body_style = cx.style("section-body", None, &[]);
202 let padding = body_style.padding();
203 let body_bg = body_style.text().bg;
204 let (cursor, drag) = {
205 let memory = cx.memory::<SectionsMemory>();
206 forget_gone(&mut memory.reveal, &self.sections);
207 memory.cursor = memory.cursor.min(count.saturating_sub(1));
208 (memory.cursor, memory.drag.filter(|drag| drag.from < count))
209 };
210
211 let progress: Vec<f32> = (0..count).map(|index| self.progress(cx, index, flow)).collect();
212 let naturals: Vec<u16> = (0..count)
213 .map(|index| {
214 let showing = self.is_open(index) || progress[index] > 0.0;
215 if showing { self.natural(&mut MeasureCx::new(cx.env()), index, area.width) } else { 0 }
216 })
217 .collect();
218 let allotted = match flow {
219 Flow::Natural => naturals.clone(),
220 Flow::Share => {
221 let titles = u16::try_from(count).unwrap_or(u16::MAX);
222 let gaps = gap.saturating_mul(titles.saturating_sub(1));
223 let mut wants = naturals.clone();
225 if let Some(drag) = drag {
226 wants[drag.from] = 0;
227 }
228 share(&wants, area.height.saturating_sub(titles + gaps))
229 }
230 };
231
232 let entries: Vec<Entry> = match drag {
233 Some(drag) => {
234 let mut order: Vec<Entry> = (0..count).filter(|i| *i != drag.from).map(Entry::Section).collect();
235 order.insert(drag.target.min(order.len()), Entry::Drop);
236 order
237 }
238 None => (0..count).map(Entry::Section).collect(),
239 };
240
241 let focused = cx.is_focused();
242 let pointer = cx.pointer();
243 let mut titles = vec![Rect::default(); count];
244 let mut blocks = vec![(0, 0); count];
245 let mut y = area.y;
246 for (position, entry) in entries.iter().enumerate() {
247 if position > 0 {
248 y += i32::from(gap);
249 }
250 let row = Rect::new(area.x, y, area.width, 1);
251 let index = match entry {
252 Entry::Drop => {
253 let color = cx.style("section-drop", None, &[]).text().bg.unwrap_or_else(|| cx.color("active"));
254 cx.clear(row, color);
255 y += 1;
256 continue;
257 }
258 Entry::Section(index) => *index,
259 };
260 let hovered = drag.is_none() && pointer.is_some_and(|(px, py)| row.contains(px, py));
261 self.paint_title(cx, row, index, hovered, focused && cursor == index, false);
262 cx.register_hit(row);
263 titles[index] = row;
264 let visible = steps(progress[index], allotted[index]);
265 let top = y;
266 y += 1;
267 if visible > 0 {
268 let body = Rect::new(area.x, y, area.width, visible);
269 if let Some(bg) = body_bg {
270 cx.clear(body, bg);
271 }
272 if let Some(node) = self.body(index) {
273 let natural = naturals[index].max(allotted[index]);
274 let full = Rect::new(area.x, y, area.width, natural).inset(padding);
275 let inner_height = allotted[index].saturating_sub(padding.vertical());
276 let content = Rect::new(full.x, full.y, full.width, inner_height);
277 cx.with_clip(body, |cx| cx.paint_child(node, content));
278 }
279 y += i32::from(visible);
280 }
281 blocks[index] = (top, y);
282 }
283
284 if let Some(drag) = drag {
285 let ghost_y = drag.pointer.clamp(area.y, last_row(area));
286 self.paint_title(cx, Rect::new(area.x, ghost_y, area.width, 1), drag.from, false, false, true);
287 } else {
288 let memory = cx.memory::<SectionsMemory>();
289 memory.titles = titles;
290 memory.blocks = blocks;
291 }
292 }
293
294 fn paint_title(&self, cx: &mut PaintCx<'_>, row: Rect, index: usize, hovered: bool, cursor: bool, ghost: bool) {
295 let section = &self.sections[index];
296 let open = self.is_open(index);
297 let mut states = Vec::new();
298 if hovered {
299 states.push(State::Hover);
300 }
301 if cursor {
302 states.push(State::Focus);
303 }
304 if open {
305 states.push(State::Checked);
306 }
307 let variant = ghost.then_some("ghost");
308 let style = cx.style("section-title", variant, &states);
309 let title_style = CellStyle { bg: None, ..style.text() };
310 cx.clear(row, style.text().bg.unwrap_or_else(|| cx.color("raised")));
312 let slide = cx.env().slide() && (hovered || cursor || ghost);
313 let detail_width = section.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
314 let show_detail = detail_width > 0 && row.width.saturating_sub(5) > detail_width.saturating_add(8);
316 let chevron_key = if open { "section-open" } else { "section-closed" };
317 let chevron = cx.env().icons().glyph(chevron_key).into_owned();
318 let chevron_style = CellStyle { bg: None, ..cx.style("section-chevron", None, &states).text() };
319 let icon: Vec<row::Mark> =
320 section.icon.iter().map(|icon| (cx.env().icons().glyph(icon).into_owned(), title_style)).collect();
321 let parts = row::Parts {
323 fixed: &[(chevron, chevron_style)],
324 sliding: &icon,
325 label: §ion.title,
326 trailing: 1 + if show_detail { detail_width } else { 0 },
327 indent: 0,
328 };
329 row::paint_parts(cx, row, &style, slide, &parts);
330 if let (true, Some(detail)) = (show_detail, §ion.detail) {
331 let detail_style = cx.style("section-detail", None, &states).text();
332 let width = text::width(detail);
333 cx.text(row.right() - 2 - i32::from(width), row.y, detail, CellStyle { bg: None, ..detail_style }, width);
334 }
335 }
336
337 fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
338 let Some(message) = &self.on_toggle else {
339 return;
340 };
341 let open = !self.is_open(index);
342 if open && self.single {
343 for other in (0..self.sections.len()).filter(|i| *i != index && self.is_open(*i)) {
344 cx.emit(message(other, false));
345 }
346 }
347 cx.emit(message(index, open));
348 }
349
350 fn move_section(&self, cx: &mut EventCx<'_, Msg>, from: usize, to: usize) {
351 if let Some(message) = &self.on_move
352 && from != to
353 && to < self.sections.len()
354 {
355 cx.memory::<SectionsMemory>().cursor = to;
356 cx.emit(message(from, to));
357 }
358 }
359
360 pub(crate) fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
361 let count = self.sections.len();
362 if count == 0 {
363 return false;
364 }
365 match event {
366 Event::Key(key) => {
367 let cursor = cx.memory::<SectionsMemory>().cursor.min(count - 1);
368 let reorder = Modifiers { ctrl: true, shift: true, alt: false };
369 if self.on_move.is_some() && key.chord.mods == reorder {
370 let to = match key.chord.key {
371 Key::Up => cursor.checked_sub(1),
372 Key::Down => Some(cursor + 1).filter(|to| *to < count),
373 _ => return false,
374 };
375 if let Some(to) = to {
376 self.move_section(cx, cursor, to);
377 }
378 return true;
379 }
380 let target = if key.is_plain(Key::Up) {
381 cursor.checked_sub(1)
382 } else if key.is_plain(Key::Down) {
383 Some(cursor + 1).filter(|next| *next < count)
384 } else if key.is_plain(Key::Home) {
385 Some(0)
386 } else if key.is_plain(Key::End) {
387 Some(count - 1)
388 } else if key.is_plain(Key::Enter) || key.is_plain(Key::Space) {
389 if self.on_toggle.is_none() {
390 return false;
391 }
392 self.toggle(cx, cursor);
393 return true;
394 } else {
395 return false;
396 };
397 match target {
398 Some(target) => {
399 cx.memory::<SectionsMemory>().cursor = target;
400 true
401 }
402 None => false,
403 }
404 }
405 Event::Mouse(mouse) => self.mouse(cx, mouse.kind, mouse.x, mouse.y),
406 _ => false,
407 }
408 }
409
410 fn mouse(&self, cx: &mut EventCx<'_, Msg>, kind: MouseKind, x: i32, y: i32) -> bool {
411 let area = cx.area();
412 let memory = cx.memory::<SectionsMemory>();
413 let under = memory.titles.iter().position(|row| row.contains(x, y));
414 match kind {
415 MouseKind::Down(MouseButton::Left) => {
416 let Some(index) = under else {
417 return false;
418 };
419 memory.cursor = index;
420 memory.pressed = Some((index, y));
421 cx.capture_pointer();
422 true
423 }
424 MouseKind::Drag(MouseButton::Left) => {
425 let Some((from, start)) = memory.pressed else {
426 return false;
427 };
428 if self.on_move.is_none() {
429 return true;
430 }
431 if memory.drag.is_none() && (y - start).abs() < DRAG_THRESHOLD {
432 return true;
433 }
434 let target = memory
435 .blocks
436 .iter()
437 .enumerate()
438 .filter(|(index, (top, bottom))| *index != from && (top + bottom) / 2 < y)
439 .count();
440 memory.drag = Some(Drag { from, pointer: y.clamp(area.y, last_row(area)), target });
441 true
442 }
443 MouseKind::Up(MouseButton::Left) => {
444 let pressed = memory.pressed.take();
445 let drag = memory.drag.take();
446 match (pressed, drag) {
447 (_, Some(drag)) => self.move_section(cx, drag.from, drag.target),
448 (Some((index, _)), None) if under == Some(index) => self.toggle(cx, index),
449 _ => {}
450 }
451 pressed.is_some()
452 }
453 _ => false,
454 }
455 }
456}
457
458fn forget_gone(reveal: &mut Vec<(String, Tween)>, sections: &[Section]) {
461 reveal.retain(|(title, _)| sections.iter().any(|section| section.title == *title));
462}
463
464fn last_row(area: Rect) -> i32 {
467 (area.bottom() - 1).max(area.y)
468}
469
470fn share(wants: &[u16], available: u16) -> Vec<u16> {
473 let mut given = vec![0; wants.len()];
474 let mut order: Vec<usize> = (0..wants.len()).filter(|i| wants[*i] > 0).collect();
475 order.sort_by_key(|i| wants[*i]);
476 let mut left = available;
477 let mut remaining = clamp_u16(i32::try_from(order.len()).unwrap_or(i32::MAX));
478 for index in order {
479 let fair = left / remaining.max(1);
480 given[index] = wants[index].min(fair);
481 left -= given[index];
482 remaining -= 1;
483 }
484 given
485}
486
487#[cfg(test)]
488mod tests {
489 use super::{Section, Tween, forget_gone, share};
490
491 #[test]
492 fn small_bodies_keep_their_height_and_large_ones_split_the_rest() {
493 assert_eq!(share(&[3, 0, 20, 20], 21), vec![3, 0, 9, 9]);
494 assert_eq!(share(&[3, 4], 30), vec![3, 4]);
495 assert_eq!(share(&[10, 10, 10], 10), vec![3, 3, 4]);
496 assert_eq!(share(&[], 10), Vec::<u16>::new());
497 }
498
499 #[test]
500 fn progress_of_sections_that_are_gone_is_forgotten() {
501 let mut reveal = vec![("Git".to_owned(), Tween::settled(1.0)), ("Old name".to_owned(), Tween::settled(0.0))];
502 forget_gone(&mut reveal, &[Section::new("Git"), Section::new("Ports")]);
503 let titles: Vec<&str> = reveal.iter().map(|(title, _)| title.as_str()).collect();
504 assert_eq!(titles, ["Git"]);
505 }
506}