perf: introduce reflow for the rendering engine (#1863)

This commit is contained in:
三咲雅 · Misaki Masa 2024-10-30 19:06:06 +08:00 committed by GitHub
parent 81b8c89ec1
commit c6687237e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 140 additions and 49 deletions

View File

@ -1,6 +1,6 @@
use ratatui::layout::Rect; use ratatui::layout::Rect;
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default, PartialEq, Eq)]
pub struct Layout { pub struct Layout {
pub current: Rect, pub current: Rect,
pub preview: Rect, pub preview: Rect,

View File

@ -4,6 +4,7 @@ yazi_macro::mod_flat!(
notify notify
plugin plugin
quit quit
reflow
render render
resize resize
resume resume

View File

@ -0,0 +1,56 @@
use std::sync::Arc;
use mlua::Value;
use ratatui::layout::Position;
use tracing::error;
use yazi_config::LAYOUT;
use yazi_macro::render;
use yazi_shared::event::Cmd;
use crate::{Root, app::App, lives::Lives};
struct Opt;
impl From<Cmd> for Opt {
fn from(_: Cmd) -> Self { Self }
}
impl From<()> for Opt {
fn from(_: ()) -> Self { Self }
}
impl App {
#[yazi_codegen::command]
pub fn reflow(&mut self, _: Opt) {
let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { return };
let mut layout = *LAYOUT.load_full();
let result = Lives::scope(&self.cx, |_| {
let comps = Root::reflow((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() {
let Value::Table(t) = v? else {
error!("`reflow()` must return a table of components");
continue;
};
let id: mlua::String = t.get("_id")?;
match id.to_str()? {
"current" => layout.current = *t.raw_get::<_, yazi_plugin::elements::Rect>("_area")?,
"preview" => layout.preview = *t.raw_get::<_, yazi_plugin::elements::Rect>("_area")?,
_ => {}
}
}
Ok(())
});
if layout != *LAYOUT.load_full() {
LAYOUT.store(Arc::new(layout));
render!();
}
if let Err(e) = result {
error!("Failed to `reflow()` the `Root` component:\n{e}");
}
}
}

View File

@ -16,7 +16,7 @@ impl App {
#[yazi_codegen::command] #[yazi_codegen::command]
pub fn resize(&mut self, _: Opt) { pub fn resize(&mut self, _: Opt) {
self.cx.manager.active_mut().preview.reset(); self.cx.manager.active_mut().preview.reset();
self.render(); self.reflow(());
self.cx.manager.current_mut().sync_page(true); self.cx.manager.current_mut().sync_page(true);
self.cx.manager.hover(None); self.cx.manager.hover(None);

View File

@ -13,7 +13,7 @@ impl Progress {
let mut patches = vec![]; let mut patches = vec![];
let mut f = || { let mut f = || {
let comp: Table = LUA.globals().raw_get("Progress")?; let comp: Table = LUA.globals().raw_get("Progress")?;
for widget in comp.call_method::<_, Vec<AnyUserData>>("partial_render", ())? { for widget in comp.call_method::<_, Vec<AnyUserData>>("partial_redraw", ())? {
let Some(w) = cast_to_renderable(&widget) else { continue }; let Some(w) = cast_to_renderable(&widget) else { continue };
let area = w.area(); let area = w.area();

View File

@ -12,6 +12,12 @@ pub(super) struct Root<'a> {
impl<'a> Root<'a> { impl<'a> Root<'a> {
pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } }
pub(super) fn reflow<'lua>(area: Rect) -> mlua::Result<Table<'lua>> {
let area = yazi_plugin::elements::Rect::from(area);
let root = LUA.globals().raw_get::<_, Table>("Root")?.call_method::<_, Table>("new", area)?;
root.call_method("reflow", ())
}
} }
impl<'a> Widget for Root<'a> { impl<'a> Widget for Root<'a> {
@ -20,11 +26,11 @@ impl<'a> Widget for Root<'a> {
let area = yazi_plugin::elements::Rect::from(area); let area = yazi_plugin::elements::Rect::from(area);
let root = LUA.globals().raw_get::<_, Table>("Root")?.call_method::<_, Table>("new", area)?; let root = LUA.globals().raw_get::<_, Table>("Root")?.call_method::<_, Table>("new", area)?;
render_widgets(root.call_method("render", ())?, buf); render_widgets(root.call_method("redraw", ())?, buf);
Ok::<_, mlua::Error>(()) Ok::<_, mlua::Error>(())
}; };
if let Err(e) = f() { if let Err(e) = f() {
error!("Failed to render the `Root` component:\n{e}"); error!("Failed to redraw the `Root` component:\n{e}");
} }
components::Preview::new(self.cx).render(area, buf); components::Preview::new(self.cx).render(area, buf);

View File

@ -23,7 +23,9 @@ function Current:empty()
} }
end end
function Current:render() function Current:reflow() return { self } end
function Current:redraw()
local files = self._folder.window local files = self._folder.window
if #files == 0 then if #files == 0 then
return self:empty() return self:empty()
@ -31,8 +33,8 @@ function Current:render()
local entities, linemodes = {}, {} local entities, linemodes = {}, {}
for _, f in ipairs(files) do for _, f in ipairs(files) do
entities[#entities + 1] = Entity:new(f):render() entities[#entities + 1] = Entity:new(f):redraw()
linemodes[#linemodes + 1] = Linemode:new(f):render() linemodes[#linemodes + 1] = Linemode:new(f):redraw()
end end
return { return {

View File

@ -76,7 +76,7 @@ function Entity:symlink()
return to and ui.Line(" -> " .. tostring(to)):italic() or ui.Line {} return to and ui.Line(" -> " .. tostring(to)):italic() or ui.Line {}
end end
function Entity:render() function Entity:redraw()
local lines = {} local lines = {}
for _, c in ipairs(self._children) do for _, c in ipairs(self._children) do
lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self)

View File

@ -95,11 +95,13 @@ function Header:tabs()
return ui.Line(spans) return ui.Line(spans)
end end
function Header:render() function Header:reflow() return { self } end
local right = self:children_render(self.RIGHT)
function Header:redraw()
local right = self:children_redraw(self.RIGHT)
self._right_width = right:width() self._right_width = right:width()
local left = self:children_render(self.LEFT) local left = self:children_redraw(self.LEFT)
return { return {
ui.Text(left):area(self._area), ui.Text(left):area(self._area),
@ -135,7 +137,7 @@ function Header:children_remove(id, side)
end end
end end
function Header:children_render(side) function Header:children_redraw(side)
local lines = {} local lines = {}
for _, c in ipairs(side == self.RIGHT and self._right or self._left) do for _, c in ipairs(side == self.RIGHT and self._right or self._left) do
lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self)

View File

@ -62,7 +62,7 @@ function Linemode:owner()
return ui.Line(string.format("%s:%s", user or "-", group or "-")) return ui.Line(string.format("%s:%s", user or "-", group or "-"))
end end
function Linemode:render() function Linemode:redraw()
local lines = {} local lines = {}
for _, c in ipairs(self._children) do for _, c in ipairs(self._children) do
lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self)

View File

@ -9,7 +9,7 @@ function Marker:new(area, folder)
}, { __index = self }) }, { __index = self })
end end
function Marker:render() function Marker:redraw()
if self._area.w * self._area.h == 0 then if self._area.w * self._area.h == 0 then
return {} return {}
elseif not self._folder or #self._folder.window == 0 then elseif not self._folder or #self._folder.window == 0 then

View File

@ -10,18 +10,20 @@ function Parent:new(area, tab)
}, { __index = self }) }, { __index = self })
end end
function Parent:render() function Parent:reflow() return { self } end
function Parent:redraw()
if not self._folder then if not self._folder then
return {} return {}
end end
local items = {} local entities = {}
for _, f in ipairs(self._folder.window) do for _, f in ipairs(self._folder.window) do
items[#items + 1] = Entity:new(f):render() entities[#entities + 1] = Entity:new(f):redraw()
end end
return { return {
ui.List(items):area(self._area), ui.List(entities):area(self._area),
} }
end end

View File

@ -10,7 +10,9 @@ function Preview:new(area, tab)
}, { __index = self }) }, { __index = self })
end end
function Preview:render() return {} end function Preview:reflow() return { self } end
function Preview:redraw() return {} end
-- Mouse events -- Mouse events
function Preview:click(event, up) function Preview:click(event, up)

View File

@ -2,20 +2,20 @@ Progress = {
_area = ui.Rect.default, -- TODO: remove this _area = ui.Rect.default, -- TODO: remove this
} }
function Progress:render(area, offset) function Progress:redraw(area, offset)
self._area = ui.Rect { self._area = ui.Rect {
x = math.max(0, area.w - offset - 21), x = math.max(0, area.w - offset - 21),
y = area.y, y = area.y,
w = ya.clamp(0, area.w - offset - 1, 20), w = ya.clamp(0, area.w - offset - 1, 20),
h = math.min(1, area.h), h = math.min(1, area.h),
} }
return self:partial_render() return self:partial_redraw()
end end
-- Progress bars usually need frequent updates to report the latest task progress. -- Progress bars usually need frequent updates to report the latest task progress.
-- We use `partial_render()` to partially render it when there is progress change, -- We use `partial_redraw()` to partially redraw it when there is progress change,
-- which has almost no cost compared to a full render by `render()`. -- which has almost no cost compared to a full redraw by `redraw()`.
function Progress:partial_render() function Progress:partial_redraw()
local progress = cx.tasks.progress local progress = cx.tasks.progress
if progress.total == 0 then if progress.total == 0 then
return { ui.Text {} } return { ui.Text {} }

View File

@ -20,12 +20,14 @@ function Rail:build()
} }
end end
function Rail:render() function Rail:reflow() return { self } end
local children = self._base or {}
function Rail:redraw()
local elements = self._base or {}
for _, child in ipairs(self._children) do for _, child in ipairs(self._children) do
children = ya.list_merge(children, ya.render_with(child)) elements = ya.list_merge(elements, ya.redraw_with(child))
end end
return children return elements
end end
-- Mouse events -- Mouse events

