tuifw-window 0.21.1

Text User Interface Framework. Overlappable hierachical windows.
Documentation
#![feature(never_type)]

#![deny(warnings)]
#![doc(test(attr(deny(warnings))))]
#![doc(test(attr(allow(dead_code))))]
#![doc(test(attr(allow(unused_variables))))]
#![allow(clippy::blocks_in_if_conditions)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::type_complexity)]

#![no_std]

extern crate alloc;

use alloc::boxed::Box;
use core::cmp::{max, min};
use core::mem::replace;
use components_arena::{Arena, Component, ComponentId, Id, NewtypeComponentId, RawId};
use educe::Educe;
use macro_attr_2018::macro_attr;
use tuifw_screen_base::{Bg, Error, Event, Fg, Point, Rect, Screen, Vector};

fn invalidate_rect(screen: &mut dyn Screen, rect: Rect) {
    let rect = rect.intersect(Rect { tl: Point { x: 0, y: 0 }, size: screen.size() });
    if rect.is_empty() { return; }
    let l = rect.l();
    let r = rect.r();
    for y in rect.t() .. rect.b() {
        let row = screen.line_invalidated_range_mut(y);
        row.start = min(row.start, l);
        row.end = max(row.end, r);
    }
}

fn rect_invalidated(screen: &dyn Screen, rect: Rect) -> bool {
    let rect = rect.intersect(Rect { tl: Point { x: 0, y: 0 }, size: screen.size() });
    if rect.is_empty() { return false; }
    let l = rect.l();
    let r = rect.r();
    for y in rect.t() .. rect.b() {
        let row = screen.line_invalidated_range(y);
        if row.end == row.start { continue; }
        if l < row.start {
            if r > row.end { return true; }
        } else if l < row.end {
            return true;
        }
    }
    false
}

#[derive(Educe)]
#[educe(Debug)]
pub struct RenderPort {
    #[educe(Debug(ignore))]
    screen: Box<dyn Screen>,
    offset: Vector,
    size: Vector,
    cursor: Option<Point>,
}

impl RenderPort {
    pub fn out(&mut self, p: Point, fg: Fg, bg: Bg, text: &str) {
        let screen_size = self.screen.size();
        if p.y as u16 >= self.size.y as u16 || self.size.x == 0 { return; }
        let p = p.offset(self.offset);
        if p.y < 0 || p.y >= self.screen.size().y { return; }
        let row = self.screen.line_invalidated_range(p.y).clone();
        if p.x >= row.end { return; }

        let window_start = Point { x: 0, y: 0 }.offset(self.offset).x;
        let window_end = Point { x: 0, y: 0 }.offset(self.size + self.offset).x;
        let chunks = if window_start <= window_end {
            if window_end <= 0 || window_start >= screen_size.x { return; }
            [max(0, window_start) .. min(screen_size.x, window_end), 0 .. 0]
        } else {
            if window_end > 0 && window_start < screen_size.x {
                [0 .. window_end, window_start .. screen_size.x]
            } else if window_end > 0 {
                [0 .. window_end, 0 .. 0]
            } else if window_start < screen_size.x {
                [window_start .. screen_size.x, 0 .. 0]
            } else {
                return
            }
        };

        for chunk in &chunks {
            if chunk.start >= chunk.end { continue; }
            let out = self.screen.out(p, fg, bg, text, chunk.clone(), row.clone());
            if out.start >= out.end { continue; }
            let row = self.screen.line_invalidated_range_mut(p.y);
            row.start = min(row.start, out.start);
            row.end = max(row.end, out.end);
            if let Some(cursor) = self.cursor {
                if cursor.y == p.y && cursor.x >= out.start && cursor.x < out.end {
                    self.cursor = None;
                }
            }
        }
    }

    pub fn cursor(&mut self, p: Point) {
        if self.cursor.is_some() { return; }
        let p = p.offset(self.offset);
        if p.y < 0 || p.y >= self.screen.size().y { return; }
        let row = &self.screen.line_invalidated_range(p.y);
        if p.x < row.start || p.x >= row.end { return; }
        self.cursor = Some(p);
    }

    pub fn fill(&mut self, mut f: impl FnMut(&mut Self, Point)) {
        for y in 0 .. self.screen.size().y {
            for x in self.screen.line_invalidated_range(y).clone() {
                f(self, Point { x, y }.offset(-self.offset));
            }
        }
    }
}

