Skip to content
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

Move xilem onto a new xilem_core, which uses a generic View trait #310

Merged
merged 45 commits into from
Jun 6, 2024

Conversation

DJMcNab
Copy link
Member

@DJMcNab DJMcNab commented May 20, 2024

This:

  1. Renames the current/old xilem_core to xilem_web_core and moves it to the xilem_web/xilem_web_core folder
  2. Creates a new xilem_core, which does not use (non-tuple) macros and instead contains a View trait which is generic over the Context type
  3. Ports xilem to this xilem_core, but with some functionality missing (namely a few of the extra views; I expect these to straightforward to port)
  4. Ports the mason and mason_android examples to this new xilem, with less functionality.

This continues ideas first explored in #235

The advantages of this new View trait are:

  1. Improved support for ad-hoc views, such as views with additional attributes.
    This will be very useful for layout algorithms, and will also enable native good multi-window (and potentially menus?)
  2. A lack of macros, to better enable using go-to-definition and other IDE features on the traits

Possible disadvantages:

  1. There are a few more traits to enable the flexibility
  2. It can be less clear what Self::Element::Mut is in the rebuild function, because of how the resolution works
  3. When implementing View, you need to specify the context (i.e. impl<State, Action> View<State, Action, [new] ViewCtx> for Button<State, Action>.

@DJMcNab DJMcNab marked this pull request as ready for review May 24, 2024 16:33
@DJMcNab DJMcNab changed the title Investigation into a more generic view trait Move xilem onto a new xilem_core, which uses a generic View trait May 24, 2024
Copy link
Member Author

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts which might be helpful for reviewers

let ret = f(child);
self.element.remove_child(self.ix);
// Also remove the implicit spacer
// TODO: Make the spacers be explicit?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want two forms of this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards only one, and more explicit, we may want some good helper views/types then probably.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably be a follow-up PR

/// state remained the same. It is expected that this type won't be used very
/// often.
#[allow(unused)]
/// This allows for sub-sections of your app to use an elm-like architecture
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How useful is this really?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet about it exactly, but I guess it (also) allows to modularize views, possibly in external libraries. Maybe we don't need it, when we have found a satisfying solution for local/modularized state.

MessageResult::Stale(event) => MessageResult::Stale(event),
MessageResult::Nop => MessageResult::Nop,
}
impl<T> Message for T
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has some footguns around e.g. Box(Message) also implementing these same methods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's indeed suboptimal...

Having Message as generic View param has certainly some advantages, since I have the feeling that View will not really be used directly unless for implementing it for types maybe worth exploring? But yeah, yet another additional generic parameter...

@@ -1,363 +1,650 @@
// Copyright 2023 the Xilem Authors
// Copyright 2024 the Xilem Authors
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about these year changes; they're mostly accidental

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean most of the code here is rewritten, so 2024 makes sense? But maybe we also want to have year-ranges to be more accurate.

Copy link
Contributor

@Philipp-M Philipp-M left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a first round of comments, I'll go deeper into this later.

/// state remained the same. It is expected that this type won't be used very
/// often.
#[allow(unused)]
/// This allows for sub-sections of your app to use an elm-like architecture
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet about it exactly, but I guess it (also) allows to modularize views, possibly in external libraries. Maybe we don't need it, when we have found a satisfying solution for local/modularized state.

@@ -1,363 +1,650 @@
// Copyright 2023 the Xilem Authors
// Copyright 2024 the Xilem Authors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean most of the code here is rewritten, so 2024 makes sense? But maybe we also want to have year-ranges to be more accurate.

prev: &Self,
seq_state: &mut Self::SeqState,
ctx: &mut Context,
elements: &mut impl ElementSplice<Element>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this makes the trait "object-unsafe".
Just noting, I see ElementSplice is already not object-safe, I remember working around that, to make it possible to use dyn, but maybe it's not necessary to have this object-safe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - it would be nice to have an AnyViewSequence (and we'll probably need it for hot reloading?), but it should still be a relatively straightforward adaptation.

PoignardAzur
PoignardAzur previously approved these changes May 30, 2024
Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we've really nailed "React architecture in Rust" yet, but this PR gets us one step closer.

@DJMcNab DJMcNab dismissed PoignardAzur’s stale review May 31, 2024 14:00

Blocking merge on tests; will need review again once those are added

Copy link
Contributor

@Philipp-M Philipp-M left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phew... not the easiest to digest code, but indeed better than #235 (when it can cover the functionality introduced there without the reborrowing)
I do think the added complexity is worth it though to avoid the macros and being more flexible with different view contexts.
And it seems, that not every contributor of xilem does need to understand all this code for (non-core) contributions...
I hope, that due to all the added typing, composing views will not let the compiler explode (in compilation time mostly).

Great, well-thought work!

I don't think we've really nailed "React architecture in Rust" yet

I'm not sure we even want that, having to deal with a lot of React ugliness in my job currently (e.g. useEffect or magic (unwanted) reconciliations).
I'm not the biggest fan of distributed "reactive" state as it's done in React (nor the "centralized state" solutions that exist there)...
We still want to find a satisfying and simple solution for modularized views and state anyway, to support convenient library crates for (composed?) views. (but that's orthogonal to this PR)

let ret = f(child);
self.element.remove_child(self.ix);
// Also remove the implicit spacer
// TODO: Make the spacers be explicit?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards only one, and more explicit, we may want some good helper views/types then probably.

MessageResult::Stale(event) => MessageResult::Stale(event),
MessageResult::Nop => MessageResult::Nop,
}
impl<T> Message for T
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's indeed suboptimal...

