Add navbars, infra for tab frames

This commit is contained in:
Condorra 2024-12-09 23:01:27 +11:00
parent 13225859a1
commit 0da685d4f5
8 changed files with 464 additions and 16 deletions

View File

@ -10,6 +10,7 @@ use crate::{
editor_view::CodeEditorView, editor_view::CodeEditorView,
html_view::HtmlView, html_view::HtmlView,
lineengine::line::{Readline, ReadlineEvent}, lineengine::line::{Readline, ReadlineEvent},
tab_panel::TabPanel,
term_split::TermSplit, term_split::TermSplit,
timer_host::TimerHost, timer_host::TimerHost,
GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel, GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel,
@ -279,6 +280,10 @@ pub fn term_view_tree(props: &TermViewTreeProps) -> Html {
second={mk_term_view_tree(global, layout, bottom)} second={mk_term_view_tree(global, layout, bottom)}
/> />
}, },
Tabs { tabs } => html! {
<TabPanel tabs={tabs.iter().map(|(k, t)| (*k, mk_term_view_tree(global, layout.clone(), t))).collect::<Vec<_>>()}
/>
},
} }
} }

View File

@ -1,7 +1,8 @@
use self::{frameroutes::*, frames::*, muds::*, storage::*}; use self::{frameroutes::*, frames::*, muds::*, navbar::*, storage::*};
use anyhow::Error; use anyhow::Error;
use help::{cmd_help, load_help}; use help::{cmd_help, load_help};
use muds::telopt::configure_telopt_table; use muds::telopt::configure_telopt_table;
use navbar::init_navbar;
use piccolo::{ use piccolo::{
async_callback::{AsyncSequence, Locals}, async_callback::{AsyncSequence, Locals},
meta_ops::{self, MetaResult}, meta_ops::{self, MetaResult},
@ -33,6 +34,7 @@ pub mod frames;
pub mod help; pub mod help;
pub mod muds; pub mod muds;
pub mod named_closure; pub mod named_closure;
pub mod navbar;
pub mod storage; pub mod storage;
impl LuaState { impl LuaState {
@ -226,6 +228,7 @@ pub fn install_lua_globals(
register_stateless_command!(cmd_include, "include"); register_stateless_command!(cmd_include, "include");
register_command!(cmd_list_logs, "listlogs"); register_command!(cmd_list_logs, "listlogs");
register_command!(mud_log, "log"); register_command!(mud_log, "log");
register_command!(syncnavbar);
register_command!(panel_merge); register_command!(panel_merge);
register_command!(panel_swap); register_command!(panel_swap);
register_command!(sendmud_raw); register_command!(sendmud_raw);
@ -398,6 +401,8 @@ pub fn install_lua_globals(
match_table_try_run_sub match_table_try_run_sub
); );
init_navbar(ctx, global_memo, &global_layout)?;
Ok(()) Ok(())
}) })
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;

117
src/lua_engine/navbar.rs Normal file
View File

@ -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<GlobalLayoutCell>,
) -> Result<()> {
let info_tbl: Table = ctx.get_global::<Table>("info")?;
let nav_tbl: Table = Table::new(&ctx);
let default_buttons: Vec<NavbarButton> = 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<GlobalLayoutState> = 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<GlobalLayoutCell>,
) -> 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::<Table>("info")?.get(ctx, "navbar")?;
new_layout.navstate.showing = tbl.get(ctx, "showing")?;
let mut buttons: Vec<NavbarButton> = 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<GlobalLayoutState> = new_layout.into();
*global_memo.layout.borrow_mut() = new_layout.clone();
global_layout.set(new_layout);
Ok(CallbackReturn::Return)
})
}

View File

