Skip to content
This repository has been archived by the owner on Mar 2, 2021. It is now read-only.

Bump-Scoped rendering and SVG class attribute fix #136

Merged
merged 10 commits into from
Feb 24, 2020
8 changes: 4 additions & 4 deletions benches/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ use std::convert::TryInto;

/// The simplest thing we can render: `<div/>`.
struct Empty;
impl Render for Empty {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Empty {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
div(&cx).finish()
}
}

/// Render a list that is `self.0` items long, has attributes and listeners.
struct SimpleList(usize);
impl Render for SimpleList {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for SimpleList {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
let mut children = bumpalo::collections::Vec::with_capacity_in(self.0, cx.bump);
children.extend((0..self.0).map(|_| {
li(&cx)
Expand Down
8 changes: 4 additions & 4 deletions crates/js-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ impl GreetingViaJs {

/// And finally the `Render` implementation! This adds a `<p>` element and some
/// text around whatever the inner JS `Greeting` component renders.
impl Render for GreetingViaJs {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for GreetingViaJs {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;
p(&cx)
.children([
Expand Down Expand Up @@ -216,8 +216,8 @@ impl JsRender {
}
}

impl Render for JsRender {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for JsRender {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
create(cx, self.render())
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/counter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ impl Counter {

// The `Render` implementation for `Counter`s displays the current count and has
// buttons to increment and decrement the count.
impl Render for Counter {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Counter {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

// Stringify the count as a bump-allocated string.
Expand Down
4 changes: 2 additions & 2 deletions examples/game-of-life/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ impl Universe {
}

/// The rendering implementation for our Game of Life.
impl Render for Universe {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Universe {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, cx.bump);
Expand Down
4 changes: 2 additions & 2 deletions examples/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ struct Hello {

// The `Render` implementation describes how to render a `Hello` component into
// HTML.
impl Render for Hello {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Hello {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
let msg = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
let msg = msg.into_bump_str();
p(&cx).children([text(msg)]).finish()
Expand Down
4 changes: 2 additions & 2 deletions examples/input-form/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ impl SayHelloTo {

// The `Render` implementation has a text `<input>` and a `<div>` that shows a
// greeting to the `<input>`'s value.
impl Render for SayHelloTo {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for SayHelloTo {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

div(&cx)
Expand Down
4 changes: 2 additions & 2 deletions examples/js-component/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ impl GreetingViaJs {

/// Here's the `Render` implementation! This adds a `<p>` element and some text
/// around whatever the inner JS `Greeting` component renders.
impl Render for GreetingViaJs {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for GreetingViaJs {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;
p(&cx)
.children([text("JavaScript says: "), self.js.render(cx)])
Expand Down
4 changes: 2 additions & 2 deletions examples/moire/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ impl Moire {
}
}

impl Render for Moire {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Moire {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

let elapsed = web_sys::window()
Expand Down
4 changes: 2 additions & 2 deletions examples/sierpinski-triangle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ impl Container {
}
}

impl Render for Container {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Container {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::div;

let elapsed = web_sys::window()
Expand Down
4 changes: 2 additions & 2 deletions examples/todomvc/src/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ impl<C> Todo<C> {
}
}

impl<C: TodoActions> Render for Todo<C> {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a, C: TodoActions> Render<'a> for Todo<C> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::{
builder::*,
bumpalo::{self, collections::String},
Expand Down
4 changes: 2 additions & 2 deletions examples/todomvc/src/todos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ impl<C: TodosActions> Todos<C> {
}
}

impl<C: TodosActions> Render for Todos<C> {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a, C: TodosActions> Render<'a> for Todos<C> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

div(&cx)
Expand Down
14 changes: 7 additions & 7 deletions src/cached.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ where
/// count: u32,
/// }
///
/// impl Render for Counter {
/// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// impl<'a> Render<'a> for Counter {
/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// // ...
/// # unimplemented!()
/// }
Expand Down Expand Up @@ -85,8 +85,8 @@ where
/// who: String
/// }
///
/// impl Render for Hello {
/// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// impl<'a> Render<'a> for Hello {
/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// use dodrio::builder::*;
/// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
/// p(&cx)
Expand Down Expand Up @@ -143,11 +143,11 @@ where
}
}

impl<R> Render for Cached<R>
impl<'a, R> Render<'a> for Cached<R>
where
R: 'static + Default + Render,
R: 'static + Default + for<'b> Render<'b>,
{
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
let template = cx.template::<R>();
let cached = match self.cached.get() {
// This does-the-cache-contain-this-id check is necessary because
Expand Down
4 changes: 2 additions & 2 deletions src/change_list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,9 @@ impl ChangeListBuilder<'_> {
self.state.emitter.replace_with();
}

pub fn set_attribute(&mut self, name: &str, value: &str) {
pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
debug_assert!(self.traversal_is_committed());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's where the SVG change begins and ends. I believe it's just this one if statement.

if name == "class" {
if name == "class" && !is_namespaced {
let class_id = self.ensure_string(value);
debug!("emit: set_class({:?})", value);
self.state.emitter.set_class(class_id.into());
Expand Down
12 changes: 6 additions & 6 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub(crate) fn diff(
return;
}
diff_listeners(change_list, registry, old_listeners, new_listeners);
diff_attributes(change_list, old_attributes, new_attributes);
diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some());
diff_children(
cached_set,
change_list,
Expand Down Expand Up @@ -204,26 +204,26 @@ fn diff_listeners(
// [... node]
//
// The change list stack is left unchanged.
fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) {
fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) {
// Do O(n^2) passes to add/update and remove attributes, since
// there are almost always very few attributes.
'outer: for new_attr in new {
if new_attr.is_volatile() {
change_list.commit_traversal();
change_list.set_attribute(new_attr.name, new_attr.value);
change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
} else {
for old_attr in old {
if old_attr.name == new_attr.name {
if old_attr.value != new_attr.value {
change_list.commit_traversal();
change_list.set_attribute(new_attr.name, new_attr.value);
change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
}
continue 'outer;
}
}

change_list.commit_traversal();
change_list.set_attribute(new_attr.name, new_attr.value);
change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
}
}

Expand Down Expand Up @@ -953,7 +953,7 @@ fn create(
}

for attr in attributes {
change_list.set_attribute(&attr.name, &attr.value);
change_list.set_attribute(&attr.name, &attr.value, namespace.is_some());
}

// Fast path: if there is a single text child, it is faster to
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
//! }
//! }
//!
//! impl<'who> Render for Hello<'who> {
//! fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
//! impl<'a, 'who> Render<'a> for Hello<'who> {
//! fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
//! use dodrio::builder::*;
//!
//! let id = bumpalo::format!(in cx.bump, "hello-{}", self.who);
Expand Down
51 changes: 39 additions & 12 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ use wasm_bindgen::UnwrapThrowExt;
///
/// pub struct MyComponent;
///
/// impl Render for MyComponent {
/// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// impl<'a> Render<'a> for MyComponent {
/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
/// use dodrio::builder::*;
///
/// p(&cx)
Expand All @@ -35,26 +35,26 @@ use wasm_bindgen::UnwrapThrowExt;
/// }
/// }
/// ```
pub trait Render {
pub trait Render<'a> {
/// Render `self` as a virtual DOM. Use the given context's `Bump` for
/// temporary allocations.
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a>;
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a>;
}

impl<'r, R> Render for &'r R
impl<'a, 'r, R> Render<'a> for &'r R
where
R: Render,
R: Render<'a>,
{
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
(**self).render(cx)
}
}

impl<R> Render for Rc<R>
impl<'a, R> Render<'a> for Rc<R>
where
R: Render,
R: Render<'a>,
{
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
(**self).render(cx)
}
}
Expand All @@ -69,7 +69,7 @@ where
/// You do not need to implement this trait by hand: there is a blanket
/// implementation for all `Render` types that fulfill the `RootRender`
/// requirements.
pub trait RootRender: Any + Render {
pub trait RootRender: Any + for<'a> Render<'a> {
Copy link
Contributor Author

@awestlake87 awestlake87 Feb 14, 2020

Choose a reason for hiding this comment

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

In #46 @fitzgen mentioned this is the same as 'static, but I'm not sure it is in this case. It would be simpler without higher-order lifetimes, so I was trying to change it to Render<'static> or Render + 'static. The compiler didn't like either of those, which kinda makes sense to me.

The way I understand it, this trait bound says RootRender requires its types to also implement Any and Render for all lifetimes 'a. 'static is just the longest possible lifetime, so it wouldn't fulfill that requirement.

Am I missing something?

Copy link
Owner

Choose a reason for hiding this comment

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

If something is valid for all lifetimes then it must be 'static which is the lifetime that is always valid. If it were not 'static then it would not be valid for all lifetimes, leading to a contradiction.

The only time that for<'a> is useful is when you are taking some generic function and calling it with parameters that contain "unknown" lifetimes or when you wish to call it multiple times with arguments of different lifetimes.

For this case, I think Render<'static> + 'static should do the trick here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I think we both might be right.

Even though Render is a generic trait, it is object safe when used with higher-order lifetimes. That indicates to me that you are correct about for<'a> Render<'a> essentially being 'static from the perspective of the implementation of this trait. The compiler is effectively able to assume that the lifetime of the bump arena in RenderContext is static since it will always be inconsequential to the functionality inside of render.

That being said, the bound for<'a> Render<'a> does seem to be required, maybe not for the callee, but the caller. Here's an example that I've boiled down:

use dodrio::{bumpalo::{self, Bump}};

struct Context<'a> {
    bump: &'a Bump
}

#[derive(Debug, PartialEq, Eq)]
struct Node<'a> {
    name: &'a str
}

trait Render<'a> {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a>;
}

struct Test;

impl<'a> Render<'a> for Test {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a> {
        Node { name: bumpalo::format!(in cx.bump, "Test{}", 1).into_bump_str() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;


    #[test]
    fn it_works() {
        let bump = Bump::default();
        let mut cx = Context { bump: &bump  };
        let comp: Box<dyn for<'a> Render<'a>> = Box::new(Test);

        assert_eq!(Node { name: "Test1" }, comp.render(&mut cx));
    }
}

This test compiles and runs as expected. It's emulating the situation I currently have in my fork of dodrio.

Now, when I switch the type of comp to Box<dyn Render<'static> + 'static>, the compiler doesn't like this example anymore.

use dodrio::{bumpalo::{self, Bump}};

struct Context<'a> {
    bump: &'a Bump
}

#[derive(Debug, PartialEq, Eq)]
struct Node<'a> {
    name: &'a str
}

trait Render<'a> {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a>;
}

struct Test;

impl<'a> Render<'a> for Test {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a> {
        Node { name: bumpalo::format!(in cx.bump, "Test{}", 1).into_bump_str() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;


    #[test]
    fn it_works() {
        let bump = Bump::default();
        let mut cx = Context { bump: &bump  };
        let comp: Box<dyn Render<'static> + 'static> = Box::new(Test);

        assert_eq!(Node { name: "Test1" }, comp.render(&mut cx));
    }
}

Compiler output:

   Compiling dodrio-test v0.1.0 (dodrio-test)
error[E0597]: `bump` does not live long enough
  --> src/lib.rs:32:38
   |
32 |         let mut cx = Context { bump: &bump  };
   |                                      ^^^^^ borrowed value does not live long enough
33 |         let comp: Box<dyn Render<'static> + 'static> = Box::new(Test);
   |                   ---------------------------------- type annotation requires that `bump` is borrowed for `'static`
...
36 |     }
   |     - `bump` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `dodrio-test`.
warning: build failed, waiting for other jobs to finish...
error: build failed

This is because while the Test component may treat its Context as 'static, the it_works function knows that cx has a lifetime that is shorter than 'static. Therefore, the trait bound of Render<'static> cannot be satisfied.

If I'm missing something, could you show me what you mean with this example? Maybe we're talking about different things.

Copy link
Owner

Choose a reason for hiding this comment

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

Ah ok, this makes sense. Thanks for breaking it down!

/// Get this `&RootRender` trait object as an `&Any` trait object reference.
fn as_any(&self) -> &dyn Any;

Expand All @@ -80,7 +80,7 @@ pub trait RootRender: Any + Render {

impl<T> RootRender for T
where
T: Any + Render,
T: Any + for<'a> Render<'a>,
{
fn as_any(&self) -> &dyn Any {
self
Expand Down Expand Up @@ -138,4 +138,31 @@ mod tests {
#[allow(dead_code)]
fn takes_dyn_render(_: &dyn super::RootRender) {}
}

#[test]
fn render_bump_scoped_child() {
use crate::{builder::*, bumpalo::collections::String, Node, Render, RenderContext};

struct Child<'a> {
name: &'a str,
}

impl<'a> Render<'a> for Child<'a> {
fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
text(self.name)
}
}

struct Parent;

impl<'a> Render<'a> for Parent {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
let child_name = String::from_str_in("child", cx.bump).into_bump_str();

div(&cx)
.children([Child { name: child_name }.render(cx)])
.finish()
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should act as a smoke test for bump-scoped lifetimes in child components

Copy link
Owner

Choose a reason for hiding this comment

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

Sorry I should have been more clear. This does ensure that code with bump-scoped child components compiles, but I was hoping for a rendering test that ensures that we also get the expected physical dom. Should be fine to put it in the render.rs test file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, ok. I'll get that taken care of sometime tonight.

}
2 changes: 1 addition & 1 deletion src/render_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl<'a> RenderContext<'a> {
/// Get or create the cached template for `Cached<R>`.
pub(crate) fn template<R>(&mut self) -> Option<CacheId>
where
R: 'static + Default + Render,
R: 'static + Default + for<'b> Render<'b>,
{
let template_id = Cached::<R>::template_id();
if let Some(cache_id) = self.templates.get(&template_id).cloned() {
Expand Down
8 changes: 4 additions & 4 deletions tests/web/cached.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ impl CountRenders {
}
}

impl Render for CountRenders {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for CountRenders {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
let count = self.render_count.get() + 1;
self.render_count.set(count);

Expand Down Expand Up @@ -153,8 +153,8 @@ impl Default for Id {
}
}

impl Render for Id {
fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for Id {
fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
text(self.0)
}
}
Expand Down
8 changes: 4 additions & 4 deletions tests/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ impl EventContainer {
}
}

impl Render for EventContainer {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for EventContainer {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;
div(&cx)
.attr("id", "target")
Expand Down Expand Up @@ -126,8 +126,8 @@ impl ListensOnlyOnFirstRender {
}
}

impl Render for ListensOnlyOnFirstRender {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a> Render<'a> for ListensOnlyOnFirstRender {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

let count = self.count.get();
Expand Down
Loading