diff --git a/src/frame_view.rs b/src/frame_view.rs index b6e0517..542de73 100644 --- a/src/frame_view.rs +++ b/src/frame_view.rs @@ -10,6 +10,7 @@ use crate::{ editor_view::CodeEditorView, html_view::HtmlView, lineengine::line::{Readline, ReadlineEvent}, + tab_panel::TabPanel, term_split::TermSplit, timer_host::TimerHost, GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel, @@ -279,6 +280,10 @@ pub fn term_view_tree(props: &TermViewTreeProps) -> Html { second={mk_term_view_tree(global, layout, bottom)} /> }, + Tabs { tabs } => html! { + >()} + /> + }, } } diff --git a/src/lua_engine.rs b/src/lua_engine.rs index 1ce7adb..f0bae60 100644 --- a/src/lua_engine.rs +++ b/src/lua_engine.rs @@ -1,7 +1,8 @@ -use self::{frameroutes::*, frames::*, muds::*, storage::*}; +use self::{frameroutes::*, frames::*, muds::*, navbar::*, storage::*}; use anyhow::Error; use help::{cmd_help, load_help}; use muds::telopt::configure_telopt_table; +use navbar::init_navbar; use piccolo::{ async_callback::{AsyncSequence, Locals}, meta_ops::{self, MetaResult}, @@ -33,6 +34,7 @@ pub mod frames; pub mod help; pub mod muds; pub mod named_closure; +pub mod navbar; pub mod storage; impl LuaState { @@ -226,6 +228,7 @@ pub fn install_lua_globals( register_stateless_command!(cmd_include, "include"); register_command!(cmd_list_logs, "listlogs"); register_command!(mud_log, "log"); + register_command!(syncnavbar); register_command!(panel_merge); register_command!(panel_swap); register_command!(sendmud_raw); @@ -398,6 +401,8 @@ pub fn install_lua_globals( match_table_try_run_sub ); + init_navbar(ctx, global_memo, &global_layout)?; + Ok(()) }) .map_err(|e| e.to_string())?; diff --git a/src/lua_engine/navbar.rs b/src/lua_engine/navbar.rs new file mode 100644 index 0000000..2fb9429 --- /dev/null +++ b/src/lua_engine/navbar.rs @@ -0,0 +1,117 @@ +use anyhow::Result; +use piccolo::{Callback, CallbackReturn, Context, IntoValue, Table}; +use std::{ops::Deref, rc::Rc}; +use yew::UseStateSetter; + +use crate::{navbar::NavbarButton, FrameId, GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell}; + +pub fn init_navbar( + ctx: Context, + global_memo: &GlobalMemoCell, + global_layout: &UseStateSetter, +) -> Result<()> { + let info_tbl: Table = ctx.get_global::("info")?; + let nav_tbl: Table = Table::new(&ctx); + + let default_buttons: Vec = vec![ + NavbarButton { + icon: "code-square".to_owned(), + tooltip: "Open the script editor".to_owned(), + frame: FrameId(1), + command: "#editor".to_owned(), + }, + NavbarButton { + icon: "arrow-left".to_owned(), + tooltip: "Go west".to_owned(), + frame: FrameId(1), + command: "w".to_owned(), + }, + NavbarButton { + icon: "arrow-up-left".to_owned(), + tooltip: "Go northwest".to_owned(), + frame: FrameId(1), + command: "nw".to_owned(), + }, + NavbarButton { + icon: "arrow-up".to_owned(), + tooltip: "Go north".to_owned(), + frame: FrameId(1), + command: "n".to_owned(), + }, + NavbarButton { + icon: "arrow-up-right".to_owned(), + tooltip: "Go northeast".to_owned(), + frame: FrameId(1), + command: "ne".to_owned(), + }, + NavbarButton { + icon: "arrow-right".to_owned(), + tooltip: "Go east".to_owned(), + frame: FrameId(1), + command: "e".to_owned(), + }, + NavbarButton { + icon: "arrow-down-right".to_owned(), + tooltip: "Go southeast".to_owned(), + frame: FrameId(1), + command: "se".to_owned(), + }, + NavbarButton { + icon: "arrow-down".to_owned(), + tooltip: "Go south".to_owned(), + frame: FrameId(1), + command: "s".to_owned(), + }, + NavbarButton { + icon: "arrow-down-left".to_owned(), + tooltip: "Go southwest".to_owned(), + frame: FrameId(1), + command: "sw".to_owned(), + }, + ]; + + let mut i: i64 = 1; + for button in &default_buttons { + nav_tbl.set(ctx, i, button.clone().into_value(ctx))?; + i += 1; + } + nav_tbl.set(ctx, "showing", true)?; + info_tbl.set(ctx, "navbar", nav_tbl)?; + + let mut new_layout: GlobalLayoutState = global_memo.layout.borrow().deref().deref().clone(); + new_layout.navstate.buttons = default_buttons; + let new_layout: Rc = new_layout.into(); + *global_memo.layout.borrow_mut() = new_layout.clone(); + global_layout.set(new_layout); + Ok(()) +} + +pub fn syncnavbar<'gc>( + ctx: Context<'gc>, + global_memo: &GlobalMemoCell, + global_layout: &UseStateSetter, +) -> Callback<'gc> { + let global_memo = global_memo.clone(); + let global_layout = global_layout.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, _stack| { + let mut new_layout: GlobalLayoutState = global_memo.layout.borrow().deref().deref().clone(); + + let tbl: Table = ctx.get_global::
("info")?.get(ctx, "navbar")?; + + new_layout.navstate.showing = tbl.get(ctx, "showing")?; + let mut buttons: Vec = vec![]; + let mut i: i64 = 1; + loop { + match tbl.get(ctx, i) { + Err(_) => break, + Ok(v) => buttons.push(v), + } + i += 1; + } + new_layout.navstate.buttons = buttons; + let new_layout: Rc = new_layout.into(); + *global_memo.layout.borrow_mut() = new_layout.clone(); + global_layout.set(new_layout); + Ok(CallbackReturn::Return) + }) +} diff --git a/src/main.rs b/src/main.rs index 586328e..bed7d3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::collections::VecDeque; use std::rc::Rc; use logging::LoggingEngine; +use navbar::{NavState, Navbar}; use parsing::ParsedCommand; use term_split::TermSplit; use web_sys::console; @@ -17,8 +18,10 @@ pub mod lineengine; pub mod logging; pub mod lua_engine; pub mod match_table; +pub mod navbar; pub mod parsing; pub mod split_panel; +pub mod tab_panel; pub mod telnet; pub mod term_split; pub mod timer_host; @@ -59,6 +62,7 @@ impl PartialEq for GlobalMemoState { pub struct GlobalLayoutState { term_splits: TermSplit, frame_views: im::OrdMap, + navstate: NavState, } // Note: Despite interior mutability, you still should always call the // setter to force a re-render. Interior mutability is used to allow this @@ -72,6 +76,7 @@ fn app() -> Html { Rc::new(GlobalLayoutState { term_splits: TermSplit::Term { frame: FrameId(1) }, frame_views: im::OrdMap::new(), + navstate: Default::default(), }) }); let global_memo = use_memo((), |_| GlobalMemoState { @@ -106,9 +111,13 @@ fn app() -> Html { }); html! { -
- +
+ +
+ +
} } diff --git a/src/navbar.rs b/src/navbar.rs new file mode 100644 index 0000000..30a6022 --- /dev/null +++ b/src/navbar.rs @@ -0,0 +1,108 @@ +use piccolo::{Context, FromValue, IntoValue, Table, Value}; +use yew::{function_component, html, Callback, Html, Properties, UseStateHandle}; + +use crate::{command_handler::command_handler, FrameId, GlobalLayoutCell, GlobalMemoCell}; + +#[derive(Properties, PartialEq)] +pub struct NavbarProps { + pub global_memo: GlobalMemoCell, + pub global_layout: UseStateHandle, +} + +#[derive(PartialEq, Clone, Debug)] +pub struct NavbarButton { + pub icon: String, + pub tooltip: String, + pub frame: FrameId, + pub command: String, +} + +impl<'gc> IntoValue<'gc> for NavbarButton { + fn into_value(self, ctx: Context<'gc>) -> Value<'gc> { + let tbl = Table::new(&ctx); + let _ = tbl.set(ctx, "icon", self.icon); + let _ = tbl.set(ctx, "tooltip", self.tooltip); + let _ = tbl.set(ctx, "frame", self.frame.0 as i64); + let _ = tbl.set(ctx, "command", self.command); + tbl.into_value(ctx) + } +} +impl<'gc> FromValue<'gc> for NavbarButton { + fn from_value(ctx: Context<'gc>, value: Value<'gc>) -> Result { + let tbl: Table = Table::from_value(ctx, value)?; + Ok(NavbarButton { + icon: tbl.get(ctx, "icon")?, + tooltip: tbl.get(ctx, "tooltip")?, + frame: FrameId(tbl.get(ctx, "frame")?), + command: tbl.get(ctx, "command")?, + }) + } +} + +#[derive(PartialEq, Clone)] +pub struct NavState { + pub showing: bool, + pub buttons: Vec, +} +impl Default for NavState { + fn default() -> Self { + Self { + showing: true, + // Overridden later in init_navbar, starts off minimal. + buttons: vec![], + } + } +} + +#[function_component(Navbar)] +pub fn navbar(props: &NavbarProps) -> Html { + if !props.global_layout.navstate.showing { + return html! { <> }; + } + html! { + + } +} + +#[cfg(test)] +mod test { + use piccolo::{FromValue, IntoValue, Lua}; + + use crate::FrameId; + + use super::NavbarButton; + + #[test] + fn button_tovalue_fromvalue_roundtrips() { + let mut lua = Lua::empty(); + lua.enter(|ctx| { + let b = NavbarButton { + icon: "myicon".to_owned(), + tooltip: "I'm a tooltip".to_owned(), + frame: FrameId(42), + command: "hello world".to_owned(), + }; + assert_eq!( + NavbarButton::from_value(ctx, b.clone().into_value(ctx)).unwrap(), + b + ); + }) + } +} diff --git a/src/tab_panel.rs b/src/tab_panel.rs new file mode 100644 index 0000000..a85af09 --- /dev/null +++ b/src/tab_panel.rs @@ -0,0 +1,38 @@ +use yew::{function_component, html, use_state_eq, Callback, Html, Properties}; + +#[derive(Properties, PartialEq)] +pub struct TabPanelProps { + pub tabs: Vec<(char, Html)>, +} + +#[function_component(TabPanel)] +pub fn tab_panel_props(props: &TabPanelProps) -> Html { + let active_tab = use_state_eq(|| props.tabs.get(0).map(|(k, _)| *k).unwrap_or('0')); + html! { +
+ + {props.tabs.iter().map(|(c, v)| { + let class_name = if *c == *active_tab { "container-fluid visible"} else { "container-fluid invisible"}; + html! { +
+ {v.clone()} +
+ } + }).collect::>()} +
+ } +} diff --git a/src/term_split.rs b/src/term_split.rs index eadb223..7dd5eb5 100644 --- a/src/term_split.rs +++ b/src/term_split.rs @@ -1,6 +1,8 @@ +use im::OrdMap; use itertools::Itertools; use std::{ collections::{BTreeMap, VecDeque}, + ops::Deref, rc::Rc, }; @@ -19,6 +21,9 @@ pub enum TermSplit { top: Rc, bottom: Rc, }, + Tabs { + tabs: OrdMap>, + }, } impl TermSplit { @@ -43,6 +48,11 @@ impl TermSplit { top.collect_term_frames(into); bottom.collect_term_frames(into); } + TermSplit::Tabs { tabs } => { + for tab in tabs.values() { + tab.collect_term_frames(into); + } + } } } @@ -105,10 +115,21 @@ impl TermSplit { }), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::())) }, + TermSplit::Tabs { tabs } => { + match pathstr.split_first() { + None => mod_with(self), + Some((c, path_rest)) => { + match tabs.get(c) { + None => Err(format!("Reference to tab '{}', which doesn't exist, before {} in split path", c, path_rest.iter().collect::())), + Some(t) => Ok(TermSplit::Tabs { tabs: tabs.update(*c, t.modify_at_pathstr_vec(path_rest, mod_with)?.into())}), + } + } + } + }, TermSplit::Term { .. } => match pathstr.split_first() { None => mod_with(self), Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::())) - } + }, } } @@ -130,6 +151,13 @@ impl TermSplit { Some(('b', path_rest)) => bottom.get_at_pathstr_vec(path_rest), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::())) }, + TermSplit::Tabs { tabs } => match pathstr.split_first() { + None => Ok(self), + Some((c, path_rest)) => match tabs.get(c) { + None => Err(format!("Reference to tab '{}', which doesn't exist, before {} in split path", c, path_rest.iter().collect::())), + Some(t) => t.get_at_pathstr_vec(path_rest), + } + }, TermSplit::Term { .. } => match pathstr.split_first() { None => Ok(self), Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::())) @@ -162,10 +190,12 @@ impl TermSplit { pub fn join(&self, pathstr: &str) -> Result { self.modify_at_pathstr(pathstr, move |n| match n { TermSplit::Term { .. } => { - Err("Can only join vertical or horizontal splits, not a terminal".to_owned()) + Err("Can only join vertical or horizontal splits (or tab with one entry), not a terminal".to_owned()) } TermSplit::Horizontal { left, .. } => Ok((**left).clone()), TermSplit::Vertical { top, .. } => Ok((**top).clone()), + TermSplit::Tabs { tabs } if tabs.len() != 1 => Err("Can only join tabs when only one tab left".to_owned()), + TermSplit::Tabs { tabs } => Ok((**tabs.iter().next().unwrap().1).clone()), }) } @@ -179,6 +209,33 @@ impl TermSplit { new.validate()?; Ok(new) } + + pub fn tabbed(&self, first_tab: char, pathstr: &str) -> Result { + self.modify_at_pathstr(pathstr, |t| { + Ok(TermSplit::Tabs { + tabs: OrdMap::unit(first_tab, t.clone().into()), + }) + }) + } + + pub fn add_tab( + &self, + pathstr: &str, + new_tab: char, + new_frame: FrameId, + ) -> Result { + let new = self.modify_at_pathstr(pathstr, |t| match t { + TermSplit::Tabs { tabs } => Ok(TermSplit::Tabs { + tabs: tabs.update(new_tab, TermSplit::Term { frame: new_frame }.into()), + }), + _ => Err(format!( + "Tried to add tab at path {}, which isn't a tab set.", + pathstr + )), + })?; + new.validate()?; + Ok(new) + } } pub struct AccessibleSplitIter<'t> { @@ -192,12 +249,15 @@ impl<'t> Iterator for AccessibleSplitIter<'t> { loop { match self.queue.pop_back() { Some(TermSplit::Horizontal { left, right }) => { - self.queue.push_front(left); - self.queue.push_front(right); + self.queue.push_back(right); + self.queue.push_back(left); } Some(TermSplit::Vertical { top, bottom }) => { - self.queue.push_front(top); - self.queue.push_front(bottom); + self.queue.push_back(bottom); + self.queue.push_back(top); + } + Some(TermSplit::Tabs { tabs }) => { + self.queue.extend(tabs.values().rev().map(|v| v.deref())); } Some(TermSplit::Term { frame }) => break Some(frame), None => break None, @@ -315,15 +375,22 @@ mod tests { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), - right: Term { frame: FrameId(64) }.into(), + right: Term { frame: FrameId(43) }.into(), } .into(), - right: Term { frame: FrameId(42) }.into(), + right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { - top: Term { frame: FrameId(43) }.into(), - bottom: Term { frame: FrameId(44) }.into(), + top: Term { frame: FrameId(45) }.into(), + bottom: Tabs { + tabs: vec![ + ('0', Term { frame: FrameId(46) }), + ('1', Term { frame: FrameId(47) }), + ] + .into(), + } + .into(), } .into(), }; @@ -334,8 +401,9 @@ mod tests { &FrameId(42), &FrameId(43), &FrameId(44), - &FrameId(42), - &FrameId(64), + &FrameId(45), + &FrameId(46), + &FrameId(47), ] ); } @@ -379,4 +447,97 @@ mod tests { }) ); } + + #[test] + fn framing_works() { + use TermSplit::*; + let t = Vertical { + top: Horizontal { + left: Horizontal { + left: Term { frame: FrameId(42) }.into(), + right: Term { frame: FrameId(43) }.into(), + } + .into(), + right: Term { frame: FrameId(44) }.into(), + } + .into(), + bottom: Vertical { + top: Term { frame: FrameId(45) }.into(), + bottom: Term { frame: FrameId(46) }.into(), + } + .into(), + }; + assert_eq!( + t.tabbed('0', "bb").unwrap(), + Vertical { + top: Horizontal { + left: Horizontal { + left: Term { frame: FrameId(42) }.into(), + right: Term { frame: FrameId(43) }.into(), + } + .into(), + right: Term { frame: FrameId(44) }.into(), + } + .into(), + bottom: Vertical { + top: Term { frame: FrameId(45) }.into(), + bottom: Tabs { + tabs: vec![('0', Term { frame: FrameId(46) })].into() + } + .into(), + } + .into(), + } + ); + } + + #[test] + fn can_add_tab() { + use TermSplit::*; + let t = Vertical { + top: Horizontal { + left: Horizontal { + left: Term { frame: FrameId(42) }.into(), + right: Term { frame: FrameId(43) }.into(), + } + .into(), + right: Term { frame: FrameId(44) }.into(), + } + .into(), + bottom: Vertical { + top: Term { frame: FrameId(45) }.into(), + bottom: Tabs { + tabs: vec![('0', Term { frame: FrameId(46) })].into(), + } + .into(), + } + .into(), + }; + assert_eq!( + t.add_tab("bb", '1', FrameId(47)).unwrap(), + Vertical { + top: Horizontal { + left: Horizontal { + left: Term { frame: FrameId(42) }.into(), + right: Term { frame: FrameId(43) }.into(), + } + .into(), + right: Term { frame: FrameId(44) }.into(), + } + .into(), + bottom: Vertical { + top: Term { frame: FrameId(45) }.into(), + bottom: Tabs { + tabs: vec![ + ('0', Term { frame: FrameId(46) }), + ('1', Term { frame: FrameId(47) }), + ] + .into() + } + .into() + } + .into(), + } + ); + } } diff --git a/styles.css b/styles.css index 2ef6893..a0a4863 100644 --- a/styles.css +++ b/styles.css @@ -92,3 +92,8 @@ body { width: 100%; height: 100%; } +.termtreewrapper { + flex-grow: 1; + flex-shrink: 1; + min-height: 1px; +}