Skip to main content

rust_pixel/ui/components/
present_list.rs

1// RustPixel UI Framework - PresentList Component
2// copyright zipxing@hotmail.com 2022~2026
3
4//! Lightweight read-only list widget for presentation/display purposes.
5//!
6//! Supports nested items with depth-based indentation, emoji bullets for
7//! unordered lists, and numbered prefixes for ordered lists. No interaction
8//! (no selection, scrolling, or keyboard handling).
9
10use crate::context::Context;
11use crate::render::Buffer;
12use crate::render::style::{Color, Style};
13use crate::util::Rect;
14use crate::ui::{
15    Widget, BaseWidget, WidgetId, WidgetState, UIEvent, UIResult,
16    next_widget_id,
17};
18use crate::impl_widget_base;
19
20/// A single item in a PresentList.
21#[derive(Debug, Clone)]
22pub struct PresentListItem {
23    pub text: String,
24    pub depth: u8,
25    pub ordered: bool,
26    pub index: usize,
27}
28
29impl PresentListItem {
30    pub fn new(text: &str) -> Self {
31        Self {
32            text: text.to_string(),
33            depth: 0,
34            ordered: false,
35            index: 1,
36        }
37    }
38
39    pub fn with_depth(mut self, depth: u8) -> Self {
40        self.depth = depth;
41        self
42    }
43
44    pub fn with_ordered(mut self, ordered: bool, index: usize) -> Self {
45        self.ordered = ordered;
46        self.index = index;
47        self
48    }
49}
50
51/// Emoji markers used for unordered list bullets at different depths.
52/// In terminal mode, use single-width Unicode symbols instead of emoji.
53#[cfg(graphics_mode)]
54pub const DEFAULT_MARKERS: [&str; 3] = ["🟢", "🔵", "🟡"];
55#[cfg(not(graphics_mode))]
56pub const DEFAULT_MARKERS: [&str; 3] = ["◆", "●", "◇"];
57
58/// Default marker style (half-scale emoji in GPU mode, normal in terminal).
59pub fn default_marker_style() -> Style {
60    if cfg!(graphics_mode) {
61        Style::default().fg(Color::Cyan).scale(0.5, 0.5)
62    } else {
63        Style::default().fg(Color::Cyan)
64    }
65}
66
67/// Lightweight read-only list widget with emoji bullets and nested indentation.
68pub struct PresentList {
69    base: BaseWidget,
70    items: Vec<PresentListItem>,
71    prefix_style: Style,
72    text_style: Style,
73    marker_style: Style,
74    markers: [String; 3],
75}
76
77impl Default for PresentList {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl PresentList {
84    pub fn new() -> Self {
85        Self {
86            base: BaseWidget::new(next_widget_id()),
87            items: Vec::new(),
88            prefix_style: Style::default().fg(Color::Cyan),
89            text_style: Style::default().fg(Color::White),
90            marker_style: default_marker_style(),
91            markers: DEFAULT_MARKERS.map(|s| s.to_string()),
92        }
93    }
94
95    pub fn with_items(mut self, items: Vec<PresentListItem>) -> Self {
96        self.items = items;
97        self
98    }
99
100    pub fn with_prefix_style(mut self, style: Style) -> Self {
101        self.prefix_style = style;
102        self
103    }
104
105    pub fn with_text_style(mut self, style: Style) -> Self {
106        self.text_style = style;
107        self
108    }
109
110    pub fn with_marker_style(mut self, style: Style) -> Self {
111        self.marker_style = style;
112        self
113    }
114
115    pub fn with_markers(mut self, markers: [String; 3]) -> Self {
116        self.markers = markers;
117        self
118    }
119
120    pub fn set_items(&mut self, items: Vec<PresentListItem>) {
121        self.items = items;
122        self.mark_dirty();
123    }
124
125    pub fn items(&self) -> &[PresentListItem] {
126        &self.items
127    }
128
129    fn render_item(&self, buf: &mut Buffer, x: u16, y: u16, item: &PresentListItem) {
130        let indent_width = item.depth as u16 * 2;
131        let indent = "  ".repeat(item.depth as usize);
132
133        if item.ordered {
134            let bullet = format!("{}{}. ", indent, item.index);
135            let w = bullet.len() as u16;
136            buf.set_string(x, y, &bullet, self.prefix_style);
137            buf.set_string(x + w, y, &item.text, self.text_style);
138        } else {
139            let marker_idx = (item.depth as usize).min(self.markers.len() - 1);
140            let marker = &self.markers[marker_idx];
141            buf.set_string(x, y, &indent, self.prefix_style);
142            buf.set_string(x + indent_width, y, marker, self.marker_style);
143            // GPU: emoji(2 cells) + space(1 cell) = 3
144            // Terminal: symbol(1 cell) + space(1 cell) = 2
145            let marker_offset: u16 = if cfg!(graphics_mode) { 3 } else { 2 };
146            buf.set_string(x + indent_width + marker_offset, y, &item.text, self.text_style);
147        }
148    }
149}
150
151impl Widget for PresentList {
152    impl_widget_base!(PresentList, base);
153
154    fn render(&self, buffer: &mut Buffer, _ctx: &Context) -> UIResult<()> {
155        if !self.state().visible {
156            return Ok(());
157        }
158        let bounds = self.bounds();
159        if bounds.width == 0 || bounds.height == 0 {
160            return Ok(());
161        }
162
163        let buffer_area = *buffer.area();
164        if bounds.y >= buffer_area.y + buffer_area.height
165            || bounds.x >= buffer_area.x + buffer_area.width
166        {
167            return Ok(());
168        }
169
170        for (i, item) in self.items.iter().enumerate() {
171            let y = bounds.y + i as u16;
172            if y >= bounds.y + bounds.height || y >= buffer_area.y + buffer_area.height {
173                break;
174            }
175            self.render_item(buffer, bounds.x, y, item);
176        }
177
178        Ok(())
179    }
180
181    fn handle_event(&mut self, _event: &UIEvent, _ctx: &mut Context) -> UIResult<bool> {
182        Ok(false)
183    }
184
185    fn preferred_size(&self, available: Rect) -> Rect {
186        let height = (self.items.len() as u16).min(available.height);
187        Rect::new(available.x, available.y, available.width, height)
188    }
189}