Pauan / rust-dominator

Zero-cost ultra-high-performance declarative DOM library using FRP signals for Rust!
MIT License
960 stars 62 forks source link

How do I get random access Signals from SignalVecs? #34

Closed NeverGivinUp closed 4 years ago

NeverGivinUp commented 4 years ago

I have a graph structure, described by edges and nodes, and a graph layout -- a Vec of node-positions. Say the graph structure won't change, but laying out the graph is an iterative process that updates the node positions 1000 to 10000 times depending on the size and complexity of the graph.

Using Dominator, I thought I'd needed to replace the Vec with a MutableVec to be able to create the SVG-Elements once, and update the their transform attribute by using a signal. I must be doing something wrong though, because for

.attribute_signal("transform", translations.signal_vec().map(|t|format!("translate({},{})", t.x(), t.y())))

I'm getting the error

the trait bound `futures_signals::signal_vec::Map<futures_signals::signal_vec::mutable_vec::MutableSignalVec<Translation>, [closure@src\lib.rs:225:55: 225:99]>: futures_signals::signal::signal::Signal` is not satisfied

despite Map implementing SignalVec.

I have no idea how to draw the edges. In a static context I'd iterate through the edges and read the positions of the start- and end-nodes to draw each edge, like

let a_center = translations[edge.a_end()];
let b_center = translations[edge.b_end()];
new_svg("line")
                    .class("edge")
                    .attribute("node-1", &edge.a_end().to_string())
                    .attribute("x1", &a_center.x().to_string())
                    .attribute("y1", &a_center.y().to_string())
                    .attribute("node-2", &edge.b_end().to_string())
                    .attribute("x2", &b_center.x().to_string())
                    .attribute("y2", &b_center.y().to_string())
                    .into_dom()

How can I get a the signal-equivalent of translations[edge.a_end()], when translations is a MutableSignalVec (where edge.a_end() is not a signal)?

Pauan commented 4 years ago

Signal and SignalVec are two separate things: Signal is a single value which changes over time, SignalVec is a vector of values which change over time.

There are methods to manually convert between them (such as to_signal_vec and to_signal_map) but there is no automatic conversion.

If the graph structure doesn't change, why do you need a MutableVec for it? MutableVec is for situations where the children change. If the children don't change, you can just use children to create a static DOM structure:

svg!("g", {
    .children(translations.iter().map(|edge| {
        let a_center = translations[edge.a_end()];
        let b_center = translations[edge.b_end()];

        svg!("line", {
            .class("edge")
            .attribute("node-1", &edge.a_end().to_string())
            .attribute("x1", &a_center.x().to_string())
            .attribute("y1", &a_center.y().to_string())
            .attribute("node-2", &edge.b_end().to_string())
            .attribute("x2", &b_center.x().to_string())
            .attribute("y2", &b_center.y().to_string())
        })
    }))
})
NeverGivinUp commented 4 years ago

What you did above is exactly what I did in the static case. (The only difference is, that I like the fluent style better than the macro-style, because CLion has very limited code assistance in macros.)

translations is a Vec of node positions of a graph. It's values change over time (though not it's length, so semantically it's a mutable slice). I want to display this change as an animation in real time, so my users can see the iterative layout computation while it is happening. During the layout computation the number of children -- the length of translations -- does not change, but the values in translations do change, and so must the x1,y1 and x2, y2 attributes.

I had been treating translations as a single signal that creates the whole SVG group element, which holds the children. But that way Dominator must delete and recreate the elements on each change. I understand that is slower than just updating the attribute values. That is why I'd like to move to a MutableVec (a MutableSlice would suffice, really). Shouldn't I?

Pauan commented 4 years ago

Trying to use MutableVec for this will just add a lot of extra complexity (and performance issues). You would need something like this...

fn lookup(translations: &MutableVec<Edge>, index: usize) -> impl Signal<Item = Edge> {
    translations.signal_vec().to_signal_map(move |translations| translations[index]).dedupe()
}