View File

@ -29,12 +29,20 @@ function Root:build()
} }
end end
function Root:render() function Root:reflow()
local children = self._base or {} local components = { self }
for _, child in ipairs(self._children) do for _, child in ipairs(self._children) do
children = ya.list_merge(children, ya.render_with(child)) components = ya.list_merge(components, child:reflow())
end end
return children return components
end
function Root:redraw()
local elements = self._base or {}
for _, child in ipairs(self._children) do
elements = ya.list_merge(elements, ya.redraw_with(child))
end
return elements
end end
-- Mouse events -- Mouse events

View File

@ -131,16 +131,18 @@ function Status:position()
} }
end end
function Status:render() function Status:reflow() return { self } end
local left = self:children_render(self.LEFT)
local right = self:children_render(self.RIGHT) function Status:redraw()
local left = self:children_redraw(self.LEFT)
local right = self:children_redraw(self.RIGHT)
local right_width = right:width() local right_width = right:width()
return { return {
ui.Text(left):area(self._area), ui.Text(left):area(self._area),
ui.Text(right):area(self._area):align(ui.Text.RIGHT), ui.Text(right):area(self._area):align(ui.Text.RIGHT),
table.unpack(Progress:render(self._area, right_width)), table.unpack(Progress:redraw(self._area, right_width)),
} }
end end
@ -172,7 +174,7 @@ function Status:children_remove(id, side)
end end
end end
function Status:children_render(side) function Status:children_redraw(side)
local lines = {} local lines = {}
for _, c in ipairs(side == self.RIGHT and self._right or self._left) do for _, c in ipairs(side == self.RIGHT and self._right or self._left) do
lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self)

