-
Notifications
You must be signed in to change notification settings - Fork 121
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
Rewrite xilem_web to support new xilem_core #403
Conversation
…n between actual attributes
… using boilerplatey functions
…cause of type-checking
Just to clarify, as I think there was already some misunderstanding, this is only true for the new (unfinished) trait-solver, not for the current one. The todomvc example takes without the new changes explained below, about 5s to compile in release mode vs 0.75s with current xilem_web. It'll be interesting to see how the new trait-solver will handle this, when it's finished, maybe we can convert it back to being completely static/stack-based again then (wishful thinking...). So indeed the idea of type-erasing the I do think it's worth it to have this kind of regression, with that we are roughly at the same compilation times as before and IMO the other advantages (disregarding the actual port to new xilem_core) outweigh the slight regression. I also think that it (the compile times) should scale this way, as the static type composition will not grow "infinitely". With incremental compilation, probably also big apps will compile somewhat fast. But that needs to be proven with bigger apps than the todomvc example... I've also started documenting xilem_web. So every item directly visible at in docs.rs should now at least hint what's going on. |
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.
So I think this is now in a good state for reviewing, I added more documentation (including simple doc tests in the interfaces, to test whether doc is up-to-date), it should now cover the more important parts of the architecture/API.
I have also added a new example (blog
), which shows at least one thing that wasn't possible before (extending memoized elements).
The last things this is depending on is #402 and the new blog
example depends also on #175, so after these are merged, this would be ready to merge as well (I'll do a merge from main into this branch before though).
As this is quite a big PR, and I doubt someone has the time or will to dive through all of this in detail while reviewing,
I'll give some hints via code-comments to the more interesting, and possibly discussion worthy things of it, as I still think this may be a reasonable direction for Xilem itself as well.
I guess, when there's use for it, I could also add some modified version of the comments as document such as ARCHITECTURE.md
or something.
// #[cfg(feature = "HtmlAnchorElement")] | ||
pub trait HtmlAnchorElement<State, Action = ()>: | ||
HtmlElement<State, Action, DomNode: AsRef<web_sys::HtmlAnchorElement>> | ||
{ | ||
} |
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 whole file is mostly generated (with a local hacky webidl based generator).
I have avoided macros because I expect this to be continuously extended overtime, and as most of us, I'm not a fan of macros, as I think DX suffers.
I've added all these commented out feature gates because I think, when some of the element props are extended (e.g. a video element has then VideoElementProps
which composes ElementProps
+ additional stuff relevant to the video element), that inevitably the wasm binary size will grow, if we don't constrain this with features.
Because I have not yet found a way that is reasonable and robust at least to do this just with static typing, such that these additional props are optimized away when they're not needed.
I don't think it's a good idea to let all the users suffer when they don't need such features.
Currently this is not necessary because as seen below, all is basically depending on the ElementProps
and I think the Element
interface/trait should be included by default.
/// A string representing the value of the HTMLOptionElement, i.e. the value attribute of the equivalent `<option>`. | ||
/// If this is not specified, the value of text is used as the value, e.g. for the associated `<select>` element's value when the form is submitted to the server. | ||
/// | ||
/// See <https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/value> for more details | ||
fn value(self, value: impl IntoAttributeValue) -> Attr<Self, State, Action> { | ||
Attr::new(self, Cow::from("value"), value.into_attr_value()) | ||
} |
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.
My vision of the API is obviously builder/modifier-pattern like as seen with all these DOM interface extension traits, and the examples.
So I added all these attributes (seen in the blog
example, which doesn't use any Element::attr
) as methods, currently just sugaring the Attr
view.
These are of course not complete, I only added the attributes needed in the blog
example, and added MDN docs on top of some.
I expect this to be extended over time (together with the feature gates explained above).
impl WithAttributes for Attributes { | ||
fn set_attribute(&mut self, name: CowStr, value: Option<AttributeValue>) { | ||
let new_modifier = if let Some(value) = value { | ||
AttributeModifier::Set(name.clone(), value) | ||
} else { | ||
AttributeModifier::Remove(name.clone()) | ||
}; | ||
|
||
if let Some(modifier) = self.attribute_modifiers.get_mut(self.idx) { | ||
if modifier != &new_modifier { | ||
if let AttributeModifier::Remove(previous_name) | ||
| AttributeModifier::Set(previous_name, _) = modifier | ||
{ | ||
if &name != previous_name { | ||
self.updated_attributes.insert(previous_name.clone(), ()); | ||
} | ||
} | ||
self.updated_attributes.insert(name, ()); | ||
*modifier = new_modifier; | ||
} | ||
// else remove it out of updated_attributes? (because previous attributes are overwritten) not sure if worth it because potentially worse perf | ||
} else { | ||
self.updated_attributes.insert(name, ()); | ||
self.attribute_modifiers.push(new_modifier); | ||
} | ||
self.idx += 1; | ||
} | ||
|
||
fn start_attribute_modifier(&mut self) { | ||
if self.build_finished { | ||
if self.idx == 0 { | ||
self.start_idx = 0; | ||
} else { | ||
let AttributeModifier::EndMarker(start_idx) = | ||
self.attribute_modifiers[self.idx - 1] | ||
else { | ||
unreachable!("this should not happen, as either `start_attribute_modifier` happens first, or follows an end_attribute_modifier") | ||
}; | ||
self.idx = start_idx; | ||
self.start_idx = start_idx; | ||
} | ||
} | ||
} | ||
|
||
fn end_attribute_modifier(&mut self) { | ||
match self.attribute_modifiers.get_mut(self.idx) { | ||
Some(AttributeModifier::EndMarker(prev_start_idx)) | ||
if *prev_start_idx == self.start_idx => {} // class modifier hasn't changed | ||
Some(modifier) => { | ||
*modifier = AttributeModifier::EndMarker(self.start_idx); | ||
} | ||
None => { | ||
self.attribute_modifiers | ||
.push(AttributeModifier::EndMarker(self.start_idx)); | ||
} | ||
} | ||
self.idx += 1; | ||
self.start_idx = self.idx; | ||
} | ||
} |
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 is one of the more interesting parts of this PR, a modifier based system (Styles
, Classes
and this Attributes
), which is designed in such a way, that Memoize
or Arc
doesn't offer weird surprises (as well as just overwriting attributes, when they're previously set, which would have problems, when just directly setting attributes on the DOM elements).
As well as OneOf
views being able to intersect on the elements and their props in a strong/statically-typed way. For example when there is OneOf2::A(html::div(()))
and OneOf2::B(mathml::mrow(())
, this OneOf2
view fulfills the contract of the Element
trait, but not of the HtmlElement
, because the trait bound AsRef<HtmlElement>
is not satisfied for mathml::mrow
, so e.g. HtmlElement::style
is not possible to use (currently at least, because MathML should be able to support style, this needs extension in wasm_bindgen I think.), but all the methods of Element
are possible on mathml::mrow
as well.
The modifier system works in such a way, that it has stateful start
and end
methods, which add markers into a Vec
for the modifier type on its props to know where the modifier view has started/ended.
Additional modifiers for this property (e.g. AttributeModifier::Set
or AttributeModifier::Remove
) are then pushed into this Vec
until the next modifier starts.
All of this happens in either View::build
as well as View::rebuild
, and these attributes are added on every reconciliation (i.e. View::rebuild
), so that it can detect changes (so all these attributes should be added always in the same order, to avoid unnecessary updates).
On each reconciliation pass, it is checked, whether the attribute at the same index has changed, and if so, the relevant props or attributes are marked dirty, such that they're correctly updated when applying the props to the actual DOM element.
This should be robust to underlying element changes in e.g. a OneOf
which would build the new view, while being in a View::rebuild
of a composing modifier view, as the actual modifiers have to be applied on the up-traversal and only the end
method actually tells via an EndMarker
where the modifier has started (which is read by the start
method on the down traversal, to jump to the next modifier).
All the possible accumulated changes are then applied when SuperElement::upcast
ing the element, which usually happens on a ViewSequence
boundary.
These modifiers are described via traits such as (currently), WithAttributes
, WithClasses
and WithStyles
and are also working with the intersection of OneOf
types.
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've not done much review, but the name of this example is concerning. If you're doing a blog, you really should be doing server side rendering, imo.
I'm not sure though, I think it's good to get another sizeable example.
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 open for alternative suggestions, could also be something like "news" or what I'm probably planning is a simple hackernews implementation. I actually want to revisit the hydration support I've experimented with quite a long time ago. So I think in the future this likely will be extended with SSR (I think it's reasonable to support SSR in xilem_web isn't it?).
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.
Yeah, SSR and hydration in Xilem Web would be incredible and is something we should support sooner rather than later, if possible.
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 think reframing it in terms of e.g. a "ticker", so having the concept that the feed will be live-updating relatively frequently might be good enough. I don't want anyone getting the idea that we want people to be using Xilem Web as-is for a blog.
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.
Yeah you're probably right, a blog is not the most interactive example and should mostly be SSR, at least short term this would be misleading. I'll update that example as you said, to something that is more interactive.
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 was too lazy to add another example, removed it for now, I guess this somewhat "fixes" the issue. I may revisit it someday later though.
…ail, the API requires an `impl ViewSequence` instead (which is boxed) for the element builder functions
…ix doc comment issue
All of the dependent PRs are merged, it got another minor cleanup round, I think it's ready to merge now. |
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 not reviewed any of the actual logic here, but found a few things on a very quick skim
xilem_web/README.md
Outdated
@@ -1,9 +1,32 @@ | |||
# `xilem_web` prototype | |||
# `xilem_web` |
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 could plausibly follow the usual readme patterns here, but that probably isn't a job for this pr
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.
You're right, I've copied a lot of the Xilem Core Readme and actually used the same pattern to include it in the lib.rs
now, you probably want to look quickly at it again before I merge.
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'll update both once this is merged. Thanks!
|
||
/// The type responsible for running your app. | ||
pub struct App<T, V: View<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>); | ||
pub struct App<T, V: DomView<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>); |
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'd obviously much prefer this avoided interior mutability, but I'll assume it's necessary - I don't have enough context to know whether it's avoidable
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.
That's actually one of the few things from the old code-base that I haven't paid too much attention to, nor written it myself. But I do think it's necessary, since the App
itself is "forgotten" to avoid blocking the event-loop, and just lives on within the view context (and message thunks).
xilem_web/src/message.rs
Outdated
/// A dynamically typed message for the [`View`] trait. | ||
/// | ||
/// Mostly equivalent to `Box<dyn Any>`, but with support for debug printing. | ||
// We can't use intra-doc links here because of |
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 I should fix this sentence fragment in both places...
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.
Yeah, I'm not really sure what you mean with it^^ (I suspect it has something to do with dyn
...)
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.
Yeah; it's the dyn Message
. Rustdoc doesn't seem to have a syntax to create an intra-doc-link to this item
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.
A few tweaks on the comment feedback, but otherwise we should be good to land!
xilem_web/src/message.rs
Outdated
/// A dynamically typed message for the [`View`] trait. | ||
/// | ||
/// Mostly equivalent to `Box<dyn Any>`, but with support for debug printing. | ||
// We can't use intra-doc links here because of |
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.
Yeah; it's the dyn Message
. Rustdoc doesn't seem to have a syntax to create an intra-doc-link to this item
</style> | ||
|
||
<!-- Hide the header section of the README when using rustdoc --> | ||
<div style=\"display:none\"> |
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 format of the README is out-of-date as compared to linebender/linebender.github.io#55
But I'll update both of them later, so this is still fine
…ociated type (#619) This was the initial plan of #403, I can't remember anymore why I didn't made it to work back then. Anyway this should simplify the type signature of `Pod` and `PodMut` a little bit. This also includes a little bit of refactoring otherwise (mostly aesthetic + removal of unnecessary wrapper `DynNode`)
This ports xilem_web to the new xilem_core.
There's also a lot of cleanup internally:
View::Element
.Arc
,Memoize
) andOneOf
views with an intersection of the modifiable properties.WidgetMut
type to apply changes.Attributes
,Classes
andStyles
to reflect what xilem_web previously offered.Downsides (currently, needs some investigation):
Due to more internal type complexity via associated types this suffers from rust-lang/rust#105900. The new trait solver should hopefully mitigate some of that, but it seems currently it completely stalls in the todomvc example (not in other examples).The deep, possibly completely static composition via associated type-bounds of the view and element tree unfortunately can take some time to compile, this gets (already) obvious in the todomvc example. The other examples don't seem to suffer that bad yet from that issue, probably because they're quite simple.I really hope we can mitigate this mostly, because I think this is the idiomatic (and more correct) way to implement what the previous API has offered.One idea is to add a
Box<dyn AnyViewSequence>
, as every element takes a "type-erased"ViewSequence
as parameter, so this may solve most of the issues (at the slight cost of dynamic dispatch/allocations).Edit: idea was mostly successful, see comment right below.
I think it also closes #274
It's a draft, as there's a lot of changes in xilem_core that should be upstreamed (and cleaned up) via separate PRs and I would like to (mostly) fix the slow-compile time issue.