wgui: Separate user and wgui assets, topmost widgets (poc)

This commit is contained in:
Aleksander
2025-10-05 13:48:58 +02:00
parent 71d7d50e35
commit 3dff9c5882
32 changed files with 442 additions and 151 deletions

1
Cargo.lock generated
View File

@@ -6285,6 +6285,7 @@ dependencies = [
"regex",
"resvg",
"roxmltree 0.20.0",
"rust-embed",
"rustc-hash 2.1.1",
"serde_json",
"slotmap",

View File

@@ -3,6 +3,7 @@ use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use chrono::Timelike;
use glam::Vec2;
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
event::{CallbackDataCommon, EventAlterables, EventListenerCollection},
globals::WguiGlobals,
@@ -56,7 +57,7 @@ impl Frontend {
params.listeners,
&ParseDocumentParams {
globals: globals.clone(),
path: "gui/dashboard.xml",
path: AssetPath::BuiltIn("gui/dashboard.xml"),
extra: Default::default(),
},
&LayoutParams { resize_to_parent: true },

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
layout::WidgetPair,
parser::{Fetchable, ParseDocumentParams, ParserData, ParserState},
@@ -36,7 +37,7 @@ impl TabApps {
pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: tab_params.globals.clone(),
path: "gui/tab/apps.xml",
path: AssetPath::BuiltIn("gui/tab/apps.xml"),
extra: Default::default(),
};

View File

@@ -1,4 +1,7 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use wgui::{
assets::AssetPath,
parser::{ParseDocumentParams, ParserState},
};
use crate::tab::{Tab, TabParams, TabType};
@@ -18,7 +21,7 @@ impl TabGames {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/games.xml",
path: AssetPath::BuiltIn("gui/tab/games.xml"),
extra: Default::default(),
},
params.layout,

View File

@@ -1,4 +1,5 @@
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
i18n::Translation,
parser::{Fetchable, ParseDocumentParams, ParserState},
@@ -38,7 +39,7 @@ impl TabHome {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/home.xml",
path: AssetPath::BuiltIn("gui/tab/home.xml"),
extra: Default::default(),
},
params.layout,

View File

@@ -1,4 +1,7 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use wgui::{
assets::AssetPath,
parser::{ParseDocumentParams, ParserState},
};
use crate::tab::{Tab, TabParams, TabType};
@@ -18,7 +21,7 @@ impl TabMonado {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/monado.xml",
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
extra: Default::default(),
},
params.layout,

View File

@@ -1,4 +1,7 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use wgui::{
assets::AssetPath,
parser::{ParseDocumentParams, ParserState},
};
use crate::tab::{Tab, TabParams, TabType};
@@ -18,7 +21,7 @@ impl TabProcesses {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/processes.xml",
path: AssetPath::BuiltIn("gui/tab/processes.xml"),
extra: Default::default(),
},
params.layout,

View File

@@ -1,4 +1,7 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use wgui::{
assets::AssetPath,
parser::{ParseDocumentParams, ParserState},
};
use crate::tab::{Tab, TabParams, TabType};
@@ -18,7 +21,7 @@ impl TabSettings {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/settings.xml",
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
},
params.layout,

View File

@@ -2,6 +2,7 @@ use gio::prelude::{AppInfoExt, IconExt};
use gtk::traits::IconThemeExt;
#[derive(Debug)]
#[allow(dead_code)] // TODO: remove this
pub struct DesktopEntry {
pub exec_path: String,
pub exec_args: Vec<String>,
@@ -10,6 +11,7 @@ pub struct DesktopEntry {
pub categories: Vec<String>,
}
#[allow(dead_code)] // TODO: remove this
pub struct EntrySearchCell {
pub exec_path: String,
pub exec_args: Vec<String>,

View File

@@ -1,15 +1,16 @@
<layout>
<elements>
<rectangle
color="#888888"
width="1000" height="500" min_width="1000" min_height="500"
width="1000" height="1000" min_width="1000" min_height="500"
gap="4" flex_direction="column"
color="#444444ee"
overflow_y="scroll">
<label text="Raw text" color="#FFFFFF" />
<label translation="TESTBED.HELLO_WORLD" color="#FFFFFF" />
<div margin_left="16" gap="8" flex_direction="column">
<label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" />
<Button id="button_popup" text="Show pop-up" width="200" height="32" color="#777777" />
<div gap="4">
<Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" />
<Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" />

View File

@@ -309,7 +309,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
log::trace!("drawing frame {frame_index}");
frame_index += 1;
limiter.start(120); // max 120 fps
limiter.start(0); // max 120 fps
profiler.start();
{

View File

@@ -4,6 +4,7 @@ use crate::{
};
use glam::Vec2;
use wgui::{
assets::AssetPath,
event::EventListenerCollection,
globals::WguiGlobals,
layout::{LayoutParams, RcLayout},
@@ -19,7 +20,7 @@ pub struct TestbedAny {
impl TestbedAny {
pub fn new(name: &str, listeners: &mut EventListenerCollection<(), ()>) -> anyhow::Result<Self> {
let path = format!("gui/{name}.xml");
let path = AssetPath::BuiltIn(&format!("gui/{name}.xml"));
let globals = WguiGlobals::new(
Box::new(assets::Asset {}),
@@ -30,7 +31,7 @@ impl TestbedAny {
listeners,
&ParseDocumentParams {
globals,
path: &path,
path,
extra: Default::default(),
},
&LayoutParams::default(),

View File

@@ -1,4 +1,4 @@
use std::rc::Rc;
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use crate::{
assets,
@@ -6,25 +6,37 @@ use crate::{
};
use glam::Vec2;
use wgui::{
assets::AssetPath,
components::{
Component,
button::{ButtonClickCallback, ComponentButton},
checkbox::ComponentCheckbox,
Component,
},
drawing::Color,
event::EventListenerCollection,
globals::WguiGlobals,
i18n::Translation,
layout::{LayoutParams, RcLayout, Widget},
layout::{Layout, LayoutParams, RcLayout, Widget},
parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState},
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
taffy::{self, prelude::length},
widget::{div::WidgetDiv, label::WidgetLabel, rectangle::WidgetRectangle},
};
pub enum TestbedTask {
ShowPopup,
}
struct Data {
tasks: VecDeque<TestbedTask>,
#[allow(dead_code)]
state: ParserState,
}
#[derive(Clone)]
pub struct TestbedGeneric {
pub layout: RcLayout,
#[allow(dead_code)]
state: ParserState,
data: Rc<RefCell<Data>>,
}
fn button_click_callback(
@@ -57,7 +69,7 @@ fn handle_button_click(button: Rc<ComponentButton>, label: Widget, text: &'stati
impl TestbedGeneric {
pub fn new(listeners: &mut EventListenerCollection<(), ()>) -> anyhow::Result<Self> {
const XML_PATH: &str = "gui/various_widgets.xml";
const XML_PATH: AssetPath = AssetPath::BuiltIn("gui/various_widgets.xml");
let globals = WguiGlobals::new(
Box::new(assets::Asset {}),
@@ -112,6 +124,7 @@ impl TestbedGeneric {
Ok(())
}));
let button_popup = state.fetch_component_as::<ComponentButton>("button_popup")?;
let button_red = state.fetch_component_as::<ComponentButton>("button_red")?;
let button_aqua = state.fetch_component_as::<ComponentButton>("button_aqua")?;
let button_yellow = state.fetch_component_as::<ComponentButton>("button_yellow")?;
@@ -133,19 +146,102 @@ impl TestbedGeneric {
Ok(())
}));
Ok(Self {
let testbed = Self {
layout: layout.as_rc(),
data: Rc::new(RefCell::new(Data {
state,
tasks: Default::default(),
})),
};
button_popup.on_click({
let testbed = testbed.clone();
Box::new(move |_, _| {
testbed.push_task(TestbedTask::ShowPopup);
Ok(())
})
});
Ok(testbed)
}
fn push_task(&self, task: TestbedTask) {
self.data.borrow_mut().tasks.push_back(task);
}
fn process_task(
&mut self,
task: &TestbedTask,
params: &mut TestbedUpdateParams,
layout: &mut Layout,
data: &mut Data,
) -> anyhow::Result<()> {
match task {
TestbedTask::ShowPopup => self.show_popup(params, layout, data)?,
}
Ok(())
}
fn show_popup(
&mut self,
params: &mut TestbedUpdateParams,
layout: &mut Layout,
_data: &mut Data,
) -> anyhow::Result<()> {
const XML_PATH: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml");
let globals = WguiGlobals::new(
Box::new(assets::Asset {}),
wgui::globals::Defaults::default(),
)?;
let (widget, _) = layout.add_topmost_child(
WidgetDiv::create(),
taffy::Style {
position: taffy::Position::Absolute,
margin: taffy::Rect {
left: length(64.0),
right: length(0.0),
top: length(64.0),
bottom: length(0.0),
},
..Default::default()
},
)?;
let _state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals,
path: XML_PATH,
extra: Default::default(),
},
layout,
params.listeners,
widget.id,
)?;
Ok(())
}
}
impl Testbed for TestbedGeneric {
fn update(&mut self, params: TestbedUpdateParams) -> anyhow::Result<()> {
self.layout.borrow_mut().update(
fn update(&mut self, mut params: TestbedUpdateParams) -> anyhow::Result<()> {
let layout = self.layout.clone();
let data = self.data.clone();
let mut layout = layout.borrow_mut();
let mut data = data.borrow_mut();
layout.update(
Vec2::new(params.width, params.height),
params.timestep_alpha,
)?;
while let Some(task) = data.tasks.pop_front() {
self.process_task(&task, &mut params, &mut layout, &mut data)?;
}
Ok(())
}

View File

@@ -29,3 +29,4 @@ smallvec = "1.15.1"
taffy = "0.9.1"
vulkano = { workspace = true }
vulkano-shaders = { workspace = true }
rust-embed = { workspace = true }

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M8.4 17L7 15.6l3.6-3.6L7 8.425l1.4-1.4l3.6 3.6l3.575-3.6l1.4 1.4l-3.6 3.575l3.6 3.6l-1.4 1.4L12 13.4z" />
</svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@@ -0,0 +1,25 @@
<layout>
<elements>
<rectangle
flex_direction="column"
round="8"
border="2"
border_color="#778899"
color="#001122ee"
padding="2">
<!-- window title -->
<rectangle width="100%" height="100%" round="4" align_items="center" justify_content="space_between"
gradient="vertical" color="#224466" color2="#113355">
<label margin_left="8" text="Window title" weight="bold" />
<sprite src_internal="wgui/close.svg" width="24" height="24" />
</rectangle>
<!-- content itself -->
<div width="100%" height="100%" padding="8" gap="4" flex_direction="column">
<label text="Window content" />
<Button color="#9911AA" text="I'm clickable." width="128" height="24" />
</div>
</rectangle>
</elements>
</layout>

View File

@@ -152,7 +152,11 @@ _Internal (assets) image path_
`src_ext`: **string**
_External image path_
_External (filesystem) image path_
`src_internal`: **string**
_wgui internal image path. Do not use directly unless it's related to the core wgui assets._
---

View File

@@ -1,5 +1,77 @@
use std::path::{Path, PathBuf};
#[derive(Clone, Copy)]
pub enum AssetPath<'a> {
WguiInternal(&'a str), // tied to internal wgui AssetProvider. Used internally
BuiltIn(&'a str), // tied to user AssetProvider
Filesystem(&'a str), // tied to filesystem path
}
#[derive(Clone)]
pub enum AssetPathOwned {
WguiInternal(PathBuf),
BuiltIn(PathBuf),
Filesystem(PathBuf),
}
impl AssetPath<'_> {
pub const fn get_str(&self) -> &str {
match &self {
AssetPath::WguiInternal(path) => path,
AssetPath::BuiltIn(path) => path,
AssetPath::Filesystem(path) => path,
}
}
pub fn to_owned(&self) -> AssetPathOwned {
match self {
AssetPath::WguiInternal(path) => AssetPathOwned::WguiInternal(PathBuf::from(path)),
AssetPath::BuiltIn(path) => AssetPathOwned::BuiltIn(PathBuf::from(path)),
AssetPath::Filesystem(path) => AssetPathOwned::Filesystem(PathBuf::from(path)),
}
}
}
impl AssetPathOwned {
pub fn as_ref(&'_ self) -> AssetPath<'_> {
match self {
AssetPathOwned::WguiInternal(buf) => AssetPath::WguiInternal(buf.to_str().unwrap()),
AssetPathOwned::BuiltIn(buf) => AssetPath::BuiltIn(buf.to_str().unwrap()),
AssetPathOwned::Filesystem(buf) => AssetPath::Filesystem(buf.to_str().unwrap()),
}
}
pub const fn get_path_buf(&self) -> &PathBuf {
match self {
AssetPathOwned::WguiInternal(buf) => buf,
AssetPathOwned::BuiltIn(buf) => buf,
AssetPathOwned::Filesystem(buf) => buf,
}
}
}
impl AssetPathOwned {
#[must_use]
pub fn push_include(&self, include: &str) -> AssetPathOwned {
let buf = self.get_path_buf();
let mut new_path = buf.parent().unwrap_or_else(|| Path::new("/")).to_path_buf();
new_path.push(include);
let new_path = normalize_path(&new_path);
match self {
AssetPathOwned::WguiInternal(_) => AssetPathOwned::WguiInternal(new_path),
AssetPathOwned::BuiltIn(_) => AssetPathOwned::BuiltIn(new_path),
AssetPathOwned::Filesystem(_) => AssetPathOwned::Filesystem(new_path),
}
}
}
impl Default for AssetPathOwned {
fn default() -> Self {
Self::WguiInternal(PathBuf::default())
}
}
pub trait AssetProvider {
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>>;
}

View File

@@ -0,0 +1,12 @@
#[derive(rust_embed::Embed)]
#[folder = "assets/"]
pub struct AssetInternal;
impl crate::assets::AssetProvider for AssetInternal {
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>> {
match AssetInternal::get(path) {
Some(data) => Ok(data.data.to_vec()),
None => anyhow::bail!("internal file {path} not found"),
}
}
}

View File

@@ -146,6 +146,7 @@ pub struct PrimitiveExtent {
}
pub enum RenderPrimitive {
NewPass,
Rectangle(PrimitiveExtent, Rectangle),
Text(PrimitiveExtent, Rc<RefCell<Buffer>>, Option<TextShadow>),
Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice
@@ -276,7 +277,7 @@ fn draw_widget(
widget_state.draw_all(state, &draw_params);
draw_children(params, state, node_id);
draw_children(params, state, node_id, false);
if scissor_pushed {
state.scissor_stack.pop();
@@ -296,7 +297,7 @@ fn draw_widget(
}
}
fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taffy::NodeId) {
fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taffy::NodeId, is_topmost: bool) {
let layout = &params.layout;
for node_id in layout.state.tree.child_ids(parent_node_id) {
@@ -316,6 +317,10 @@ fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taf
};
draw_widget(params, state, node_id, style, widget);
if is_topmost {
state.primitives.push(RenderPrimitive::NewPass);
}
}
}
@@ -324,14 +329,6 @@ pub fn draw(params: &mut DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
let mut transform_stack = TransformStack::new();
let mut scissor_stack = ScissorStack::new();
let Some(root_widget) = params.layout.state.widgets.get(params.layout.root_widget) else {
panic!();
};
let Ok(style) = params.layout.state.tree.style(params.layout.root_node) else {
panic!();
};
let mut alterables = EventAlterables::default();
let mut state = DrawState {
@@ -342,7 +339,8 @@ pub fn draw(params: &mut DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
alterables: &mut alterables,
};
draw_widget(params, &mut state, params.layout.root_node, style, root_widget);
draw_children(params, &mut state, params.layout.tree_root_node, true);
params.layout.process_alterables(alterables)?;
Ok(primitives)

View File

@@ -1,14 +1,14 @@
use std::{marker::PhantomData, ops::Range, sync::Arc};
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use vulkano::{
buffer::{
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
BufferContents, BufferUsage, Subbuffer,
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
},
descriptor_set::{
layout::{DescriptorBindingFlags, DescriptorSetLayoutCreateFlags},
DescriptorSet, WriteDescriptorSet,
layout::{DescriptorBindingFlags, DescriptorSetLayoutCreateFlags},
},
format::Format,
image::{
@@ -17,8 +17,9 @@ use vulkano::{
},
memory::allocator::MemoryTypeFilter,
pipeline::{
DynamicState, GraphicsPipeline, Pipeline, PipelineLayout,
graphics::{
self,
self, GraphicsPipelineCreateInfo,
color_blend::{AttachmentBlend, ColorBlendAttachmentState, ColorBlendState},
input_assembly::{InputAssemblyState, PrimitiveTopology},
multisample::MultisampleState,
@@ -26,15 +27,13 @@ use vulkano::{
subpass::PipelineRenderingCreateInfo,
vertex_input::{Vertex, VertexDefinition, VertexInputState},
viewport::ViewportState,
GraphicsPipelineCreateInfo,
},
layout::PipelineDescriptorSetLayoutCreateInfo,
DynamicState, GraphicsPipeline, Pipeline, PipelineLayout,
},
shader::{EntryPoint, ShaderModule},
};
use super::{pass::WGfxPass, WGfx};
use super::{WGfx, pass::WGfxPass};
pub struct WGfxPipeline<V> {
pub graphics: Arc<WGfx>,
@@ -47,6 +46,7 @@ impl<V> WGfxPipeline<V>
where
V: Sized,
{
#[allow(clippy::too_many_arguments)]
fn new_from_stages(
graphics: Arc<WGfx>,
format: Format,
@@ -207,21 +207,25 @@ impl WPipelineCreateInfo {
}
}
pub fn use_blend(mut self, blend: AttachmentBlend) -> Self {
#[must_use]
pub const fn use_blend(mut self, blend: AttachmentBlend) -> Self {
self.blend = Some(blend);
self
}
pub fn use_topology(mut self, topology: PrimitiveTopology) -> Self {
#[must_use]
pub const fn use_topology(mut self, topology: PrimitiveTopology) -> Self {
self.topology = topology;
self
}
pub fn use_instanced(mut self) -> Self {
#[must_use]
pub const fn use_instanced(mut self) -> Self {
self.instanced = true;
self
}
#[must_use]
pub fn use_updatable_descriptors(mut self, updatable_sets: SmallVec<[usize; 8]>) -> Self {
self.updatable_sets = updatable_sets;
self

View File

@@ -1,9 +1,14 @@
use std::{
cell::{RefCell, RefMut},
io::Read,
rc::Rc,
};
use crate::{assets::AssetProvider, drawing, i18n::I18n};
use crate::{
assets::{AssetPath, AssetProvider},
assets_internal, drawing,
i18n::I18n,
};
pub struct Defaults {
pub dark_mode: bool,
@@ -22,8 +27,9 @@ impl Default for Defaults {
}
pub struct Globals {
pub assets: Box<dyn AssetProvider>,
pub i18n: I18n,
pub assets_internal: Box<dyn AssetProvider>,
pub assets_builtin: Box<dyn AssetProvider>,
pub i18n_builtin: I18n,
pub defaults: Defaults,
}
@@ -31,10 +37,33 @@ pub struct Globals {
pub struct WguiGlobals(Rc<RefCell<Globals>>);
impl WguiGlobals {
pub fn new(mut assets: Box<dyn AssetProvider>, defaults: Defaults) -> anyhow::Result<Self> {
let i18n = I18n::new(&mut assets)?;
pub fn new(mut assets_builtin: Box<dyn AssetProvider>, defaults: Defaults) -> anyhow::Result<Self> {
let i18n_builtin = I18n::new(&mut assets_builtin)?;
let assets_internal = Box::new(assets_internal::AssetInternal {});
Ok(Self(Rc::new(RefCell::new(Globals { assets, i18n, defaults }))))
Ok(Self(Rc::new(RefCell::new(Globals {
assets_internal,
assets_builtin,
i18n_builtin,
defaults,
}))))
}
pub fn get_asset(&self, asset_path: AssetPath) -> anyhow::Result<Vec<u8>> {
match asset_path {
AssetPath::WguiInternal(path) => self.assets_internal().load_from_path(path),
AssetPath::BuiltIn(path) => self.assets_builtin().load_from_path(path),
AssetPath::Filesystem(path) => {
let mut file = std::fs::File::open(path)?;
/* 16 MiB safeguard */
if file.metadata()?.len() > 16 * 1024 * 1024 {
anyhow::bail!("Too large file size");
}
let mut data = Vec::new();
file.read_to_end(&mut data)?;
Ok(data)
}
}
}
pub fn get(&self) -> RefMut<'_, Globals> {
@@ -42,10 +71,14 @@ impl WguiGlobals {
}
pub fn i18n(&self) -> RefMut<'_, I18n> {
RefMut::map(self.0.borrow_mut(), |x| &mut x.i18n)
RefMut::map(self.0.borrow_mut(), |x| &mut x.i18n_builtin)
}
pub fn assets(&self) -> RefMut<'_, Box<dyn AssetProvider>> {
RefMut::map(self.0.borrow_mut(), |x| &mut x.assets)
pub fn assets_internal(&self) -> RefMut<'_, Box<dyn AssetProvider>> {
RefMut::map(self.0.borrow_mut(), |x| &mut x.assets_internal)
}
pub fn assets_builtin(&self) -> RefMut<'_, Box<dyn AssetProvider>> {
RefMut::map(self.0.borrow_mut(), |x| &mut x.assets_builtin)
}
}

View File

@@ -6,14 +6,14 @@ use std::{
use crate::{
animation::Animations,
components::{Component, InitData},
drawing::{push_scissor_stack, push_transform_stack, Boundary},
drawing::{Boundary, push_scissor_stack, push_transform_stack},
event::{self, CallbackDataCommon, EventAlterables, EventListenerCollection},
globals::WguiGlobals,
widget::{self, div::WidgetDiv, EventParams, WidgetObj, WidgetState},
widget::{self, EventParams, WidgetObj, WidgetState, div::WidgetDiv},
};
use glam::{vec2, Vec2};
use slotmap::{new_key_type, HopSlotMap, SecondaryMap};
use glam::{Vec2, vec2};
use slotmap::{HopSlotMap, SecondaryMap, new_key_type};
use taffy::{NodeId, TaffyTree, TraversePartialTree};
new_key_type! {
@@ -113,8 +113,15 @@ pub struct Layout {
pub components_to_init: Vec<Component>,
pub widgets_to_tick: Vec<WidgetID>,
pub root_widget: WidgetID,
pub root_node: taffy::NodeId,
// *Main root*
// contains content_root_widget and topmost widgets
pub tree_root_widget: WidgetID,
pub tree_root_node: taffy::NodeId,
// *Main topmost widget*
// main topmost widget, always present, parent of `tree_root_widget`
pub content_root_widget: WidgetID,
pub content_root_node: taffy::NodeId,
pub prev_size: Vec2,
pub content_size: Vec2,
@@ -165,6 +172,22 @@ impl Layout {
Rc::new(RefCell::new(self))
}
pub fn add_topmost_child(
&mut self,
widget: WidgetState,
style: taffy::Style,
) -> anyhow::Result<(WidgetPair, taffy::NodeId)> {
self.mark_redraw();
add_child_internal(
&mut self.state.tree,
&mut self.state.widgets,
&mut self.state.nodes,
Some(self.tree_root_node),
widget,
style,
)
}
pub fn add_child(
&mut self,
parent_widget_id: WidgetID,
@@ -217,7 +240,7 @@ impl Layout {
self.needs_redraw = true;
}
fn process_pending_components(&mut self, alterables: &mut EventAlterables) -> anyhow::Result<()> {
fn process_pending_components(&mut self, alterables: &mut EventAlterables) {
for comp in &self.components_to_init {
let mut common = CallbackDataCommon {
state: &self.state,
@@ -227,7 +250,6 @@ impl Layout {
comp.0.init(&mut InitData { common: &mut common });
}
self.components_to_init.clear();
Ok(())
}
fn process_pending_widget_ticks(&mut self, alterables: &mut EventAlterables) {
@@ -358,9 +380,7 @@ impl Layout {
mut user_data: (&mut U1, &mut U2),
) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default();
self.push_event_widget(listeners, self.root_node, event, &mut alterables, &mut user_data)?;
self.push_event_widget(listeners, self.tree_root_node, event, &mut alterables, &mut user_data)?;
self.process_alterables(alterables)?;
listeners.gc();
@@ -376,7 +396,7 @@ impl Layout {
globals,
};
let (root_widget, root_node) = add_child_internal(
let (tree_root_widget, tree_root_node) = add_child_internal(
&mut state.tree,
&mut state.widgets,
&mut state.nodes,
@@ -392,12 +412,23 @@ impl Layout {
},
)?;
let (content_root_widget, content_root_node) = add_child_internal(
&mut state.tree,
&mut state.widgets,
&mut state.nodes,
Some(tree_root_node),
WidgetDiv::create(),
taffy::Style::default(),
)?;
Ok(Self {
state,
prev_size: Vec2::default(),
content_size: Vec2::default(),
root_node,
root_widget: root_widget.id,
tree_root_node,
tree_root_widget: tree_root_widget.id,
content_root_node,
content_root_widget: content_root_widget.id,
needs_redraw: true,
haptics_triggered: false,
animations: Animations::default(),
@@ -407,7 +438,7 @@ impl Layout {
}
fn try_recompute_layout(&mut self, size: Vec2) -> anyhow::Result<()> {
if !self.state.tree.dirty(self.root_node)? && self.prev_size == size {
if !self.state.tree.dirty(self.tree_root_node)? && self.prev_size == size {
// Nothing to do
return Ok(());
}
@@ -417,7 +448,7 @@ impl Layout {
self.prev_size = size;
self.state.tree.compute_layout_with_measure(
self.root_node,
self.tree_root_node,
taffy::Size {
width: taffy::AvailableSpace::Definite(size.x),
height: taffy::AvailableSpace::Definite(size.y),
@@ -443,7 +474,7 @@ impl Layout {
}
},
)?;
let root_size = self.state.tree.layout(self.root_node).unwrap().size;
let root_size = self.state.tree.layout(self.tree_root_node).unwrap().size;
if self.content_size.x != root_size.width || self.content_size.y != root_size.height {
log::debug!(
"content size changed: {:.0}x{:.0} → {:.0}x{:.0}",
@@ -470,7 +501,7 @@ impl Layout {
pub fn tick(&mut self) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default();
self.animations.tick(&self.state, &mut alterables);
self.process_pending_components(&mut alterables)?;
self.process_pending_components(&mut alterables);
self.process_pending_widget_ticks(&mut alterables);
self.process_alterables(alterables)?;
Ok(())

View File

@@ -23,6 +23,7 @@
pub mod animation;
pub mod any;
pub mod assets;
mod assets_internal;
pub mod components;
pub mod drawing;
pub mod event;

View File

@@ -8,7 +8,7 @@ mod widget_rectangle;
mod widget_sprite;
use crate::{
assets::{self, AssetProvider},
assets::{AssetPath, AssetPathOwned, normalize_path},
components::{Component, ComponentWeak},
drawing::{self},
event::EventListenerCollection,
@@ -22,12 +22,7 @@ use crate::{
};
use ouroboros::self_referencing;
use smallvec::SmallVec;
use std::{
cell::RefMut,
collections::HashMap,
path::{Path, PathBuf},
rc::Rc,
};
use std::{cell::RefMut, collections::HashMap, path::Path, rc::Rc};
#[self_referencing]
struct XmlDocument {
@@ -44,7 +39,7 @@ pub struct Template {
}
struct ParserFile {
path: PathBuf,
path: AssetPathOwned,
document: Rc<XmlDocument>,
template_parameters: HashMap<Rc<str>, Rc<str>>,
}
@@ -201,7 +196,7 @@ impl Fetchable for ParserData {
#[derive(Default)]
pub struct ParserState {
pub data: ParserData,
pub path: PathBuf,
pub path: AssetPathOwned,
}
impl ParserState {
@@ -558,11 +553,22 @@ fn parse_tag_include<U1, U2>(
#[allow(clippy::single_match)]
match pair.attrib.as_ref() {
"src" => {
let mut new_path = file.path.parent().unwrap_or_else(|| Path::new("/")).to_path_buf();
new_path.push(pair.value.as_ref());
let new_path = assets::normalize_path(&new_path);
let new_path = {
let this = &file.path.clone();
let include: &str = &pair.value;
let buf = this.get_path_buf();
let mut new_path = buf.parent().unwrap_or_else(|| Path::new("/")).to_path_buf();
new_path.push(include);
let new_path = normalize_path(&new_path);
let (new_file, node_layout) = get_doc_from_path(ctx, &new_path)?;
match this {
AssetPathOwned::WguiInternal(_) => AssetPathOwned::WguiInternal(new_path),
AssetPathOwned::BuiltIn(_) => AssetPathOwned::BuiltIn(new_path),
AssetPathOwned::Filesystem(_) => AssetPathOwned::Filesystem(new_path),
}
};
let new_path_ref = new_path.as_ref();
let (new_file, node_layout) = get_doc_from_asset_path(ctx, new_path_ref)?;
parse_document_root(&new_file, ctx, parent_id, node_layout)?;
return Ok(());
@@ -992,7 +998,7 @@ pub struct ParseDocumentExtra {
// filled-in by you in `new_layout_from_assets` function
pub struct ParseDocumentParams<'a> {
pub globals: WguiGlobals, // mandatory field
pub path: &'a str, // mandatory field
pub path: AssetPath<'a>, // mandatory field
pub extra: ParseDocumentExtra, // optional field, can be Default-ed
}
@@ -1002,18 +1008,15 @@ pub fn parse_from_assets<U1, U2>(
listeners: &mut EventListenerCollection<U1, U2>,
parent_id: WidgetID,
) -> anyhow::Result<ParserState> {
let path = PathBuf::from(doc_params.path);
let parser_data = ParserData::default();
let mut ctx = create_default_context(doc_params, layout, listeners, &parser_data);
let (file, node_layout) = get_doc_from_path(&ctx, &path)?;
let (file, node_layout) = get_doc_from_asset_path(&ctx, doc_params.path)?;
parse_document_root(&file, &mut ctx, parent_id, node_layout)?;
// move everything essential to the result
let result = ParserState {
data: std::mem::take(&mut ctx.data_local),
path,
path: doc_params.path.to_owned(),
};
drop(ctx);
@@ -1027,21 +1030,18 @@ pub fn new_layout_from_assets<U1, U2>(
layout_params: &LayoutParams,
) -> anyhow::Result<(Layout, ParserState)> {
let mut layout = Layout::new(doc_params.globals.clone(), layout_params)?;
let widget = layout.root_widget;
let widget = layout.content_root_widget;
let state = parse_from_assets(doc_params, &mut layout, listeners, widget)?;
Ok((layout, state))
}
fn assets_path_to_xml(assets: &mut Box<dyn AssetProvider>, path: &Path) -> anyhow::Result<String> {
let data = assets.load_from_path(&path.to_string_lossy())?;
Ok(String::from_utf8(data)?)
}
fn get_doc_from_path<U1, U2>(
fn get_doc_from_asset_path<U1, U2>(
ctx: &ParserContext<U1, U2>,
path: &Path,
asset_path: AssetPath,
) -> anyhow::Result<(ParserFile, roxmltree::NodeId)> {
let xml = assets_path_to_xml(&mut ctx.layout.state.globals.assets(), path)?;
let data = ctx.layout.state.globals.get_asset(asset_path)?;
let xml = String::from_utf8(data)?;
let document = Rc::new(XmlDocument::new(xml, |xml| {
let opt = roxmltree::ParsingOptions {
allow_dtd: true,
@@ -1054,7 +1054,7 @@ fn get_doc_from_path<U1, U2>(
let tag_layout = require_tag_by_name(&root, "layout")?;
let file = ParserFile {
path: PathBuf::from(path),
path: asset_path.to_owned(),
document: document.clone(),
template_parameters: Default::default(),
};

View File

@@ -1,4 +1,5 @@
use crate::{
assets::AssetPath,
layout::WidgetID,
parser::{AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, style::parse_style},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
@@ -21,9 +22,16 @@ pub fn parse_widget_sprite<'a, U1, U2>(
for pair in attribs {
let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
match key {
"src" => {
"src" | "src_ext" | "src_internal" => {
let asset_path = match key {
"src" => AssetPath::BuiltIn(value),
"src_ext" => AssetPath::Filesystem(value),
"src_internal" => AssetPath::WguiInternal(value),
_ => unreachable!(),
};
if !value.is_empty() {
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), value) {
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals, asset_path) {
Ok(glyph) => Some(glyph),
Err(e) => {
log::warn!("failed to load {value}: {e}");
@@ -32,11 +40,6 @@ pub fn parse_widget_sprite<'a, U1, U2>(
}
}
}
"src_ext" => {
if !value.is_empty() && std::fs::exists(value).unwrap_or(false) {
glyph = CustomGlyphContent::from_file(value).ok();
}
}
"color" => {
if let Some(color) = parse_color_hex(value) {
params.color = Some(color);

View File

@@ -243,6 +243,9 @@ impl Context {
let pass = passes.last_mut().unwrap(); // always safe
match &primitive {
drawing::RenderPrimitive::NewPass => {
needs_new_pass = true;
}
drawing::RenderPrimitive::Rectangle(extent, rectangle) => {
pass
.rect_renderer

View File

@@ -10,7 +10,7 @@ use cosmic_text::SubpixelBin;
use image::RgbaImage;
use resvg::usvg::{Options, Tree};
use crate::assets::AssetProvider;
use crate::{assets::AssetPath, globals::WguiGlobals};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
@@ -32,19 +32,10 @@ impl CustomGlyphContent {
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub fn from_assets(provider: &mut Box<dyn AssetProvider>, path: &str) -> anyhow::Result<Self> {
let data = provider.load_from_path(path)?;
if path.ends_with(".svg") || path.ends_with(".svgz") {
Ok(Self::from_bin_svg(&data)?)
} else {
Ok(Self::from_bin_raster(&data)?)
}
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub fn from_file(path: &str) -> anyhow::Result<Self> {
let data = std::fs::read(path)?;
if path.ends_with(".svg") || path.ends_with(".svgz") {
pub fn from_assets(globals: &mut WguiGlobals, path: AssetPath) -> anyhow::Result<Self> {
let path_str = path.get_str();
let data = globals.get_asset(path)?;
if path_str.ends_with(".svg") || path_str.ends_with(".svgz") {
Ok(Self::from_bin_svg(&data)?)
} else {
Ok(Self::from_bin_raster(&data)?)
@@ -165,11 +156,7 @@ impl RasterizedCustomGlyph {
}
}
pub(super) fn validate(
&self,
input: &RasterizeCustomGlyphRequest,
expected_type: Option<ContentType>,
) {
pub(super) fn validate(&self, input: &RasterizeCustomGlyphRequest, expected_type: Option<ContentType>) {
if let Some(expected_type) = expected_type {
assert_eq!(
self.content_type, expected_type,
@@ -222,10 +209,7 @@ impl ContentType {
}
}
fn rasterize_svg(
tree: &Tree,
input: &RasterizeCustomGlyphRequest,
) -> Option<RasterizedCustomGlyph> {
fn rasterize_svg(tree: &Tree, input: &RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph> {
// Calculate the scale based on the "glyph size".
let svg_size = tree.size();
let scale_x = f32::from(input.width) / svg_size.width();

View File

@@ -46,7 +46,7 @@ impl WidgetLabel {
buffer.set_wrap(wrap);
buffer.set_rich_text(
[(params.content.generate(&mut globals.i18n).as_ref(), attrs)],
[(params.content.generate(&mut globals.i18n_builtin).as_ref(), attrs)],
&Attrs::new(),
Shaping::Advanced,
params.style.align.map(Into::into),

View File

@@ -1,10 +1,11 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use button::setup_custom_button;
use glam::{vec2, Affine2, Vec2};
use glam::{Affine2, Vec2, vec2};
use label::setup_custom_label;
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use wgui::{
assets::AssetPath,
drawing,
event::{
Event as WguiEvent, EventListenerCollection, InternalStateChangeEvent, ListenerHandleVec,
@@ -21,7 +22,7 @@ use crate::{
backend::input::{Haptics, PointerHit, PointerMode},
graphics::{CommandBuffers, ExtentExt},
state::AppState,
windowing::backend::{ui_transform, FrameMeta, OverlayBackend, ShouldRender},
windowing::backend::{FrameMeta, OverlayBackend, ShouldRender, ui_transform},
};
use super::{timer::GuiTimer, timestep::Timestep};
@@ -72,7 +73,7 @@ impl<S> GuiPanel<S> {
let doc_params = wgui::parser::ParseDocumentParams {
globals: app.wgui_globals.clone(),
path,
path: AssetPath::BuiltIn(path),
extra: wgui::parser::ParseDocumentExtra {
on_custom_attribs: Some(Box::new({
let custom_elems = custom_elems.clone();

View File

@@ -1,8 +1,9 @@
use std::{collections::HashMap, rc::Rc};
use glam::{vec2, vec3, Affine3A, Mat4, Quat, Vec2, Vec3};
use glam::{Affine3A, Mat4, Quat, Vec2, Vec3, vec2, vec3};
use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
drawing::Color,
event::{self, CallbackMetadata, EventListenerKind},
layout::LayoutParams,
@@ -19,14 +20,14 @@ use wgui::{
use crate::{
gui::panel::GuiPanel,
state::AppState,
subsystem::hid::{XkbKeymap, ALT, CTRL, META, SHIFT, SUPER},
subsystem::hid::{ALT, CTRL, META, SHIFT, SUPER, XkbKeymap},
windowing::window::{OverlayWindowConfig, OverlayWindowState, Positioning},
};
use super::{
handle_press, handle_release,
KEYBOARD_NAME, KeyButtonData, KeyState, KeyboardBackend, KeyboardState, handle_press,
handle_release,
layout::{self, AltModifier, KeyCapType},
KeyButtonData, KeyState, KeyboardBackend, KeyboardState, KEYBOARD_NAME,
};
const BACKGROUND_PADDING: f32 = 4.;
@@ -54,7 +55,7 @@ pub fn create_keyboard(
let mut panel = GuiPanel::new_blank(app, state)?;
let (background, _) = panel.layout.add_child(
panel.layout.root_widget,
panel.layout.content_root_widget,
WidgetRectangle::create(WidgetRectangleParams {
color: wgui::drawing::Color::new(0., 0., 0., 0.6),
round: WLength::Units(4.0),
@@ -75,7 +76,7 @@ pub fn create_keyboard(
let parse_doc_params = wgui::parser::ParseDocumentParams {
globals: app.wgui_globals.clone(),
path: "gui/keyboard.xml",
path: AssetPath::BuiltIn("gui/keyboard.xml"),
extra: Default::default(),
};

View File

@@ -5,7 +5,7 @@ use std::{
time::Instant,
};
use glam::{vec3, Affine3A, Quat, Vec3};
use glam::{Affine3A, Quat, Vec3, vec3};
use idmap_derive::IntegerId;
use serde::{Deserialize, Serialize};
use wgui::{
@@ -28,8 +28,8 @@ use crate::{
gui::panel::GuiPanel,
state::{AppState, LeftRight},
windowing::{
window::{OverlayWindowConfig, OverlayWindowState, Positioning},
OverlaySelector, Z_ORDER_TOAST,
window::{OverlayWindowConfig, OverlayWindowState, Positioning},
},
};
@@ -144,8 +144,7 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<OverlayWindowConfig> {
Positioning::FollowHead { lerp: 0.1 },
),
DisplayMethod::Watch => {
let mut watch_pos =
Vec3::from(app.session.config.watch_pos) + vec3(-0.005, -0.05, 0.02);
let mut watch_pos = app.session.config.watch_pos + vec3(-0.005, -0.05, 0.02);
let mut watch_rot = app.session.config.watch_rot;
let relative_to = match app.session.config.watch_hand {
LeftRight::Left => Positioning::FollowHand { hand: 0, lerp: 1.0 },
@@ -172,7 +171,7 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<OverlayWindowConfig> {
let (rect, _) = panel
.layout
.add_child(
panel.layout.root_widget,
panel.layout.content_root_widget,
WidgetRectangle::create(WidgetRectangleParams {
color: parse_color_hex("#1e2030").unwrap(),
border_color: parse_color_hex("#5e7090").unwrap(),