View File

@ -29,12 +29,20 @@ function Tab:build()
} }
end end
function Tab:render() function Tab:reflow()
local children = self._base or {} local components = { self }
for _, child in ipairs(self._children) do for _, child in ipairs(self._children) do
children = ya.list_merge(children, ya.render_with(child)) components = ya.list_merge(components, child:reflow())
end end
return children return components
end
function Tab:redraw()
local elements = self._base or {}
for _, child in ipairs(self._children) do
elements = ya.list_merge(elements, ya.redraw_with(child))
end
return elements
end end
-- Mouse events -- Mouse events

View File

@ -17,14 +17,14 @@ function M:peek()
}) })
end end
local items = {} local entities = {}
for _, f in ipairs(folder.window) do for _, f in ipairs(folder.window) do
items[#items + 1] = Entity:new(f):render() entities[#entities + 1] = Entity:new(f):redraw()
end end
ya.preview_widgets(self, { ya.preview_widgets(self, {
ui.List(items):area(self.area), ui.List(entities):area(self.area),
table.unpack(Marker:new(self.area, folder):render()), table.unpack(Marker:new(self.area, folder):redraw()),
}) })
end end

View File

@ -37,7 +37,7 @@ impl Utils {
)?; )?;
ya.raw_set( ya.raw_set(
"render_with", "redraw_with",
lua.create_function(|lua, c: Table| { lua.create_function(|lua, c: Table| {
let id: mlua::String = c.get("_id")?; let id: mlua::String = c.get("_id")?;
let id = id.to_str()?; let id = id.to_str()?;
@ -58,9 +58,9 @@ impl Utils {
_ => {} _ => {}
} }
match c.call_method::<_, Table>("render", ()) { match c.call_method::<_, Table>("redraw", ()) {
Err(e) => { Err(e) => {
error!("Failed to `render()` the `{id}` component:\n{e}"); error!("Failed to `redraw()` the `{id}` component:\n{e}");
lua.create_table() lua.create_table()
} }
ok => ok, ok => ok,