Skip to main content

futtlui/
lib.rs

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        // Calculate the space consumed by non-Auto children.
286        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        // Divide the remaining space equally between Auto children.
306        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            // Auto position means "place me after the previous child".
345            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            // Every child consumes space in the flow.
354            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        // If the text starts outside the left edge, remove the
608        // characters that would have been drawn before x = 0.
609        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}