dash-frontend: tabs, other fixes (desc)

- set rustfmt line width to 120 columns by default for wgui
- dashboard tabs
- wgui: `remove_children`
This commit is contained in:
Aleksander
2025-09-15 20:37:55 +02:00
parent f115d2d2cf
commit 54767d75da
30 changed files with 624 additions and 191 deletions

1
Cargo.lock generated
View File

@@ -1367,6 +1367,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glam", "glam",
"log",
"rust-embed", "rust-embed",
"wgui", "wgui",
] ]

View File

@@ -7,4 +7,5 @@ edition = "2024"
anyhow.workspace = true anyhow.workspace = true
wgui = { path = "../wgui/" } wgui = { path = "../wgui/" }
glam = { workspace = true } glam = { workspace = true }
log = { workspace = true }
rust-embed = "8.7.2" rust-embed = "8.7.2"

View File

@@ -1,11 +1,25 @@
<layout> <layout>
<include src="theme.xml" /> <include src="theme.xml" />
<include src="templates.xml" />
<theme> <theme>
<var key="side_size" value="48" />
<var key="side_sprite_size" value="26" /> <var key="side_sprite_size" value="26" />
<var key="side_button_size" value="48" />
</theme> </theme>
<template name="SideButton">
<Button
id="${id}"
round="100%"
width="~side_button_size"
height="~side_button_size"
color="#44444400"
hover_color="#333333ff"
hover_border_color="#555555ff">
<sprite src="${src}" width="~side_sprite_size" height="~side_sprite_size" />
</Button>
</template>
<elements> <elements>
<!-- background for testing --> <!-- background for testing -->
<rectangle position="absolute" color="#333333" width="100%" height="100%" /> <rectangle position="absolute" color="#333333" width="100%" height="100%" />
@@ -13,7 +27,7 @@
<!-- left/right separator (menu and rest) --> <!-- left/right separator (menu and rest) -->
<div flex_direction="row" gap="8" width="100%" height="100%"> <div flex_direction="row" gap="8" width="100%" height="100%">
<!-- LEFT MENU --> <!-- LEFT MENU -->
<div id="menu" width="48" min_width="48" max_width="48" height="100%" align_items="center" justify_content="center"> <div id="menu" width="~size_size" min_width="~side_size" max_width="~side_size" height="100%" align_items="center" justify_content="center">
<rectangle <rectangle
width="100%" width="100%"
round="100%" round="100%"
@@ -21,17 +35,15 @@
flex_direction="column" flex_direction="column"
justify_content="center" justify_content="center"
align_items="center" align_items="center"
gap="24" gap="4"
padding_top="16"
padding_bottom="16"
> >
<sprite src="dashboard/wayvr_dashboard_mono.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_home" src="dashboard/wayvr_dashboard_mono.svg" />
<sprite src="dashboard/apps.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_apps" src="dashboard/apps.svg" />
<sprite src="dashboard/games.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_games" src="dashboard/games.svg" />
<sprite src="dashboard/monado.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_monado" src="dashboard/monado.svg" />
<sprite src="dashboard/window.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_processes" src="dashboard/window.svg" />
<rectangle height="2" color="#FFFFFF33" width="~side_sprite_size" /> <rectangle height="2" color="#FFFFFF33" width="~side_sprite_size" />
<sprite src="dashboard/settings.svg" width="~side_sprite_size" height="~side_sprite_size" /> <SideButton id="btn_side_settings" src="dashboard/settings.svg" />
</rectangle> </rectangle>
</div> </div>
<!-- REST --> <!-- REST -->
@@ -52,18 +64,7 @@
flex_direction="column" flex_direction="column"
gap="24" gap="24"
> >
<sprite src="dashboard/wayvr_dashboard.svg" min_width="96" min_height="96" /> <!-- filled-in at runtime -->
<label text="Hello, user!" size="32" weight="bold" color="#FFFFFF" />
<label text="Connected to wlx-overlay-s" size="16" weight="bold" color="#bbffbb" />
<!-- main button list -->
<div flex_direction="row" gap="8" margin_top="32">
<MenuButton icon="dashboard/apps.svg" text="Apps" />
<MenuButton icon="dashboard/games.svg" text="Games" />
<MenuButton icon="dashboard/monado.svg" text="Monado" />
<MenuButton icon="dashboard/window.svg" text="Processes" />
<MenuButton icon="dashboard/settings.svg" text="Settings" />
</div>
</div> </div>
</rectangle> </rectangle>
<!-- BOTTOM PANEL --> <!-- BOTTOM PANEL -->

View File

@@ -1,6 +1,9 @@
<layout> <layout>
<include src="theme.xml" />
<template name="MenuButton"> <template name="MenuButton">
<Button <Button
id="${id}"
width="120" width="120"
height="82" height="82"
color="#00000033" color="#00000033"

View File

@@ -0,0 +1,7 @@
<layout>
<elements>
<label text="Apps" size="64" weight="bold" color="#FFFFFF" />
<label text="bottom text" size="16" weight="bold" color="#FFFFFF88" />
</elements>
</layout>

View File

@@ -0,0 +1,5 @@
<layout>
<elements>
<label text="Games" size="32" weight="bold" color="#FFFFFF" />
</elements>
</layout>

View File

