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,
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! {
<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 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())?;

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 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<FrameId, FrameViewType>,
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! {
<div class="toplevel" data-bs-theme="dark">
<TermViewTree global_memo={global_memo.clone()}
global_layout={global_layout.clone()}/>
<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()}
global_layout={global_layout.clone()}/>
</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 std::{
collections::{BTreeMap, VecDeque},
ops::Deref,
rc::Rc,
};
@ -19,6 +21,9 @@ pub enum TermSplit {
top: Rc<TermSplit>,
bottom: Rc<TermSplit>,
},
Tabs {
tabs: OrdMap<char, Rc<TermSplit>>,
},
}
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::<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() {
None => mod_with(self),
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((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() {
None => Ok(self),
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> {
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<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> {
@ -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(),
}
);
}
}

View File

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