1use futtl::Futtl;
2use std::ops::{Add, Sub};
3
4pub const SINGLE_BORDER: BorderStyle = BorderStyle {
5 top: '─' as i32,
6 bottom: '─' as i32,
7 left: '│' as i32,
8 right: '│' as i32,
9 top_left: '┌' as i32,
10 top_right: '┐' as i32,
11 bottom_right: '┘' as i32,
12 bottom_left: '└' as i32,
13};
14
15pub const DOUBLE_BORDER: BorderStyle = BorderStyle {
16 top: '═' as i32,
17 bottom: '═' as i32,
18 left: '║' as i32,
19 right: '║' as i32,
20 top_left: '╔' as i32,
21 top_right: '╗' as i32,
22 bottom_right: '╝' as i32,
23 bottom_left: '╚' as i32,
24};
25
26pub const NO_BORDER: BorderStyle = BorderStyle {
27 top: '\0' as i32,
28 bottom: '\0' as i32,
29 left: '\0' as i32,
30 right: '\0' as i32,
31 top_left: '\0' as i32,
32 top_right: '\0' as i32,
33 bottom_right: '\0' as i32,
34 bottom_left: '\0' as i32,
35};
36
37pub struct IdGenerator {
38 next: u32,
39}
40
41impl Default for IdGenerator {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl IdGenerator {
48 pub fn new() -> Self {
49 Self { next: 0 }
50 }
51
52 pub fn next(&mut self) -> u32 {
53 let id = self.next;
54 self.next += 1;
55 id
56 }
57}
58
59#[derive(Clone)]
60pub enum Layout {
61 Vh(i32),
62 Vw(i32),
63 Percent(i32),
64 Cell(i32),
65 Auto,
66
67 Add(Box<Layout>, Box<Layout>),
68 Sub(Box<Layout>, Box<Layout>),
69}
70
71impl Add for Layout {
72 type Output = Layout;
73
74 fn add(self, rhs: Layout) -> Layout {
75 Layout::Add(Box::new(self), Box::new(rhs))
76 }
77}
78
79impl Sub for Layout {
80 type Output = Layout;
81
82 fn sub(self, rhs: Layout) -> Layout {
83 Layout::Sub(Box::new(self), Box::new(rhs))
84 }
85}
86
87#[derive(Copy, Clone)]
88pub enum Alignment {
89 Left,
90 Center,
91 Right
92}
93
94#[derive(Copy, Clone)]
95pub struct BorderStyle {
96 pub top: i32,
97 pub bottom: i32,
98 pub left: i32,
99 pub right: i32,
100 pub top_left: i32,
101 pub top_right: i32,
102 pub bottom_right: i32,
103 pub bottom_left: i32,
104}
105
106#[derive(Copy, Clone)]
107pub enum Border {
108 Single,
109 Double,
110 Custom(BorderStyle),
111 None
112}
113
114#[derive(Clone)]
115pub struct LayoutVec2 {
116 pub x: Layout,
117 pub y: Layout
118}
119
120impl LayoutVec2 {
121 pub fn new(x: Layout, y: Layout) -> Self {
122 Self {x, y}
123 }
124}
125
126#[derive(Clone)]
127pub struct Style {
128 pub position: LayoutVec2,
129 pub size: LayoutVec2,
130 pub margin: LayoutVec2,
131 pub color: futtl::ColorSet,
132 pub attributes: futtl::Attr,
133 pub widget_border: Border,
134 pub alignment_type: Alignment
135}
136
137pub enum WidgetCtx {
138 Box(usize),
139 None
140}
141
142pub struct Text {
143 pub text: String,
144 pub style: Style,
145 pub id: u32
146}
147
148pub struct Widget {
149 pub ctx: WidgetCtx,
150 pub style: Style,
151 pub children: Vec<Widget>,
152 pub content: Vec<Text>,
153 pub pos: futtl::Vec2,
154 pub id: u32,
155}
156
157pub struct Win {
158 pub ctx: futtl::Win,
159 pub children: Vec<Widget>,
160 pub color: futtl::ColorSet,
161 pub ids: IdGenerator,
162}
163
164impl Style {
165 pub fn new() -> Self {
166 Self {
167 position: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
168 size: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
169 margin: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
170 color: futtl::ColorSet::new_default(),
171 attributes: futtl::Attr::NONE,
172 widget_border: Border::None,
173 alignment_type: Alignment::Left
174 }
175 }
176}
177
178#[derive(Copy, Clone)]
179enum Axis {
180 X,
181 Y,
182}
183
184fn evaluate_layout(term_size: futtl::Vec2, parent_size: futtl::Vec2, layout: &Layout, axis: Axis) -> i32 {
185 match layout {
186 Layout::Auto => 0,
187
188 Layout::Vw(n) => match axis {
189 Axis::X => term_size.x * n / 100,
190 Axis::Y => 0,
191 },
192
193 Layout::Vh(n) => match axis {
194 Axis::X => 0,
195 Axis::Y => term_size.y * n / 100,
196 },
197
198 Layout::Percent(n) => match axis {
199 Axis::X => parent_size.x * n / 100,
200 Axis::Y => parent_size.y * n / 100,
201 },
202
203 Layout::Cell(n) => *n,
204
205 Layout::Add(a, b) => {
206 evaluate_layout(term_size, parent_size, a, axis)
207 + evaluate_layout(term_size, parent_size, b, axis)
208 }
209
210 Layout::Sub(a, b) => {
211 evaluate_layout(term_size, parent_size, a, axis)
212 - evaluate_layout(term_size, parent_size, b, axis)
213 }
214 }
215}
216
217fn evaluate_size(term_size: futtl::Vec2, parent_size: futtl::Vec2, size_obj: LayoutVec2) -> futtl::Vec2 {
218 futtl::Vec2::new(
219 evaluate_layout(term_size, parent_size, &size_obj.x, Axis::X),
220 evaluate_layout(term_size, parent_size, &size_obj.y, Axis::Y),
221 )
222}
223
224fn evaluate_pos(term_size: futtl::Vec2, parent_size: futtl::Vec2, pos_obj: LayoutVec2) -> futtl::Vec2 {
225 futtl::Vec2::new(
226 evaluate_layout(term_size, parent_size, &pos_obj.x, Axis::X),
227 evaluate_layout(term_size, parent_size, &pos_obj.y, Axis::Y),
228 )
229}
230
231impl Win {
232 pub fn new_with_max_id(max_id: i32) -> Self {
233 Self {
234 ctx: futtl::Win::new(max_id, futtl::Attr::NONE),
235 children: Vec::new(),
236 color: futtl::ColorSet::new(
237 futtl::Color::ANSI(futtl::AnsiColor::DefaultFG),
238 futtl::Color::ANSI(futtl::AnsiColor::DefaultBG),
239 ),
240 ids: IdGenerator::new(),
241 }
242 }
243
244 pub fn new() -> Self {
245 Self::new_with_max_id(1024)
246 }
247
248 fn next_id(&mut self) -> u32 {
249 self.ids.next()
250 }
251
252 fn assign_ids(&mut self, widget: &mut Widget) {
253 widget.id = self.next_id();
254
255 for text in &mut widget.content {
256 text.id = self.next_id();
257 }
258
259 for child in &mut widget.children {
260 self.assign_ids(child);
261 }
262 }
263
264 pub fn pack(&mut self, mut widget: Widget) {
265 self.assign_ids(&mut widget);
266 self.children.push(widget);
267 }
268
269 fn evaluate_members(&mut self) {
270 let term_size = self.ctx.get_size();
271
272 let auto_x = self.children
273 .iter()
274 .filter(|child| matches!(child.style.size.x, Layout::Auto))
275 .count();
276
277 let auto_y = self.children
278 .iter()
279 .filter(|child| matches!(child.style.size.y, Layout::Auto))
280 .count();
281
282 let mut remaining_x = term_size.x;
283 let mut remaining_y = term_size.y;
284
285 for child in &self.children {
287 let size = evaluate_size(
288 term_size,
289 term_size,
290 child.style.size.clone(),
291 );
292
293 if !matches!(child.style.size.x, Layout::Auto) {
294 remaining_x -= size.x;
295 }
296
297 if !matches!(child.style.size.y, Layout::Auto) {
298 remaining_y -= size.y;
299 }
300 }
301
302 remaining_x = remaining_x.max(0);
303 remaining_y = remaining_y.max(0);
304
305 let auto_x_size = if auto_x > 0 {
307 remaining_x / auto_x as i32
308 } else {
309 0
310 };
311
312 let auto_y_size = if auto_y > 0 {
313 remaining_y / auto_y as i32
314 } else {
315 0
316 };
317
318 let mut current_x = 0;
319 let mut current_y = 0;
320
321 for child in self.children.iter_mut() {
322 let child_size = evaluate_size(
323 term_size,
324 term_size,
325 child.style.size.clone(),
326 );
327
328 let x_size = match child.style.size.x {
329 Layout::Auto => auto_x_size,
330 _ => child_size.x.min(term_size.x),
331 };
332
333 let y_size = match child.style.size.y {
334 Layout::Auto => auto_y_size,
335 _ => child_size.y.min(term_size.y),
336 };
337
338 child.pos = evaluate_pos(
339 term_size,
340 term_size,
341 child.style.position.clone(),
342 );
343
344 if matches!(child.style.position.x, Layout::Auto) {
346 child.pos.x = current_x;
347 }
348
349 if matches!(child.style.position.y, Layout::Auto) {
350 child.pos.y = current_y;
351 }
352
353 current_x += x_size;
355 current_y += y_size;
356
357 let idx = self.ctx.create_container(x_size, y_size);
358
359 child.ctx = WidgetCtx::Box(idx);
360 }
361 }
362
363 pub fn mainloop(&mut self) {
364 self.evaluate_members();
365
366 for child in self.children.iter_mut() {
367 match child.ctx {
368 WidgetCtx::None => {},
369 WidgetCtx::Box(_) => {
370 child.show(child.pos, &mut self.ctx);
371 }
372 }
373 }
374
375 futtl::flush();
376
377 let c = self.ctx.get_in();
378 if !((c == 'q' as i32) || (c == 27)) {
379 self.mainloop();
380 }
381 }
382}
383
384pub trait Stylable: Sized {
385 fn get_style(&mut self) -> &mut Style;
386
387 fn foreground(mut self, col: futtl::Color) -> Self {
388 self.get_style().color.fg = col;
389 self
390 }
391
392 fn background(mut self, col: futtl::Color) -> Self {
393 self.get_style().color.bg = col;
394 self
395 }
396
397 fn position(mut self, x: Layout, y: Layout) -> Self {
398 self.get_style().position.x = x;
399 self.get_style().position.y = y;
400 self
401 }
402
403 fn position_x(mut self, x: Layout) -> Self {
404 self.get_style().position.x = x;
405 self
406 }
407
408 fn position_y(mut self, y: Layout) -> Self {
409 self.get_style().position.y = y;
410 self
411 }
412
413 fn size(mut self, x: Layout, y: Layout) -> Self {
414 self.get_style().size.x = x;
415 self.get_style().size.y = y;
416 self
417 }
418
419 fn size_x(mut self, x: Layout) -> Self {
420 self.get_style().size.x = x;
421 self
422 }
423
424 fn size_y(mut self, y: Layout) -> Self {
425 self.get_style().size.y = y;
426 self
427 }
428
429 fn margin(mut self, x: Layout, y: Layout) -> Self {
430 self.get_style().margin.x = x;
431 self.get_style().margin.y = y;
432 self
433 }
434
435 fn margin_x(mut self, x: Layout) -> Self {
436 self.get_style().margin.x = x;
437 self
438 }
439
440 fn margin_y(mut self, y: Layout) -> Self {
441 self.get_style().margin.y = y;
442 self
443 }
444
445 fn attribute(mut self, attr: futtl::Attr) -> Self {
446 self.get_style().attributes |= attr;
447 self
448 }
449
450 fn remove_attribute(mut self, attr: futtl::Attr) -> Self {
451 self.get_style().attributes &= !attr;
452 self
453 }
454
455 fn border(mut self, border: Border) -> Self {
456 self.get_style().widget_border = border;
457 self
458 }
459
460 fn color(mut self, cs: futtl::ColorSet) -> Self {
461 self.get_style().color = cs;
462 self
463 }
464
465 fn align(mut self, alignment: Alignment) -> Self {
466 self.get_style().alignment_type = alignment;
467 self
468 }
469
470 fn style(mut self, style: Style) -> Self {
471 *self.get_style() = style;
472 self
473 }
474}
475
476pub trait BorrowStylable {
477 fn get_style(&mut self) -> &mut Style;
478
479 fn foreground(&mut self, col: futtl::Color) -> &mut Self {
480 self.get_style().color.fg = col;
481 self
482 }
483
484 fn background(&mut self, col: futtl::Color) -> &mut Self {
485 self.get_style().color.bg = col;
486 self
487 }
488
489 fn position(&mut self, x: Layout, y: Layout) -> &mut Self {
490 self.get_style().position.x = x;
491 self.get_style().position.y = y;
492 self
493 }
494
495 fn position_x(&mut self, x: Layout) -> &mut Self {
496 self.get_style().position.x = x;
497 self
498 }
499
500 fn position_y(&mut self, y: Layout) -> &mut Self {
501 self.get_style().position.y = y;
502 self
503 }
504
505 fn size(&mut self, x: Layout, y: Layout) -> &mut Self {
506 self.get_style().size.x = x;
507 self.get_style().size.y = y;
508 self
509 }
510
511 fn size_x(&mut self, x: Layout) -> &mut Self {
512 self.get_style().size.x = x;
513 self
514 }
515
516 fn size_y(&mut self, y: Layout) -> &mut Self {
517 self.get_style().size.y = y;
518 self
519 }
520
521 fn margin(&mut self, x: Layout, y: Layout) -> &mut Self {
522 self.get_style().margin.x = x;
523 self.get_style().margin.y = y;
524 self
525 }
526
527 fn margin_x(&mut self, x: Layout) -> &mut Self {
528 self.get_style().margin.x = x;
529 self
530 }
531
532 fn margin_y(&mut self, y: Layout) -> &mut Self {
533 self.get_style().margin.y = y;
534 self
535 }
536
537 fn attribute(&mut self, attr: futtl::Attr) -> &mut Self {
538 self.get_style().attributes |= attr;
539 self
540 }
541
542 fn remove_attribute(&mut self, attr: futtl::Attr) -> &mut Self {
543 self.get_style().attributes &= !attr;
544 self
545 }
546
547 fn border(&mut self, border: Border) -> &mut Self {
548 self.get_style().widget_border = border;
549 self
550 }
551
552 fn color(&mut self, cs: futtl::ColorSet) -> &mut Self {
553 self.get_style().color = cs;
554 self
555 }
556
557 fn align(&mut self, alignment: Alignment) -> &mut Self {
558 self.get_style().alignment_type = alignment;
559 self
560 }
561
562 fn style(&mut self, style: Style) -> &mut Self {
563 *self.get_style() = style;
564 self
565 }
566}
567
568impl Stylable for Widget {
569 fn get_style(&mut self) -> &mut Style {
570 &mut self.style
571 }
572}
573
574impl BorrowStylable for Style {
575 fn get_style(&mut self) -> &mut Style {
576 self
577 }
578}
579
580impl BorrowStylable for Text {
581 fn get_style(&mut self) -> &mut Style {
582 &mut self.style
583 }
584}
585
586impl Text {
587 pub fn show(&self, parent_ctx: &mut futtl::Container, offset: futtl::Vec2, size: futtl::Vec2) {
588 let pos = evaluate_pos(size, size, self.style.position.clone());
589
590 let text_size = futtl::Vec2::new(
591 self.text
592 .lines()
593 .map(|line| line.chars().count())
594 .max()
595 .unwrap_or(0) as i32,
596 self.text.lines().count() as i32,
597 );
598
599 let text_offset = match self.style.alignment_type {
600 Alignment::Left => futtl::Vec2::new(0, 0),
601 Alignment::Right => futtl::Vec2::new(text_size.x, 0),
602 Alignment::Center => futtl::Vec2::new(text_size.x / 2, 0),
603 };
604
605 let text_x = pos.x - text_offset.x + offset.x;
606
607 let text: String = self.text
610 .chars()
611 .skip((-text_x).max(0) as usize)
612 .collect();
613
614 parent_ctx.set_curs(
615 text_x.max(0),
616 pos.y.max(0) + offset.y,
617 );
618
619 parent_ctx.add_color(self.style.color, self.id as usize);
620 parent_ctx.set_color(self.id as usize);
621 parent_ctx.remove_attribute(futtl::Attr::ALL);
622 parent_ctx.add_attribute(self.style.attributes);
623
624 parent_ctx.write_str(&text);
625 }
626}
627
628impl Widget {
629 pub fn new() -> Self {
630 Self {
631 ctx: WidgetCtx::None,
632 style: Style::new(),
633 children: Vec::new(),
634 content: Vec::new(),
635 pos: futtl::Vec2::new(0, 0),
636 id: 0,
637 }
638 }
639
640 pub fn style(&mut self, style: Style) {
641 self.style = style;
642 }
643
644 pub fn text(&mut self, text: impl Into<String>) -> &mut Text {
645 let mut style = Style::new();
646 style.color = self.style.color;
647 style.attributes = self.style.attributes;
648 style.alignment_type = self.style.alignment_type;
649
650 self.content.push(Text {
651 text: text.into(),
652 style,
653 id: 0,
654 });
655
656 self.content.last_mut().unwrap()
657 }
658
659 fn set_border(&mut self, box_ctx: &mut futtl::Container, border: Border) {
660 let border = match border {
661 Border::Single => SINGLE_BORDER,
662 Border::Double => DOUBLE_BORDER,
663 Border::Custom(border) => border,
664 Border::None => NO_BORDER
665 };
666
667 box_ctx.add_color(self.style.color, self.id as usize);
668
669 box_ctx.set_border(
670 border.top as u32,
671 border.right as u32,
672 border.bottom as u32,
673 border.left as u32,
674 border.top_right as u32,
675 border.bottom_right as u32,
676 border.bottom_left as u32,
677 border.top_left as u32,
678 self.id as i32,
679 );
680 }
681
682 pub fn show(&mut self, widget_pos: futtl::Vec2, f_ctx: &mut futtl::Win) {
683 match self.ctx {
684 WidgetCtx::None => {}
685
686 WidgetCtx::Box(ctx) => {
687 let box_ctx = &mut f_ctx.children()[ctx];
688
689 box_ctx.set_color(self.id as usize);
690 box_ctx.remove_attribute(futtl::Attr::ALL);
691 box_ctx.add_attribute(self.style.attributes);
692 box_ctx.clear();
693
694 self.set_border(box_ctx, self.style.widget_border);
695
696 let offset = match self.style.widget_border {
697 Border::None => futtl::Vec2::new(0, 0),
698 _ => futtl::Vec2::new(1, 1),
699 };
700
701 let size = match self.style.widget_border {
702 Border::None => box_ctx.get_size(),
703 _ => futtl::Vec2::new(box_ctx.get_size().x - 2, box_ctx.get_size().y - 2),
704 };
705
706 for item in &self.content {
707 item.show(box_ctx, offset, size);
708
709 box_ctx.remove_attribute(futtl::Attr::ALL);
710 box_ctx.add_attribute(self.style.attributes);
711 }
712
713 box_ctx.show(widget_pos.x, widget_pos.y);
714 }
715 }
716 }
717}
718
719
720impl Default for Win {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726impl Default for Widget {
727 fn default() -> Self {
728 Self::new()
729 }
730}
731
732impl Default for Style {
733 fn default() -> Self {
734 Self::new()
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 #[test]
743 fn test() {
744 let mut ctx = Win::new();
745
746 let mut text_widget = Widget::new()
747 .position_x(Layout::Cell(0))
748 .position_y(Layout::Cell(0))
749 .size_x(Layout::Vw(50))
750 .size_y(Layout::Vh(100) - Layout::Cell(2))
751 .border(Border::Double)
752 .foreground(futtl::Color::ANSI256(69));
753
754 text_widget.text("test")
755 .position_x(Layout::Percent(50))
756 .align(Alignment::Left);
757
758
759 ctx.pack(text_widget);
760
761 let mut widget2 = Widget::new()
762 .position_x(Layout::Auto)
763 .position_y(Layout::Cell(0))
764 .size_x(Layout::Vw(50))
765 .size_y(Layout::Vh(100) - Layout::Cell(1))
766 .border(Border::Single);
767
768 widget2.text("Left".to_string())
769 .position_x(Layout::Percent(50))
770 .color(futtl::ColorSet::new(
771 futtl::Color::ANSI(futtl::AnsiColor::Red),
772 futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
773 ));
774
775 widget2.text("Center".to_string())
776 .position(
777 Layout::Percent(50),
778 Layout::Cell(1)
779 )
780 .color(futtl::ColorSet::new(
781 futtl::Color::ANSI(futtl::AnsiColor::Green),
782 futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
783 ))
784 .align(Alignment::Center);
785
786 widget2.text("Right".to_string())
787 .position(
788 Layout::Percent(50),
789 Layout::Cell(2)
790 )
791 .color(futtl::ColorSet::new(
792 futtl::Color::ANSI(futtl::AnsiColor::Blue),
793 futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
794 ))
795 .align(Alignment::Right)
796 .attribute(futtl::Attr::UNDERLINE);
797
798
799 ctx.pack(widget2);
800
801 ctx.mainloop();
802 }
803}