@@ -0,0 +1,18 @@
<layout>
<include src="../t_menu_button.xml" />
<elements>
<sprite src="dashboard/wayvr_dashboard.svg" min_width="96" min_height="96" />
<label text="Hello, user!" size="32" weight="bold" color="#FFFFFF" />
<label text="Connected to wlx-overlay-s" size="16" weight="bold" color="#bbffbb" />
<!-- main button list -->
<div flex_direction="row" gap="8" margin_top="32">
<MenuButton id="btn_apps" icon="dashboard/apps.svg" text="Apps" />
<MenuButton id="btn_games" icon="dashboard/games.svg" text="Games" />
<MenuButton id="btn_monado" icon="dashboard/monado.svg" text="Monado" />
<MenuButton id="btn_processes" icon="dashboard/window.svg" text="Processes" />
<MenuButton id="btn_settings" icon="dashboard/settings.svg" text="Settings" />
</div>
</elements>
</layout>

View File

@@ -0,0 +1,5 @@
<layout>
<elements>
<label text="Monado" size="32" weight="bold" color="#FFFFFF" />
</elements>
</layout>

View File

@@ -0,0 +1,5 @@
<layout>
<elements>
<label text="Processes" size="32" weight="bold" color="#FFFFFF" />
</elements>
</layout>

View File

@@ -0,0 +1,5 @@
<layout>
<elements>
<label text="Settings" size="32" weight="bold" color="#FFFFFF" />
</elements>
</layout>

View File

@@ -1,2 +1,3 @@
tab_spaces = 2 tab_spaces = 2
hard_tabs = true hard_tabs = true
max_width = 120

View File

@@ -1,18 +1,38 @@
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use glam::Vec2; use glam::Vec2;
use wgui::{ use wgui::{
components::button::ComponentButton,
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
layout::{Layout, LayoutParams}, layout::{LayoutParams, RcLayout},
parser::{ParseDocumentParams, ParserState}, parser::{ParseDocumentParams, ParserState},
}; };
use crate::tab::{
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
settings::TabSettings,
};
mod assets; mod assets;
mod tab;
pub struct Frontend { pub struct Frontend {
pub layout: Layout, pub layout: RcLayout,
globals: WguiGlobals,
#[allow(dead_code)] #[allow(dead_code)]
state: ParserState, state: ParserState,
current_tab: Option<Box<dyn Tab>>,
tasks: VecDeque<FrontendTask>,
}
pub type RcFrontend = Rc<RefCell<Frontend>>;
pub enum FrontendTask {
SetTab(TabType),
} }
pub struct FrontendParams<'a> { pub struct FrontendParams<'a> {
@@ -20,32 +40,126 @@ pub struct FrontendParams<'a> {
} }
impl Frontend { impl Frontend {
pub fn new(params: FrontendParams) -> anyhow::Result<Self> { pub fn new(params: FrontendParams) -> anyhow::Result<(RcFrontend, RcLayout)> {
let globals = WguiGlobals::new(Box::new(assets::Asset {}))?; let globals = WguiGlobals::new(Box::new(assets::Asset {}))?;
let (layout, state) = wgui::parser::new_layout_from_assets( let (layout, state) = wgui::parser::new_layout_from_assets(
params.listeners, params.listeners,
&ParseDocumentParams { &ParseDocumentParams {
globals, globals: globals.clone(),
path: "gui/dashboard.xml", path: "gui/dashboard.xml",
extra: Default::default(), extra: Default::default(),
}, },
&LayoutParams { &LayoutParams { resize_to_parent: true },
resize_to_parent: true,
},
)?; )?;
Ok(Self { layout, state }) let rc_layout = layout.as_rc();
let mut tasks = VecDeque::<FrontendTask>::new();
tasks.push_back(FrontendTask::SetTab(TabType::Home));
let res = Rc::new(RefCell::new(Self {
layout: rc_layout.clone(),
state,
current_tab: None,
globals,
tasks,
}));
Frontend::register_buttons(&res)?;
Ok((res, rc_layout))
} }
pub fn update(&mut self, width: f32, height: f32, timeste_alpha: f32) -> anyhow::Result<()> { pub fn update(
self rc_this: &RcFrontend,
listeners: &mut EventListenerCollection<(), ()>,
width: f32,
height: f32,
timestep_alpha: f32,
) -> anyhow::Result<()> {
let mut this = rc_this.borrow_mut();
while let Some(task) = this.tasks.pop_front() {
this.process_task(rc_this, task, listeners)?;
}
this
.layout .layout
.update(Vec2::new(width, height), timeste_alpha)?; .borrow_mut()
.update(Vec2::new(width, height), timestep_alpha)?;
Ok(()) Ok(())
} }
pub fn get_layout(&mut self) -> &mut Layout { pub fn get_layout(&self) -> &RcLayout {
&mut self.layout &self.layout
}
pub fn push_task(&mut self, task: FrontendTask) {
self.tasks.push_back(task);
}
fn process_task(
&mut self,
rc_this: &RcFrontend,
task: FrontendTask,
listeners: &mut EventListenerCollection<(), ()>,
) -> anyhow::Result<()> {
match task {
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type, rc_this, listeners)?,
}
Ok(())
}
fn set_tab(
&mut self,
tab_type: TabType,
rc_this: &RcFrontend,
listeners: &mut EventListenerCollection<(), ()>,
) -> anyhow::Result<()> {
log::info!("Setting tab to {tab_type:?}");
let mut layout = self.layout.borrow_mut();
let widget_content = self.state.fetch_widget(&layout.state, "content")?;
layout.remove_children(widget_content.id);
let tab_params = TabParams {
globals: &self.globals,
layout: &mut layout,
parent_id: widget_content.id,
listeners,
frontend: rc_this,
};
let tab: Box<dyn Tab> = match tab_type {
TabType::Home => Box::new(TabHome::new(tab_params)?),
TabType::Apps => Box::new(TabApps::new(tab_params)?),
TabType::Games => Box::new(TabGames::new(tab_params)?),
TabType::Monado => Box::new(TabMonado::new(tab_params)?),
TabType::Processes => Box::new(TabProcesses::new(tab_params)?),
TabType::Settings => Box::new(TabSettings::new(tab_params)?),
};
self.current_tab = Some(tab);
Ok(())
}
fn register_buttons(rc_this: &RcFrontend) -> anyhow::Result<()> {
let this = rc_this.borrow_mut();
let btn_home = this.state.fetch_component_as::<ComponentButton>("btn_side_home")?;
let btn_apps = this.state.fetch_component_as::<ComponentButton>("btn_side_apps")?;
let btn_games = this.state.fetch_component_as::<ComponentButton>("btn_side_games")?;
let btn_monado = this.state.fetch_component_as::<ComponentButton>("btn_side_monado")?;
let btn_processes = this.state.fetch_component_as::<ComponentButton>("btn_side_processes")?;
let btn_settings = this.state.fetch_component_as::<ComponentButton>("btn_side_settings")?;
TabType::register_button(rc_this.clone(), &btn_home, TabType::Home);
TabType::register_button(rc_this.clone(), &btn_apps, TabType::Apps);
TabType::register_button(rc_this.clone(), &btn_games, TabType::Games);
TabType::register_button(rc_this.clone(), &btn_monado, TabType::Monado);
TabType::register_button(rc_this.clone(), &btn_processes, TabType::Processes);
TabType::register_button(rc_this.clone(), &btn_settings, TabType::Settings);
Ok(())
} }
} }

