1extern crate alloc;
21
22use alloc::string::String;
23use alloc::vec::Vec;
24
25use rlvgl_core::draw::draw_widget_bg;
26use rlvgl_core::event::Event;
27use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
28use rlvgl_core::renderer::Renderer;
29use rlvgl_core::style::Style;
30use rlvgl_core::widget::{Color, Rect, Widget};
31
32pub const DEFAULT_HEADER_HEIGHT: i32 = 24;
38const TITLE_PAD_X: i32 = 6;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct WindowButtonId(pub u16);
50
51struct HeaderButtonDesc {
56 icon: Option<String>,
58 width: i32,
60}
61
62pub struct Window {
72 bounds: Rect,
74 title: String,
76 header_height: i32,
78 buttons: Vec<HeaderButtonDesc>,
80 next_btn_id: u16,
82 last_pressed: Option<WindowButtonId>,
84 pub style: Style,
86 pub header_color: Color,
88 pub title_color: Color,
90 pub button_color: Color,
92 pub button_text_color: Color,
94 font: WidgetFont,
97}
98
99impl Window {
100 pub fn new(bounds: Rect, header_height: i32) -> Self {
104 let header_height = header_height.max(0);
105 Self {
106 bounds,
107 title: String::new(),
108 header_height,
109 buttons: Vec::new(),
110 next_btn_id: 0,
111 last_pressed: None,
112 style: Style::default(),
113 header_color: Color(50, 50, 80, 255),
114 title_color: Color(240, 240, 240, 255),
115 button_color: Color(70, 70, 100, 255),
116 button_text_color: Color(240, 240, 240, 255),
117 font: WidgetFont::new(),
118 }
119 }
120
121 pub fn set_title(&mut self, text: &str) {
123 self.title = String::from(text);
124 }
125
126 pub fn title(&self) -> &str {
128 &self.title
129 }
130
131 pub fn add_header_button(&mut self, icon: Option<&str>, width: i32) -> WindowButtonId {
136 let id = WindowButtonId(self.next_btn_id);
137 self.next_btn_id = self.next_btn_id.saturating_add(1);
138 self.buttons.push(HeaderButtonDesc {
139 icon: icon.map(String::from),
140 width: width.max(0),
141 });
142 id
143 }
144
145 pub fn last_button_pressed(&mut self) -> Option<WindowButtonId> {
149 self.last_pressed.take()
150 }
151
152 pub fn header_bounds(&self) -> Rect {
154 Rect {
155 x: self.bounds.x,
156 y: self.bounds.y,
157 width: self.bounds.width,
158 height: self.header_height,
159 }
160 }
161
162 pub fn content_bounds(&self) -> Rect {
164 let remaining = (self.bounds.height - self.header_height).max(0);
165 Rect {
166 x: self.bounds.x,
167 y: self.bounds.y + self.header_height,
168 width: self.bounds.width,
169 height: remaining,
170 }
171 }
172
173 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
176 self.font.set(font);
177 }
178
179 pub fn set_header_height(&mut self, h: i32) {
181 self.header_height = h.max(0);
182 }
183
184 fn draw_header(&self, renderer: &mut dyn Renderer) {
190 let header = self.header_bounds();
191 if header.width <= 0 || header.height <= 0 {
192 return;
193 }
194 if self.header_color.3 > 0 {
196 renderer.fill_rect(header, self.header_color);
197 }
198 let total_btn_width: i32 = self.buttons.iter().map(|b| b.width).sum();
200 let title_w = (header.width - total_btn_width - TITLE_PAD_X * 2).max(0);
201
202 if !self.title.is_empty() && title_w > 0 && self.title_color.3 > 0 {
204 let font = self.font.resolve();
205 let metrics = rlvgl_core::font::FontMetrics::line_metrics(font);
206 let baseline =
207 header.y + metrics.ascent as i32 + (header.height - metrics.line_height as i32) / 2;
208 let shaped = shape_text_ltr(font, &self.title, (header.x + TITLE_PAD_X, baseline), 0);
209 renderer.draw_text_shaped(&shaped, (0, 0), self.title_color);
210 }
211
212 let mut btn_right = header.x + header.width;
214 for btn in self.buttons.iter().rev() {
215 let bx = btn_right - btn.width;
216 let btn_rect = Rect {
217 x: bx,
218 y: header.y,
219 width: btn.width,
220 height: header.height,
221 };
222 if self.button_color.3 > 0 {
223 renderer.fill_rect(btn_rect, self.button_color);
224 }
225 if let Some(icon) = &btn.icon
226 && self.button_text_color.3 > 0
227 {
228 let font = self.font.resolve();
229 let metrics = rlvgl_core::font::FontMetrics::line_metrics(font);
230 let baseline = header.y
231 + metrics.ascent as i32
232 + (header.height - metrics.line_height as i32) / 2;
233 let shaped = shape_text_ltr(font, icon, (bx + 2, baseline), 0);
234 renderer.draw_text_shaped(&shaped, (0, 0), self.button_text_color);
235 }
236 btn_right = bx;
237 }
238 }
239}
240
241impl Widget for Window {
242 fn bounds(&self) -> Rect {
243 self.bounds
244 }
245
246 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
247 Some(&mut self.font)
248 }
249
250 fn set_bounds(&mut self, bounds: Rect) {
251 self.bounds = bounds;
252 }
255
256 fn draw(&self, renderer: &mut dyn Renderer) {
257 if self.bounds.width <= 0 || self.bounds.height <= 0 {
258 return;
259 }
260 draw_widget_bg(renderer, self.bounds, &self.style);
262 let content = self.content_bounds();
264 if content.width > 0 && content.height > 0 && self.style.bg_color.3 > 0 {
265 renderer.fill_rect(content, self.style.bg_color);
266 }
267 self.draw_header(renderer);
269 }
270
271 fn handle_event(&mut self, _event: &Event) -> bool {
272 false
273 }
274}
275
276#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
285 Rect {
286 x,
287 y,
288 width: w,
289 height: h,
290 }
291 }
292
293 struct NullRenderer;
294 impl rlvgl_core::renderer::Renderer for NullRenderer {
295 fn fill_rect(&mut self, _r: Rect, _c: Color) {}
296 fn draw_text(&mut self, _pos: (i32, i32), _t: &str, _c: Color) {}
297 }
298
299 #[test]
300 fn header_and_content_bounds_split_outer_rect() {
301 let w = Window::new(rect(0, 0, 200, 120), 24);
302 assert_eq!(w.header_bounds(), rect(0, 0, 200, 24));
303 assert_eq!(w.content_bounds(), rect(0, 24, 200, 96));
304 }
305
306 #[test]
307 fn set_title_and_retrieve() {
308 let mut w = Window::new(rect(0, 0, 200, 120), 24);
309 w.set_title("Settings");
310 assert_eq!(w.title(), "Settings");
311 }
312
313 #[test]
314 fn set_header_height_adjusts_content() {
315 let mut w = Window::new(rect(0, 0, 200, 120), 24);
316 w.set_header_height(40);
317 assert_eq!(w.header_bounds().height, 40);
318 assert_eq!(w.content_bounds().y, 40);
319 assert_eq!(w.content_bounds().height, 80);
320 }
321
322 #[test]
323 fn add_header_button_returns_sequential_ids() {
324 let mut w = Window::new(rect(0, 0, 200, 120), 24);
325 let a = w.add_header_button(Some("X"), 20);
326 let b = w.add_header_button(None, 20);
327 assert_eq!(a, WindowButtonId(0));
328 assert_eq!(b, WindowButtonId(1));
329 }
330
331 #[test]
332 fn last_button_pressed_drains_slot() {
333 let mut w = Window::new(rect(0, 0, 200, 120), 24);
334 w.last_pressed = Some(WindowButtonId(0));
336 assert_eq!(w.last_button_pressed(), Some(WindowButtonId(0)));
337 assert_eq!(w.last_button_pressed(), None);
339 }
340
341 #[test]
342 fn set_bounds_updates_outer_rect() {
343 let mut w = Window::new(rect(0, 0, 200, 120), 24);
344 w.set_bounds(rect(10, 10, 300, 200));
345 assert_eq!(w.bounds(), rect(10, 10, 300, 200));
346 assert_eq!(w.header_bounds(), rect(10, 10, 300, 24));
347 assert_eq!(w.content_bounds().y, 34);
348 }
349
350 #[test]
351 fn zero_area_window_draws_without_panic() {
352 let w = Window::new(rect(0, 0, 0, 0), 24);
353 let mut r = NullRenderer;
354 w.draw(&mut r);
355 }
356
357 #[test]
358 fn content_area_does_not_go_negative_when_header_taller_than_bounds() {
359 let w = Window::new(rect(0, 0, 100, 10), 24);
360 let content = w.content_bounds();
361 assert_eq!(content.height, 0); }
363}