diff options
| author | Nathan Reiner <nathan@nathanreiner.xyz> | 2024-08-01 20:13:55 +0200 |
|---|---|---|
| committer | Nathan Reiner <nathan@nathanreiner.xyz> | 2024-08-01 20:13:55 +0200 |
| commit | a807d4e6fb96c4d8b8585b7dbb862e53486562ec (patch) | |
| tree | 92481d73423fd925f4dd034d68cbfd2e30e216d5 /src | |
| parent | 417cee4eeeaf7516dfeb59cdbe34fed18f30e0f7 (diff) | |
add evalsto
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/evalsto.rs | 64 | ||||
| -rw-r--r-- | src/config/mod.rs | 2 | ||||
| -rw-r--r-- | src/config/theme/colorpair.rs | 106 | ||||
| -rw-r--r-- | src/config/theme/mod.rs | 4 | ||||
| -rw-r--r-- | src/config/theme/sheetview.rs | 39 | ||||
| -rw-r--r-- | src/config/theme/style.rs | 119 | ||||
| -rw-r--r-- | src/lua/mod.rs | 6 | ||||
| -rw-r--r-- | src/lua/ownedfunction.rs | 17 | ||||
| -rw-r--r-- | src/widgets/sheetview/mod.rs | 110 | ||||
| -rw-r--r-- | src/widgets/statusbar.rs | 85 |
10 files changed, 399 insertions, 153 deletions
diff --git a/src/config/evalsto.rs b/src/config/evalsto.rs new file mode 100644 index 0000000..10b2dd1 --- /dev/null +++ b/src/config/evalsto.rs @@ -0,0 +1,64 @@ +use std::{marker::PhantomData, sync::Arc}; + +use mlua::{FromLua, IntoLua, Lua, Result, Value}; + +use crate::lua::ownedfunction::OwnedFunction; + +#[derive(Clone, Debug)] +pub enum EvalTo<T, A> { + Function(Arc<OwnedFunction>, PhantomData<A>), + Value(T), +} + +impl<T, A> EvalTo<T, A> { + pub fn get<'lua>(&'lua self, args: A, lua: &'lua Lua) -> Result<T> + where + T: FromLua<'lua> + Clone, + A: IntoLua<'lua>, + { + match self { + EvalTo::Function(value, _) => { + let func = value.get(lua); + T::from_lua(func.call(args)?, lua) + } + EvalTo::Value(value) => Ok(value.clone()), + } + } + + pub const fn new(value: T) -> Self { + Self::Value(value) + } +} + +impl<'lua, T, A> FromLua<'lua> for EvalTo<T, A> +where + T: FromLua<'lua> + Clone, + A: IntoLua<'lua>, +{ + fn from_lua( + value: mlua::prelude::LuaValue<'lua>, + lua: &'lua mlua::prelude::Lua, + ) -> Result<Self> { + if value.is_function() { + Ok(Self::Function( + Arc::new(OwnedFunction::new(value, lua)), + PhantomData, + )) + } else { + Ok(Self::Value(T::from_lua(value, lua)?)) + } + } +} + +impl<'lua, T, A> IntoLua<'lua> for EvalTo<T, A> +where + T: FromLua<'lua> + Clone + IntoLua<'lua>, + A: IntoLua<'lua>, +{ + fn into_lua(self, lua: &'lua mlua::prelude::Lua) -> Result<Value<'lua>> { + match self { + EvalTo::Function(value, _) => Ok(value.get(lua).into_lua(lua)?), + EvalTo::Value(value) => value.into_lua(lua), + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index e6c4952..6e88306 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -6,7 +6,9 @@ use self::theme::Theme; pub mod theme; pub mod constants; +pub mod evalsto; +#[derive(Debug)] pub struct GlobalConfig { pub theme: Theme, } diff --git a/src/config/theme/colorpair.rs b/src/config/theme/colorpair.rs deleted file mode 100644 index 77e6af4..0000000 --- a/src/config/theme/colorpair.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::str::FromStr; - -use mlua::{FromLua, IntoLua}; -use ratatui::style::{Color, Styled, Stylize}; - -#[derive(Clone)] -pub struct ColorPair { - pub fg: Color, - pub bg: Color, -} - -impl ColorPair { - pub const fn new(fg: Color, bg: Color) -> Self { - ColorPair { bg, fg } - } - - pub fn apply<S, I>(&self, s: S) -> <I as Styled>::Item - where - S: Styled<Item = I>, - I: Styled, - { - s.bg(self.bg).fg(self.fg) - } -} - -impl<'lua> FromLua<'lua> for ColorPair { - fn from_lua( - value: mlua::prelude::LuaValue<'lua>, - _lua: &'lua mlua::prelude::Lua, - ) -> mlua::prelude::LuaResult<Self> { - if value.is_table() { - let table = value.as_table().unwrap(); - if let Ok(fg) = table.get::<_, LuaColor>(1) { - if let Ok(bg) = table.get::<_, LuaColor>(2) { - return Ok(ColorPair::new(fg.into(), bg.into())); - } - } else if let Ok(fg) = table.get::<_, LuaColor>("fg") { - if let Ok(bg) = table.get::<_, LuaColor>("bg") { - return Ok(ColorPair::new(fg.into(), bg.into())); - } - } - } - - return Err(mlua::Error::runtime("could not parse colorpair")); - } -} - -impl<'lua> IntoLua<'lua> for ColorPair { - fn into_lua( - self, - lua: &'lua mlua::prelude::Lua, - ) -> mlua::prelude::LuaResult<mlua::prelude::LuaValue<'lua>> { - let table = lua.create_table()?; - table.set("fg", LuaColor::from(self.fg))?; - table.set("bg", LuaColor::from(self.bg))?; - Ok(table.into_lua(lua)?) - } -} - -struct LuaColor { - color: Color, -} - -impl LuaColor { - fn new(color: Color) -> Self { - Self { color } - } -} - -impl From<Color> for LuaColor { - fn from(value: Color) -> Self { - LuaColor::new(value) - } -} - -impl Into<Color> for LuaColor { - fn into(self) -> Color { - self.color - } -} - -impl<'lua> FromLua<'lua> for LuaColor { - fn from_lua( - value: mlua::prelude::LuaValue<'lua>, - _lua: &'lua mlua::prelude::Lua, - ) -> mlua::prelude::LuaResult<Self> { - if value.is_string() { - let str = value.as_str().unwrap(); - match Color::from_str(str) { - Ok(color) => Ok(color.into()), - Err(_) => Err(mlua::Error::runtime("color has wrong format")), - } - } else { - Err(mlua::Error::runtime("color has wrong format")) - } - } -} - -impl<'lua> IntoLua<'lua> for LuaColor { - fn into_lua( - self, - lua: &'lua mlua::prelude::Lua, - ) -> mlua::prelude::LuaResult<mlua::prelude::LuaValue<'lua>> { - Ok(self.color.to_string().to_lowercase().into_lua(lua)?) - } -} diff --git a/src/config/theme/mod.rs b/src/config/theme/mod.rs index b7baf3a..35101c5 100644 --- a/src/config/theme/mod.rs +++ b/src/config/theme/mod.rs @@ -5,10 +5,10 @@ use self::sheetview::SheetViewTheme; use super::DUMMY_CONFIG; -pub mod colorpair; +pub mod style; pub mod sheetview; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Theme { pub sheetview: SheetViewTheme, } diff --git a/src/config/theme/sheetview.rs b/src/config/theme/sheetview.rs index de5751a..5590349 100644 --- a/src/config/theme/sheetview.rs +++ b/src/config/theme/sheetview.rs @@ -1,26 +1,23 @@ -use mlua::{IntoLua, Table, UserData}; +use mlua::{IntoLua, UserData}; use ratatui::style::Color; -use crate::config::GlobalConfig; +use crate::{config::{evalsto::EvalTo, GlobalConfig}, sheet::cell::CellRef}; -use super::colorpair::ColorPair; +use super::style::Style; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct SheetViewTheme { - pub cursor: ColorPair, - pub selection: ColorPair, - pub cell: (ColorPair, ColorPair), + pub cursor: Style, + pub selection: Style, + pub cell: EvalTo<Style, CellRef>, } impl SheetViewTheme { pub const fn new() -> Self { Self { - cursor: ColorPair::new(Color::Black, Color::White), - selection: ColorPair::new(Color::White, Color::DarkGray), - cell: ( - ColorPair::new(Color::White, Color::Black), - ColorPair::new(Color::White, Color::DarkGray), - ), + cursor: Style::new().fg(Color::Black).bg(Color::White), + selection: Style::new().fg(Color::White).bg(Color::DarkGray), + cell: EvalTo::Value(Style::new().fg(Color::White).bg(Color::Black)), } } } @@ -38,31 +35,27 @@ macro_rules! cfg_mut { } impl UserData for SheetViewTheme { - fn add_fields<'lua, F: mlua::prelude::LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields<'lua, F: mlua::prelude::LuaUserDataFields<'lua, Self>>(fields: & mut F) { fields.add_field_function_get("cursor", |_, _| Ok(cfg!().cursor.clone())); - fields.add_field_function_set("cursor", |_, _, pair: ColorPair| { + fields.add_field_function_set("cursor", |_, _, pair: Style| { cfg_mut!().cursor = pair; Ok(()) }); fields.add_field_function_get("selection", |_, _| Ok(cfg!().selection.clone())); - fields.add_field_function_set("selection", |_, _, pair: ColorPair| { + fields.add_field_function_set("selection", |_, _, pair: Style| { cfg_mut!().selection = pair; Ok(()) }); fields.add_field_function_get("cell", |lua, _| { - let table = lua.create_table()?; - let sel = cfg!().cell.clone(); - table.set(1, sel.0)?; - table.set(2, sel.1)?; - Ok(table.into_lua(lua)?) + Ok(cfg!().cell.clone().into_lua(lua)?) }); - fields.add_field_function_set("cell", |_, _, pair: Table| { - cfg_mut!().cell = (pair.get(1)?, pair.get(2)?); + fields.add_field_function_set("cell", |_, _, cell: EvalTo<Style, CellRef>| { + cfg_mut!().cell = cell; Ok(()) }); } diff --git a/src/config/theme/style.rs b/src/config/theme/style.rs new file mode 100644 index 0000000..6e8d161 --- /dev/null +++ b/src/config/theme/style.rs @@ -0,0 +1,119 @@ +use std::str::FromStr; + +use mlua::{FromLua, IntoLua}; +use ratatui::style::{Color, Styled, Stylize}; + +#[derive(Clone, Copy, Debug)] +pub struct Style { + pub fg: Option<Color>, + pub bg: Option<Color>, + pub italic: bool, + pub bold: bool, + pub underlined: bool +} + +impl Style { + pub const fn new() -> Self { + Self { + fg: None, + bg: None, + italic: false, + bold: false, + underlined: false, + } + } + + pub const fn fg(mut self, color: Color) -> Self { + self.fg = Some(color); + self + } + + pub const fn bg(mut self, color: Color) -> Self { + self.bg = Some(color); + self + } + + pub fn apply<S, I>(&self, s: S) -> <S as Styled>::Item + where + S: Styled<Item = I>, + I: Styled, + { + let mut style = ratatui::style::Style::default(); + + if let Some(fg) = self.fg { + style = style.fg(fg) + } + + if let Some(bg) = self.bg { + style = style.bg(bg) + } + + if self.italic { + style = style.italic() + } + + if self.bold { + style = style.bold() + } + + if self.underlined { + style = style.underlined() + } + + s.set_style(style) + } +} + +impl<'lua> FromLua<'lua> for Style { + fn from_lua( + value: mlua::prelude::LuaValue<'lua>, + _lua: &'lua mlua::prelude::Lua, + ) -> mlua::prelude::LuaResult<Self> { + if value.is_table() { + let table = value.as_table().unwrap(); + let mut style = Style::new(); + + if let Ok(fg) = table.get::<_, String>("fg").or_else(|_| table.get(1)) { + match Color::from_str(&fg) { + Ok(fg) => style.fg = Some(fg), + Err(_) => return Err(mlua::Error::runtime("could not parse fg")) + } + } + + if let Ok(bg) = table.get::<_, String>("bg").or_else(|_| table.get(2)) { + match Color::from_str(&bg) { + Ok(bg) => style.bg = Some(bg), + Err(_) => return Err(mlua::Error::runtime("could not parse bg")) + } + } + + if let Ok(italic) = table.get::<_, bool>("italic") { + style.italic = italic + } + + if let Ok(bold) = table.get::<_, bool>("bold") { + style.bold = bold + } + + if let Ok(underlined) = table.get::<_, bool>("underlined") { + style.underlined = underlined + } + + return Ok(style) + } + + return Err(mlua::Error::runtime("could not parse style")); + } +} + +impl<'lua> IntoLua<'lua> for Style { + fn into_lua( + self, + lua: &'lua mlua::prelude::Lua, + ) -> mlua::prelude::LuaResult<mlua::prelude::LuaValue<'lua>> { + let table = lua.create_table()?; + table.set("fg", self.fg.map(|s| s.to_string()).into_lua(lua)?)?; + table.set("bg", self.bg.map(|s| s.to_string()).into_lua(lua)?)?; + Ok(table.into_lua(lua)?) + } +} diff --git a/src/lua/mod.rs b/src/lua/mod.rs index 0ddcd7f..7388890 100644 --- a/src/lua/mod.rs +++ b/src/lua/mod.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Borrow, path::Path, sync::{Mutex, MutexGuard}, }; @@ -6,9 +7,10 @@ use std::{ use lazy_static::lazy_static; use mlua::prelude::*; -use crate::sheet::cell::CellRef; +use crate::{config::GlobalConfig, sheet::cell::CellRef}; pub mod iobuffer; +pub mod ownedfunction; pub mod runtime; lazy_static! { @@ -37,6 +39,8 @@ fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> { if let Some(ud) = arg.as_userdata() { if ud.is::<CellRef>() { writer.write(ud.borrow::<CellRef>().unwrap().value().to_string()); + } else if ud.is::<GlobalConfig>() { + writer.write(format!("{:#?}", GlobalConfig::instance().borrow())) } else { writer.write(format!("{:#?}", ud)); } diff --git a/src/lua/ownedfunction.rs b/src/lua/ownedfunction.rs new file mode 100644 index 0000000..a8d81cf --- /dev/null +++ b/src/lua/ownedfunction.rs @@ -0,0 +1,17 @@ +use mlua::{Function, Lua, RegistryKey, Value}; + +#[derive(Debug)] +pub struct OwnedFunction { + key: RegistryKey, +} + +impl OwnedFunction { + pub fn new<'lua>(value: Value<'lua>, lua: &'lua Lua) -> Self { + let key = lua.create_registry_value(value).unwrap(); + Self { key } + } + + pub fn get<'lua>(&self, lua: &'lua Lua) -> Function<'lua> { + lua.registry_value(&self.key).unwrap() + } +} diff --git a/src/widgets/sheetview/mod.rs b/src/widgets/sheetview/mod.rs index 40adfc7..fd0919d 100644 --- a/src/widgets/sheetview/mod.rs +++ b/src/widgets/sheetview/mod.rs @@ -9,7 +9,8 @@ use ratatui::{ }; use crate::{ - config::GlobalConfig, + config::{theme::style, GlobalConfig}, + lua, sheet::{ eval::EvalFunction, register::{Register, SheetId}, @@ -144,7 +145,7 @@ impl SheetView { } pub fn handle_key_event(&mut self, event: KeyEvent) { - match self.mode { + match &self.mode { SheetViewMode::Normal => match event.code { KeyCode::Char('j') => self.move_cursor_by((1, 0)), KeyCode::Char('k') => self.move_cursor_by((-1, 0)), @@ -153,11 +154,15 @@ impl SheetView { KeyCode::Char('v') => { self.selection_anchor = Some(self.cursor.clone()); self.mode = SheetViewMode::Visual - }, + } KeyCode::Char('s') => { self.editor.set_text("function(cell)\n\treturn \"\"\nend"); self.mode = SheetViewMode::Script; } + KeyCode::Char(':') => { + self.mode = SheetViewMode::Command; + self.bar.set_input_mode(true) + } _ => {} }, SheetViewMode::Insert => {} @@ -169,16 +174,42 @@ impl SheetView { KeyCode::Esc | KeyCode::Char('v') => { self.selection_anchor = None; self.mode = SheetViewMode::Normal; - }, + } KeyCode::Char('s') => { self.editor.set_text("function(cell)\n\treturn \"\"\nend"); self.mode = SheetViewMode::Script; } + KeyCode::Char(':') => { + self.mode = SheetViewMode::Command; + self.bar.set_input_mode(true) + } _ => {} }, - SheetViewMode::Command => {} + SheetViewMode::Command => match event.code { + KeyCode::Enter => { + if let Err(error) = lua::get().load(self.bar.input().unwrap_or("")).exec() { + self.mode = SheetViewMode::CommandError(error.to_string()) + } else { + if self.selection_anchor.is_some() { + self.mode = SheetViewMode::Visual; + } else { + self.mode = SheetViewMode::Normal; + } + } + + self.bar.set_input_mode(false); + } + KeyCode::Esc => { + if self.selection_anchor.is_some() { + self.mode = SheetViewMode::Visual; + } else { + self.mode = SheetViewMode::Normal; + } + } + _ => self.bar.handle_keyevent(event), + }, SheetViewMode::CommandError(_) => match event.code { - KeyCode::Esc => self.mode = SheetViewMode::Normal, + KeyCode::Esc | KeyCode::Enter => self.mode = SheetViewMode::Normal, _ => {} }, SheetViewMode::Script => match event.code { @@ -226,7 +257,7 @@ impl SheetView { self.join_process_handle_on_finished(); } SheetViewMode::EditorError(_) => match event.code { - KeyCode::Esc => self.mode = SheetViewMode::Script, + KeyCode::Esc | KeyCode::Enter => self.mode = SheetViewMode::Script, _ => {} }, } @@ -255,11 +286,22 @@ impl SheetView { } } - fn areas(&self, area: Rect) -> (Rect, Rect, Rect, Rect) { + fn areas(&self, area: Rect) -> (Rect, Rect, Rect, Rect, Rect) { let mut sheet = area; let mut editor = Rect::default(); let mut error = Rect::default(); let mut progress = Rect::default(); + let mut command_error = Rect::default(); + + if let SheetViewMode::CommandError(message) = &self.mode { + let layout = Layout::vertical([ + Constraint::Min(1), + Constraint::Length(message.lines().count().min(15) as u16 + 1), + ]) + .split(sheet); + sheet = layout[0]; + command_error = layout[1]; + } if self.is_editor_visible() { let layout = @@ -282,7 +324,7 @@ impl SheetView { } } - (sheet, editor, error, progress) + (sheet, editor, error, progress, command_error) } } @@ -293,7 +335,7 @@ impl Widget for &mut SheetView { { let theme = GlobalConfig::instance().theme.sheetview.clone(); let progress; - let (sheet_area, editor_area, error_area, progress_area) = self.areas(area); + let (sheet_area, editor_area, error_area, progress_area, cmd_error_area) = self.areas(area); let separator = Block::default().borders(Borders::RIGHT).border_style( Style::default() .bg(Color::Rgb(29, 32, 33)) @@ -331,17 +373,20 @@ impl Widget for &mut SheetView { for column in 0..(viewport_columns + 1) as u16 { let (cell_pos_y, cell_pos_x) = (row + self.scroll.0, column + self.scroll.1); - if let Some(cell) = sheet.get_cell(cell_pos_y as usize, cell_pos_x as usize) { - let cell = cell.to_string() + " "; + if let Some(cell_ref) = sheet.get_ref(cell_pos_y as usize, cell_pos_x as usize) + { + let cell = cell_ref.value().to_string() + " "; let line = if (cell_pos_y, cell_pos_x) == self.cursor { theme.cursor.apply(cell.to_line()) } else if self.is_in_selection(cell_pos_y, cell_pos_x) { theme.selection.apply(cell.to_line()) - } else if (row + column) % 2 == 0 { - theme.cell.0.apply(cell.to_line()) } else { - theme.cell.1.apply(cell.to_line()) + theme + .cell + .get(cell_ref, &lua::get()) + .unwrap_or(style::Style::new()) + .apply(cell.to_line()) }; let rect = Rect::new( @@ -357,6 +402,22 @@ impl Widget for &mut SheetView { } } + match &self.mode { + SheetViewMode::Command => { + self.bar.set_left(" COMMAND "); + } + + _ => { + if self.selection_anchor.is_some() { + self.bar.set_left(" VISUAL "); + } else { + self.bar.set_left(" NORMAL "); + self.bar.set_middle_alignment(Alignment::Center); + self.bar.set_middle("Sheet"); + } + } + } + self.bar.render(sheet_area, buf); if self.is_editor_visible() { @@ -383,6 +444,25 @@ impl Widget for &mut SheetView { .render(error_inner, buf); } + if let SheetViewMode::CommandError(message) = &self.mode { + let lines = message.lines().collect::<Vec<_>>(); + + let block = Block::default() + .title_bottom(" Error ") + .title_style(Style::default().on_red()) + .border_style(Style::default().black().on_black()) + .borders(Borders::BOTTOM); + let error_inner = block.inner(cmd_error_area); + block.render(cmd_error_area, buf); + + let text = Text::from_iter(lines.iter().map(|s| s.to_line().white())); + + Paragraph::new(text) + .wrap(Wrap { trim: true }) + .bg(Color::Rgb(70, 25, 25)) + .render(error_inner, buf); + } + progress = sheet.progress(); } diff --git a/src/widgets/statusbar.rs b/src/widgets/statusbar.rs index 97b514d..60b58a6 100644 --- a/src/widgets/statusbar.rs +++ b/src/widgets/statusbar.rs @@ -1,4 +1,10 @@ -use ratatui::{layout::{Alignment, Rect}, style::Style, text::ToLine, widgets::Widget}; +use ratatui::{ + crossterm::event::{KeyCode, KeyEvent}, + layout::{Alignment, Rect}, + style::{Style, Stylize}, + text::{ToLine, ToSpan}, + widgets::Widget, +}; #[derive(Clone)] pub struct StatusBar { @@ -9,6 +15,8 @@ pub struct StatusBar { middle_alignment: Alignment, right: String, right_style: Style, + input: Option<String>, + cursor: u16, } impl StatusBar { @@ -21,6 +29,8 @@ impl StatusBar { middle_alignment: Alignment::Center, right: String::new(), right_style: Style::default(), + input: None, + cursor: 0, } } @@ -105,12 +115,49 @@ impl StatusBar { self.middle_alignment = alignment; } + pub fn set_input_mode(&mut self, input: bool) { + if input { + self.input = Some(String::new()); + self.cursor = 0; + } else { + self.input = None; + } + } + + pub fn input(&self) -> Option<&str> { + self.input.as_ref().map(|s| s.as_str()) + } + pub fn area(&self, rect: Rect) -> Rect { let mut inner = rect.clone(); inner.height -= 1; inner } + pub fn handle_keyevent(&mut self, event: KeyEvent) { + if let Some(input) = &mut self.input { + match event.code { + KeyCode::Char(c) => { + input.insert(self.cursor as usize, c); + self.cursor += 1; + } + KeyCode::Backspace => { + if self.cursor > 0 { + self.cursor -= 1; + input.remove(self.cursor as usize).to_string(); + } + } + KeyCode::Left => { + self.cursor = (self.cursor as i32 - 1).max(0) as u16; + } + KeyCode::Right => { + self.cursor = (self.cursor + 1).min(input.len() as u16); + } + _ => {} + } + } + } + fn layout(&self, mut area: Rect) -> (Rect, Rect, Rect) { area.y += area.height - 1; area.height = 1; @@ -142,11 +189,37 @@ impl Widget for &mut StatusBar { .left_aligned() .style(self.left_style) .render(left, buf); - self.middle - .to_line() - .alignment(self.middle_alignment) - .style(self.middle_style) - .render(middle, buf); + + if let Some(input) = &self.input { + (":".to_owned() + input) + .to_line() + .alignment(Alignment::Left) + .style(self.middle_style) + .render(middle, buf); + + input + .chars() + .nth(self.cursor as usize) + .unwrap_or(' ') + .to_span() + .reversed() + .render( + Rect::new( + middle.x + 1 + self.cursor, + middle.y, + middle.width, + middle.height, + ), + buf, + ); + } else { + self.middle + .to_line() + .alignment(self.middle_alignment) + .style(self.middle_style) + .render(middle, buf); + } + self.right .to_line() .right_aligned() |