presentar_terminal/widgets/
layout.rs1use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Constraints, Event,
8 LayoutResult, Rect, Size, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy)]
15pub enum SizeSpec {
16 Fixed(f32),
18 Percent(f32),
20 Flex(f32),
22 Auto,
24}
25
26impl Default for SizeSpec {
27 fn default() -> Self {
28 Self::Flex(1.0)
29 }
30}
31
32pub struct LayoutItem {
34 widget: Box<dyn Widget>,
35 size: SizeSpec,
36}
37
38impl std::fmt::Debug for LayoutItem {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("LayoutItem")
41 .field("widget", &"..")
42 .field("size", &self.size)
43 .finish()
44 }
45}
46
47impl LayoutItem {
48 pub fn new(widget: impl Widget + 'static) -> Self {
50 Self {
51 widget: Box::new(widget),
52 size: SizeSpec::default(),
53 }
54 }
55
56 #[must_use]
58 pub fn fixed(mut self, size: f32) -> Self {
59 self.size = SizeSpec::Fixed(size);
60 self
61 }
62
63 #[must_use]
65 pub fn percent(mut self, pct: f32) -> Self {
66 self.size = SizeSpec::Percent(pct);
67 self
68 }
69
70 #[must_use]
72 pub fn expanded(mut self) -> Self {
73 self.size = SizeSpec::Flex(1.0);
74 self
75 }
76
77 #[must_use]
79 pub fn flex(mut self, weight: f32) -> Self {
80 self.size = SizeSpec::Flex(weight);
81 self
82 }
83
84 #[must_use]
86 pub fn auto(mut self) -> Self {
87 self.size = SizeSpec::Auto;
88 self
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Direction {
95 Vertical,
97 Horizontal,
99}
100
101pub struct Layout {
115 direction: Direction,
116 children: Vec<LayoutItem>,
117 bounds: Rect,
118 child_bounds: Vec<Rect>,
120}
121
122impl std::fmt::Debug for Layout {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.debug_struct("Layout")
125 .field("direction", &self.direction)
126 .field("children", &self.children.len())
127 .field("bounds", &self.bounds)
128 .field("child_bounds", &self.child_bounds)
129 .finish()
130 }
131}
132
133impl Layout {
134 pub fn rows(items: impl IntoIterator<Item = LayoutItem>) -> Self {
136 Self {
137 direction: Direction::Vertical,
138 children: items.into_iter().collect(),
139 bounds: Rect::default(),
140 child_bounds: Vec::new(),
141 }
142 }
143
144 pub fn columns(items: impl IntoIterator<Item = LayoutItem>) -> Self {
146 Self {
147 direction: Direction::Horizontal,
148 children: items.into_iter().collect(),
149 bounds: Rect::default(),
150 child_bounds: Vec::new(),
151 }
152 }
153
154 #[must_use]
156 pub fn into_item(self) -> LayoutItem {
157 LayoutItem::new(self)
158 }
159
160 pub fn push(&mut self, item: LayoutItem) {
162 self.children.push(item);
163 }
164
165 #[must_use]
167 pub fn len(&self) -> usize {
168 self.children.len()
169 }
170
171 #[must_use]
173 pub fn is_empty(&self) -> bool {
174 self.children.is_empty()
175 }
176
177 fn calculate_sizes(&self, total: f32) -> Vec<f32> {
179 let mut sizes = vec![0.0; self.children.len()];
180 let mut remaining = total;
181 let mut flex_total = 0.0;
182
183 for (i, item) in self.children.iter().enumerate() {
185 match item.size {
186 SizeSpec::Fixed(s) => {
187 sizes[i] = s.min(remaining);
188 remaining -= sizes[i];
189 }
190 SizeSpec::Percent(pct) => {
191 sizes[i] = (total * pct / 100.0).min(remaining);
192 remaining -= sizes[i];
193 }
194 SizeSpec::Auto => {
195 sizes[i] = 1.0_f32.min(remaining);
198 remaining -= sizes[i];
199 }
200 SizeSpec::Flex(weight) => {
201 flex_total += weight;
202 }
203 }
204 }
205
206 if flex_total > 0.0 && remaining > 0.0 {
208 for (i, item) in self.children.iter().enumerate() {
209 if let SizeSpec::Flex(weight) = item.size {
210 sizes[i] = remaining * (weight / flex_total);
211 }
212 }
213 }
214
215 sizes
216 }
217}
218
219impl Brick for Layout {
220 fn brick_name(&self) -> &'static str {
221 "layout"
222 }
223
224 fn assertions(&self) -> &[BrickAssertion] {
225 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
226 ASSERTIONS
227 }
228
229 fn budget(&self) -> BrickBudget {
230 BrickBudget::uniform(8)
231 }
232
233 fn verify(&self) -> BrickVerification {
234 BrickVerification {
235 passed: vec![BrickAssertion::max_latency_ms(8)],
236 failed: vec![],
237 verification_time: Duration::from_micros(10),
238 }
239 }
240
241 fn to_html(&self) -> String {
242 String::new()
243 }
244
245 fn to_css(&self) -> String {
246 String::new()
247 }
248}
249
250impl Widget for Layout {
251 fn type_id(&self) -> TypeId {
252 TypeId::of::<Self>()
253 }
254
255 fn measure(&self, constraints: Constraints) -> Size {
256 constraints.constrain(Size::new(constraints.max_width, constraints.max_height))
257 }
258
259 fn layout(&mut self, bounds: Rect) -> LayoutResult {
260 self.bounds = bounds;
261 self.child_bounds.clear();
262
263 let total = match self.direction {
264 Direction::Vertical => bounds.height,
265 Direction::Horizontal => bounds.width,
266 };
267
268 let sizes = self.calculate_sizes(total);
269 let mut offset = 0.0;
270
271 for (i, item) in self.children.iter_mut().enumerate() {
272 let size = sizes[i];
273 let child_bounds = match self.direction {
274 Direction::Vertical => Rect::new(bounds.x, bounds.y + offset, bounds.width, size),
275 Direction::Horizontal => {
276 Rect::new(bounds.x + offset, bounds.y, size, bounds.height)
277 }
278 };
279
280 self.child_bounds.push(child_bounds);
281 item.widget.layout(child_bounds);
282 offset += size;
283 }
284
285 LayoutResult {
286 size: Size::new(bounds.width, bounds.height),
287 }
288 }
289
290 fn paint(&self, canvas: &mut dyn Canvas) {
291 for (i, item) in self.children.iter().enumerate() {
292 if i < self.child_bounds.len() {
293 canvas.push_clip(self.child_bounds[i]);
295 item.widget.paint(canvas);
296 canvas.pop_clip();
297 }
298 }
299 }
300
301 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
302 for item in &mut self.children {
304 if let Some(result) = item.widget.event(event) {
305 return Some(result);
306 }
307 }
308 None
309 }
310
311 fn children(&self) -> &[Box<dyn Widget>] {
312 &[]
314 }
315
316 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
317 &mut []
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::widgets::Border;
325
326 #[test]
327 fn test_layout_rows_empty() {
328 let layout = Layout::rows([]);
329 assert!(layout.is_empty());
330 }
331
332 #[test]
333 fn test_layout_columns_empty() {
334 let layout = Layout::columns([]);
335 assert!(layout.is_empty());
336 }
337
338 #[test]
339 fn test_layout_item_fixed() {
340 let item = LayoutItem::new(Border::new()).fixed(10.0);
341 assert!(matches!(item.size, SizeSpec::Fixed(10.0)));
342 }
343
344 #[test]
345 fn test_layout_item_percent() {
346 let item = LayoutItem::new(Border::new()).percent(25.0);
347 assert!(matches!(item.size, SizeSpec::Percent(25.0)));
348 }
349
350 #[test]
351 fn test_layout_item_expanded() {
352 let item = LayoutItem::new(Border::new()).expanded();
353 assert!(matches!(item.size, SizeSpec::Flex(1.0)));
354 }
355
356 #[test]
357 fn test_layout_item_flex() {
358 let item = LayoutItem::new(Border::new()).flex(2.0);
359 assert!(matches!(item.size, SizeSpec::Flex(2.0)));
360 }
361
362 #[test]
363 fn test_layout_calculate_sizes_fixed() {
364 let layout = Layout::rows([
365 LayoutItem::new(Border::new()).fixed(10.0),
366 LayoutItem::new(Border::new()).fixed(20.0),
367 ]);
368 let sizes = layout.calculate_sizes(100.0);
369 assert_eq!(sizes[0], 10.0);
370 assert_eq!(sizes[1], 20.0);
371 }
372
373 #[test]
374 fn test_layout_calculate_sizes_percent() {
375 let layout = Layout::rows([
376 LayoutItem::new(Border::new()).percent(25.0),
377 LayoutItem::new(Border::new()).percent(75.0),
378 ]);
379 let sizes = layout.calculate_sizes(100.0);
380 assert_eq!(sizes[0], 25.0);
381 assert_eq!(sizes[1], 75.0);
382 }
383
384 #[test]
385 fn test_layout_calculate_sizes_flex() {
386 let layout = Layout::rows([
387 LayoutItem::new(Border::new()).flex(1.0),
388 LayoutItem::new(Border::new()).flex(3.0),
389 ]);
390 let sizes = layout.calculate_sizes(100.0);
391 assert_eq!(sizes[0], 25.0);
392 assert_eq!(sizes[1], 75.0);
393 }
394
395 #[test]
396 fn test_layout_calculate_sizes_mixed() {
397 let layout = Layout::rows([
398 LayoutItem::new(Border::new()).fixed(20.0),
399 LayoutItem::new(Border::new()).expanded(),
400 ]);
401 let sizes = layout.calculate_sizes(100.0);
402 assert_eq!(sizes[0], 20.0);
403 assert_eq!(sizes[1], 80.0);
404 }
405
406 #[test]
407 fn test_layout_nested() {
408 let inner = Layout::columns([
409 LayoutItem::new(Border::new()).percent(50.0),
410 LayoutItem::new(Border::new()).percent(50.0),
411 ]);
412 let outer = Layout::rows([
413 LayoutItem::new(Border::new()).fixed(5.0),
414 inner.into_item().expanded(),
415 ]);
416 assert_eq!(outer.len(), 2);
417 }
418
419 #[test]
420 fn test_layout_push() {
421 let mut layout = Layout::rows([]);
422 layout.push(LayoutItem::new(Border::new()));
423 assert_eq!(layout.len(), 1);
424 }
425
426 #[test]
427 fn test_layout_brick_name() {
428 let layout = Layout::rows([]);
429 assert_eq!(layout.brick_name(), "layout");
430 }
431
432 #[test]
433 fn test_layout_verify() {
434 let layout = Layout::rows([]);
435 assert!(layout.verify().is_valid());
436 }
437
438 #[test]
439 fn test_layout_measure() {
440 let layout = Layout::rows([]);
441 let size = layout.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
442 assert_eq!(size.width, 100.0);
443 assert_eq!(size.height, 50.0);
444 }
445
446 #[test]
447 fn test_layout_vertical_layout() {
448 let mut layout = Layout::rows([
449 LayoutItem::new(Border::new()).fixed(10.0),
450 LayoutItem::new(Border::new()).fixed(20.0),
451 ]);
452 layout.layout(Rect::new(0.0, 0.0, 80.0, 40.0));
453 assert_eq!(layout.child_bounds.len(), 2);
454 assert_eq!(layout.child_bounds[0].y, 0.0);
455 assert_eq!(layout.child_bounds[0].height, 10.0);
456 assert_eq!(layout.child_bounds[1].y, 10.0);
457 assert_eq!(layout.child_bounds[1].height, 20.0);
458 }
459
460 #[test]
461 fn test_layout_horizontal_layout() {
462 let mut layout = Layout::columns([
463 LayoutItem::new(Border::new()).fixed(20.0),
464 LayoutItem::new(Border::new()).fixed(30.0),
465 ]);
466 layout.layout(Rect::new(0.0, 0.0, 80.0, 40.0));
467 assert_eq!(layout.child_bounds.len(), 2);
468 assert_eq!(layout.child_bounds[0].x, 0.0);
469 assert_eq!(layout.child_bounds[0].width, 20.0);
470 assert_eq!(layout.child_bounds[1].x, 20.0);
471 assert_eq!(layout.child_bounds[1].width, 30.0);
472 }
473
474 #[test]
475 fn test_size_spec_default() {
476 let spec = SizeSpec::default();
477 assert!(matches!(spec, SizeSpec::Flex(1.0)));
478 }
479
480 #[test]
481 fn test_layout_item_auto() {
482 let item = LayoutItem::new(Border::new()).auto();
483 assert!(matches!(item.size, SizeSpec::Auto));
484 }
485
486 #[test]
487 fn test_layout_calculate_sizes_auto() {
488 let layout = Layout::rows([
489 LayoutItem::new(Border::new()).auto(),
490 LayoutItem::new(Border::new()).expanded(),
491 ]);
492 let sizes = layout.calculate_sizes(100.0);
493 assert_eq!(sizes[0], 1.0);
495 assert_eq!(sizes[1], 99.0);
497 }
498
499 #[test]
500 fn test_layout_brick_assertions() {
501 let layout = Layout::rows([]);
502 let assertions = layout.assertions();
503 assert!(!assertions.is_empty());
504 }
505
506 #[test]
507 fn test_layout_brick_budget() {
508 let layout = Layout::rows([]);
509 let budget = layout.budget();
510 assert_eq!(budget.total_ms, 8);
511 }
512
513 #[test]
514 fn test_layout_to_html() {
515 let layout = Layout::rows([]);
516 assert_eq!(layout.to_html(), "");
517 }
518
519 #[test]
520 fn test_layout_to_css() {
521 let layout = Layout::rows([]);
522 assert_eq!(layout.to_css(), "");
523 }
524
525 #[test]
526 fn test_layout_type_id() {
527 let layout = Layout::rows([]);
528 let id = Widget::type_id(&layout);
529 assert_eq!(id, TypeId::of::<Layout>());
530 }
531
532 #[test]
533 fn test_layout_paint() {
534 use crate::tools::HeadlessCanvas;
535 let mut layout = Layout::rows([
536 LayoutItem::new(Border::new()).fixed(10.0),
537 LayoutItem::new(Border::new()).fixed(10.0),
538 ]);
539 layout.layout(Rect::new(0.0, 0.0, 80.0, 24.0));
540 let mut canvas = HeadlessCanvas::new(80, 24);
541 layout.paint(&mut canvas);
542 }
544
545 #[test]
546 fn test_layout_event() {
547 use presentar_core::Event;
548 let mut layout = Layout::rows([LayoutItem::new(Border::new()).fixed(10.0)]);
549 let event = Event::Resize {
550 width: 80.0,
551 height: 24.0,
552 };
553 let result = layout.event(&event);
554 assert!(result.is_none()); }
556
557 #[test]
558 fn test_layout_children() {
559 let layout = Layout::rows([LayoutItem::new(Border::new()).fixed(10.0)]);
560 assert!(layout.children().is_empty());
562 }
563
564 #[test]
565 fn test_layout_children_mut() {
566 let mut layout = Layout::rows([LayoutItem::new(Border::new()).fixed(10.0)]);
567 assert!(layout.children_mut().is_empty());
569 }
570}