fn switch<S>(translations: MutableVec<Edge>, signal: S) -> impl Signal<Item = Edge>
    where S: Signal<Item = usize> {
    signal.switch(move |index| lookup(&translations, index))
}

fn a_end(translations: &MutableVec<Edge>, index: usize) -> impl Signal<Item = usize> {
    lookup(translations, index).map(|edge| edge.a_end())
}

fn b_end(translations: &MutableVec<Edge>, index: usize) -> impl Signal<Item = usize> {
    lookup(translations, index).map(|edge| edge.b_end())
}

svg!("g", {
    .children((0..translations.lock_ref().len()).map(clone!(translations => move |index| {
        svg!("line", {
            .class("edge")
            .attribute("node-1", a_end(translations, index).map(|a| a.to_string()))
            .attribute("x1", switch(translations.clone(), a_end(translations, index)).map(|a_center| a_center.x().to_string()))
            .attribute("y1", switch(translations.clone(), a_end(translations, index)).map(|a_center| a_center.y().to_string()))
            .attribute("node-2", b_end(translations, index).map(|b| b.to_string()))
            .attribute("x2", switch(translations.clone(), b_end(translations, index)).map(|b_center| b_center.x().to_string()))
            .attribute("y2", switch(translations.clone(), b_end(translations, index)).map(|b_center| b_center.y().to_string()))
        })
    })))
})

But this means that on every change of translations it will do ten lookups for every edge. So if you have 1,000 edges, that means it will do 10,000 lookups on every change. And it has to create new buffers for each of those, so you'll have 10,000 buffers. That's an awful lot of overhead.

So I really don't recommend doing that. Instead you should use an Rc<Vec<Mutable<Edge>>> rather than a MutableVec:

svg!("g", {
    .children(translations.iter().map(|edge| {
        fn lookup<A, S, F>(translations: Rc<Vec<Mutable<Edge>>>, signal: S, f: F) -> impl Signal<Item = A>
            where S: Signal<Item = usize>,
                  F: FnMut(&Edge) -> A {
            signal.switch(move |index| translations[index].signal_ref(f))
        }

        fn a_end(edge: &Mutable<Edge>) -> impl Signal<Item = usize> {
            edge.signal_ref(|edge| edge.a_end())
        }

        fn b_end(edge: &Mutable<Edge>) -> impl Signal<Item = usize> {
            edge.signal_ref(|edge| edge.b_end())
        }

        svg!("line", {
            .class("edge")
            .attribute("node-1", a_end(edge).map(|a| a.to_string()))
            .attribute("x1", lookup(translations.clone(), a_end(edge), |a_center| a_center.x().to_string()))
            .attribute("y1", lookup(translations.clone(), a_end(edge), |a_center| a_center.y().to_string()))
            .attribute("node-2", b_end(edge).map(|b| b.to_string()))
            .attribute("x2", lookup(translations.clone(), b_end(edge), |b_center| b_center.x().to_string()))
            .attribute("y2", lookup(translations.clone(), b_end(edge), |b_center| b_center.y().to_string())) 
        })
    }))
})

This will also be far faster. It will only update the minimum necessary, and it has no buffers. It's basically optimal performance.

The idea with signals is that they should match your update patterns. Since you're not updating the Vec, using MutableVec doesn't make sense. But you are updating each individual edge, so using Vec<Mutable<Edge>> makes perfect sense. Or if you were updating individual fields, then putting a Mutable on each field of Edge would make perfect sense.

Pauan commented 4 years ago

P.S. You mentioned you wanted animations, but animated_map will only animate when an element is inserted or removed. It is designed for easily fading elements in/out when they are inserted/removed.

If you want to do something more complicated than that, then you need to use MutableAnimation, which gives you full control over the animation.

NeverGivinUp commented 4 years ago

Thank you @Pauan for the detailed explanation how to design this and what to urgently avoid. I'm obviously a raw beginner to designing with signals and your guidance on design patterns and anti-patterns (with examples) is very much appreciated. I'd recommend putting some in the documentation of the Dominator and Signals crate, too, when you approach version 1.0