diff --git a/examples/example_actor_per_row.rs b/examples/example_actor_per_row.rs index 6864afa006..408a4c523b 100644 --- a/examples/example_actor_per_row.rs +++ b/examples/example_actor_per_row.rs @@ -1,9 +1,9 @@ #[derive(woab::Factories)] pub struct Factories { #[factory(extra(buf_sum))] - win_app: woab::Factory, + win_app: woab::BuilderFactory, #[factory(extra(buf_addend))] - row_addend: woab::Factory, + row_addend: woab::BuilderFactory, } #[derive(woab::WidgetsFromBuilder)] @@ -46,14 +46,16 @@ impl actix::StreamHandler for WindowActor { use gtk::prelude::*;; match signal { WindowSignal::ClickButton => { - let addend = self.factories.row_addend.build().actor(|_, widgets| { - self.widgets.lst_addition.add(&widgets.row_addend); - AddendActor { - widgets, - window: ctx.address(), - number: Some(0), - } - }).unwrap(); + let addend = self.factories.row_addend.instantiate() + .new_actor(|builder_ctx| { + let widgets: AddendWidgets = builder_ctx.connect_widgets().unwrap(); + self.widgets.lst_addition.add(&widgets.row_addend); + AddendActor { + widgets, + window: ctx.address(), + number: Some(0), + } + }); self.addends.push(addend); ctx.address().do_send(Recalculate); } @@ -162,11 +164,15 @@ fn main() -> Result<(), Box> { gtk::init()?; woab::run_actix_inside_gtk_event_loop("example")?; - factories.win_app.build().actor(|_, widgets| WindowActor { - widgets, - factories, - addends: Vec::new(), - })?; + factories.win_app.instantiate() + .new_actor(|ctx| { + ctx.connect_signals::(); + WindowActor { + widgets: ctx.connect_widgets().unwrap(), + factories, + addends: Vec::new(), + } + }); gtk::main(); Ok(()) diff --git a/examples/example_events.rs b/examples/example_events.rs index 9ef63f223e..1418580eab 100644 --- a/examples/example_events.rs +++ b/examples/example_events.rs @@ -3,7 +3,7 @@ use std::time::{Instant, Duration}; #[derive(woab::Factories)] pub struct Factories { #[factory(extra(buf_count_pressed_time))] - win_app: woab::Factory, + win_app: woab::BuilderFactory, } #[derive(woab::WidgetsFromBuilder)] @@ -88,11 +88,15 @@ fn main() -> Result<(), Box> { gtk::init()?; woab::run_actix_inside_gtk_event_loop("example")?; - factories.win_app.build().actor(|_, widgets| WindowActor { - widgets, - press_times: Default::default(), - total_durations: Default::default(), - })?; + factories.win_app.instantiate() + .new_actor(|ctx| { + ctx.connect_signals::(); + WindowActor { + widgets: ctx.connect_widgets().unwrap(), + press_times: Default::default(), + total_durations: Default::default(), + } + }); gtk::main(); Ok(()) diff --git a/examples/example_taggged_signals.rs b/examples/example_taggged_signals.rs index 4dda442090..691e1563d9 100644 --- a/examples/example_taggged_signals.rs +++ b/examples/example_taggged_signals.rs @@ -3,9 +3,9 @@ use gtk::prelude::*; #[derive(woab::Factories)] pub struct Factories { #[factory(extra(buf_sum))] - win_app: woab::Factory, + win_app: woab::BuilderFactory, #[factory(extra(buf_addend))] - row_addend: woab::Factory<(), AddendWidgets, AddendSignal>, + row_addend: woab::BuilderFactory, } #[derive(woab::WidgetsFromBuilder)] @@ -46,9 +46,9 @@ impl actix::StreamHandler for WindowActor { WindowSignal::ClickButton => { let addend_id = self.next_addend_id; self.next_addend_id += 1; - let widgets = self.factories.row_addend.build() - .connect_tagged_builder_signals(ctx, addend_id) - .widgets().unwrap(); + let mut builder = self.factories.row_addend.instantiate(); + builder.connect_signals_tagged(addend_id, ctx); + let widgets = builder.connect_widgets::().unwrap(); self.widgets.lst_addition.add(&widgets.row_addend); self.addends.insert(addend_id, (widgets, Some(0))); self.recalculate(); @@ -111,12 +111,16 @@ fn main() -> Result<(), Box> { gtk::init()?; woab::run_actix_inside_gtk_event_loop("example")?; - factories.win_app.build().actor(|_, widgets| WindowActor { - widgets, - factories, - next_addend_id: 0, - addends: Default::default(), - })?; + factories.win_app.instantiate() + .new_actor(|ctx| { + ctx.connect_signals::(); + WindowActor { + widgets: ctx.connect_widgets().unwrap(), + factories, + next_addend_id: 0, + addends: Default::default(), + } + }); gtk::main(); Ok(()) diff --git a/macros/src/builder_signal_derive.rs b/macros/src/builder_signal_derive.rs index 5fba9aa397..d3ffc78dab 100644 --- a/macros/src/builder_signal_derive.rs +++ b/macros/src/builder_signal_derive.rs @@ -14,7 +14,7 @@ pub fn impl_builder_signal_derive(ast: &syn::DeriveInput) -> Result Result quote!(#enum_ident::#variant_ident), syn::Fields::Named(_) => return Err(Error::new_spanned(variant, "BuilderSignal only supports unit or tuple variants (even if they are empty)")), }; - Ok(quote! { - #ident_as_str => Some(Box::new(move |args| { - match tx.clone().try_send(#msg_construction) { - Ok(_) => #signal_return_value, - Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { - panic!("Unable to send {} signal - channel is closed", #ident_as_str); - }, - Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { - panic!("Unable to send {} signal - channel is full", #ident_as_str); - }, - } - })), - }) + Ok(( + /* Match arms */ + quote! { + #ident_as_str => Some(Box::new(move |args| { + match tx.clone().try_send(#msg_construction) { + Ok(_) => #signal_return_value, + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + panic!("Unable to send {} signal - channel is closed", #ident_as_str); + }, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + panic!("Unable to send {} signal - channel is full", #ident_as_str); + }, + } + })), + }, + /* Signal names */ + quote! { + #ident_as_str, + }, + )) }).collect::, Error>>()?; + /* We cannot use unzip with error handling, so here's a workaround */ + let (match_arms, signal_names) = vec_of_tuples.into_iter().unzip::<_, _, Vec<_>, Vec<_>>(); Ok(quote! { impl woab::BuilderSignal for #enum_ident { - fn transmit_signal_in_stream_function(signal: &str, tx: tokio::sync::mpsc::Sender) -> Option Option>> { + fn bridge_signal(signal: &str, tx: tokio::sync::mpsc::Sender) -> Option { use tokio::sync::mpsc::error::TrySendError; match signal { #(#match_arms)* _ => None, } } + + fn list_signals() -> &'static [&'static str] { + &[ + #(#signal_names)* + ] + } } }) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index e813c5ba9a..4e57f5304e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -4,7 +4,7 @@ mod factories_derive; mod removable_derive; mod util; -#[proc_macro_derive(WidgetsFromBuilder)] +#[proc_macro_derive(WidgetsFromBuilder, attributes(widget))] pub fn derive_widgets_from_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); match widgets_from_builder_derive::impl_widgets_from_builder_derive(&input) { diff --git a/macros/src/widgets_from_builder_derive.rs b/macros/src/widgets_from_builder_derive.rs index 8c429cdbe8..6b662820bc 100644 --- a/macros/src/widgets_from_builder_derive.rs +++ b/macros/src/widgets_from_builder_derive.rs @@ -1,6 +1,7 @@ use syn::parse::Error; use syn::spanned::Spanned; use quote::quote; +use crate::util::{iter_attrs_parameters, path_to_single_string}; pub fn impl_widgets_from_builder_derive(ast: &syn::DeriveInput) -> Result { let fields = if let syn::Data::Struct(syn::DataStruct { @@ -13,9 +14,31 @@ pub fn impl_widgets_from_builder_derive(ast: &syn::DeriveInput) -> Result { + let value = value.ok_or_else(|| Error::new_spanned(attr_name, "attribute `name` must have a value"))?; + if name.is_some() { + return Err(Error::new_spanned(value, "attribute `name` can only be specified once")); + } + name = Some(value); + }, + _ => { + return Err(Error::new_spanned(attr_name, "unknown attribute")); + }, + } + Ok(()) + })?; + let field_ident = field.ident.as_ref().ok_or_else(|| Error::new(field.span(), "Nameless field"))?; let field_type = &field.ty; - let ident_as_str = syn::LitStr::new(&field_ident.to_string(), field_ident.span()); + let ident_as_str = match name { + Some(syn::Expr::Lit(syn::ExprLit {lit: syn::Lit::Str(name), ..})) => name, + None => syn::LitStr::new(&field_ident.to_string(), field_ident.span()), + _ => return Err(Error::new_spanned(name, "`name` attribute must have a string literal value")), + }; Ok(quote!{ #field_ident: builder.get_object(#ident_as_str).ok_or_else(|| { if let Some(object) = builder.get_object::(#ident_as_str) { @@ -42,5 +65,13 @@ pub fn impl_widgets_from_builder_derive(ast: &syn::DeriveInput) -> Result for #struct_ident { + type Error = woab::Error; + + fn try_from(builder: gtk::Builder) -> Result { + >::try_from(&builder) + } + } }) } diff --git a/src/factories.rs b/src/builder.rs similarity index 53% rename from src/factories.rs rename to src/builder.rs index cba7af6859..fe4eede599 100644 --- a/src/factories.rs +++ b/src/builder.rs @@ -1,11 +1,10 @@ +use std::collections::HashMap; use core::convert::TryInto; use gtk::Builder; use tokio::sync::mpsc; use tokio::stream::StreamExt; -use crate::BuilderSignal; - /// Holds instructions for generating a GTK builder. /// /// ```no_run @@ -38,8 +37,8 @@ impl BuilderFactory { /// Create a `gtk::Builder` from the instructions inside this factory. /// /// Note that "creating a builder" means that the GTK widgets are created (but not yet shown) - pub fn build(&self) -> Builder { - Builder::from_string(&self.0) + pub fn instantiate(&self) -> BuilderConnector { + Builder::from_string(&self.0).into() } } @@ -127,95 +126,107 @@ impl BuilderFactory { /// }).unwrap(); /// } /// ``` -pub struct Factory { - xml: String, - _phantom: std::marker::PhantomData<(A, W, S)>, + +pub struct ActorBuilderContext <'a, A> +where + A: actix::Actor> +{ + builder: &'a mut BuilderConnector, + actor: std::marker::PhantomData, + ctx: &'a mut actix::Context, } -impl From for Factory { - fn from(xml: String) -> Self { - Self { - xml, - _phantom: Default::default(), - } +impl <'a, A> ActorBuilderContext<'a, A> +where + A: actix::Actor> +{ + + pub fn connect_widgets(&self) -> Result>::Error> + where gtk::Builder: TryInto + { + self.builder.connect_widgets() + } + + pub fn connect_signals(&mut self) + where + A: actix::Actor>, + A: actix::StreamHandler, + S: BuilderSignal, + { + self.builder.connect_signals(self.ctx); + } + + pub fn connect_signals_tagged(&mut self, tag: T) + where + T: Clone + 'static, + A: actix::Actor>, + A: actix::StreamHandler<(T, S)>, + S: BuilderSignal, + { + self.builder.connect_signals_tagged(tag, self.ctx); } } -impl Factory { - /// Create the `gtk::Builder` (inside a [`woab::BuilderUtilizer`](struct.BuilderUtilizer.html)) - /// - /// Note that this causes the GTK widgets to be created (but not to be shown or be connected to - /// anything) - pub fn build(&self) -> BuilderUtilizer { - Builder::from_string(&self.xml).into() +impl <'a, A> std::ops::Deref for ActorBuilderContext<'a, A> +where + A: actix::Actor> +{ + type Target = actix::Context; + + fn deref(&self) -> &Self::Target { + &self.ctx + } +} + +impl <'a, A> std::ops::DerefMut for ActorBuilderContext<'a, A> +where + A: actix::Actor> +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ctx } } /// Context for utilizing a `gtk::Builder` and connecting it to he Actix world. +/// +/// It wraps a `gtk::Builder` instance and provides methods to create actors that are +/// connected to the widgets in that builder. /// /// See [`woab::Factory`](struct.Factory.html) for usage example. -pub struct BuilderUtilizer { +pub struct BuilderConnector { builder: gtk::Builder, - _phantom: std::marker::PhantomData<(A, W, S)>, + callbacks: HashMap, } -impl From for BuilderUtilizer { +impl From for BuilderConnector { fn from(builder: gtk::Builder) -> Self { Self { builder, - _phantom: Default::default(), + callbacks: HashMap::new(), } } } -impl BuilderUtilizer -where - for<'a> &'a gtk::Builder: TryInto -{ +impl BuilderConnector { /// Create a widgets struct (as defined by the `W` generic parameter of /// [`woab::Factory`](struct.Factory.html)) and map the builder's widgets to its fields. - pub fn widgets(&self) -> Result>::Error> { - (&self.builder).try_into() - } -} - -impl BuilderUtilizer -where - A: actix::Actor>, - for<'a> &'a gtk::Builder: TryInto, - S: BuilderSignal, - A: actix::StreamHandler -{ - /// Create an Actix actor and connect it to the builder's widgets and signals. - /// - /// `make_actor` is a function that receives the actor context and the widgets, and is - /// responsible for constructing the actor struct with the widgets inside it. It can also be - /// used for configuring and or showing the widgets GTK-wise (though this can also be handled - /// by the actor afterwards) - pub fn actor(&self, make_actor: impl FnOnce(&mut A::Context, W) -> A) -> Result, <>k::Builder as TryInto>::Error> { - let widgets: W = self.widgets()?; - Ok(::create(move |ctx| { - S::connect_builder_signals::(ctx, &self.builder); - make_actor(ctx, widgets) - })) + pub fn connect_widgets(&self) -> Result>::Error> + where gtk::Builder: TryInto + { + self.builder.clone().try_into() } -} -impl BuilderUtilizer -where - S: BuilderSignal, -{ - /// Create a stream (based on Tokio's MSPC) of signals that arrive from the builder. - /// - /// * The signals are all represented by the third generic parameter (`S`) of - /// [`woab::Factory`](struct.Factory.html) - if the builder sends signals not covered by - /// `S`'s variants they'll be ignored. - /// * If the builder defines no signals, or if none of the signals it defines are covered by - /// `S`, this method will return `None`. This is important because otherwise it would have - /// returned a stream stream will be closed automatically for having no transmitters, which - - /// by default - will make Actix close the actor. - pub fn stream_builder_signals(&self) -> Option> { - S::stream_builder_signals(&self.builder) + pub fn connect_signals(&mut self, ctx: &mut actix::Context) + where + A: actix::Actor>, + A: actix::StreamHandler, + S: BuilderSignal, + { + let (tx, rx) = mpsc::channel(16); + for signal in S::list_signals() { + S::bridge_signal(signal, tx.clone()); + } + A::add_stream(rx, ctx); } /// Stream the signals generated by the builder to an actor represented by `ctx`, together with @@ -228,17 +239,105 @@ where /// **If multiple tagged signals are streamed to the same actor - which is the typical use case /// for tagged signals - `StreamHandler::finished` should be overridden to avoid stopping the /// actor when one instance of the widgets is removed!!!** - pub fn connect_tagged_builder_signals(&self, ctx: &mut C, tag: T) -> &Self + pub fn connect_signals_tagged(&mut self, tag: T, ctx: &mut actix::Context) where T: Clone + 'static, - AA: actix::Actor, - C: actix::AsyncContext, - AA: actix::StreamHandler<(T, S)> + A: actix::Actor>, + A: actix::StreamHandler<(T, S)>, + S: BuilderSignal, { - if let Some(rx) = self.stream_builder_signals() { - let stream = rx.map(move |s| (tag.clone(), s)); - ctx.add_stream(stream); + let (tx, rx) = mpsc::channel(16); + let rx = rx.map(move |s| (tag.clone(), s)); + for signal in S::list_signals() { + S::bridge_signal(signal, tx.clone()); } - self + use actix::AsyncContext; + ctx.add_stream(rx); + } + + /// Create an Actix actor and connect it to the builder's widgets and signals. + /// + /// `make_actor` is a function that receives the actor context and the widgets, and is + /// responsible for constructing the actor struct with the widgets inside it. It can also be + /// used for configuring and or showing the widgets GTK-wise (though this can also be handled + /// by the actor afterwards) + pub fn new_actor(&mut self, make_actor: F) -> actix::Addr + where + A: actix::Actor>, + F: FnOnce(&mut ActorBuilderContext) -> A, + { + ::create(move |ctx| { + let mut ctx = ActorBuilderContext { + builder: self, + actor: std::marker::PhantomData, + ctx, + }; + make_actor(&mut ctx) + }) + } + + /// Create a stream of all the signals. + /// + /// Will return `None` if there are no signals, to allow avoiding closed streams. + pub fn finish(self) { + std::mem::drop(self) } } + +impl Drop for BuilderConnector { + fn drop(&mut self) { + use gtk::prelude::BuilderExtManual; + + let callbacks = &mut self.callbacks; + self.builder.connect_signals(move |_, signal| { + callbacks.remove(signal).unwrap_or_else(|| Box::new(|_| None)) + }); + } +} + +/// Type of a gtk signal callback function that operates on uncast glib values +pub type RawSignalCallback = Box Option>; + +pub fn make_signal_handler( + handler_name: &str, + ctx: &mut A::Context, +) -> RawSignalCallback +where + A: actix::Actor>, + A: actix::StreamHandler, + S: BuilderSignal, +{ + let (tx, rx) = mpsc::channel(16); + A::add_stream(rx, ctx); + S::bridge_signal(handler_name, tx) + .ok_or_else(|| format!("Handler '{}' was requested, but only {:?} exist", handler_name, S::list_signals())) + .unwrap() +} + +pub fn connect_signal_handler( + object: &O, + gtk_signal_name: &str, + handler_name: &str, + ctx: &mut A::Context, +) +where + A: actix::Actor>, + A: actix::StreamHandler, + S: BuilderSignal, + O: glib::object::ObjectExt, +{ + let callback = make_signal_handler::(handler_name, ctx); + object.connect_local(gtk_signal_name.as_ref(), false, callback).unwrap(); +} + +/// Represent a GTK signal that originates from a GTK builder. Refer to [the corresponding derive](derive.BuilderSignal.html). +pub trait BuilderSignal: Sized + 'static { + + /// Generate a signal handler function for GTK. + /// + /// The returned function should convert the signals it revceives to the signal type, and + /// transmit them over `tx`. + fn bridge_signal(signal: &str, tx: mpsc::Sender) -> Option; + + fn list_signals() -> &'static [&'static str]; +} diff --git a/src/builder_signal.rs b/src/builder_signal.rs deleted file mode 100644 index 01a9048cda..0000000000 --- a/src/builder_signal.rs +++ /dev/null @@ -1,43 +0,0 @@ -use tokio::sync::mpsc; - -/// Represent a GTK signal that originates from a GTK builder. Refer to [the corresponding derive](derive.BuilderSignal.html). -pub trait BuilderSignal: Sized + 'static { - /// Generate a signal handler function for GTK. - /// - /// The returned function should convert the signals it revceives to the signal type, and - /// transmit them over `tx`. - fn transmit_signal_in_stream_function(signal: &str, tx: mpsc::Sender) -> Option Option>>; - - /// Create a stream of all the signals. - /// - /// Will return `None` if there are no signals, to allow avoiding closed streams. - fn stream_builder_signals(builder: >k::Builder) -> Option> { - use gtk::prelude::BuilderExtManual; - - let (tx, rx) = mpsc::channel(16); - let mut connected_any = false; - builder.connect_signals(|_, signal| { - if let Some(handler) = Self::transmit_signal_in_stream_function(signal, tx.clone()) { - connected_any = true; - handler - } else { - Box::new(|_| None) - } - }); - if connected_any { - Some(rx) - } else { - None - } - } - - /// Connect the signals created from a GTK builder to an actor's context. - fn connect_builder_signals>(ctx: &mut H::Context, builder: >k::Builder) - where ::Context: actix::AsyncContext - { - if let Some(rx) = Self::stream_builder_signals(builder) { - H::add_stream(rx, ctx); - } - } -} - diff --git a/src/lib.rs b/src/lib.rs index 811c322481..a0f330e08e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,9 +99,8 @@ //! ``` mod event_loops_bridge; -mod builder_signal; +mod builder; mod builder_dissect; -mod factories; /// Represent a set of GTK widgets created by a GTK builder. /// @@ -224,9 +223,9 @@ pub use woab_macros::Factories; pub use woab_macros::Removable; pub use event_loops_bridge::run_actix_inside_gtk_event_loop; -pub use builder_signal::BuilderSignal; pub use builder_dissect::dissect_builder_xml; -pub use factories::{BuilderFactory, Factory, BuilderUtilizer}; +// pub use factories::{BuilderFactory, Factory, BuilderUtilizer, BuilderConnector, ActorBuilder, ActorWidgetsBuilder}; +pub use builder::*; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/tests/basic.rs b/tests/basic.rs index cfbbb880db..9324247970 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -7,7 +7,7 @@ mod util; #[derive(woab::Factories)] struct Factories { #[factory(extra(buf_left, buf_right))] - win_test: woab::Factory, + win_test: woab::BuilderFactory, } struct TestActor { @@ -60,10 +60,12 @@ fn test_basic() -> anyhow::Result<()> { gtk::init()?; woab::run_actix_inside_gtk_event_loop("test")?; let mut put_widgets_in = None; - factories.win_test.build().actor(|_, widgets| { - put_widgets_in = Some(widgets.clone()); - TestActor { widgets } - })?; + factories.win_test.instantiate() + .new_actor(|ctx| { + let widgets = ctx.connect_widgets::().unwrap(); + put_widgets_in = Some(widgets.clone()); + TestActor { widgets } + }); let widgets = put_widgets_in.unwrap(); widgets.buf_left.set_text("test left"); wait_for!(get_text(&widgets.buf_right) == "")?; diff --git a/tests/no_signals.rs b/tests/no_signals.rs index 48e3b4f515..56dbf21437 100644 --- a/tests/no_signals.rs +++ b/tests/no_signals.rs @@ -9,7 +9,7 @@ mod util; #[derive(woab::Factories)] struct Factories { - win_test: woab::Factory, + win_test: woab::BuilderFactory, } struct TestActor { @@ -49,9 +49,10 @@ fn test_no_signals() -> anyhow::Result<()> { gtk::init()?; woab::run_actix_inside_gtk_event_loop("test")?; let output = Rc::new(RefCell::new(Vec::new())); - factories.win_test.build().actor(|_, _| { - TestActor { output: output.clone() } - })?; + factories.win_test.instantiate() + .new_actor(|_ctx| { + TestActor { output: output.clone() } + }); wait_for!(*output.borrow() == [ "before spawned future", "inside spawned future",