View File

@@ -0,0 +1,31 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabApps {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabApps {
fn get_type(&self) -> TabType {
TabType::Apps
}
}
impl TabApps {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/apps.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
Ok(Self { state })
}
}

View File

@@ -0,0 +1,31 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabGames {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabGames {
fn get_type(&self) -> TabType {
TabType::Games
}
}
impl TabGames {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/games.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
Ok(Self { state })
}
}

View File

@@ -0,0 +1,47 @@
use wgui::{
components::button::ComponentButton,
parser::{ParseDocumentParams, ParserState},
};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabHome {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabHome {
fn get_type(&self) -> TabType {
TabType::Home
}
}
impl TabHome {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/home.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?;
let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?;
let btn_monado = state.fetch_component_as::<ComponentButton>("btn_monado")?;
let btn_processes = state.fetch_component_as::<ComponentButton>("btn_processes")?;
let btn_settings = state.fetch_component_as::<ComponentButton>("btn_settings")?;
let frontend = params.frontend;
TabType::register_button(frontend.clone(), &btn_apps, TabType::Apps);
TabType::register_button(frontend.clone(), &btn_games, TabType::Games);
TabType::register_button(frontend.clone(), &btn_monado, TabType::Monado);
TabType::register_button(frontend.clone(), &btn_processes, TabType::Processes);
TabType::register_button(frontend.clone(), &btn_settings, TabType::Settings);
Ok(Self { state })
}
}

View File

@@ -0,0 +1,51 @@
use std::rc::Rc;
use wgui::{
components::button::ComponentButton,
event::EventListenerCollection,
globals::WguiGlobals,
layout::{Layout, WidgetID},
};
use crate::{FrontendTask, RcFrontend};
pub mod apps;
pub mod games;
pub mod home;
pub mod monado;
pub mod processes;
pub mod settings;
#[derive(Clone, Copy, Debug)]
pub enum TabType {
Home,
Apps,
Games,
Monado,
Processes,
Settings,
}
pub struct TabParams<'a> {
pub globals: &'a WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend: &'a RcFrontend,
pub listeners: &'a mut EventListenerCollection<(), ()>,
}
pub trait Tab {
#[allow(dead_code)]
fn get_type(&self) -> TabType;
}
impl TabType {
pub fn register_button(this_rc: RcFrontend, btn: &Rc<ComponentButton>, tab: TabType) {
btn.on_click({
Box::new(move |_evt| {
this_rc.borrow_mut().push_task(FrontendTask::SetTab(tab));
Ok(())
})
});
}
}

View File

@@ -0,0 +1,31 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabMonado {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabMonado {
fn get_type(&self) -> TabType {
TabType::Games
}
}
impl TabMonado {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/monado.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
Ok(Self { state })
}
}

View File

@@ -0,0 +1,31 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabProcesses {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabProcesses {
fn get_type(&self) -> TabType {
TabType::Games
}
}
impl TabProcesses {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/processes.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
Ok(Self { state })
}
}

View File

@@ -0,0 +1,31 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use crate::tab::{Tab, TabParams, TabType};
pub struct TabSettings {
#[allow(dead_code)]
pub state: ParserState,
}
impl Tab for TabSettings {
fn get_type(&self) -> TabType {
TabType::Settings
}
}
impl TabSettings {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/settings.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
)?;
Ok(Self { state })
}
}

