-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow to build multiple actors from a BuilderUtilizer #4
Conversation
This is a big PR, and I've only skimmed it (maybe I'll have more time to thoughtfully read it during the weekend), but this is what I understood so far:
That's about it? I have three issues with this:
And one more thing - I have a fix for #2 which is going to conflict with this PR. I haven't pushed it yet because I wanted to finish its tests and documentations, but maybe I should push it now so that you can include it in your design. It passes a optional closure ( |
Thanks for looking into this. I'm sorry, I should have written a few words of explanation but I forgot. Regarding you critique points:
In the attempt to improve the API, I'll try my best to collect all usages so far into a list:
Some additional notes:
|
The parsing of the signal arguments needs no state, so maybe we can have a method on the fn signal_parser(signal_name: &str) -> Option<fn(&[Value]) -> Result<Self, woab::Error>>; And use it to build both one-signal parsers and all-signals parsers? I don't think one more dynamic call is going to make that much of a difference here...
But as soon as you call
Maybe I should explain The problem is that GTK builder does not exactly support this style. You can design rows individually in Glade, but then you have no way to programatically create more of them at runtime. So what I do is design one row (for each type of row) and then extract the XMLs of these rows to separate builder XMLs. This I do with the Because its so convenient, I also use it when I have multiple windows - though it's not that necessary there as I can just use multiple .glade files. |
I see. How about connecting the signals in I think I now understand
I have an idea of some typed-builder-pattern-state-machine-thingy that I'm going to try out soon. |
The idea turned out to work out rather well IMO. Some discussion points before I clean this up:
|
src/factories.rs
Outdated
let hooks = self.constructor_hooks; | ||
<A as actix::Actor>::create(move |ctx| { | ||
hooks.into_iter() | ||
.for_each(|hook| hook(builder, ctx)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for hook in hooks {
...
}
Another style we should consider is the closure-with-context style: factories.app_win.make(|bcx| { // bcx stands for "builder context" so that ctx can be used for the actor context
// I wish we could use this style, but it probably won't work because it needs to
// borrow bcx mutably to register the callbacks (unless we use a refcell?)
bcx.actor().with_signals::<Signal1>().make(|ctx| Actor1 {
widgets: bcx.widgets(),
});
// This style is not as pretty but has better chances to pass the borrow checker
woab::actor().with_signals::<Signal2>(bcx).make(|ctx| Actor2 {
widgets: bcx.widgets(),
})
}); |
Wait - maybe we can combine the contexts? factories.app_win.make(|ctx| { // ctx here is BuilderContext
ctx.actor(|ctx| { // ctx here is of type ActorBuilderContext<Actor1> which is DerefMut<Target = Actor::Context>
ctx.connect_signals::<Signal1>();
Actor1 {
widgets: ctx.widgets(),
}
});
}); I'm kind of hoping that Rust will recognize that the inner closure returns an |
I'm not sure I understand what the appealing points of this approach are. (At least it solves the widgets problem rather well I think? Does is solve the For comparison, here's what my code looks like at the moment: builder
.new_actor()
.connect_signals::<LibrarySignal>()
.with_widgets()
.build(|_ctx, widgets| {
let library = futures::executor::block_on(library::Library::load()).unwrap();
LibraryActor {
widgets,
library: Rc::new(library),
}
}).unwrap(); And here the "reuse actor" use case: let mut builder = self.factories.row_addend.instantiate();
builder.connect_signals_tagged(addend_id, ctx);
let widgets = builder.connect_widgets::<AddendWidgets>().unwrap(); |
Actually, now that I think about it, my last two comments are orthogonal. The first one, the one about making a context around the builder utilizer, is mainly about solving the So, if we drop (pun intended) the builder.actor(|ctx| {
ctx.connect_signals::<LibrarySignal>();
let library = futures::executor::block_on(library::Library::load()).unwrap();
LibraryActor {
widgets: ctx.widgets(),
library: Rc::new(library),
}
}); The reuse actor example would look about the same (though I would rather call the widget generation method If we do use the factories.library.make(|ctx| {
ctx.actor(|ctx| {
ctx.connect_signals::<LibrarySignal>();
let library = futures::executor::block_on(library::Library::load()).unwrap();
LibraryActor {
widgets: ctx.widgets(),
library: Rc::new(library),
}
});
}); And actor reuse would look like this: self.factories.row_addend.make(|ctx| {
builder.connect_signals_tagged(addend_id, ctx);
let widgets = builder.connect_widgets::<AddendWidgets>().unwrap();
// Either use `widgets` here, or - since it does not depend on `ctx`, we can just return it and
// have `make` return the value returned from the closure.
}); I'll need to check later if |
Some points before I start giving this a try:
|
Without the error handling (for now), my code looks like this builder.new_actor_2(|ctx| {
ctx.connect_signals::<LibrarySignal>();
let library = futures::executor::block_on(library::Library::load()).unwrap();
LibraryActor {
widgets: ctx.connect_widgets(),
library: Rc::new(library),
}
}); and I like it. The trick with I won't try doing the outer closure, because the registration in the The key problem is that |
How did you manage to not pass
How about we provide both
I think we can both agree there are no great solutions for this and we have to settle for a good enough solution. I can live with registering everything in the
I thought about this too and disqualified it for the same reason. |
Honestly, I don't know. It just … worked? I've had a look at your gist and it has a lot of subtle differences in the type signatures to my code. I tried quite a few things, but none of them made the compile error go away :(
Yes, and also |
I think this PR is starting to get big, and already has enough content, so let's start cutting some things out. We'll get them in in other PRs - maybe even for 0.2! - but this is a big change that needs to get in. Especially since all the other tickets you've opened are going to be solved in this new style, so neither of us can work on them before this refactor is completed.
This is an amazing refactor. Thank you for putting all this time into this project! |
STOP THE PRESS!!! I think one of the core problems is that in order to use Actix' context when builder the actor we had to use And then you mentioned error handling, and I said it's a problem because let (_, rx) = actix::dev::channel::channel(16);
let ctx = Context::<MyActor>::with_receiver(rx);
ctx.run(MyActor) And this can be used for error handling, because you don't have to call Here is a PoC for this syntax: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8926f3e787bc0bc59c60ab858e8bc6db I'll copy the fn main() {
let builder = BuilderUtilizer::default();
builder
.actor()
.connect_signals(Signal1::connector())
.connect_signals(Signal2::connector())
.run(Actor1 {
widgets: builder.widgets().unwrap(),
});
builder
.actor()
.connect_signals(Signal3::connector())
.connect_signals(Signal4::connector())
.run(Actor2 {
widgets: builder.widgets().unwrap(),
});
BuilderUtilizer::default()
.actor::<Actor3>()
.connect_signals(Signal1::connector())
.connect_signals(Signal4::connector())
.create(|ctx| Actor3 {
my_own_address: ctx.address(),
widgets: ctx.widgets().unwrap(),
});
} (for Notice that for
Which is kind of ugly. |
/me was about to push 😅 |
Sorry for always changing the syntax... But this is a good brainstorm. I don't think #8 should be implemented here - this is already too much. It's enough, for this PR, to only implement builder
.actor()
.connect_signals::<Signal1>()
.connect_signals_with_tags::<Signal2>(some_tag) And I'll do #8 in a later PR. Or maybe even not implement |
So, consider the current state as a checkpoint—it has an API that is really usable. Please have a look at it, if there are mistakes or other things that should be changed. Regarding your updated builder API proposal: what are the benefits and downsides compared to the current way of doing so? (Apart from potentially better error handling; but actually I'm fine with panicking there because I don't think there are many situations where one can recover from such an error.) |
Very well. I'll revert 79abc12 and start reviewing this PR.
Let's continue this discussion in #9. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some comments, but these are things that I'm going to change anyways with #9.
} | ||
}).unwrap(); | ||
let addend = self.factories.row_addend.instantiate() | ||
.new_actor(|builder_ctx| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ctx.connect_signals::<AddendSignal>();
.connect_tagged_builder_signals(ctx, addend_id) | ||
.widgets().unwrap(); | ||
let mut builder = self.factories.row_addend.instantiate(); | ||
builder.connect_signals_tagged(addend_id, ctx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess Rust can deduce that this does not need ::<AddendSignal>
, but it's probably a good idea to add it anyway...
S::connect_builder_signals::<A>(ctx, &self.builder); | ||
make_actor(ctx, widgets) | ||
})) | ||
pub fn connect_widgets<W>(&self) -> Result<W, <gtk::Builder as TryInto<W>>::Error> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method's name should just be widgets
. We are not really connecting anything here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are creating references to an instantiated builder and putting them into a new struct. It's not totally "connecting", but I don't know a better word and I liked the symmetry to the signals methods.
It may be a left-over from my Java time, but I really dislike method names that don't have a verb in them (except maybe if they're a simple getter).
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())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not a good idea to render a list of all the signals. We don't know how long it'll be. It's better to use std::any::type_name<S>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it because it was useful for debugging. But maybe we cut it if in the case it exceeds a specific length?
/// transmit them over `tx`. | ||
fn bridge_signal(signal: &str, tx: mpsc::Sender<Self>) -> Option<RawSignalCallback>; | ||
|
||
fn list_signals() -> &'static [&'static str]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this be a const
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not into the const
game. The second one probably yes, the first one maybe?
Well, that was a bit too quick ^^ I made a small mistake that breaks everything. Here's the patch: From 9079d86afd1dc5d0a069044ca7dae64713738ed1 Mon Sep 17 00:00:00 2001
From: piegames <[email protected]>
Date: Sat, 6 Feb 2021 13:08:16 +0100
Subject: [PATCH] Fix small bug
---
src/builder.rs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/builder.rs b/src/builder.rs
index fe4eede5..f37a80c8 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -195,7 +195,7 @@ where
/// See [`woab::Factory`](struct.Factory.html) for usage example.
pub struct BuilderConnector {
builder: gtk::Builder,
- callbacks: HashMap<String, RawSignalCallback>,
+ callbacks: HashMap<&'static str, RawSignalCallback>,
}
impl From<gtk::Builder> for BuilderConnector {
@@ -224,7 +224,7 @@ impl BuilderConnector {
{
let (tx, rx) = mpsc::channel(16);
for signal in S::list_signals() {
- S::bridge_signal(signal, tx.clone());
+ self.callbacks.insert(signal, S::bridge_signal(signal, tx.clone()).unwrap());
}
A::add_stream(rx, ctx);
}
@@ -249,7 +249,7 @@ impl BuilderConnector {
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.callbacks.insert(signal, S::bridge_signal(signal, tx.clone()).unwrap());
}
use actix::AsyncContext;
ctx.add_stream(rx);
--
2.29.2 |
I had that idea in mind that matched my use case pretty well, but it somewhat clashes with other usage styles of the
BuilderUtilizer
. I got rid of all compile error except one (I think), and didn't yet bother to clean things up or document.Closes #3.