use futtl::Futtl;
use std::ops::{Add, Sub};
pub const SINGLE_BORDER: BorderStyle = BorderStyle {
top: '─' as i32,
bottom: '─' as i32,
left: '│' as i32,
right: '│' as i32,
top_left: '┌' as i32,
top_right: '┐' as i32,
bottom_right: '┘' as i32,
bottom_left: '└' as i32,
};
pub const DOUBLE_BORDER: BorderStyle = BorderStyle {
top: '═' as i32,
bottom: '═' as i32,
left: '║' as i32,
right: '║' as i32,
top_left: '╔' as i32,
top_right: '╗' as i32,
bottom_right: '╝' as i32,
bottom_left: '╚' as i32,
};
pub const NO_BORDER: BorderStyle = BorderStyle {
top: '\0' as i32,
bottom: '\0' as i32,
left: '\0' as i32,
right: '\0' as i32,
top_left: '\0' as i32,
top_right: '\0' as i32,
bottom_right: '\0' as i32,
bottom_left: '\0' as i32,
};
pub struct IdGenerator {
next: u32,
}
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
impl IdGenerator {
pub fn new() -> Self {
Self { next: 0 }
}
pub fn next(&mut self) -> u32 {
let id = self.next;
self.next += 1;
id
}
}
#[derive(Clone)]
pub enum Layout {
Vh(i32),
Vw(i32),
Percent(i32),
Cell(i32),
Auto,
Add(Box<Layout>, Box<Layout>),
Sub(Box<Layout>, Box<Layout>),
}
impl Add for Layout {
type Output = Layout;
fn add(self, rhs: Layout) -> Layout {
Layout::Add(Box::new(self), Box::new(rhs))
}
}
impl Sub for Layout {
type Output = Layout;
fn sub(self, rhs: Layout) -> Layout {
Layout::Sub(Box::new(self), Box::new(rhs))
}
}
#[derive(Copy, Clone)]
pub enum Alignment {
Left,
Center,
Right
}
#[derive(Copy, Clone)]
pub struct BorderStyle {
pub top: i32,
pub bottom: i32,
pub left: i32,
pub right: i32,
pub top_left: i32,
pub top_right: i32,
pub bottom_right: i32,
pub bottom_left: i32,
}
#[derive(Copy, Clone)]
pub enum Border {
Single,
Double,
Custom(BorderStyle),
None
}
#[derive(Clone)]
pub struct LayoutVec2 {
pub x: Layout,
pub y: Layout
}
impl LayoutVec2 {
pub fn new(x: Layout, y: Layout) -> Self {
Self {x, y}
}
}
#[derive(Clone)]
pub struct Style {
pub position: LayoutVec2,
pub size: LayoutVec2,
pub margin: LayoutVec2,
pub color: futtl::ColorSet,
pub attributes: futtl::Attr,
pub widget_border: Border,
pub alignment_type: Alignment,
pub wrap_text: bool
}
pub enum WidgetCtx {
Box(usize),
None
}
pub struct Text {
pub text: String,
pub style: Style,
pub id: u32
}
pub struct Widget {
pub ctx: WidgetCtx,
pub style: Style,
pub children: Vec<Widget>,
pub content: Vec<Text>,
pub pos: futtl::Vec2,
pub id: u32,
pub callback: fn(&mut Widget)
}
#[derive(Clone)]
pub enum Event {
Key(i32),
Quit,
}
pub struct Win {
pub ctx: futtl::Win,
pub children: Vec<Widget>,
pub color: futtl::ColorSet,
pub ids: IdGenerator,
events: Vec<Event>,
exit_keys: Vec<i32>
}
impl Style {
pub fn new() -> Self {
Self {
position: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
size: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
margin: LayoutVec2 {x: Layout::Auto, y: Layout::Auto},
color: futtl::ColorSet::new_default(),
attributes: futtl::Attr::NONE,
widget_border: Border::None,
alignment_type: Alignment::Left,
wrap_text: false
}
}
}
#[derive(Copy, Clone)]
enum Axis {
X,
Y,
}
fn evaluate_layout(term_size: futtl::Vec2, parent_size: futtl::Vec2, layout: &Layout, axis: Axis) -> i32 {
match layout {
Layout::Auto => 0,
Layout::Vw(n) => match axis {
Axis::X => term_size.x * n / 100,
Axis::Y => 0,
},
Layout::Vh(n) => match axis {
Axis::X => 0,
Axis::Y => term_size.y * n / 100,
},
Layout::Percent(n) => match axis {
Axis::X => parent_size.x * n / 100,
Axis::Y => parent_size.y * n / 100,
},
Layout::Cell(n) => *n,
Layout::Add(a, b) => {
evaluate_layout(term_size, parent_size, a, axis)
+ evaluate_layout(term_size, parent_size, b, axis)
}
Layout::Sub(a, b) => {
evaluate_layout(term_size, parent_size, a, axis)
- evaluate_layout(term_size, parent_size, b, axis)
}
}
}
fn evaluate_size(term_size: futtl::Vec2, parent_size: futtl::Vec2, size_obj: LayoutVec2) -> futtl::Vec2 {
futtl::Vec2::new(
evaluate_layout(term_size, parent_size, &size_obj.x, Axis::X),
evaluate_layout(term_size, parent_size, &size_obj.y, Axis::Y),
)
}
fn evaluate_pos(term_size: futtl::Vec2, parent_size: futtl::Vec2, pos_obj: LayoutVec2) -> futtl::Vec2 {
futtl::Vec2::new(
evaluate_layout(term_size, parent_size, &pos_obj.x, Axis::X),
evaluate_layout(term_size, parent_size, &pos_obj.y, Axis::Y),
)
}
impl Win {
pub fn new_with_max_id(max_id: i32) -> Self {
Self {
ctx: futtl::Win::new(max_id, futtl::Attr::NONE),
children: Vec::new(),
color: futtl::ColorSet::new(
futtl::Color::ANSI(futtl::AnsiColor::DefaultFG),
futtl::Color::ANSI(futtl::AnsiColor::DefaultBG),
),
ids: IdGenerator::new(),
events: Vec::new(),
exit_keys: Vec::new()
}
}
pub fn new() -> Self {
Self::new_with_max_id(1024)
}
fn next_id(&mut self) -> u32 {
self.ids.next()
}
fn assign_ids(&mut self, widget: &mut Widget) {
widget.id = self.next_id();
for text in &mut widget.content {
text.id = self.next_id();
}
for child in &mut widget.children {
self.assign_ids(child);
}
}
pub fn pack(&mut self, mut widget: Widget) {
self.assign_ids(&mut widget);
self.children.push(widget);
}
fn evaluate_members(&mut self) {
let term_size = self.ctx.get_size();
let auto_x = self.children
.iter()
.filter(|child| matches!(child.style.size.x, Layout::Auto))
.count();
let auto_y = self.children
.iter()
.filter(|child| matches!(child.style.size.y, Layout::Auto))
.count();
let mut remaining_x = term_size.x;
let mut remaining_y = term_size.y;
for child in &self.children {
let size = evaluate_size(
term_size,
term_size,
child.style.size.clone(),
);
if !matches!(child.style.size.x, Layout::Auto) {
remaining_x -= size.x;
}
if !matches!(child.style.size.y, Layout::Auto) {
remaining_y -= size.y;
}
}
remaining_x = remaining_x.max(0);
remaining_y = remaining_y.max(0);
let auto_x_size = if auto_x > 0 {
remaining_x / auto_x as i32
} else {
0
};
let auto_y_size = if auto_y > 0 {
remaining_y / auto_y as i32
} else {
0
};
let mut current_x = 0;
let mut current_y = 0;
for child in self.children.iter_mut() {
let child_size = evaluate_size(
term_size,
term_size,
child.style.size.clone(),
);
let x_size = match child.style.size.x {
Layout::Auto => auto_x_size,
_ => child_size.x.min(term_size.x),
};
let y_size = match child.style.size.y {
Layout::Auto => auto_y_size,
_ => child_size.y.min(term_size.y),
};
child.pos = evaluate_pos(
term_size,
term_size,
child.style.position.clone(),
);
if matches!(child.style.position.x, Layout::Auto) {
child.pos.x = current_x;
}
if matches!(child.style.position.y, Layout::Auto) {
child.pos.y = current_y;
}
current_x += x_size;
current_y += y_size;
let idx = self.ctx.create_container(x_size, y_size);
child.ctx = WidgetCtx::Box(idx);
}
}
pub fn clear_child(&mut self, id: usize) {
self.children[id].clear();
}
pub fn get_events(&mut self) -> Vec<Event> {
let curr_events = self.events.clone();
self.events.clear();
curr_events
}
fn handle_input(&mut self) {
let c = self.ctx.get_in();
match c {
-1 => {
},
_ => {
self.events.push(Event::Key(c));
for key in &self.exit_keys {
if c == *key {
self.events.push(Event::Quit)
}
}
}
}
}
pub fn add_exit_key(&mut self, key: i32) {
if !self.exit_keys.contains(&key) {
self.exit_keys.push(key);
}
}
pub fn remove_exit_key(&mut self, key: i32) {
if let Some(i) = self.exit_keys.iter().position(|&x| x == key) {
self.exit_keys.remove(i);
}
}
pub fn render(&mut self) {
self.evaluate_members();
for child in self.children.iter_mut() {
match child.ctx {
WidgetCtx::None => {},
WidgetCtx::Box(_) => {
child.show(child.pos, &mut self.ctx);
}
}
}
futtl::flush();
self.handle_input();
}
}
pub trait Stylable: Sized {
fn get_style(&mut self) -> &mut Style;
fn foreground(mut self, col: futtl::Color) -> Self {
self.get_style().color.fg = col;
self
}
fn background(mut self, col: futtl::Color) -> Self {
self.get_style().color.bg = col;
self
}
fn position(mut self, x: Layout, y: Layout) -> Self {
self.get_style().position.x = x;
self.get_style().position.y = y;
self
}
fn position_x(mut self, x: Layout) -> Self {
self.get_style().position.x = x;
self
}
fn position_y(mut self, y: Layout) -> Self {
self.get_style().position.y = y;
self
}
fn size(mut self, x: Layout, y: Layout) -> Self {
self.get_style().size.x = x;
self.get_style().size.y = y;
self
}
fn size_x(mut self, x: Layout) -> Self {
self.get_style().size.x = x;
self
}
fn size_y(mut self, y: Layout) -> Self {
self.get_style().size.y = y;
self
}
fn margin(mut self, x: Layout, y: Layout) -> Self {
self.get_style().margin.x = x;
self.get_style().margin.y = y;
self
}
fn margin_x(mut self, x: Layout) -> Self {
self.get_style().margin.x = x;
self
}
fn margin_y(mut self, y: Layout) -> Self {
self.get_style().margin.y = y;
self
}
fn attribute(mut self, attr: futtl::Attr) -> Self {
self.get_style().attributes |= attr;
self
}
fn remove_attribute(mut self, attr: futtl::Attr) -> Self {
self.get_style().attributes &= !attr;
self
}
fn border(mut self, border: Border) -> Self {
self.get_style().widget_border = border;
self
}
fn color(mut self, cs: futtl::ColorSet) -> Self {
self.get_style().color = cs;
self
}
fn align(mut self, alignment: Alignment) -> Self {
self.get_style().alignment_type = alignment;
self
}
fn set_wrap(mut self, wrapping: bool) -> Self {
self.get_style().wrap_text = wrapping;
self
}
fn style(mut self, style: Style) -> Self {
*self.get_style() = style;
self
}
}
pub trait BorrowStylable {
fn get_style(&mut self) -> &mut Style;
fn foreground(&mut self, col: futtl::Color) -> &mut Self {
self.get_style().color.fg = col;
self
}
fn background(&mut self, col: futtl::Color) -> &mut Self {
self.get_style().color.bg = col;
self
}
fn position(&mut self, x: Layout, y: Layout) -> &mut Self {
self.get_style().position.x = x;
self.get_style().position.y = y;
self
}
fn position_x(&mut self, x: Layout) -> &mut Self {
self.get_style().position.x = x;
self
}
fn position_y(&mut self, y: Layout) -> &mut Self {
self.get_style().position.y = y;
self
}
fn size(&mut self, x: Layout, y: Layout) -> &mut Self {
self.get_style().size.x = x;
self.get_style().size.y = y;
self
}
fn size_x(&mut self, x: Layout) -> &mut Self {
self.get_style().size.x = x;
self
}
fn size_y(&mut self, y: Layout) -> &mut Self {
self.get_style().size.y = y;
self
}
fn margin(&mut self, x: Layout, y: Layout) -> &mut Self {
self.get_style().margin.x = x;
self.get_style().margin.y = y;
self
}
fn margin_x(&mut self, x: Layout) -> &mut Self {
self.get_style().margin.x = x;
self
}
fn margin_y(&mut self, y: Layout) -> &mut Self {
self.get_style().margin.y = y;
self
}
fn attribute(&mut self, attr: futtl::Attr) -> &mut Self {
self.get_style().attributes |= attr;
self
}
fn remove_attribute(&mut self, attr: futtl::Attr) -> &mut Self {
self.get_style().attributes &= !attr;
self
}
fn border(&mut self, border: Border) -> &mut Self {
self.get_style().widget_border = border;
self
}
fn color(&mut self, cs: futtl::ColorSet) -> &mut Self {
self.get_style().color = cs;
self
}
fn align(&mut self, alignment: Alignment) -> &mut Self {
self.get_style().alignment_type = alignment;
self
}
fn set_wrap(&mut self, wrapping: bool) -> &mut Self {
self.get_style().wrap_text = wrapping;
self
}
fn style(&mut self, style: Style) -> &mut Self {
*self.get_style() = style;
self
}
}
impl Stylable for Widget {
fn get_style(&mut self) -> &mut Style {
&mut self.style
}
}
impl BorrowStylable for Style {
fn get_style(&mut self) -> &mut Style {
self
}
}
impl BorrowStylable for Text {
fn get_style(&mut self) -> &mut Style {
&mut self.style
}
}
impl Text {
pub fn text(&mut self, text: impl Into<String>) {
self.text = text.into();
}
pub fn show(&self, parent_ctx: &mut futtl::Container, offset: futtl::Vec2, size: futtl::Vec2) {
let pos = evaluate_pos(size, size, self.style.position.clone());
let text_size = futtl::Vec2::new(
self.text
.lines()
.map(|line| line.chars().count())
.max()
.unwrap_or(0) as i32,
self.text.lines().count() as i32,
);
let text_offset = match self.style.alignment_type {
Alignment::Left => futtl::Vec2::new(0, 0),
Alignment::Right => futtl::Vec2::new(text_size.x, 0),
Alignment::Center => futtl::Vec2::new(text_size.x / 2, 0),
};
let text_x = pos.x - text_offset.x + offset.x;
let text: String = self.text
.chars()
.skip((-text_x).max(0) as usize)
.collect();
parent_ctx.set_curs(
text_x.max(0),
pos.y.max(0) + offset.y,
);
parent_ctx.add_color(self.style.color, self.id as usize);
parent_ctx.set_color(self.id as usize);
parent_ctx.remove_attribute(futtl::Attr::ALL);
parent_ctx.add_attribute(self.style.attributes);
if self.style.wrap_text {
parent_ctx.write_wrap_str(&text);
} else {
parent_ctx.write_str(&text);
}
}
}
fn default_callback(_ctx: &mut Widget) {
}
impl Widget {
pub fn new() -> Self {
Self {
ctx: WidgetCtx::None,
style: Style::new(),
children: Vec::new(),
content: Vec::new(),
pos: futtl::Vec2::new(0, 0),
id: 0,
callback: default_callback
}
}
pub fn style(&mut self, style: Style) {
self.style = style;
}
pub fn text(&mut self, text: impl Into<String>) -> &mut Text {
let mut style = Style::new();
style.color = self.style.color;
style.attributes = self.style.attributes;
style.alignment_type = self.style.alignment_type;
style.wrap_text = self.style.wrap_text;
self.content.push(Text {
text: text.into(),
style,
id: 0,
});
self.content.last_mut().unwrap()
}
fn set_border(&mut self, box_ctx: &mut futtl::Container, border: Border) {
let border = match border {
Border::Single => SINGLE_BORDER,
Border::Double => DOUBLE_BORDER,
Border::Custom(border) => border,
Border::None => NO_BORDER
};
box_ctx.add_color(self.style.color, self.id as usize);
box_ctx.set_border(
border.top as u32,
border.right as u32,
border.bottom as u32,
border.left as u32,
border.top_right as u32,
border.bottom_right as u32,
border.bottom_left as u32,
border.top_left as u32,
self.id as i32,
);
}
pub fn clear(&mut self) {
self.content.clear();
}
pub fn show(&mut self, widget_pos: futtl::Vec2, f_ctx: &mut futtl::Win) {
match self.ctx {
WidgetCtx::None => {}
WidgetCtx::Box(ctx) => {
let box_ctx = &mut f_ctx.children()[ctx];
(self.callback)(self);
box_ctx.set_color(self.id as usize);
box_ctx.remove_attribute(futtl::Attr::ALL);
box_ctx.add_attribute(self.style.attributes);
box_ctx.clear();
self.set_border(box_ctx, self.style.widget_border);
let offset = match self.style.widget_border {
Border::None => futtl::Vec2::new(0, 0),
_ => futtl::Vec2::new(1, 1),
};
let size = match self.style.widget_border {
Border::None => box_ctx.get_size(),
_ => futtl::Vec2::new(box_ctx.get_size().x - 2, box_ctx.get_size().y - 2),
};
for item in &self.content {
item.show(box_ctx, offset, size);
box_ctx.remove_attribute(futtl::Attr::ALL);
box_ctx.add_attribute(self.style.attributes);
}
box_ctx.show(widget_pos.x, widget_pos.y);
}
}
}
}
impl Default for Win {
fn default() -> Self {
Self::new()
}
}
impl Default for Widget {
fn default() -> Self {
Self::new()
}
}
impl Default for Style {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
let mut ctx = Win::new();
let mut text_widget = Widget::new()
.position_x(Layout::Cell(0))
.position_y(Layout::Cell(0))
.size_x(Layout::Vw(50))
.size_y(Layout::Vh(100) - Layout::Cell(2))
.border(Border::Double)
.foreground(futtl::Color::ANSI256(69));
text_widget.text("test")
.position_x(Layout::Percent(50))
.align(Alignment::Left);
ctx.pack(text_widget);
let mut widget2 = Widget::new()
.position_x(Layout::Auto)
.position_y(Layout::Cell(0))
.size_x(Layout::Vw(50))
.size_y(Layout::Vh(100) - Layout::Cell(1))
.border(Border::Single);
widget2.text("Left".to_string())
.position_x(Layout::Percent(50))
.color(futtl::ColorSet::new(
futtl::Color::ANSI(futtl::AnsiColor::Red),
futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
));
widget2.text("Center".to_string())
.position(
Layout::Percent(50),
Layout::Cell(1)
)
.color(futtl::ColorSet::new(
futtl::Color::ANSI(futtl::AnsiColor::Green),
futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
))
.align(Alignment::Center);
widget2.text("Right".to_string())
.position(
Layout::Percent(50),
Layout::Cell(2)
)
.color(futtl::ColorSet::new(
futtl::Color::ANSI(futtl::AnsiColor::Blue),
futtl::Color::ANSI(futtl::AnsiColor::DefaultBG)
))
.align(Alignment::Right)
.attribute(futtl::Attr::UNDERLINE);
ctx.pack(widget2);
ctx.add_exit_key(27);
ctx.add_exit_key('q' as i32);
let mut events: Vec<Event> = Vec::new();
'mainloop: loop {
for event in events {
match event {
Event::Key(_) => {
},
Event::Quit => {
break 'mainloop;
}
}
}
ctx.render();
events = ctx.get_events();
}
}
}