dash-frontend: Application list

This commit is contained in:
Aleksander
2025-09-28 13:13:37 +02:00
parent eb12a6a319
commit b5a3ba2954
18 changed files with 907 additions and 115 deletions

View File

@@ -1,5 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use taffy::{AlignItems, JustifyContent, prelude::length};
use taffy::{AlignItems, JustifyContent};
use crate::{
animation::{Animation, AnimationEasing},
@@ -18,7 +18,7 @@ use crate::{
};
pub struct Params {
pub text: Translation,
pub text: Option<Translation>, // if unset, label will not be populated
pub color: Option<drawing::Color>,
pub border_color: Option<drawing::Color>,
pub hover_border_color: Option<drawing::Color>,
@@ -31,7 +31,7 @@ pub struct Params {
impl Default for Params {
fn default() -> Self {
Self {
text: Translation::from_raw_text(""),
text: Some(Translation::from_raw_text("")),
color: None,
hover_color: None,
border_color: None,
@@ -232,7 +232,6 @@ pub fn construct<U1, U2>(
// force-override style
style.align_items = Some(AlignItems::Center);
style.justify_content = Some(JustifyContent::Center);
style.padding = length(1.0);
style.overflow.x = taffy::Overflow::Hidden;
style.overflow.y = taffy::Overflow::Hidden;
@@ -277,25 +276,30 @@ pub fn construct<U1, U2>(
let light_text = (color.r + color.g + color.b) < 1.5;
let (id_label, _node_label) = layout.add_child(
id_rect,
WidgetLabel::create(
globals,
WidgetLabelParams {
content: params.text,
style: TextStyle {
weight: Some(FontWeight::Bold),
color: Some(if light_text {
Color::new(1.0, 1.0, 1.0, 1.0)
} else {
Color::new(0.0, 0.0, 0.0, 1.0)
}),
..params.text_style
let id_label = if let Some(content) = params.text {
let (id_label, _node_label) = layout.add_child(
id_rect,
WidgetLabel::create(
globals,
WidgetLabelParams {
content,
style: TextStyle {
weight: Some(FontWeight::Bold),
color: Some(if light_text {
Color::new(1.0, 1.0, 1.0, 1.0)
} else {
Color::new(0.0, 0.0, 0.0, 1.0)
}),
..params.text_style
},
},
},
),
Default::default(),
)?;
),
Default::default(),
)?;
id_label
} else {
WidgetID::default()
};
let data = Rc::new(Data {
id_label,

View File

@@ -21,7 +21,7 @@ pub fn parse_component_button<'a, U1, U2>(
let mut hover_color: Option<Color> = None;
let mut hover_border_color: Option<Color> = None;
let mut round = WLength::Units(4.0);
let mut translation = Translation::default();
let mut translation: Option<Translation> = None;
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
let text_style = parse_text_style(&attribs);
@@ -30,10 +30,10 @@ pub fn parse_component_button<'a, U1, U2>(
for (key, value) in attribs {
match key.as_ref() {
"text" => {
translation = Translation::from_raw_text(&value);
translation = Some(Translation::from_raw_text(&value));
}
"translation" => {
translation = Translation::from_translation_key(&value);
translation = Some(Translation::from_translation_key(&value));
}
"round" => {
parse_round(&value, &mut round);

View File

@@ -49,6 +49,11 @@ struct ParserFile {
template_parameters: HashMap<Rc<str>, Rc<str>>,
}
/*
`components` could contain connected listener handles.
Do not drop them unless you don't need to handle any events,
including mouse-hover animations.
*/
#[derive(Default, Clone)]
pub struct ParserData {
pub components_by_id: HashMap<Rc<str>, ComponentWeak>,
@@ -60,6 +65,29 @@ pub struct ParserData {
macro_attribs: HashMap<Rc<str>, MacroAttribs>,
}
pub trait Fetchable {
/// Return a component by its string ID
fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component>;
/// Return a component by the ID of the widget that owns it
fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component>;
/// Fetch a component by string ID and downcast it to a concrete component type `T` (see `components/mod.rs`)
fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>>;
/// Fetch a component by widget ID and downcast it to a concrete component type `T` (see `components/mod.rs`)
fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>>;
/// Return a widget by its string ID
fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID>;
/// Retrieve the widget associated with a string ID, returning a `WidgetPair` (id and widget itself)
fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair>;
/// Retrieve a widget by string ID and downcast its inner value to type `T` (see `widget/mod.rs`)
fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>>;
}
impl ParserData {
fn take_results_from(&mut self, from: &mut Self) {
let ids = std::mem::take(&mut from.ids);
@@ -85,19 +113,9 @@ impl ParserData {
}
}
/*
WARNING: this struct could contain valid components with already bound listener handles.
Make sure to store them somewhere in your code.
*/
#[derive(Default)]
pub struct ParserState {
pub data: ParserData,
pub path: PathBuf,
}
impl ParserState {
pub fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component> {
let Some(weak) = self.data.components_by_id.get(id) else {
impl Fetchable for ParserData {
fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component> {
let Some(weak) = self.components_by_id.get(id) else {
anyhow::bail!("Component by ID \"{id}\" doesn't exist");
};
@@ -108,8 +126,8 @@ impl ParserState {
Ok(Component(component))
}
pub fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component> {
let Some(weak) = self.data.components_by_widget_id.get(&widget_id) else {
fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component> {
let Some(weak) = self.components_by_widget_id.get(&widget_id) else {
anyhow::bail!("Component by widget ID \"{widget_id:?}\" doesn't exist");
};
@@ -120,37 +138,36 @@ impl ParserState {
Ok(Component(component))
}
pub fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>> {
fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>> {
let component = self.fetch_component_by_id(id)?;
if !(*component.0).as_any().is::<T>() {
anyhow::bail!("fetch_component_as({id}): type not matching");
}
// safety: we already checked it above, should be safe to directly cast it
// safety: we just checked the type
unsafe { Ok(Rc::from_raw(Rc::into_raw(component.0).cast())) }
}
pub fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>> {
fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>> {
let component = self.fetch_component_by_widget_id(widget_id)?;
if !(*component.0).as_any().is::<T>() {
anyhow::bail!("fetch_component_by_widget_id({widget_id:?}): type not matching");
}
// safety: we already checked it above, should be safe to directly cast it
// safety: we just checked the type
unsafe { Ok(Rc::from_raw(Rc::into_raw(component.0).cast())) }
}
pub fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID> {
match self.data.ids.get(id) {
fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID> {
match self.ids.get(id) {
Some(id) => Ok(*id),
None => anyhow::bail!("Widget by ID \"{id}\" doesn't exist"),
}
}
// returns widget and its id at once
pub fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
let widget_id = self.get_widget_id(id)?;
let widget = state
.widgets
@@ -162,7 +179,7 @@ impl ParserState {
})
}
pub fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>> {
fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>> {
let widget_id = self.get_widget_id(id)?;
let widget = state
.widgets
@@ -175,8 +192,23 @@ impl ParserState {
Ok(casted)
}
}
pub fn process_template<U1, U2>(
/*
WARNING: this struct could contain valid components with already bound listener handles.
Make sure to store them somewhere in your code.
*/
#[derive(Default)]
pub struct ParserState {
pub data: ParserData,
pub path: PathBuf,
}
impl ParserState {
/// This function is suitable in cases if you don't want to pollute main parser state with dynamic IDs
/// Use `instantiate_template` instead unless you want to handle `components` results yourself.
/// Make sure not to drop them if you want to have your listener handles valid
pub fn parse_template<U1, U2>(
&mut self,
doc_params: &ParseDocumentParams,
template_name: &str,
@@ -184,7 +216,7 @@ impl ParserState {
listeners: &mut EventListenerCollection<U1, U2>,
widget_id: WidgetID,
template_parameters: HashMap<Rc<str>, Rc<str>>,
) -> anyhow::Result<()> {
) -> anyhow::Result<ParserData> {
let Some(template) = self.data.templates.get(template_name) else {
anyhow::bail!("no template named \"{template_name}\" found");
};
@@ -204,13 +236,64 @@ impl ParserState {
};
parse_widget_other_internal(&template.clone(), template_parameters, &file, &mut ctx, widget_id)?;
Ok(ctx.data_local)
}
self.data.take_results_from(&mut ctx.data_local);
/// Instantinate template by saving all the results into the main `ParserState`
pub fn instantiate_template<U1, U2>(
&mut self,
doc_params: &ParseDocumentParams,
template_name: &str,
layout: &mut Layout,
listeners: &mut EventListenerCollection<U1, U2>,
widget_id: WidgetID,
template_parameters: HashMap<Rc<str>, Rc<str>>,
) -> anyhow::Result<()> {
let mut data_local = self.parse_template(
doc_params,
template_name,
layout,
listeners,
widget_id,
template_parameters,
)?;
self.data.take_results_from(&mut data_local);
Ok(())
}
}
// convenience wrapper functions for `data`
impl Fetchable for ParserState {
fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component> {
self.data.fetch_component_by_id(id)
}
fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component> {
self.data.fetch_component_by_widget_id(widget_id)
}
fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>> {
self.data.fetch_component_as(id)
}
fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>> {
self.data.fetch_component_from_widget_id_as(widget_id)
}
fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID> {
self.data.get_widget_id(id)
}
fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
self.data.fetch_widget(state, id)
}
fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>> {
self.data.fetch_widget_as(state, id)
}
}
#[derive(Debug, Clone)]
struct MacroAttribs {
attribs: HashMap<Rc<str>, Rc<str>>,

View File

@@ -1,9 +1,6 @@
use crate::{
layout::WidgetID,
parser::{
ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal,
style::parse_style,
},
parser::{ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal, style::parse_style},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
widget::sprite::{WidgetSprite, WidgetSpriteParams},
};
@@ -24,17 +21,18 @@ pub fn parse_widget_sprite<'a, U1, U2>(
for (key, value) in attribs {
match key.as_ref() {
"src" => {
glyph =
match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), &value) {
if !value.is_empty() {
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), &value) {
Ok(glyph) => Some(glyph),
Err(e) => {
log::warn!("failed to load {value}: {e}");
None
}
}
}
}
"src_ext" => {
if std::fs::exists(value.as_ref()).unwrap_or(false) {
if !value.is_empty() && std::fs::exists(value.as_ref()).unwrap_or(false) {
glyph = CustomGlyphContent::from_file(&value).ok();
}
}
@@ -55,9 +53,7 @@ pub fn parse_widget_sprite<'a, U1, U2>(
log::warn!("No source for sprite node!");
}
let (new_id, _) = ctx
.layout
.add_child(parent_id, WidgetSprite::create(params), style)?;
let (new_id, _) = ctx.layout.add_child(parent_id, WidgetSprite::create(params), style)?;
parse_widget_universal(file, ctx, node, new_id);
parse_children(file, ctx, node, new_id)?;

View File

@@ -160,6 +160,10 @@ pub struct Context {
empty_text: Rc<RefCell<Buffer>>,
}
pub struct ContextDrawResult {
pub pass_count: u32,
}
impl Context {
pub fn new(shared: &mut SharedContext, pixel_scale: f32) -> anyhow::Result<Self> {
let viewport = Viewport::new(&shared.gfx)?;
@@ -216,7 +220,7 @@ impl Context {
shared: &mut SharedContext,
cmd_buf: &mut GfxCommandBuffer,
primitives: &[drawing::RenderPrimitive],
) -> anyhow::Result<()> {
) -> anyhow::Result<ContextDrawResult> {
self.dirty = false;
let atlas = shared.atlas_map.get_mut(self.shared_ctx_key).unwrap();
@@ -279,12 +283,14 @@ impl Context {
}
}
log::info!("count {}", passes.len());
let res = ContextDrawResult {
pass_count: passes.len() as u32,
};
for mut pass in passes {
pass.submit(&shared.gfx, &mut self.viewport, cmd_buf, &mut atlas.text_atlas)?;
}
Ok(())
Ok(res)
}
}