macro_attr! {
    #[derive(Component!)]
    #[derive(Debug)]
    struct WindowNode {
        parent: Option<Id<WindowNode>>,
        prev: Id<WindowNode>,
        next: Id<WindowNode>,
        first_child: Option<Id<WindowNode>>,
        bounds: Rect,
        tag: Option<RawId>,
    }
}

fn offset_from_root<State: ?Sized>(
    mut window: Id<WindowNode>,
    tree: &WindowTree<State>
) -> Vector {
    let mut offset = Vector::null();
    loop {
        offset += tree.arena[window].bounds.tl.offset_from(Point { x: 0, y: 0 });
        if let Some(parent) = tree.arena[window].parent {
            window = parent;
        } else {
            break;
        }
    }
    offset
}

macro_attr! {
    #[derive(NewtypeComponentId!)]
    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
    pub struct Window(Id<WindowNode>);
}

impl Window {
    pub fn new<State: ?Sized>(
        tree: &mut WindowTree<State>,
        parent: Option<Self>,
        prev: Option<Self>,
    ) -> Result<Window, Error> {
        tree.arena.try_reserve().map_err(|_| Error::Oom)?;
        let parent = parent.map_or(tree.root, |w| w.0);
        let window = tree.arena.insert(|window| {
            (WindowNode {
                parent: Some(parent),
                prev: window,
                next: window,
                first_child: None,
                bounds: Rect { tl: Point { x: 0, y: 0 }, size: Vector::null() },
                tag: None
            }, Window(window))
        });
        window.attach(tree, parent, prev);
        Ok(window)
    }

    pub fn bounds<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Rect {
        tree.arena[self.0].bounds
    }

    pub fn inner_bounds<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Rect {
        let bounds = self.bounds(tree);
        Rect { tl: Point { x: 0, y: 0 }, size: bounds.size }
    }

    pub fn set_tag<Tag: ComponentId, State: ?Sized>(
        self,
        tree: &mut WindowTree<State>,
        tag: Tag
    ) {
        tree.arena[self.0].tag = Some(tag.into_raw());
    }

    pub fn reset_tag<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>
    ) {
        tree.arena[self.0].tag = None;
    }

    pub fn tag<Tag: ComponentId, State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Option<Tag> {
        tree.arena[self.0].tag.map(Tag::from_raw)
    }

    pub fn parent<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Option<Window> {
        let parent = tree.arena[self.0].parent.unwrap();
        if parent == tree.root { None } else { Some(Window(parent)) }
    }

    pub fn first_child<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Option<Window> {
        tree.arena[self.0].first_child.map(Window)
    }

    pub fn prev<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Window {
        Window(tree.arena[self.0].prev)
    }

    pub fn next<State: ?Sized>(
        self,
        tree: &WindowTree<State>
    ) -> Window {
        Window(tree.arena[self.0].next)
    }

    pub fn move_xy<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>,
        bounds: Rect
    ) {
        let parent = tree.arena[self.0].parent.unwrap();
        let screen_bounds = bounds.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_bounds);
        let bounds = replace(&mut tree.arena[self.0].bounds, bounds);
        let screen_bounds = bounds.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_bounds);
    }

    pub fn move_z<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>,
        prev: Option<Window>
    ) {
        let parent = self.detach(tree);
        self.attach(tree, parent, prev);
        let bounds = tree.arena[self.0].bounds;
        let screen_bounds = bounds.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_bounds);
    }

    fn detach<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>
    ) -> Id<WindowNode> {
        let node = &mut tree.arena[self.0];
        let prev = replace(&mut node.prev, self.0);
        let next = replace(&mut node.next, self.0);
        let parent = node.parent.take().unwrap();
        tree.arena[prev].next = next;
        tree.arena[next].prev = prev;
        let parent_node = &mut tree.arena[parent];
        if parent_node.first_child.unwrap() == self.0 {
            parent_node.first_child = if next == self.0 { None } else { Some(next) };
        }
        parent
    }

    fn attach<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>,
        parent: Id<WindowNode>,
        prev: Option<Window>
    ) {
        let (prev, next) = if let Some(prev) = prev {
            assert_eq!(tree.arena[prev.0].parent.unwrap(), parent);
            let prev = prev.0;
            let next = replace(&mut tree.arena[prev].next, self.0);
            tree.arena[next].prev = self.0;
            (prev, next)
        } else {
            let parent_node = &mut tree.arena[parent];
            let next = parent_node.first_child.replace(self.0).unwrap_or(self.0);
            let prev = replace(&mut tree.arena[next].prev, self.0);
            tree.arena[prev].next = self.0;
            (prev, next)
        };
        let node = &mut tree.arena[self.0];
        node.parent = Some(parent);
        node.prev = prev;
        node.next = next;
    }

    pub fn drop_window<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>
    ) {
        let parent = self.detach(tree);
        let node = tree.arena.remove(self.0);
        let screen_bounds = node.bounds.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_bounds);
        Self::drop_node_tree(node, tree);
    }

    fn drop_node_tree<State: ?Sized>(
        node: WindowNode,
        tree: &mut WindowTree<State>
    ) {
        if let Some(first_child) = node.first_child {
            let mut child = first_child;
            loop {
                let child_node = tree.arena.remove(child);
                child = child_node.next;
                Self::drop_node_tree(child_node, tree);
                if child == first_child { break; }
            }
        }
    }

    pub fn invalidate_rect<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>,
        rect: Rect
    ) {
        let bounds = tree.arena[self.0].bounds;
        let rect = rect.offset(bounds.tl.offset_from(Point { x: 0, y: 0 })).intersect(bounds);
        let parent = tree.arena[self.0].parent.unwrap();
        let screen_rect = rect.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_rect);
    }
 
    pub fn invalidate<State: ?Sized>(
        self,
        tree: &mut WindowTree<State>
    ) {
        let bounds = tree.arena[self.0].bounds;
        let parent = tree.arena[self.0].parent.unwrap();
        let screen_bounds = bounds.offset(offset_from_root(parent, tree));
        invalidate_rect(tree.screen(), screen_bounds);
    }
}