@ -3,6 +3,7 @@ use std::collections::VecDeque;
use std::rc::Rc; use std::rc::Rc;
use logging::LoggingEngine; use logging::LoggingEngine;
use navbar::{NavState, Navbar};
use parsing::ParsedCommand; use parsing::ParsedCommand;
use term_split::TermSplit; use term_split::TermSplit;
use web_sys::console; use web_sys::console;
@ -17,8 +18,10 @@ pub mod lineengine;
pub mod logging; pub mod logging;
pub mod lua_engine; pub mod lua_engine;
pub mod match_table; pub mod match_table;
pub mod navbar;
pub mod parsing; pub mod parsing;
pub mod split_panel; pub mod split_panel;
pub mod tab_panel;
pub mod telnet; pub mod telnet;
pub mod term_split; pub mod term_split;
pub mod timer_host; pub mod timer_host;
@ -59,6 +62,7 @@ impl PartialEq for GlobalMemoState {
pub struct GlobalLayoutState { pub struct GlobalLayoutState {
term_splits: TermSplit, term_splits: TermSplit,
frame_views: im::OrdMap<FrameId, FrameViewType>, frame_views: im::OrdMap<FrameId, FrameViewType>,
navstate: NavState,
} }
// Note: Despite interior mutability, you still should always call the // Note: Despite interior mutability, you still should always call the
// setter to force a re-render. Interior mutability is used to allow this // setter to force a re-render. Interior mutability is used to allow this
@ -72,6 +76,7 @@ fn app() -> Html {
Rc::new(GlobalLayoutState { Rc::new(GlobalLayoutState {
term_splits: TermSplit::Term { frame: FrameId(1) }, term_splits: TermSplit::Term { frame: FrameId(1) },
frame_views: im::OrdMap::new(), frame_views: im::OrdMap::new(),
navstate: Default::default(),
}) })
}); });
let global_memo = use_memo((), |_| GlobalMemoState { let global_memo = use_memo((), |_| GlobalMemoState {
@ -106,10 +111,14 @@ fn app() -> Html {
}); });
html! { html! {
<div class="toplevel" data-bs-theme="dark"> <div class="toplevel d-flex flex-column" data-bs-theme="dark">
<Navbar global_memo={global_memo.clone()}
global_layout={global_layout.clone()}/>
<div class="termtreewrapper">
<TermViewTree global_memo={global_memo.clone()} <TermViewTree global_memo={global_memo.clone()}
global_layout={global_layout.clone()}/> global_layout={global_layout.clone()}/>
</div> </div>
</div>
} }
} }

108
src/navbar.rs Normal file
View File

@ -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<GlobalLayoutCell>,
}
#[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<Self, piccolo::TypeError> {
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<NavbarButton>,
}
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! {
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div>
{
props.global_layout.navstate.buttons.iter().map(|button| {
let global_memo = props.global_memo.clone();
let button = button.clone();
let onclick = Callback::from(move |_| {
command_handler(&global_memo, &button.frame, &button.command);
});
html! {
<button type="button" title={button.tooltip} class="btn" {onclick}>
<i class={format!("bi bi-{}", button.icon)}></i>
</button>
}
}).collect::<Vec<_>>()
}
</div>
</nav>
}
}
#[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
);
})
}
}

38
src/tab_panel.rs Normal file
View File

@ -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! {
<div class="container-fluid d-flex flex-column">
<ul class="nav nav-tabs">
{props.tabs.iter().map(|(c, _)| {
let class_name = if *c == *active_tab { "nav-link active" } else { "nav-link" };
let c_click: char = *c;
let active_tab_click = active_tab.clone();
let on_click = Callback::from(move |_| {
active_tab_click.set(c_click);
});
html! {
<li class="nav-item" key={format!("nav-{}", c)}>
<a class={class_name} onclick={on_click} href="#">{c}</a>
</li>
}}).collect::<Vec<_>>()
}
</ul>
{props.tabs.iter().map(|(c, v)| {
let class_name = if *c == *active_tab { "container-fluid visible"} else { "container-fluid invisible"};
html! {
<div class={class_name} key={format!("content-{}", c)}>
{v.clone()}
</div>
}
}).collect::<Vec<_>>()}
</div>
}
}

View File

