feat: add flowbox widget

This commit is contained in:
Byson94
2025-10-22 22:11:51 +05:30
parent ac8375bf47
commit 926c5bae7e
12 changed files with 202 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- `orientation` property to eventbox.
- `spacing` property to eventbox.
- `space_evenly` property to eventbox.
- An advanced widget named `flowbox`.
### Fixed

18
Cargo.lock generated
View File

@@ -1636,9 +1636,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.37"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
@@ -1773,6 +1773,7 @@ dependencies = [
"regex",
"rhai",
"rhai_trace",
"scan_prop_proc",
"serde",
"shared_utils",
"tokio",
@@ -1830,6 +1831,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scan_prop_proc"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2004,9 +2014,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.106"
version = "2.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,11 +1,12 @@
[workspace]
members = ["crates/*", "tools/*"]
members = ["crates/*", "tools/*", "proc_macros/*"]
resolver = "2"
[workspace.dependencies]
shared_utils = { version = "0.1.0", path = "crates/shared_utils" }
rhai_impl = { version = "0.1.0", path = "crates/rhai_impl" }
scan_prop_proc = { version = "0.1.0", path = "proc_macros/scan_prop_proc" }
ewwii_plugin_api = { version = "0.6.1", path = "crates/ewwii_plugin_api" }
anyhow = "1.0.86"
@@ -45,6 +46,9 @@ thiserror = "1.0"
tokio = { version = "1.39.2", features = ["full"] }
unescape = "0.1"
wait-timeout = "0.2"
syn = "2.0.107"
quote = "1.0.41"
proc-macro2 = "1.0.101"
[profile.dev]
split-debuginfo = "unpacked"

View File

@@ -41,6 +41,7 @@ fn build_gtk_widget_from_node(
let gtk_widget = match root_node {
WidgetNode::Box { props, children } => build_gtk_box(props, children, widget_reg)?.upcast(),
WidgetNode::FlowBox { props, children } => build_gtk_flowbox(props, children, widget_reg)?.upcast(),
WidgetNode::EventBox { props, children } => {
build_event_box(props, children, widget_reg)?.upcast()
}

View File

@@ -756,6 +756,98 @@ pub(super) fn build_event_box(
Ok(gtk_widget)
}
struct FlowBoxCtrlData {
onaccept_cmd: String,
cmd_timeout: Duration,
}
pub(crate) fn build_gtk_flowbox(
props: &Map,
children: &Vec<WidgetNode>,
widget_registry: &mut WidgetRegistry,
) -> Result<gtk4::FlowBox> {
let gtk_widget = gtk4::FlowBox::new();
let controller_data = Rc::new(RefCell::new(FlowBoxCtrlData {
onaccept_cmd: String::new(),
cmd_timeout: Duration::from_millis(200)
}));
gtk_widget.connect_child_activated(glib::clone!(#[strong] controller_data, move |_: &gtk4::FlowBox, child: &gtk4::FlowBoxChild| {
let controller = controller_data.borrow();
let index = child.index();
if index != -1 {
run_command(controller.cmd_timeout, &controller.onaccept_cmd, &[index as usize]);
} else {
log::error!("Failed to get child index.");
}
}));
for child in children {
let child_widget = build_gtk_widget(&WidgetInput::BorrowedNode(child), widget_registry)?;
if let Some(props) = child.props() {
if let Ok(id) = get_i32_prop(&props, "child_index", None) {
gtk_widget.insert(&child_widget, id);
} else {
log::error!("Every child of a flowbox MUST have a property named `child_index`.");
}
} else {
log::error!("Failed to extract properties from the child.");
}
}
let apply_props = |props: &Map, gtk_widget: &gtk4::FlowBox, controller_data: Rc<RefCell<FlowBoxCtrlData>>| -> Result<()> {
if let Ok(space_evenly) = get_bool_prop(&props, "space_evenly", Some(true)) {
gtk_widget.set_homogeneous(space_evenly);
}
let orientation = props
.get("orientation")
.and_then(|v| v.clone().try_cast::<String>())
.map(|s| parse_orientation(&s))
.transpose()?
.unwrap_or(gtk4::Orientation::Horizontal);
gtk_widget.set_orientation(orientation);
if let Ok(selection_model_raw) = get_string_prop(&props, "selection_model", None) {
let selection_model = parse_selection_model(&selection_model_raw)?;
gtk_widget.set_selection_mode(selection_model);
}
if let Ok(onaccept) = get_string_prop(&props, "onaccept", None) {
controller_data.borrow_mut().onaccept_cmd = onaccept;
}
Ok(())
};
apply_props(&props, &gtk_widget, controller_data.clone())?;
let gtk_widget_clone = gtk_widget.clone();
let update_fn: UpdateFn = Box::new(move |props: &Map| {
let _ = apply_props(props, &gtk_widget_clone, controller_data.clone());
// now re-apply generic widget attrs
if let Err(err) =
resolve_rhai_widget_attrs(&gtk_widget_clone.clone().upcast::<gtk4::Widget>(), &props)
{
eprintln!("Failed to update widget attrs: {:?}", err);
}
});
let id = hash_props_and_type(&props, "FlowBox");
widget_registry
.widgets
.insert(id, WidgetEntry { update_fn, widget: gtk_widget.clone().upcast() });
resolve_rhai_widget_attrs(&gtk_widget.clone().upcast::<gtk4::Widget>(), &props)?;
Ok(gtk_widget)
}
pub(super) fn build_gtk_stack(
props: &Map,
children: &Vec<WidgetNode>,

View File

@@ -159,6 +159,17 @@ pub(super) fn parse_position_type(s: &str) -> Result<gtk4::PositionType> {
}
}
/// Gtk flow box
pub(super) fn parse_selection_model(s: &str) -> Result<gtk4::SelectionMode> {
match s.to_ascii_lowercase().as_str() {
"none" => Ok(gtk4::SelectionMode::None),
"single" => Ok(gtk4::SelectionMode::Single),
"browse" => Ok(gtk4::SelectionMode::Browse),
"multiple" => Ok(gtk4::SelectionMode::Multiple),
_ => Err(anyhow!("Invalid position type: '{}'", s)),
}
}
/// Helper of helpers
fn replace_placeholders<T>(cmd: &str, args: &[T]) -> String
where

View File

@@ -10,6 +10,7 @@ homepage = "https://github.com/byson94/ewwii"
[dependencies]
shared_utils.workspace = true
scan_prop_proc.workspace = true
rhai = { workspace = true, features = ["internals"] }
anyhow.workspace = true

View File

@@ -3,11 +3,14 @@ use anyhow::Result;
use rhai::Map;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use scan_prop_proc::scan_prop;
#[derive(Debug, Clone)]
#[scan_prop]
pub enum WidgetNode {
Label { props: Map },
Box { props: Map, children: Vec<WidgetNode> },
FlowBox { props: Map, children: Vec<WidgetNode> },
Button { props: Map },
Image { props: Map },
Icon { props: Map },
@@ -72,6 +75,13 @@ pub fn get_id_to_widget_info<'a>(
get_id_to_widget_info(child, id_to_props, Some(id))?;
}
}
WidgetNode::FlowBox { props, children } => {
let id = hash_props_and_type(props, "FlowBox");
insert_wdgt_info(node, props, "FlowBox", children.as_slice(), parent_id, id_to_props)?;
for child in children {
get_id_to_widget_info(child, id_to_props, Some(id))?;
}
}
WidgetNode::EventBox { props, children } => {
let id = hash_props_and_type(props, "EventBox");
insert_wdgt_info(node, props, "EventBox", children.as_slice(), parent_id, id_to_props)?;

View File

@@ -67,6 +67,7 @@ pub fn register_all_widgets(engine: &mut Engine, all_nodes: &Rc<RefCell<Vec<Widg
}
register_with_children!("box", Box);
register_with_children!("flowbox", FlowBox);
register_with_children!("expander", Expander);
register_with_children!("revealer", Revealer);
register_with_children!("scroll", Scroll);

View File

@@ -39,6 +39,10 @@ impl WidgetNode {
props: with_dyn_id(props.clone(), parent_path),
children: process_children(children, parent_path, "box"),
},
WidgetNode::FlowBox { props, children } => WidgetNode::FlowBox {
props: with_dyn_id(props.clone(), parent_path),
children: process_children(children, parent_path, "flowbox"),
},
WidgetNode::Expander { props, children } => WidgetNode::Expander {
props: with_dyn_id(props.clone(), parent_path),
children: process_children(children, parent_path, "expander"),

View File

@@ -0,0 +1,17 @@
[package]
name = "scan_prop_proc"
version = "0.1.0"
authors = ["byson94 <byson94wastaken@gmail.com>"]
edition = "2021"
license = "GPL-3.0-or-later"
description = "A procedual macro for generating properties on a WidgetNode"
repository = "https://github.com/byson94/ewwii"
homepage = "https://github.com/byson94/ewwii"
[lib]
proc-macro = true
[dependencies]
syn.workspace = true
quote.workspace = true
proc-macro2.workspace = true

View File

@@ -0,0 +1,45 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_attribute]
pub fn scan_prop(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let props_matches = if let Data::Enum(data_enum) = &input.data {
data_enum.variants.iter().filter_map(|v| {
match &v.fields {
Fields::Named(fields) => {
for f in &fields.named {
if f.ident.as_ref().map(|id| id == "props").unwrap_or(false) {
let vname = &v.ident;
return Some(quote! {
#name::#vname { props, .. } => Some(props)
});
}
}
None
}
_ => None,
}
}).collect::<Vec<_>>()
} else {
vec![]
};
let expanded = quote! {
#input
impl #name {
pub fn props(&self) -> Option<&Map> {
match self {
#(#props_matches),*,
_ => None
}
}
}
};
TokenStream::from(expanded)
}