#[derive(Educe)]
#[educe(Debug)]
pub struct WindowTree<State: ?Sized> {
    #[educe(Debug(ignore))]
    screen: Option<Box<dyn Screen>>,
    arena: Arena<WindowNode>,
    root: Id<WindowNode>,
    #[educe(Debug(ignore))]
    render: fn(
        tree: &WindowTree<State>,
        window: Option<Window>,
        port: &mut RenderPort,
        state: &mut State,
    ),
    cursor: Option<Point>,
    screen_size: Vector,
}

impl<State: ?Sized> WindowTree<State> {
    pub fn new(
        screen: Box<dyn Screen>,
        render: fn(
            tree: &WindowTree<State>,
            window: Option<Window>,
            port: &mut RenderPort,
            state: &mut State,
        )
    ) -> Result<Self, Error> {
        let mut arena = Arena::new();
        arena.try_reserve().map_err(|_| Error::Oom)?;
        let screen_size = screen.size();
        let root = arena.insert(|window| (WindowNode {
            parent: None,
            prev: window,
            next: window,
            first_child: None,
            bounds: Rect { tl: Point { x: 0, y: 0 }, size: screen_size },
            tag: None
        }, window));
        Ok(WindowTree { screen: Some(screen), arena, root, render, cursor: None, screen_size })
    }

    pub fn screen_size(&self) -> Vector { self.screen_size }

    pub fn invalidate_rect(&mut self, rect: Rect) {
        invalidate_rect(self.screen(), rect);
    }
 
    pub fn invalidate_screen(&mut self) {
        let size = self.screen_size;
        invalidate_rect(self.screen(), Rect { tl: Point { x: 0, y: 0 }, size });
    }

    fn screen(&mut self) -> &mut dyn Screen {
        self.screen.as_mut().expect("WindowTree is in invalid state").as_mut()
    }

    fn render_window(&mut self, window: Id<WindowNode>, offset: Vector, render_state: &mut State) {
        let bounds = self.arena[window].bounds.offset(offset);
        let screen = self.screen();
        if !rect_invalidated(screen, bounds) { return; }
        let offset = bounds.tl.offset_from(Point { x: 0, y: 0 });
        let screen = self.screen.take().expect("WindowTree is in invalid state");
        let mut port = RenderPort {
            screen,
            cursor: self.cursor,
            offset,
            size: bounds.size,
        };
        (self.render)(
            self,
            if window == self.root { None } else { Some(Window(window)) },
            &mut port,
            render_state
        );
        self.screen.replace(port.screen);
        self.cursor = port.cursor;
        if let Some(first_child) = self.arena[window].first_child {
            let mut child = first_child;
            loop {
                self.render_window(child, offset, render_state);
                child = self.arena[child].next;
                if child == first_child { break; }
            }
        }
    }