@ -1,6 +1,8 @@
use im::OrdMap;
use itertools::Itertools; use itertools::Itertools;
use std::{ use std::{
collections::{BTreeMap, VecDeque}, collections::{BTreeMap, VecDeque},
ops::Deref,
rc::Rc, rc::Rc,
}; };
@ -19,6 +21,9 @@ pub enum TermSplit {
top: Rc<TermSplit>, top: Rc<TermSplit>,
bottom: Rc<TermSplit>, bottom: Rc<TermSplit>,
}, },
Tabs {
tabs: OrdMap<char, Rc<TermSplit>>,
},
} }
impl TermSplit { impl TermSplit {
@ -43,6 +48,11 @@ impl TermSplit {
top.collect_term_frames(into); top.collect_term_frames(into);
bottom.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::<String>())) Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::<String>()))
}, },
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::<String>())),
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() { TermSplit::Term { .. } => match pathstr.split_first() {
None => mod_with(self), None => mod_with(self),
Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>())) Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>()))
} },
} }
} }
@ -130,6 +151,13 @@ impl TermSplit {
Some(('b', path_rest)) => bottom.get_at_pathstr_vec(path_rest), 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::<String>())) Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::<String>()))
}, },
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::<String>())),
Some(t) => t.get_at_pathstr_vec(path_rest),
}
},
TermSplit::Term { .. } => match pathstr.split_first() { TermSplit::Term { .. } => match pathstr.split_first() {
None => Ok(self), None => Ok(self),
Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>())) Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>()))
@ -162,10 +190,12 @@ impl TermSplit {
pub fn join(&self, pathstr: &str) -> Result<TermSplit, String> { pub fn join(&self, pathstr: &str) -> Result<TermSplit, String> {
self.modify_at_pathstr(pathstr, move |n| match n { self.modify_at_pathstr(pathstr, move |n| match n {
TermSplit::Term { .. } => { 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::Horizontal { left, .. } => Ok((**left).clone()),
TermSplit::Vertical { top, .. } => Ok((**top).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()?; new.validate()?;
Ok(new) Ok(new)
} }
pub fn tabbed(&self, first_tab: char, pathstr: &str) -> Result<TermSplit, String> {
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<TermSplit, String> {
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> { pub struct AccessibleSplitIter<'t> {
@ -192,12 +249,15 @@ impl<'t> Iterator for AccessibleSplitIter<'t> {
loop { loop {
match self.queue.pop_back() { match self.queue.pop_back() {
Some(TermSplit::Horizontal { left, right }) => { Some(TermSplit::Horizontal { left, right }) => {
self.queue.push_front(left); self.queue.push_back(right);
self.queue.push_front(right); self.queue.push_back(left);
} }
Some(TermSplit::Vertical { top, bottom }) => { Some(TermSplit::Vertical { top, bottom }) => {
self.queue.push_front(top); self.queue.push_back(bottom);
self.queue.push_front(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), Some(TermSplit::Term { frame }) => break Some(frame),
None => break None, None => break None,
@ -315,15 +375,22 @@ mod tests {
top: Horizontal { top: Horizontal {
left: Horizontal { left: Horizontal {
left: Term { frame: FrameId(42) }.into(), left: Term { frame: FrameId(42) }.into(),
right: Term { frame: FrameId(64) }.into(), right: Term { frame: FrameId(43) }.into(),
} }
.into(), .into(),
right: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(44) }.into(),
} }
.into(), .into(),
bottom: Vertical { bottom: Vertical {
top: Term { frame: FrameId(43) }.into(), top: Term { frame: FrameId(45) }.into(),
bottom: Term { frame: FrameId(44) }.into(), bottom: Tabs {
tabs: vec![
('0', Term { frame: FrameId(46) }),
('1', Term { frame: FrameId(47) }),
]
.into(),
}
.into(),
} }
.into(), .into(),
}; };
@ -334,8 +401,9 @@ mod tests {
&FrameId(42), &FrameId(42),
&FrameId(43), &FrameId(43),
&FrameId(44), &FrameId(44),
&FrameId(42), &FrameId(45),
&FrameId(64), &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(),
}
);
}
} }

View File

@ -92,3 +92,8 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.termtreewrapper {
flex-grow: 1;
flex-shrink: 1;
min-height: 1px;
}