feat: add flowbox widget
This commit is contained in:
@@ -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
18
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 |_: >k4::FlowBox, child: >k4::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: >k4::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, >k_widget, controller_data.clone())?;
|
||||
|
||||
let gtk_widget_clone = gtk_widget.clone();
|
||||
let update_fn: UpdateFn = Box::new(move |props: &Map| {
|
||||
let _ = apply_props(props, >k_widget_clone, controller_data.clone());
|
||||
|
||||
// now re-apply generic widget attrs
|
||||
if let Err(err) =
|
||||
resolve_rhai_widget_attrs(>k_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(>k_widget.clone().upcast::<gtk4::Widget>(), &props)?;
|
||||
|
||||
Ok(gtk_widget)
|
||||
}
|
||||
|
||||
pub(super) fn build_gtk_stack(
|
||||
props: &Map,
|
||||
children: &Vec<WidgetNode>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
17
proc_macros/scan_prop_proc/Cargo.toml
Normal file
17
proc_macros/scan_prop_proc/Cargo.toml
Normal 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
|
||||
45
proc_macros/scan_prop_proc/src/lib.rs
Normal file
45
proc_macros/scan_prop_proc/src/lib.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user