    pub fn update(&mut self, wait: bool, render_state: &mut State) -> Result<Option<Event>, Error> {
        if let Some(cursor) = self.cursor {
            let screen = self.screen();
            if rect_invalidated(screen, Rect { tl: cursor, size: Vector { x: 1, y: 1 } }) {
                self.cursor = None;
            }
        }
        self.render_window(self.root, Vector::null(), render_state);
        let screen = self.screen.as_mut().expect("WindowTree is in invalid state");
        let event = screen.update(self.cursor, wait)?;
        if event == Some(Event::Resize) {
            self.screen_size = screen.size();
            self.arena[self.root].bounds = Rect { tl: Point { x: 0, y: 0 }, size: self.screen_size };
        }
        Ok(event)
    }
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    fn window_tree_new_window() {
        fn render<State: ?Sized>(_: &WindowTree<State>, _: Option<Window>, _: &mut RenderPort, _: &mut State) { }
        let screen = tuifw_screen_test::Screen::new(Vector::null());
        let screen = Box::new(screen) as _;
        let tree = &mut WindowTree::<()>::new(screen, render).unwrap();
        assert!(tree.arena[tree.root].first_child.is_none());
        let one = Window::new(tree, None, None).unwrap();
        one.move_xy(tree, Rect { tl: Point { x: 0, y: 0 }, size: Vector::null() });
        assert!(tree.arena[one.0].first_child.is_none());
        assert_eq!(tree.arena[one.0].parent, Some(tree.root));
        assert_eq!(tree.arena[tree.root].first_child, Some(one.0));
        assert_eq!(tree.arena[one.0].next, one.0);
        let two = Window::new(tree, None, Some(one)).unwrap();
        two.move_xy(tree, Rect { tl: Point { x: 0, y: 0 }, size: Vector::null() });
        assert!(tree.arena[two.0].first_child.is_none());
        assert_eq!(tree.arena[two.0].parent, Some(tree.root));
        assert_eq!(tree.arena[tree.root].first_child, Some(one.0));
        assert_eq!(tree.arena[one.0].next, two.0);
        assert_eq!(tree.arena[two.0].next, one.0);
        let three = Window::new(tree, None, Some(two)).unwrap();
        three.move_xy(tree, Rect { tl: Point { x: 0, y: 0 }, size: Vector::null() });
        assert!(tree.arena[three.0].first_child.is_none());
        assert_eq!(tree.arena[three.0].parent, Some(tree.root));
        assert_eq!(tree.arena[tree.root].first_child, Some(one.0));
        assert_eq!(tree.arena[one.0].next, two.0);
        assert_eq!(tree.arena[two.0].next, three.0);
        assert_eq!(tree.arena[three.0].next, one.0);
        let four = Window::new(tree, None, None).unwrap();
        four.move_xy(tree, Rect { tl: Point { x: 0, y: 0 }, size: Vector::null() });
        assert!(tree.arena[four.0].first_child.is_none());
        assert_eq!(tree.arena[four.0].parent, Some(tree.root));
        assert_eq!(tree.arena[tree.root].first_child, Some(four.0));
        assert_eq!(tree.arena[one.0].next, two.0);
        assert_eq!(tree.arena[two.0].next, three.0);
        assert_eq!(tree.arena[three.0].next, four.0);
        assert_eq!(tree.arena[four.0].next, one.0);
    }

    #[test]
    fn drop_subtree() {
        fn render<State: ?Sized>(_: &WindowTree<State>, _: Option<Window>, _: &mut RenderPort, _: &mut State) { }
        let screen = tuifw_screen_test::Screen::new(Vector::null());
        let screen = Box::new(screen) as _;
        let tree = &mut WindowTree::<()>::new(screen, render).unwrap();
        let w = Window::new(tree, None, None).unwrap();
        let _ = Window::new(tree, Some(w), None).unwrap();
        w.drop_window(tree);
        assert_eq!(tree.arena.items().len(), 1);
     }
}