This commit adds subtree memoization to Dioxus. Subtree memoization is basically a compile-time step that drastically reduces the amount of work the diffing engine needs to do at runtime by extracting non-changing nodes out into a static "template." Templates are then understood by the various renderers in the ecosystem as a faster way of rendering the same items. For example, in the web, templates are simply a set of DOM Nodes created once and then cloned later. This is the same pattern frameworks like Lithtml and SolidJS use to achieve near-perfect performance. Subtree memoization adds an additional level of complexity to Dioxus. The RSX macro needs to be much smarter to identify changing/nonchanging nodes and generate a mapping between the Template and its runtime counterparts. This commit represents a working starter point for this work, adding support for templates for the web, desktop, liveview, ssr, and native-core renderers. In the future we will try to shrink code generation, generally improve performance, and simplify our implementation.
426 lines
16 KiB
Rust
426 lines
16 KiB
Rust
use std::io::Stdout;
|
|
use taffy::{
|
|
geometry::Point,
|
|
prelude::{Dimension, Layout, Size},
|
|
Taffy,
|
|
};
|
|
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
|
|
|
|
use crate::{
|
|
style::{RinkColor, RinkStyle},
|
|
style_attributes::{BorderEdge, BorderStyle},
|
|
widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
|
|
Config, Dom, Node,
|
|
};
|
|
|
|
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
|
|
|
|
pub(crate) fn render_vnode(
|
|
frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
|
|
layout: &Taffy,
|
|
rdom: &Dom,
|
|
node: &Node,
|
|
cfg: Config,
|
|
parent_location: Point<f32>,
|
|
) {
|
|
use dioxus_native_core::real_dom::NodeType;
|
|
|
|
if let NodeType::Placeholder = &node.node_data.node_type {
|
|
return;
|
|
}
|
|
|
|
let Layout {
|
|
mut location, size, ..
|
|
} = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
|
location.x += parent_location.x;
|
|
location.y += parent_location.y;
|
|
|
|
let Point { x, y } = location;
|
|
let Size { width, height } = size;
|
|
|
|
match &node.node_data.node_type {
|
|
NodeType::Text { text } => {
|
|
#[derive(Default)]
|
|
struct Label<'a> {
|
|
text: &'a str,
|
|
style: RinkStyle,
|
|
}
|
|
|
|
impl<'a> RinkWidget for Label<'a> {
|
|
fn render(self, area: Rect, mut buf: RinkBuffer) {
|
|
for (i, c) in self.text.char_indices() {
|
|
let mut new_cell = RinkCell::default();
|
|
new_cell.set_style(self.style);
|
|
new_cell.symbol = c.to_string();
|
|
buf.set(area.left() + i as u16, area.top(), new_cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
let label = Label {
|
|
text,
|
|
style: node.state.style.core,
|
|
};
|
|
let area = Rect::new(x as u16, y as u16, *width as u16, *height as u16);
|
|
|
|
// the renderer will panic if a node is rendered out of range even if the size is zero
|
|
if area.width > 0 && area.height > 0 {
|
|
frame.render_widget(WidgetWithContext::new(label, cfg), area);
|
|
}
|
|
}
|
|
NodeType::Element { children, .. } => {
|
|
let area = Rect::new(x as u16, y as u16, *width as u16, *height as u16);
|
|
|
|
// the renderer will panic if a node is rendered out of range even if the size is zero
|
|
if area.width > 0 && area.height > 0 {
|
|
frame.render_widget(WidgetWithContext::new(node, cfg), area);
|
|
}
|
|
|
|
for c in children {
|
|
render_vnode(frame, layout, rdom, &rdom[*c], cfg, location);
|
|
}
|
|
}
|
|
NodeType::Placeholder => unreachable!(),
|
|
}
|
|
}
|
|
|
|
impl RinkWidget for &Node {
|
|
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
|
|
use tui::symbols::line::*;
|
|
|
|
enum Direction {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
}
|
|
|
|
fn draw(
|
|
buf: &mut RinkBuffer,
|
|
points_history: [[i32; 2]; 3],
|
|
symbols: &Set,
|
|
pos: [u16; 2],
|
|
color: &Option<RinkColor>,
|
|
) {
|
|
let [before, current, after] = points_history;
|
|
let start_dir = match [before[0] - current[0], before[1] - current[1]] {
|
|
[1, 0] => Direction::Right,
|
|
[-1, 0] => Direction::Left,
|
|
[0, 1] => Direction::Down,
|
|
[0, -1] => Direction::Up,
|
|
[a, b] => {
|
|
panic!(
|
|
"draw({:?} {:?} {:?}) {}, {} no cell adjacent",
|
|
before, current, after, a, b
|
|
)
|
|
}
|
|
};
|
|
let end_dir = match [after[0] - current[0], after[1] - current[1]] {
|
|
[1, 0] => Direction::Right,
|
|
[-1, 0] => Direction::Left,
|
|
[0, 1] => Direction::Down,
|
|
[0, -1] => Direction::Up,
|
|
[a, b] => {
|
|
panic!(
|
|
"draw({:?} {:?} {:?}) {}, {} no cell adjacent",
|
|
before, current, after, a, b
|
|
)
|
|
}
|
|
};
|
|
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = color {
|
|
new_cell.fg = *c;
|
|
}
|
|
new_cell.symbol = match [start_dir, end_dir] {
|
|
[Direction::Down, Direction::Up] => symbols.vertical,
|
|
[Direction::Down, Direction::Right] => symbols.top_left,
|
|
[Direction::Down, Direction::Left] => symbols.top_right,
|
|
[Direction::Up, Direction::Down] => symbols.vertical,
|
|
[Direction::Up, Direction::Right] => symbols.bottom_left,
|
|
[Direction::Up, Direction::Left] => symbols.bottom_right,
|
|
[Direction::Right, Direction::Left] => symbols.horizontal,
|
|
[Direction::Right, Direction::Up] => symbols.bottom_left,
|
|
[Direction::Right, Direction::Down] => symbols.top_left,
|
|
[Direction::Left, Direction::Up] => symbols.bottom_right,
|
|
[Direction::Left, Direction::Right] => symbols.horizontal,
|
|
[Direction::Left, Direction::Down] => symbols.top_right,
|
|
_ => panic!(
|
|
"{:?} {:?} {:?} cannont connect cell to itself",
|
|
before, current, after
|
|
),
|
|
}
|
|
.to_string();
|
|
buf.set(
|
|
(current[0] + pos[0] as i32) as u16,
|
|
(current[1] + pos[1] as i32) as u16,
|
|
new_cell,
|
|
);
|
|
}
|
|
|
|
fn draw_arc(
|
|
pos: [u16; 2],
|
|
starting_angle: f32,
|
|
arc_angle: f32,
|
|
radius: f32,
|
|
symbols: &Set,
|
|
buf: &mut RinkBuffer,
|
|
color: &Option<RinkColor>,
|
|
) {
|
|
if radius < 0.0 {
|
|
return;
|
|
}
|
|
|
|
let num_points = (radius * arc_angle) as i32;
|
|
let starting_point = [
|
|
(starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32,
|
|
(starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32,
|
|
];
|
|
// keep track of the last 3 point to allow filling diagonals
|
|
let mut points_history = [
|
|
[0, 0],
|
|
{
|
|
// change the x or y value based on which one is changing quicker
|
|
let ddx = -starting_angle.sin();
|
|
let ddy = starting_angle.cos();
|
|
if ddx.abs() > ddy.abs() {
|
|
[starting_point[0] - ddx.signum() as i32, starting_point[1]]
|
|
} else {
|
|
[starting_point[0], starting_point[1] - ddy.signum() as i32]
|
|
}
|
|
},
|
|
starting_point,
|
|
];
|
|
|
|
for i in 1..=num_points {
|
|
let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle;
|
|
let x = angle.cos() * radius * RADIUS_MULTIPLIER[0];
|
|
let y = angle.sin() * radius * RADIUS_MULTIPLIER[1];
|
|
let new = [x as i32, y as i32];
|
|
|
|
if new != points_history[2] {
|
|
points_history = [points_history[1], points_history[2], new];
|
|
|
|
let dx = points_history[2][0] - points_history[1][0];
|
|
let dy = points_history[2][1] - points_history[1][1];
|
|
// fill diagonals
|
|
if dx != 0 && dy != 0 {
|
|
let connecting_point = match [dx, dy] {
|
|
[1, 1] => [points_history[1][0] + 1, points_history[1][1]],
|
|
[1, -1] => [points_history[1][0], points_history[1][1] - 1],
|
|
[-1, 1] => [points_history[1][0], points_history[1][1] + 1],
|
|
[-1, -1] => [points_history[1][0] - 1, points_history[1][1]],
|
|
_ => todo!(),
|
|
};
|
|
draw(
|
|
buf,
|
|
[points_history[0], points_history[1], connecting_point],
|
|
symbols,
|
|
pos,
|
|
color,
|
|
);
|
|
points_history = [points_history[1], connecting_point, points_history[2]];
|
|
}
|
|
|
|
draw(buf, points_history, symbols, pos, color);
|
|
}
|
|
}
|
|
|
|
points_history = [points_history[1], points_history[2], {
|
|
// change the x or y value based on which one is changing quicker
|
|
let ddx = -(starting_angle + arc_angle).sin();
|
|
let ddy = (starting_angle + arc_angle).cos();
|
|
if ddx.abs() > ddy.abs() {
|
|
[
|
|
points_history[2][0] + ddx.signum() as i32,
|
|
points_history[2][1],
|
|
]
|
|
} else {
|
|
[
|
|
points_history[2][0],
|
|
points_history[2][1] + ddy.signum() as i32,
|
|
]
|
|
}
|
|
}];
|
|
|
|
draw(buf, points_history, symbols, pos, color);
|
|
}
|
|
|
|
fn get_radius(border: &BorderEdge, area: Rect) -> f32 {
|
|
match border.style {
|
|
BorderStyle::Hidden => 0.0,
|
|
BorderStyle::None => 0.0,
|
|
_ => match border.radius {
|
|
Dimension::Percent(p) => p * area.width as f32 / 100.0,
|
|
Dimension::Points(p) => p,
|
|
_ => todo!(),
|
|
}
|
|
.abs()
|
|
.min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0)
|
|
.min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0),
|
|
}
|
|
}
|
|
|
|
if area.area() == 0 {
|
|
return;
|
|
}
|
|
|
|
// todo: only render inside borders
|
|
for x in area.left()..area.right() {
|
|
for y in area.top()..area.bottom() {
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = self.state.style.core.bg {
|
|
new_cell.bg = c;
|
|
}
|
|
if self.state.focused {
|
|
new_cell.bg.alpha = 100;
|
|
new_cell.bg.color = new_cell.bg.blend(Color::White);
|
|
}
|
|
buf.set(x, y, new_cell);
|
|
}
|
|
}
|
|
|
|
let borders = &self.state.style.modifier.borders;
|
|
|
|
let last_edge = &borders.left;
|
|
let current_edge = &borders.top;
|
|
if let Some(symbols) = current_edge.style.symbol_set() {
|
|
// the radius for the curve between this line and the next
|
|
let r = get_radius(current_edge, area);
|
|
let radius = [
|
|
(r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
// the radius for the curve between this line and the last
|
|
let last_r = get_radius(last_edge, area);
|
|
let last_radius = [
|
|
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
let color = current_edge.color.or(self.state.style.core.fg);
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = color {
|
|
new_cell.fg = c;
|
|
}
|
|
for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
|
|
new_cell.symbol = symbols.horizontal.to_string();
|
|
buf.set(x, area.top(), new_cell.clone());
|
|
}
|
|
draw_arc(
|
|
[area.right() - radius[0] - 1, area.top() + radius[1]],
|
|
std::f32::consts::FRAC_PI_2 * 3.0,
|
|
std::f32::consts::FRAC_PI_2,
|
|
r,
|
|
&symbols,
|
|
&mut buf,
|
|
&color,
|
|
);
|
|
}
|
|
|
|
let last_edge = &borders.top;
|
|
let current_edge = &borders.right;
|
|
if let Some(symbols) = current_edge.style.symbol_set() {
|
|
// the radius for the curve between this line and the next
|
|
let r = get_radius(current_edge, area);
|
|
let radius = [
|
|
(r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
// the radius for the curve between this line and the last
|
|
let last_r = get_radius(last_edge, area);
|
|
let last_radius = [
|
|
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
let color = current_edge.color.or(self.state.style.core.fg);
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = color {
|
|
new_cell.fg = c;
|
|
}
|
|
for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
|
|
new_cell.symbol = symbols.vertical.to_string();
|
|
buf.set(area.right() - 1, y, new_cell.clone());
|
|
}
|
|
draw_arc(
|
|
[area.right() - radius[0] - 1, area.bottom() - radius[1] - 1],
|
|
0.0,
|
|
std::f32::consts::FRAC_PI_2,
|
|
r,
|
|
&symbols,
|
|
&mut buf,
|
|
&color,
|
|
);
|
|
}
|
|
|
|
let last_edge = &borders.right;
|
|
let current_edge = &borders.bottom;
|
|
if let Some(symbols) = current_edge.style.symbol_set() {
|
|
// the radius for the curve between this line and the next
|
|
let r = get_radius(current_edge, area);
|
|
let radius = [
|
|
(r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
// the radius for the curve between this line and the last
|
|
let last_r = get_radius(last_edge, area);
|
|
let last_radius = [
|
|
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
let color = current_edge.color.or(self.state.style.core.fg);
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = color {
|
|
new_cell.fg = c;
|
|
}
|
|
for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
|
|
new_cell.symbol = symbols.horizontal.to_string();
|
|
buf.set(x, area.bottom() - 1, new_cell.clone());
|
|
}
|
|
draw_arc(
|
|
[area.left() + radius[0], area.bottom() - radius[1] - 1],
|
|
std::f32::consts::FRAC_PI_2,
|
|
std::f32::consts::FRAC_PI_2,
|
|
r,
|
|
&symbols,
|
|
&mut buf,
|
|
&color,
|
|
);
|
|
}
|
|
|
|
let last_edge = &borders.bottom;
|
|
let current_edge = &borders.left;
|
|
if let Some(symbols) = current_edge.style.symbol_set() {
|
|
// the radius for the curve between this line and the next
|
|
let r = get_radius(current_edge, area);
|
|
let radius = [
|
|
(r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
// the radius for the curve between this line and the last
|
|
let last_r = get_radius(last_edge, area);
|
|
let last_radius = [
|
|
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
|
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
|
];
|
|
let color = current_edge.color.or(self.state.style.core.fg);
|
|
let mut new_cell = RinkCell::default();
|
|
if let Some(c) = color {
|
|
new_cell.fg = c;
|
|
}
|
|
for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
|
|
new_cell.symbol = symbols.vertical.to_string();
|
|
buf.set(area.left(), y, new_cell.clone());
|
|
}
|
|
draw_arc(
|
|
[area.left() + radius[0], area.top() + radius[1]],
|
|
std::f32::consts::PI,
|
|
std::f32::consts::FRAC_PI_2,
|
|
r,
|
|
&symbols,
|
|
&mut buf,
|
|
&color,
|
|
);
|
|
}
|
|
}
|
|
}
|