View File

@@ -31,7 +31,9 @@ use winit::{
keyboard::{KeyCode, PhysicalKey}, keyboard::{KeyCode, PhysicalKey},
}; };
use crate::testbed::{testbed_dashboard::TestbedDashboard, testbed_generic::TestbedGeneric}; use crate::testbed::{
TestbedUpdateParams, testbed_dashboard::TestbedDashboard, testbed_generic::TestbedGeneric,
};
mod assets; mod assets;
mod profiler; mod profiler;
@@ -124,6 +126,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} => match delta { } => match delta {
MouseScrollDelta::LineDelta(x, y) => testbed MouseScrollDelta::LineDelta(x, y) => testbed
.layout() .layout()
.borrow_mut()
.push_event( .push_event(
&mut listeners, &mut listeners,
&wgui::event::Event::MouseWheel(MouseWheelEvent { &wgui::event::Event::MouseWheel(MouseWheelEvent {
@@ -136,6 +139,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.unwrap(), .unwrap(),
MouseScrollDelta::PixelDelta(pos) => testbed MouseScrollDelta::PixelDelta(pos) => testbed
.layout() .layout()
.borrow_mut()
.push_event( .push_event(
&mut listeners, &mut listeners,
&wgui::event::Event::MouseWheel(MouseWheelEvent { &wgui::event::Event::MouseWheel(MouseWheelEvent {
@@ -155,6 +159,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if matches!(state, winit::event::ElementState::Pressed) { if matches!(state, winit::event::ElementState::Pressed) {
testbed testbed
.layout() .layout()
.borrow_mut()
.push_event( .push_event(
&mut listeners, &mut listeners,
&wgui::event::Event::MouseDown(MouseDownEvent { &wgui::event::Event::MouseDown(MouseDownEvent {
@@ -168,6 +173,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} else { } else {
testbed testbed
.layout() .layout()
.borrow_mut()
.push_event( .push_event(
&mut listeners, &mut listeners,
&wgui::event::Event::MouseUp(MouseUpEvent { &wgui::event::Event::MouseUp(MouseUpEvent {
@@ -188,6 +194,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
mouse = vec2(position.x as _, position.y as _); mouse = vec2(position.x as _, position.y as _);
testbed testbed
.layout() .layout()
.borrow_mut()
.push_event( .push_event(
&mut listeners, &mut listeners,
&wgui::event::Event::MouseMotion(MouseMotionEvent { &wgui::event::Event::MouseMotion(MouseMotionEvent {
@@ -261,18 +268,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
while timestep.on_tick() { while timestep.on_tick() {
testbed.layout().tick().unwrap(); testbed.layout().borrow_mut().tick().unwrap();
} }
testbed testbed
.update( .update(TestbedUpdateParams {
(swapchain_size[0] as f32 / scale) as _, listeners: &mut listeners,
(swapchain_size[1] as f32 / scale) as _, width: (swapchain_size[0] as f32 / scale) as _,
timestep.alpha, height: (swapchain_size[1] as f32 / scale) as _,
) timestep_alpha: timestep.alpha,
})
.unwrap(); .unwrap();
if !render_context.dirty && !testbed.layout().check_toggle_needs_redraw() { if !render_context.dirty && !testbed.layout().borrow_mut().check_toggle_needs_redraw() {
// no need to redraw // no need to redraw
std::thread::sleep(std::time::Duration::from_millis(5)); // dirty fix to prevent cpu burning precious cycles doing a busy loop std::thread::sleep(std::time::Duration::from_millis(5)); // dirty fix to prevent cpu burning precious cycles doing a busy loop
return; return;
@@ -301,7 +309,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.unwrap(); .unwrap();
cmd_buf.begin_rendering(tgt).unwrap(); cmd_buf.begin_rendering(tgt).unwrap();
let primitives = wgui::drawing::draw(testbed.layout()).unwrap(); let primitives = wgui::drawing::draw(&testbed.layout().borrow_mut()).unwrap();
render_context render_context
.draw(&mut shared_context, &mut cmd_buf, &primitives) .draw(&mut shared_context, &mut cmd_buf, &primitives)
.unwrap(); .unwrap();

View File

@@ -1,10 +1,17 @@
use wgui::layout::Layout; use wgui::{event::EventListenerCollection, layout::RcLayout};
pub mod testbed_any; pub mod testbed_any;
pub mod testbed_dashboard; pub mod testbed_dashboard;
pub mod testbed_generic; pub mod testbed_generic;
pub trait Testbed { pub struct TestbedUpdateParams<'a> {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()>; pub listeners: &'a mut EventListenerCollection<(), ()>,
fn layout(&mut self) -> &mut Layout; pub width: f32,
pub height: f32,
pub timestep_alpha: f32,
}
pub trait Testbed {
fn update(&mut self, params: TestbedUpdateParams) -> anyhow::Result<()>;
fn layout(&self) -> &RcLayout;
} }

View File

@@ -1,14 +1,17 @@
use crate::{assets, testbed::Testbed}; use crate::{
assets,
testbed::{Testbed, TestbedUpdateParams},
};
use glam::Vec2; use glam::Vec2;
use wgui::{ use wgui::{
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
layout::{Layout, LayoutParams}, layout::{LayoutParams, RcLayout},
parser::{ParseDocumentParams, ParserState}, parser::{ParseDocumentParams, ParserState},
}; };
pub struct TestbedAny { pub struct TestbedAny {
pub layout: Layout, pub layout: RcLayout,
#[allow(dead_code)] #[allow(dead_code)]
state: ParserState, state: ParserState,
@@ -29,19 +32,23 @@ impl TestbedAny {
}, },
&LayoutParams::default(), &LayoutParams::default(),
)?; )?;
Ok(Self { layout, state }) Ok(Self {
layout: layout.as_rc(),
state,
})
} }
} }
impl Testbed for TestbedAny { impl Testbed for TestbedAny {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { fn update(&mut self, params: TestbedUpdateParams) -> anyhow::Result<()> {
self self.layout.borrow_mut().update(
.layout Vec2::new(params.width, params.height),
.update(Vec2::new(width, height), timestep_alpha)?; params.timestep_alpha,
)?;
Ok(()) Ok(())
} }
fn layout(&mut self) -> &mut Layout { fn layout(&self) -> &RcLayout {
&mut self.layout &self.layout
} }
} }

View File

@@ -1,25 +1,32 @@
use crate::testbed::Testbed; use crate::testbed::{Testbed, TestbedUpdateParams};
use wgui::{event::EventListenerCollection, layout::Layout}; use dash_frontend::Frontend;
use wgui::{event::EventListenerCollection, layout::RcLayout};
pub struct TestbedDashboard { pub struct TestbedDashboard {
frontend: dash_frontend::Frontend, layout: RcLayout,
frontend: dash_frontend::RcFrontend,
} }
impl TestbedDashboard { impl TestbedDashboard {
pub fn new(listeners: &mut EventListenerCollection<(), ()>) -> anyhow::Result<Self> { pub fn new(listeners: &mut EventListenerCollection<(), ()>) -> anyhow::Result<Self> {
Ok(Self { let (frontend, layout) =
frontend: dash_frontend::Frontend::new(dash_frontend::FrontendParams { listeners })?, dash_frontend::Frontend::new(dash_frontend::FrontendParams { listeners })?;
}) Ok(Self { frontend, layout })
} }
} }
impl Testbed for TestbedDashboard { impl Testbed for TestbedDashboard {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { fn update(&mut self, params: TestbedUpdateParams) -> anyhow::Result<()> {
self.frontend.update(width, height, timestep_alpha)?; Frontend::update(
Ok(()) &self.frontend,
params.listeners,
params.width,
params.height,
params.timestep_alpha,
)
} }
fn layout(&mut self) -> &mut Layout { fn layout(&self) -> &RcLayout {
self.frontend.get_layout() &self.layout
} }
} }

View File

@@ -1,6 +1,9 @@
use std::rc::Rc; use std::rc::Rc;
use crate::{assets, testbed::Testbed}; use crate::{
assets,
testbed::{Testbed, TestbedUpdateParams},
};
use glam::Vec2; use glam::Vec2;
use wgui::{ use wgui::{
components::{ components::{
@@ -12,13 +15,13 @@ use wgui::{
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, LayoutParams, Widget}, layout::{LayoutParams, RcLayout, Widget},
parser::{ParseDocumentExtra, ParseDocumentParams, ParserState}, parser::{ParseDocumentExtra, ParseDocumentParams, ParserState},
widget::{label::WidgetLabel, rectangle::WidgetRectangle}, widget::{label::WidgetLabel, rectangle::WidgetRectangle},
}; };
pub struct TestbedGeneric { pub struct TestbedGeneric {
pub layout: Layout, pub layout: RcLayout,
#[allow(dead_code)] #[allow(dead_code)]
state: ParserState, state: ParserState,
@@ -103,12 +106,16 @@ impl TestbedGeneric {
let button_aqua = state.fetch_component_as::<ComponentButton>("button_aqua")?; let button_aqua = state.fetch_component_as::<ComponentButton>("button_aqua")?;
let button_yellow = state.fetch_component_as::<ComponentButton>("button_yellow")?; let button_yellow = state.fetch_component_as::<ComponentButton>("button_yellow")?;
handle_button_click(button_red, label_cur_option.clone(), "Clicked red"); handle_button_click(button_red, label_cur_option.widget.clone(), "Clicked red");
handle_button_click(button_aqua, label_cur_option.clone(), "Clicked aqua"); handle_button_click(button_aqua, label_cur_option.widget.clone(), "Clicked aqua");
handle_button_click(button_yellow, label_cur_option.clone(), "Clicked yellow"); handle_button_click(
button_yellow,
label_cur_option.widget.clone(),
"Clicked yellow",
);
let cb_first = state.fetch_component_as::<ComponentCheckbox>("cb_first")?; let cb_first = state.fetch_component_as::<ComponentCheckbox>("cb_first")?;
let label = label_cur_option.clone(); let label = label_cur_option.widget.clone();
cb_first.on_toggle(Box::new(move |e| { cb_first.on_toggle(Box::new(move |e| {
let mut widget = label.get_as_mut::<WidgetLabel>(); let mut widget = label.get_as_mut::<WidgetLabel>();
widget.set_text( widget.set_text(
@@ -118,19 +125,23 @@ impl TestbedGeneric {
Ok(()) Ok(())
})); }));
Ok(Self { layout, state }) Ok(Self {
layout: layout.as_rc(),
state,
})
} }
} }
impl Testbed for TestbedGeneric { impl Testbed for TestbedGeneric {
fn update(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { fn update(&mut self, params: TestbedUpdateParams) -> anyhow::Result<()> {
self self.layout.borrow_mut().update(
.layout Vec2::new(params.width, params.height),
.update(Vec2::new(width, height), timestep_alpha)?; params.timestep_alpha,
)?;
Ok(()) Ok(())
} }
fn layout(&mut self) -> &mut Layout { fn layout(&self) -> &RcLayout {
&mut self.layout &self.layout
} }
} }

View File

@@ -1,2 +1,3 @@
tab_spaces = 2 tab_spaces = 2
hard_tabs = true hard_tabs = true
max_width = 120

View File

@@ -43,6 +43,11 @@ impl Widget {
pub struct WidgetMap(HopSlotMap<WidgetID, Widget>); pub struct WidgetMap(HopSlotMap<WidgetID, Widget>);
pub type WidgetNodeMap = SecondaryMap<WidgetID, taffy::NodeId>; pub type WidgetNodeMap = SecondaryMap<WidgetID, taffy::NodeId>;
pub struct WidgetPair {
pub id: WidgetID,
pub widget: Widget,
}
impl WidgetMap { impl WidgetMap {
fn new() -> Self { fn new() -> Self {
Self(HopSlotMap::with_key()) Self(HopSlotMap::with_key())
@@ -60,6 +65,10 @@ impl WidgetMap {
self.0.insert(obj) self.0.insert(obj)
} }
pub fn remove_single(&mut self, handle: WidgetID) {
self.0.remove(handle);
}
// cast to specific widget type, does nothing if widget ID is expired // cast to specific widget type, does nothing if widget ID is expired
// panics in case if the widget type is wrong // panics in case if the widget type is wrong
// TODO: panic-less alternative // TODO: panic-less alternative
@@ -101,6 +110,8 @@ pub struct Layout {
pub animations: Animations, pub animations: Animations,
} }
pub type RcLayout = Rc<RefCell<Layout>>;
#[derive(Default)] #[derive(Default)]
pub struct LayoutParams { pub struct LayoutParams {
pub resize_to_parent: bool, pub resize_to_parent: bool,
@@ -128,6 +139,10 @@ fn add_child_internal(
} }
impl Layout { impl Layout {
pub fn as_rc(self) -> RcLayout {
Rc::new(RefCell::new(self))
}
pub fn add_child( pub fn add_child(
&mut self, &mut self,
parent_widget_id: WidgetID, parent_widget_id: WidgetID,
@@ -148,6 +163,34 @@ impl Layout {
) )
} }
fn collect_children_ids_recursive(&self, widget_id: WidgetID, out: &mut Vec<(WidgetID, taffy::NodeId)>) {
let Some(node_id) = self.state.nodes.get(widget_id) else {
return;
};
for child_id in self.state.tree.child_ids(*node_id) {
let child_widget_id = self.state.tree.get_node_context(child_id).unwrap();
out.push((*child_widget_id, child_id));
self.collect_children_ids_recursive(*child_widget_id, out);
}
}
// removes all children of a specific widget
pub fn remove_children(&mut self, widget_id: WidgetID) {
let mut ids = Vec::new();
self.collect_children_ids_recursive(widget_id, &mut ids);
if !ids.is_empty() {
self.needs_redraw = true;
}
for (widget_id, node_id) in ids {
self.state.widgets.remove_single(widget_id);
self.state.nodes.remove(widget_id);
self.state.tree.remove(node_id).unwrap();
}
}
fn process_pending_components(&mut self) -> anyhow::Result<()> { fn process_pending_components(&mut self) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default(); let mut alterables = EventAlterables::default();
@@ -224,20 +267,11 @@ impl Layout {
let mut widget = widget.0.borrow_mut(); let mut widget = widget.0.borrow_mut();
match widget.process_event( match widget.process_event(widget_id, listeners_vec, node_id, event, user_data, &mut params)? {
widget_id,
listeners_vec,
node_id,
event,
user_data,
&mut params,
)? {
widget::EventResult::Pass => { widget::EventResult::Pass => {
// go on // go on
} }
widget::EventResult::Consumed widget::EventResult::Consumed | widget::EventResult::Outside | widget::EventResult::Unused => {
| widget::EventResult::Outside
| widget::EventResult::Unused => {
iter_children = false; iter_children = false;
} }
} }
@@ -279,13 +313,7 @@ impl Layout {
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default(); let mut alterables = EventAlterables::default();
self.push_event_widget( self.push_event_widget(listeners, self.root_node, event, &mut alterables, &mut user_data)?;
listeners,
self.root_node,
event,
&mut alterables,
&mut user_data,
)?;
self.process_alterables(alterables)?; self.process_alterables(alterables)?;
@@ -360,10 +388,7 @@ impl Layout {
None => taffy::Size::ZERO, None => taffy::Size::ZERO,
Some(h) => { Some(h) => {
if let Some(w) = self.state.widgets.get(*h) { if let Some(w) = self.state.widgets.get(*h) {
w.0 w.0.borrow_mut().obj.measure(known_dimensions, available_space)
.borrow_mut()
.obj
.measure(known_dimensions, available_space)
} else { } else {
taffy::Size::ZERO taffy::Size::ZERO
} }
@@ -388,9 +413,7 @@ impl Layout {
pub fn update(&mut self, size: Vec2, timestep_alpha: f32) -> anyhow::Result<()> { pub fn update(&mut self, size: Vec2, timestep_alpha: f32) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default(); let mut alterables = EventAlterables::default();
self self.animations.process(&self.state, &mut alterables, timestep_alpha);
.animations
.process(&self.state, &mut alterables, timestep_alpha);
self.process_alterables(alterables)?; self.process_alterables(alterables)?;
self.try_recompute_layout(size)?; self.try_recompute_layout(size)?;
@@ -428,11 +451,7 @@ impl Layout {
for request in alterables.style_set_requests { for request in alterables.style_set_requests {
if let Err(e) = self.state.tree.set_style(request.0, request.1) { if let Err(e) = self.state.tree.set_style(request.0, request.1) {
log::error!( log::error!("failed to set style for taffy widget ID {:?}: {:?}", request.0, e);
"failed to set style for taffy widget ID {:?}: {:?}",
request.0,
e
);
} }
} }

View File

@@ -3,8 +3,7 @@ use crate::{
i18n::Translation, i18n::Translation,
layout::WidgetID, layout::WidgetID,
parser::{ parser::{
ParserContext, ParserFile, iter_attribs, parse_check_f32, parse_check_i32, parse_children, ParserContext, ParserFile, iter_attribs, parse_check_f32, parse_check_i32, process_component, style::parse_style,
process_component, style::parse_style,
}, },
}; };

View File

@@ -1,10 +1,7 @@
use crate::{ use crate::{
components::{Component, slider}, components::{Component, slider},
layout::WidgetID, layout::WidgetID,
parser::{ parser::{ParserContext, ParserFile, iter_attribs, parse_check_f32, process_component, style::parse_style},
ParserContext, ParserFile, iter_attribs, parse_check_f32, parse_children, process_component,
style::parse_style,
},
}; };
pub fn parse_component_slider<'a, U1, U2>( pub fn parse_component_slider<'a, U1, U2>(

View File

@@ -13,12 +13,11 @@ use crate::{
drawing::{self}, drawing::{self},
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap}, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
parser::{ parser::{
component_button::parse_component_button, component_checkbox::parse_component_checkbox, component_button::parse_component_button, component_checkbox::parse_component_checkbox,
component_slider::parse_component_slider, widget_div::parse_widget_div, component_slider::parse_component_slider, widget_div::parse_widget_div, widget_label::parse_widget_label,
widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
widget_sprite::parse_widget_sprite,
}, },
}; };
use ouroboros::self_referencing; use ouroboros::self_referencing;
@@ -95,13 +94,16 @@ impl ParserState {
} }
} }
pub fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<Widget> { pub fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
let widget_id = self.get_widget_id(id)?; let widget_id = self.get_widget_id(id)?;
let widget = state let widget = state
.widgets .widgets
.get(widget_id) .get(widget_id)
.ok_or_else(|| anyhow::anyhow!("fetch_widget({}): widget not found", id))?; .ok_or_else(|| anyhow::anyhow!("fetch_widget({}): widget not found", id))?;
Ok(widget.clone()) Ok(WidgetPair {
id: widget_id,
widget: widget.clone(),
})
} }
pub fn process_template<U1, U2>( pub fn process_template<U1, U2>(
@@ -121,9 +123,9 @@ impl ParserState {
layout, layout,
listeners, listeners,
ids: Default::default(), ids: Default::default(),
macro_attribs: self.macro_attribs.clone(), // FIXME: prevent copying macro_attribs: self.macro_attribs.clone(), // FIXME: prevent copying
var_map: self.var_map.clone(), // FIXME: prevent copying var_map: self.var_map.clone(), // FIXME: prevent copying
components: self.components.clone(), // FIXME: prevent copying components: self.components.clone(), // FIXME: prevent copying
components_id_map: self.components_id_map.clone(), // FIXME: prevent copying components_id_map: self.components_id_map.clone(), // FIXME: prevent copying
templates: Default::default(), templates: Default::default(),
doc_params, doc_params,
@@ -135,13 +137,7 @@ impl ParserState {
template_parameters: template_parameters.clone(), // FIXME: prevent copying template_parameters: template_parameters.clone(), // FIXME: prevent copying
}; };
parse_widget_other_internal( parse_widget_other_internal(&template.clone(), template_parameters, &file, &mut ctx, widget_id)?;
&template.clone(),
template_parameters,
&file,
&mut ctx,
widget_id,
)?;
// FIXME? // FIXME?
ctx.ids.into_iter().for_each(|(id, key)| { ctx.ids.into_iter().for_each(|(id, key)| {
@@ -203,19 +199,11 @@ pub fn parse_color_hex(html_hex: &str) -> Option<drawing::Color> {
None None
} }
fn get_tag_by_name<'a>( fn get_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> Option<roxmltree::Node<'a, 'a>> {
node: &roxmltree::Node<'a, 'a>, node.children().find(|&child| child.tag_name().name() == name)
name: &str,
) -> Option<roxmltree::Node<'a, 'a>> {
node
.children()
.find(|&child| child.tag_name().name() == name)
} }
fn require_tag_by_name<'a>( fn require_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> anyhow::Result<roxmltree::Node<'a, 'a>> {
node: &roxmltree::Node<'a, 'a>,
name: &str,
) -> anyhow::Result<roxmltree::Node<'a, 'a>> {
get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{}\" not found", name)) get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{}\" not found", name))
} }
@@ -332,8 +320,7 @@ fn parse_widget_other<'a, U1, U2>(
return Ok(()); // not critical return Ok(()); // not critical
}; };
let template_parameters: HashMap<Rc<str>, Rc<str>> = let template_parameters: HashMap<Rc<str>, Rc<str>> = iter_attribs(file, ctx, &node, false).collect();
iter_attribs(file, ctx, &node, false).collect();
parse_widget_other_internal(&template.clone(), template_parameters, file, ctx, parent_id) parse_widget_other_internal(&template.clone(), template_parameters, file, ctx, parent_id)
} }
@@ -350,11 +337,7 @@ fn parse_tag_include<'a, U1, U2>(
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match key { match key {
"src" => { "src" => {
let mut new_path = file let mut new_path = file.path.parent().unwrap_or_else(|| Path::new("/")).to_path_buf();
.path
.parent()
.unwrap_or_else(|| Path::new("/"))
.to_path_buf();
new_path.push(value); new_path.push(value);
let (new_file, node_layout) = get_doc_from_path(ctx, &new_path)?; let (new_file, node_layout) = get_doc_from_path(ctx, &new_path)?;
@@ -443,10 +426,7 @@ fn process_attrib<'a, U1, U2>(
}, },
) )
} else { } else {
( (Rc::from(key), replace_vars(value, &file.template_parameters))
Rc::from(key),
replace_vars(value, &file.template_parameters),
)
} }
} }
@@ -501,11 +481,7 @@ fn parse_tag_theme<'a, U1, U2>(ctx: &mut ParserContext<U1, U2>, node: roxmltree:
} }
} }
fn parse_tag_template<U1, U2>( fn parse_tag_template<U1, U2>(file: &ParserFile, ctx: &mut ParserContext<U1, U2>, node: roxmltree::Node<'_, '_>) {
file: &ParserFile,
ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'_, '_>,
) {
let mut template_name: Option<Rc<str>> = None; let mut template_name: Option<Rc<str>> = None;
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
@@ -535,11 +511,7 @@ fn parse_tag_template<U1, U2>(
); );
} }
fn parse_tag_macro<U1, U2>( fn parse_tag_macro<U1, U2>(file: &ParserFile, ctx: &mut ParserContext<U1, U2>, node: roxmltree::Node<'_, '_>) {
file: &ParserFile,
ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'_, '_>,
) {
let mut macro_name: Option<Rc<str>> = None; let mut macro_name: Option<Rc<str>> = None;
let attribs: Vec<_> = iter_attribs(file, ctx, &node, true).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, true).collect();
@@ -563,12 +535,7 @@ fn parse_tag_macro<U1, U2>(
return; return;
}; };
ctx.macro_attribs.insert( ctx.macro_attribs.insert(name, MacroAttribs { attribs: macro_attribs });
name,
MacroAttribs {
attribs: macro_attribs,
},
);
} }
fn process_component<'a, U1, U2>( fn process_component<'a, U1, U2>(
@@ -583,11 +550,7 @@ fn process_component<'a, U1, U2>(
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match key.as_ref() { match key.as_ref() {
"id" => { "id" => {
if ctx if ctx.components_id_map.insert(value.clone(), component.weak()).is_some() {
.components_id_map
.insert(value.clone(), component.weak())
.is_some()
{
log::warn!("duplicate component ID \"{value}\" in the same layout file!"); log::warn!("duplicate component ID \"{value}\" in the same layout file!");
} }
} }

View File

@@ -6,8 +6,8 @@ use crate::{
any::AnyTrait, any::AnyTrait,
drawing, drawing,
event::{ event::{
self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, EventListenerKind,
EventListenerKind, EventListenerVec, MouseWheelEvent, EventListenerVec, MouseWheelEvent,
}, },
layout::{Layout, LayoutState, WidgetID}, layout::{Layout, LayoutState, WidgetID},
transform_stack::TransformStack, transform_stack::TransformStack,
@@ -138,8 +138,8 @@ impl EventParams<'_> {
pub enum EventResult { pub enum EventResult {
Pass, // widget acknowledged it and allows the event to pass to the children Pass, // widget acknowledged it and allows the event to pass to the children
Consumed, // widget triggered an action, do not pass to children Consumed, // widget triggered an action, do not pass to children
Outside, // widget acknowledged this event but ignores it due the fact the mouse is not hovered over it Outside, // widget acknowledged this event but ignores it due the fact the mouse is not hovered over it
Unused, // widget doesn't have any events attached Unused, // widget doesn't have any events attached
} }
fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) { fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) {
@@ -223,12 +223,7 @@ impl WidgetState {
self.obj.draw(state, params); self.obj.draw(state, params);
} }
pub fn draw_scrollbars( pub fn draw_scrollbars(&mut self, state: &mut DrawState, params: &DrawParams, info: &ScrollbarInfo) {
&mut self,
state: &mut DrawState,
params: &DrawParams,
info: &ScrollbarInfo,
) {
let (enabled_horiz, enabled_vert) = get_scroll_enabled(params.style); let (enabled_horiz, enabled_vert) = get_scroll_enabled(params.style);
if !enabled_horiz && !enabled_vert { if !enabled_horiz && !enabled_vert {
return; return;