Having Message as generic View param has certainly some advantages, since I have the feeling that View will not really be used directly unless for implementing it for types maybe worth exploring? But yeah, yet another additional generic parameter...

Comment on lines +35 to +36
/// In order to support the open-ended [`DynMessage`] type, this trait requires an
/// allocator to be available.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason to make it generic then I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allocator story isn't great, but a lot of views need it - i.e. sequences and AnyView

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, in that case we may want to wait for Allocator traits etc. anyways before committing to something like that.
I don't think there are good ways for these cases without allocators?

#[doc(hidden)]
pub struct WasAView;

impl<State, Action, Context, V, Element> ViewSequence<State, Action, Context, Element, WasAView>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl<State, Action, Context, V, Element> ViewSequence<State, Action, Context, Element, WasAView>
impl<State, Action, Context, V, Element> ViewSequence<State, Action, Context, Element, fn() -> WasAView>

Otherwise it's e.g. not possible to Clone a ViewSequence (I just ran into that).
Alternatively we could also do

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Copy)]
pub struct WasAView;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me how this impacts Clone. Can you explain please?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider all the views would implement Clone (which I have tested with either xilem_web and xilem right now, they currently do not, probably something for a follow-up PR, because I don't see a reason against implementing all these derive traits).

This doesn't work without this addition:

fn my_clonable_view() -> impl WidgetView<()> + Clone {
    flex((button(...), button(...)))
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, so it's a perfect derive issue?
I'd say that you should just implement Clone for Flex manually; it's a logical mistake that Flex: Clone would require Marker: Clone, since that implementation is never used - working around it here isn't very principled.

That the same issue arises with Debug is unfortunate, and was a hazard I didn't see before. Overall, I'm happy to add the Debug derive here, but I'm going to push back on the fixes to make Clone work like that here.

Comment on lines 104 to 105
/// Insert a new element at the current index in the resulting collection.
fn push(&mut self, element: Element);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this, when there's with_scratch (which I'm not super happy with the naming, maybe it should be called append or this should be the new push instead)?
If we do keep this, it should either push at the end (this was the behavior before IIRC) or be called insert as the doc comment already implies IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's needed to avoid additional e.g. allocations when they're not needed.

It's also a bit simpler to use. Although really, manual implementation of ViewSequence seem so unlikely that it wouldn't be unreasonable to remove this

// The `View` trait could have been made generic over the message type,
// primarily to enable flexibility around Send/Sync and avoid the need
// for allocation.
pub trait Message: 'static + Send {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently running into the issue while rewriting xilem_web event listeners, that this requires Send. web_sys::Event doesn't implement Send.

So the Send bound should be optional (another reason to make it generic?).

@Philipp-M
Copy link
Contributor

Hmmm, I just discovered one major issue because of the orphan rule (in hindsight it's obvious):
The trait can not be implemented for foreign types in downstream crates, and I think this is somewhat a dealbreaker for nice API design, following e.g. is not possible:

impl<State, Action> View<State, Action, ViewCtx> for &'static str {..}

@Philipp-M
Copy link
Contributor

The trait can not be implemented for foreign types in downstream crates

I mean I'm honest, I really don't like it, as it convolutes everything with wrapper types such as text("text") or number(123), but I guess it will be difficult to have different implementations in one project anyway (e.g. we have a masonry text node and a web text node and want to use it in the same project (after a potential merger of those crates), e.g. in an embedded web-view).

I'm asking myself, if there's a general way to provide such an implementation in core and have different contexts define which implementation/element is used or something like that... But I'm not sure if that's possible... That makes this even more relevant

@DJMcNab
Copy link
Member Author

DJMcNab commented Jun 3, 2024

Those general implementations can be added, as shown in DJMcNab#4

They do lead to some jankiness however. In particular, you can't have:

fn test()->impl WidgetView<...> {
    "my string"
}

because of the new required marker

DJMcNab and others added 4 commits June 3, 2024 16:24
* xilem: Port rest of examples and add `Memoize` to `xilem_core`

* cargo fmt

* Address review comments

* Add License header
DJMcNab and others added 11 commits June 3, 2024 16:34
…iewElement::Mut`

It's not possible to modify the element otherwise, after descendent `View`s have used the element in `View::rebuild`
Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
xilem_core: return element in `View::rebuild`
Copy link
Contributor

@Philipp-M Philipp-M left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, certainly a big improvement in code quality.
Nice job also with all those exhaustive tests (seems like they surfaced some issues as well?)

@@ -17,6 +17,8 @@ pub enum MessageResult<Action> {
/// This allows for sub-sections of your app to use an elm-like architecture
Action(Action),
// TODO: What does this mean?
/// This message's handler needs a rebuild to happen.
/// The exact semantics of this method haven't been determined.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it has been, but that was when we had async in xilem, it was used when a future has completed, sent a message and in View::message a rebuild was requested (e.g. when a list view entry in the async list has finished loading)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But my question is how is that different from the implied result of an action handler at the root?

The semantics here are clearly not very principled

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean via MessageResult:Action?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, exactly.

@DJMcNab DJMcNab added this pull request to the merge queue Jun 6, 2024
Merged via the queue into linebender:main with commit 86d9592 Jun 6, 2024
8 checks passed
@DJMcNab DJMcNab deleted the core-two branch August 5, 2024 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants