terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
#![cfg(feature = "client")]

use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;

use terrazzo::envelope;
use terrazzo::prelude::XSignal;
use wasm_bindgen_futures::spawn_local;
use web_sys::MouseEvent;

use super::api::Direction;
use super::api::Side;
use super::api::Tile as TileDto;
use super::api::Tiles as TilesDto;
use super::app::App;
use super::id::TileId;
use super::ui::RootTree;
use super::visitor::TilesTreeVisitor;
use super::visitor::UiStateVisitor;
use crate::frontend::menu::MenuState;
use crate::frontend::remotes::Remote;

pub enum Tiles {
    Tile(TilePtr),
    Array {
        id: TileId,
        direction: XSignal<Direction>,
        nodes: Vec<Rc<Tiles>>,
    },
}

#[envelope]
pub struct Tile {
    pub id: TileId,
    pub app: XSignal<App>,
    pub remote: XSignal<Remote>,
    pub menu: MenuState,
}

impl Tiles {
    pub fn update(&self, tiles: &TilesDto) -> Self {
        let mut signals = TileSignals::default();
        signals.visit_node(self);
        transform(&mut signals, tiles)
    }
}

fn transform(signals: &mut TileSignals, tile_tree_dto: &TilesDto) -> Tiles {
    match tile_tree_dto {
        TilesDto::Tile(TileDto { id, app, remote }) => {
            let ui_tile = if let Some(ui_tile) = signals.tile_ids.remove(id) {
                ui_tile.app.set(*app);
                ui_tile.remote.set(remote.clone());
                ui_tile
            } else {
                Tile {
                    id: *id,
                    app: XSignal::new("app", *app),
                    remote: XSignal::new("remote", remote.clone()),
                    menu: MenuState::default(),
                }
                .into()
            };
            Tiles::Tile(ui_tile)
        }
        TilesDto::Array {
            id,
            direction,
            nodes,
        } => {
            let ui_direction = if let Some(ui_direction) = signals.directions.remove(id) {
                ui_direction.set(*direction);
                ui_direction
            } else {
                XSignal::new("direction", *direction)
            };
            let mut ui_nodes = Vec::with_capacity(nodes.len());
            for node in nodes {
                ui_nodes.push(transform(signals, node).into());
            }
            Tiles::Array {
                id: *id,
                direction: ui_direction,
                nodes: ui_nodes,
            }
        }
    }
}

impl Default for Tiles {
    fn default() -> Self {
        let id = TileId::first_tile_id();
        Tile {
            id,
            app: XSignal::new("app", App::default()),
            remote: XSignal::new("remote", Remote::default()),
            menu: MenuState::default(),
        }
        .into()
    }
}

impl From<Tile> for Tiles {
    fn from(tile: Tile) -> Self {
        Self::Tile(tile.into())
    }
}

#[derive(Default)]
struct TileSignals {
    directions: HashMap<TileId, XSignal<Direction>>,
    tile_ids: HashMap<TileId, TilePtr>,
}

impl<'l> TilesTreeVisitor<UiStateVisitor<'l>> for TileSignals {
    fn visit_tree(&mut self, id: TileId, direction: &XSignal<Direction>) {
        self.directions.insert(id, direction.clone());
    }
    fn visit_tile(&mut self, tile: &TilePtr) {
        self.tile_ids.insert(tile.id, tile.clone());
    }
}

#[derive(Clone)]
pub struct TilesCmp<T>(T);

impl<T> TilesCmp<T> {
    pub fn new(t: T) -> Self {
        Self(t)
    }
}

impl<T: AsRef<Tiles>> PartialEq for TilesCmp<T> {
    fn eq(&self, other: &Self) -> bool {
        match (self.as_ref(), other.as_ref()) {
            (Tiles::Tile(a), Tiles::Tile(b)) => a.id == b.id,
            (
                Tiles::Array {
                    id: a_id,
                    nodes: a_nodes,
                    ..
                },
                Tiles::Array {
                    id: b_id,
                    nodes: b_nodes,
                    ..
                },
            ) => {
                a_id == b_id
                    && a_nodes.len() == b_nodes.len()
                    && Iterator::zip(a_nodes.iter(), b_nodes.iter())
                        .all(|(a, b)| TilesCmp(a) == TilesCmp(b))
            }
            _ => false,
        }
    }
}

impl<T: AsRef<Tiles>> Eq for TilesCmp<T> {}

impl<T: AsRef<Tiles>> AsRef<Tiles> for TilesCmp<T> {
    fn as_ref(&self) -> &Tiles {
        self.0.as_ref()
    }
}

impl<T> Deref for TilesCmp<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> std::fmt::Debug for TilesCmp<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Tiles(...)")
    }
}

impl Tile {
    pub fn split_horz(&self) -> impl Fn(MouseEvent) + 'static {
        self.split(Direction::Horizontal)
    }

    pub fn split_vert(&self) -> impl Fn(MouseEvent) + 'static {
        self.split(Direction::Vertical)
    }

    fn split(&self, direction: Direction) -> impl Fn(MouseEvent) + 'static {
        let tile_id = self.id;
        move |_| {
            spawn_local(async move {
                RootTree::update(super::api::add(direction, tile_id, Side::After).await)
            })
        }
    }

    pub fn close(&self) -> impl Fn(MouseEvent) + 'static {
        let tile_id = self.id;
        move |_| spawn_local(async move { RootTree::update(super::api::remove(tile_id).await) })
    }
}