Add '> ' prefix before each line of a blockquote, even after line-wrapping

This commit is contained in:
Val Lorentz 2023-11-26 12:21:13 +01:00
parent 61f30cfbf3
commit 13965a7e67
8 changed files with 344 additions and 131 deletions

View File

@ -89,7 +89,7 @@ impl Buffer for LogBuffer {
.iter() .iter()
.rev() .rev()
.map(|(line_id, line, prerender)| BufferItem { .map(|(line_id, line, prerender)| BufferItem {
content: BufferItemContent::Text(line.clone().into_text().unwrap_or_else(|e| { content: BufferItemContent::SimpleText(line.clone().into_text().unwrap_or_else(|e| {
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e); tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
Text::raw(line) Text::raw(line)
})), })),

View File

@ -22,6 +22,7 @@ use futures::StreamExt;
use matrix_sdk::async_trait; use matrix_sdk::async_trait;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use ratatui::text::Text; use ratatui::text::Text;
use smallvec::SmallVec;
use sorted_vec::SortedVec; use sorted_vec::SortedVec;
use crate::widgets::Prerender; use crate::widgets::Prerender;
@ -92,7 +93,9 @@ impl PartialOrd for BufferSortKey {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BufferItemContent<'buf> { pub enum BufferItemContent<'buf> {
Text(Text<'buf>), SimpleText(Text<'buf>),
/// Pairs of `(padding, content)`
Text(Vec<(String, Text<'buf>)>),
Divider(Text<'buf>), Divider(Text<'buf>),
Empty, Empty,
} }

View File

@ -58,11 +58,17 @@ use crate::widgets::Prerender;
/// Like [`BufferItemContent`] but owned. /// Like [`BufferItemContent`] but owned.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum OwnedBufferItemContent { pub enum OwnedBufferItemContent {
Text { SimpleText {
event_id: Option<OwnedEventId>, event_id: Option<OwnedEventId>,
is_message: bool, is_message: bool,
text: Text<'static>, text: Text<'static>,
}, },
Text {
event_id: Option<OwnedEventId>,
is_message: bool,
/// `(padding, content)` pairs
text: Vec<(String, Text<'static>)>,
},
Divider(String), Divider(String),
Empty, Empty,
} }
@ -71,19 +77,32 @@ impl DynamicUsage for OwnedBufferItemContent {
fn dynamic_usage(&self) -> usize { fn dynamic_usage(&self) -> usize {
std::mem::size_of::<Self>() std::mem::size_of::<Self>()
+ match self { + match self {
OwnedBufferItemContent::Text { text, .. } => { OwnedBufferItemContent::SimpleText { text, .. } => {
text.width() * text.height() * 4 // FIXME: rough approx text.width() * text.height() * 4 // FIXME: rough approx
}, },
OwnedBufferItemContent::Text { text, .. } => {
text
.iter()
.map(|item| item.1.width() * item.1.height() * 4)
.sum() // FIXME: rough approx
},
OwnedBufferItemContent::Divider(s) => s.dynamic_usage(), OwnedBufferItemContent::Divider(s) => s.dynamic_usage(),
OwnedBufferItemContent::Empty => 0, OwnedBufferItemContent::Empty => 0,
} }
} }
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) { fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
let (min, max) = match self { let (min, max) = match self {
OwnedBufferItemContent::Text { text, .. } => { OwnedBufferItemContent::SimpleText { text, .. } => {
let area = text.width() * text.height(); let area = text.width() * text.height();
(area, Some(area * 12)) // FIXME: rough approx (area, Some(area * 12)) // FIXME: rough approx
}, },
OwnedBufferItemContent::Text { text, .. } => {
let area = text
.iter()
.map(|item| item.1.width() * item.1.height())
.sum();
(area, Some(area * 12)) // FIXME: rough approx
},
OwnedBufferItemContent::Divider(s) => s.dynamic_usage_bounds(), OwnedBufferItemContent::Divider(s) => s.dynamic_usage_bounds(),
OwnedBufferItemContent::Empty => (0, Some(0)), OwnedBufferItemContent::Empty => (0, Some(0)),
}; };
@ -175,22 +194,22 @@ impl SingleClientRoomBuffer {
// Like `format!()` but returns OwnedBufferItemContent::Text, with is_message=false // Like `format!()` but returns OwnedBufferItemContent::Text, with is_message=false
macro_rules! text { macro_rules! text {
($($tokens:tt)*) => { ($prefix: expr, $($tokens:tt)*) => {
OwnedBufferItemContent::Text { OwnedBufferItemContent::Text {
event_id: event.event_id().map(ToOwned::to_owned), event_id: event.event_id().map(ToOwned::to_owned),
is_message: false, is_message: false,
text: format_html(&self.config, &format!($($tokens)*)) text: format_html(&self.config, $prefix, &format!($($tokens)*))
} }
} }
} }
// Like `format!()` but returns OwnedBufferItemContent::Text, with is_message=true // Like `format!()` but returns OwnedBufferItemContent::Text, with is_message=true
macro_rules! msg { macro_rules! msg {
($($tokens:tt)*) => { ($prefix: expr, $($tokens:tt)*) => {
OwnedBufferItemContent::Text { OwnedBufferItemContent::Text {
event_id: event.event_id().map(ToOwned::to_owned), event_id: event.event_id().map(ToOwned::to_owned),
is_message: true, is_message: true,
text: format_html(&self.config, &format!($($tokens)*)) text: format_html(&self.config, $prefix, &format!($($tokens)*))
} }
} }
} }
@ -217,73 +236,95 @@ impl SingleClientRoomBuffer {
"FormattedBody::sanitize_html set type to {:?} instead of Html", "FormattedBody::sanitize_html set type to {:?} instead of Html",
formatted.format formatted.format
); );
msg!(" &lt;{}> {}", sender, formatted.body) msg!(" ", "&lt;{}> {}", sender, formatted.body)
}, },
MessageType::Text(TextMessageEventContent { body, .. }) => { MessageType::Text(TextMessageEventContent { body, .. }) => {
msg!(" &lt;{}> {}", sender, escape_html(body)) msg!(" ", "&lt;{}> {}", sender, escape_html(body))
}, },
_ => _ =>
// Fallback to text body // Fallback to text body
{ {
msg!( msg!(
" &lt;{}> {}", " ",
"&lt;{}> {}",
sender, sender,
escape_html(&message.body().replace('\n', "\n ")) escape_html(&message.body().replace('\n', "\n "))
) )
}, },
}, },
RedactedMessage => msg!("xx &lt;{}> [redacted]", sender), RedactedMessage => msg!("xx ", "&lt;{}> [redacted]", sender),
Sticker(sticker) => msg!( Sticker(sticker) => msg!(
"st &lt;{}> {}", "st ",
"&lt;{}> {}",
sender, sender,
escape_html(&sticker.content().body) escape_html(&sticker.content().body)
), ),
UnableToDecrypt(_) => text!("xx &lt;{}> [unable to decrypt]", sender), UnableToDecrypt(_) => text!("xx ", "&lt;{}> [unable to decrypt]", sender),
MembershipChange(change) => { MembershipChange(change) => {
use matrix_sdk_ui::timeline::MembershipChange::*; use matrix_sdk_ui::timeline::MembershipChange::*;
if change.user_id() == event.sender() { if change.user_id() == event.sender() {
let Some(change_kind) = change.change() else { let Some(change_kind) = change.change() else {
return text!("--- {} made incomprehensible changes to themselves", sender); return text!(
"--- ",
"{} made incomprehensible changes to themselves",
sender
);
}; };
match change_kind { match change_kind {
None => text!("--- {} made no discernable changes to themselves", sender), None => text!(
Error => text!( "--- ",
"xxx {} made a change to themselves that made matrix-sdk-ui error", "{} made no discernable changes to themselves",
sender sender
), ),
Joined => text!("--> {} joined", sender), Error => text!(
Left => text!("&lt;-- {} left", sender), "xxx ",
Banned => text!("-x- {} banned themselves", sender), "{} made a change to themselves that made matrix-sdk-ui error",
Unbanned => text!("-x- {} unbanned themselves", sender), sender
Kicked => text!("&lt;!- {} kicked themselves", sender), ),
Invited => text!("-o- {} invited themselves", sender), Joined => text!("--> ", "{} joined", sender),
KickedAndBanned => text!("&lt;!x {} kicked and banned themselves", sender), Left => text!("&lt;-- ", "{} left", sender),
InvitationAccepted => text!("-o> {} accepted an invite", sender), Banned => text!("-x- ", "{} banned themselves", sender),
InvitationRejected => text!("-ox {} rejected an invite", sender), Unbanned => text!("-x- ", "{} unbanned themselves", sender),
InvitationRevoked => text!("--x {} revoked an invite", sender), Kicked => text!("&lt;!- ", "{} kicked themselves", sender),
Knocked => text!("-?> {} knocked", sender), Invited => text!("-o- ", "{} invited themselves", sender),
KnockAccepted => text!("-?o {} accepted a knock", sender), KickedAndBanned => text!("&lt;!x ", "{} kicked and banned themselves", sender),
KnockRetracted => text!("-?x {} retracted a knock", sender), InvitationAccepted => text!("-o> ", "{} accepted an invite", sender),
KnockDenied => text!("-?x {} denied a knock", sender), InvitationRejected => text!("-ox ", "{} rejected an invite", sender),
InvitationRevoked => text!("--x ", "{} revoked an invite", sender),
Knocked => text!("-?> ", "{} knocked", sender),
KnockAccepted => text!("-?o ", "{} accepted a knock", sender),
KnockRetracted => text!("-?x ", "{} retracted a knock", sender),
KnockDenied => text!("-?x ", "{} denied a knock", sender),
NotImplemented => text!( NotImplemented => text!(
"xxx {} made a change matrix-sdk-ui does not support yet", "xxx ",
"{} made a change matrix-sdk-ui does not support yet",
sender sender
), ),
} }
} else if change.user_id() == "" { } else if change.user_id() == "" {
let Some(change_kind) = change.change() else { let Some(change_kind) = change.change() else {
return text!("--- {} made incomprehensible changes", sender); return text!("--- ", "{} made incomprehensible changes", sender);
}; };
match change_kind { match change_kind {
None => text!("--- {} made no discernable changes", sender), None => text!("--- ", "{} made no discernable changes", sender),
Error => text!("xxx {} made a change that made matrix-sdk-ui error", sender), Error => text!(
"xxx ",
"{} made a change that made matrix-sdk-ui error",
sender
),
Joined | Left | Banned | Unbanned | Kicked | Invited | KickedAndBanned Joined | Left | Banned | Unbanned | Kicked | Invited | KickedAndBanned
| InvitationAccepted | InvitationRejected | InvitationRevoked | Knocked | InvitationAccepted | InvitationRejected | InvitationRevoked | Knocked
| KnockAccepted | KnockRetracted | KnockDenied => { | KnockAccepted | KnockRetracted | KnockDenied => {
text!("--> {} made a non-sensical change: {:?}", sender, change) text!(
"--> ",
"{} made a non-sensical change: {:?}",
sender,
change
)
}, },
NotImplemented => text!( NotImplemented => text!(
"xxx {} made a change matrix-sdk-ui does not support yet", "xxx ",
"{} made a change matrix-sdk-ui does not support yet",
sender sender
), ),
} }
@ -297,35 +338,48 @@ impl SingleClientRoomBuffer {
); );
let target = markup_colored_by_mxid(&target, &target); let target = markup_colored_by_mxid(&target, &target);
let Some(change_kind) = change.change() else { let Some(change_kind) = change.change() else {
return text!("--- {} made incomprehensible changes to {}", sender, target); return text!(
"--- ",
"{} made incomprehensible changes to {}",
sender,
target
);
}; };
match change_kind { match change_kind {
None => text!("--- {} made no discernable changes to {}", sender, target), None => text!(
"--- ",
"{} made no discernable changes to {}",
sender,
target
),
Error => text!( Error => text!(
"xxx {} made a change to {} that made matrix-sdk-ui error", "xxx ",
"{} made a change to {} that made matrix-sdk-ui error",
sender, sender,
target target
), ),
Joined | Left => text!( Joined | Left => text!(
"--> {} made a non-sensical change to {}: {:?}", "--> ",
"{} made a non-sensical change to {}: {:?}",
sender, sender,
target, target,
change change
), ),
Banned => text!("-x- {} banned {}", sender, target), Banned => text!("-x- ", "{} banned {}", sender, target),
Unbanned => text!("-x- {} unbanned {}", sender, target), Unbanned => text!("-x- ", "{} unbanned {}", sender, target),
Kicked => text!("&lt;!- {} kicked {}", sender, target), Kicked => text!("&lt;!- ", "{} kicked {}", sender, target),
Invited => text!("-o- {} invited {}", sender, target), Invited => text!("-o- ", "{} invited {}", sender, target),
KickedAndBanned => text!("&lt;!x {} kicked and banned {}", sender, target), KickedAndBanned => text!("&lt;!x ", "{} kicked and banned {}", sender, target),
InvitationAccepted => text!("-o> {} accepted an invite to {}", sender, target), InvitationAccepted => text!("-o> ", "{} accepted an invite to {}", sender, target),
InvitationRejected => text!("-ox {} rejected an invite to {}", sender, target), InvitationRejected => text!("-ox ", "{} rejected an invite to {}", sender, target),
InvitationRevoked => text!("--x {} revoked an invite to {}", sender, target), InvitationRevoked => text!("--x ", "{} revoked an invite to {}", sender, target),
Knocked => text!("-?> {} made {} knock", sender, target), Knocked => text!("-?> ", "{} made {} knock", sender, target),
KnockAccepted => text!("-?o {} accepted {}'s knock", sender, target), KnockAccepted => text!("-?o ", "{} accepted {}'s knock", sender, target),
KnockRetracted => text!("-?x {} retracted {}'s knock", sender, target), KnockRetracted => text!("-?x ", "{} retracted {}'s knock", sender, target),
KnockDenied => text!("-?x {} denied {}'s knock", sender, target), KnockDenied => text!("-?x ", "{} denied {}'s knock", sender, target),
NotImplemented => text!( NotImplemented => text!(
"xxx {} made a change to {} that matrix-sdk-ui does not support yet", "xxx ",
"{} made a change to {} that matrix-sdk-ui does not support yet",
sender, sender,
target target
), ),
@ -333,13 +387,14 @@ impl SingleClientRoomBuffer {
} }
}, },
ProfileChange(_) => text!("--- {} updated their profile", sender), ProfileChange(_) => text!("--- ", "{} updated their profile", sender),
OtherState(state) => { OtherState(state) => {
if state.state_key() == "" { if state.state_key() == "" {
text!("--- {} changed the room: {:?}", sender, state.content()) text!("--- ", "{} changed the room: {:?}", sender, state.content())
} else { } else {
text!( text!(
"--- {} changed {}: {:?}", "--- ",
"{} changed {}: {:?}",
sender, sender,
escape_html(state.state_key()), escape_html(state.state_key()),
state.content() // TODO: escape html state.content() // TODO: escape html
@ -347,7 +402,8 @@ impl SingleClientRoomBuffer {
} }
}, },
FailedToParseMessageLike { event_type, error } => text!( FailedToParseMessageLike { event_type, error } => text!(
"xxx {} sent a {} message that made matrix-sdk-ui error: {:?}", "xxx ",
"{} sent a {} message that made matrix-sdk-ui error: {:?}",
sender, sender,
escape_html(&event_type.to_string()), escape_html(&event_type.to_string()),
error // TODO: escape html error // TODO: escape html
@ -358,14 +414,15 @@ impl SingleClientRoomBuffer {
error, error,
} => { } => {
text!( text!(
"xxx {} made a {} change to {} that made matrix-sdk-ui error: {:?}", "xxx ",
"{} made a {} change to {} that made matrix-sdk-ui error: {:?}",
sender, sender,
escape_html(&event_type.to_string()), escape_html(&event_type.to_string()),
escape_html(state_key), escape_html(state_key),
error // TODO: escape html error // TODO: escape html
) )
}, },
Poll(_) => text!("-?- {} acted on a poll", sender), Poll(_) => text!("-?- ", "{} acted on a poll", sender),
} }
}, },
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => {
@ -492,7 +549,7 @@ impl RoomBuffer {
.map(|item| { .map(|item| {
( (
None, None,
OwnedBufferItemContent::Text { OwnedBufferItemContent::SimpleText {
event_id: None, event_id: None,
is_message: false, is_message: false,
text: Text::raw(format!("Initial item: {:#?}", item)), text: Text::raw(format!("Initial item: {:#?}", item)),
@ -638,6 +695,9 @@ impl Buffer for RoomBuffer {
.rev() .rev()
.map(|(line_id, line, prerender)| BufferItem { .map(|(line_id, line, prerender)| BufferItem {
content: match line { content: match line {
OwnedBufferItemContent::SimpleText { text, .. } => {
BufferItemContent::SimpleText(text.clone())
},
OwnedBufferItemContent::Text { text, .. } => BufferItemContent::Text(text.clone()), OwnedBufferItemContent::Text { text, .. } => BufferItemContent::Text(text.clone()),
OwnedBufferItemContent::Divider(text) => BufferItemContent::Divider(Text::raw(text)), OwnedBufferItemContent::Divider(text) => BufferItemContent::Divider(Text::raw(text)),
OwnedBufferItemContent::Empty => BufferItemContent::Empty, OwnedBufferItemContent::Empty => BufferItemContent::Empty,

View File

@ -26,7 +26,8 @@ use crate::components::Action;
use crate::config::{Config, ScrollAmount}; use crate::config::{Config, ScrollAmount};
use crate::widgets::prerender::{PrerenderInner, PrerenderValue}; use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
use crate::widgets::{ use crate::widgets::{
BacklogItemWidget, BottomAlignedParagraph, Divider, EmptyWidget, OverlappableWidget, BacklogItemWidget, BottomAlignedContainer, BottomAlignedParagraph, Divider, EmptyWidget,
OverlappableWidget,
}; };
use super::Component; use super::Component;
@ -128,7 +129,17 @@ impl Backlog {
fn build_widget<'a>(&self, content: BufferItemContent<'a>, scroll: u64) -> BacklogItemWidget<'a> { fn build_widget<'a>(&self, content: BufferItemContent<'a>, scroll: u64) -> BacklogItemWidget<'a> {
match content { match content {
BufferItemContent::Text(text) => BottomAlignedParagraph::new(text).scroll(scroll).into(), BufferItemContent::SimpleText(text) => {
BottomAlignedParagraph::new(text).scroll(scroll).into()
},
BufferItemContent::Text(text) => BottomAlignedContainer::new(
text
.into_iter()
.map(|(padding, text)| (padding, BottomAlignedParagraph::new(text)))
.collect(),
)
.scroll(scroll)
.into(),
BufferItemContent::Divider(text) => { BufferItemContent::Divider(text) => {
if scroll == 0 { if scroll == 0 {
Divider::new(Paragraph::new(text).alignment(Alignment::Center)).into() Divider::new(Paragraph::new(text).alignment(Alignment::Center)).into()

View File

@ -95,23 +95,25 @@ fn format_tree(
config: &Config, config: &Config,
tree: Rc<Node>, tree: Rc<Node>,
state: &mut FormatState, state: &mut FormatState,
text: &mut Text<'static>, text: &mut Vec<(String, Text<'static>)>,
mut previous_sibling_is_block: bool, mut previous_sibling_is_block: bool,
) -> bool { ) -> bool {
use markup5ever_rcdom::NodeData::*;
let mut state = match &tree.data { let mut state = match &tree.data {
Document | Doctype { .. } | Comment { .. } | ProcessingInstruction { .. } => state.to_owned(), NodeData::Document
Text { contents } => { | NodeData::Doctype { .. }
| NodeData::Comment { .. }
| NodeData::ProcessingInstruction { .. } => state.to_owned(),
NodeData::Text { contents } => {
let s: String = contents.clone().into_inner().into(); let s: String = contents.clone().into_inner().into();
let s = s.replace('\n', ""); // Lines are insignificant in HTML let s = s.replace('\n', ""); // Lines are insignificant in HTML
if previous_sibling_is_block && !s.is_empty() { if previous_sibling_is_block && !s.is_empty() {
text.lines.push(Line { text.push((state.padding.clone(), Text::raw("")));
spans: vec![Span::styled(state.padding.to_owned(), state.style)],
alignment: None,
});
previous_sibling_is_block = false; previous_sibling_is_block = false;
} }
text text
.last_mut()
.unwrap()
.1
.lines .lines
.last_mut() .last_mut()
.unwrap() .unwrap()
@ -119,7 +121,7 @@ fn format_tree(
.push(Span::styled(s, state.style)); .push(Span::styled(s, state.style));
state.to_owned() state.to_owned()
}, },
Element { NodeData::Element {
name: QualName { name: QualName {
ns: ns!(html), ns: ns!(html),
local: name, local: name,
@ -173,7 +175,7 @@ fn format_tree(
state state
}, },
local_name!("br") => { local_name!("br") => {
text.lines.push(Line::raw(state.padding.to_owned())); text.push((state.padding.clone(), Text::raw("")));
state.to_owned() state.to_owned()
}, },
local_name!("p") => { local_name!("p") => {
@ -202,7 +204,7 @@ fn format_tree(
.borrow() .borrow()
.iter() .iter()
.map(|child| match &child.data { .map(|child| match &child.data {
Element { NodeData::Element {
name: name:
QualName { QualName {
ns: ns!(html), ns: ns!(html),
@ -228,11 +230,8 @@ fn format_tree(
ListState::None => state.to_owned(), ListState::None => state.to_owned(),
ListState::Unordered => { ListState::Unordered => {
let mut line = Line::default(); let mut line = Line::default();
line
.spans
.push(Span::styled(state.padding.to_owned(), state.style));
line.spans.push(Span::styled("* ", state.style)); line.spans.push(Span::styled("* ", state.style));
text.lines.push(line); text.push((state.padding.to_owned(), line.into()));
FormatState { FormatState {
padding: state.padding.to_owned() + " ", padding: state.padding.to_owned() + " ",
..state.to_owned() ..state.to_owned()
@ -243,9 +242,6 @@ fn format_tree(
digits, digits,
} => { } => {
let mut line = Line::default(); let mut line = Line::default();
line
.spans
.push(Span::styled(state.padding.to_owned(), state.style));
*counter += 1; *counter += 1;
line line
.spans .spans
@ -256,7 +252,7 @@ fn format_tree(
state.style, state.style,
)); ));
} }
text.lines.push(line); text.push((state.padding.to_owned(), line.into()));
FormatState { FormatState {
padding: state.padding.to_owned() + " " + &" ".repeat(digits.into()), padding: state.padding.to_owned() + " " + &" ".repeat(digits.into()),
..state.to_owned() ..state.to_owned()
@ -278,7 +274,7 @@ fn format_tree(
}, },
_ => state.to_owned(), _ => state.to_owned(),
}, },
Element { .. } => state.to_owned(), // Element not in the HTML namespace NodeData::Element { .. } => state.to_owned(), // Element not in the HTML namespace
}; };
for subtree in tree.children.borrow().iter() { for subtree in tree.children.borrow().iter() {
@ -292,9 +288,12 @@ fn format_tree(
} }
match &tree.data { match &tree.data {
Document | Doctype { .. } | Comment { .. } | ProcessingInstruction { .. } => {}, NodeData::Document
Text { .. } => {}, | NodeData::Doctype { .. }
Element { | NodeData::Comment { .. }
| NodeData::ProcessingInstruction { .. } => {},
NodeData::Text { .. } => {},
NodeData::Element {
name: QualName { name: QualName {
ns: ns!(html), ns: ns!(html),
local: name, local: name,
@ -308,13 +307,16 @@ fn format_tree(
}, },
_ => {}, _ => {},
}, },
Element { .. } => {}, NodeData::Element { .. } => {},
} }
previous_sibling_is_block previous_sibling_is_block
} }
pub fn format_html(config: &Config, s: &str) -> Text<'static> { /// Returns a list of pairs `(padding, text)`.
///
/// When rendering, the padding should be added left of every line of the text.
pub fn format_html(config: &Config, prefix: &'static str, s: &str) -> Vec<(String, Text<'static>)> {
let tree = parse_fragment( let tree = parse_fragment(
RcDom::default(), RcDom::default(),
Default::default(), Default::default(),
@ -323,11 +325,12 @@ pub fn format_html(config: &Config, s: &str) -> Text<'static> {
) )
.one(s) .one(s)
.document; .document;
let prefix = Text::raw(prefix);
let mut state = FormatState { let mut state = FormatState {
padding: " ".to_owned(), padding: " ".repeat(prefix.width() + 1), // TODO: make +1 configurable
..Default::default() ..Default::default()
}; };
let mut text = Text::raw(""); let mut text = vec![("".to_owned(), prefix)];
format_tree(config, tree, &mut state, &mut text, false); format_tree(config, tree, &mut state, &mut text, false);
text text
} }

View File

@ -0,0 +1,133 @@
/*
* Copyright (C) 2023 Valentin Lorentz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use ratatui::prelude::*;
use ratatui::widgets::{Paragraph, Widget};
use super::{BottomAlignedParagraph, OverlappableWidget};
/// Container of multiple [`BottomAlignedParagraph`]
#[derive(Debug)]
pub struct BottomAlignedContainer<'a> {
/// Pairs of `(padding, content)`
paragraphs: Vec<(String, BottomAlignedParagraph<'a>)>,
/// Number of lines at the bottom that should not be rendered
scroll: u64,
}
impl<'a> BottomAlignedContainer<'a> {
pub fn new(paragraphs: Vec<(String, BottomAlignedParagraph<'a>)>) -> BottomAlignedContainer<'a> {
BottomAlignedContainer {
paragraphs,
scroll: 0,
}
}
/// How many lines should be skipped at the bottom
///
/// This is like [`Paragraph::scroll`](ratatui::widgets::Paragraph::scroll), but it's only vertical.
pub fn scroll(mut self, offset: u64) -> BottomAlignedContainer<'a> {
self.scroll = offset;
self
}
fn padding_width(padding: &Text<'_>, max_width: u16) -> u16 {
usize::min(
(max_width - 10).into(), // TODO: make the minimum content width (10) configurable
padding.width(),
) as u16
}
}
impl<'a> OverlappableWidget for BottomAlignedContainer<'a> {
fn height(&self, width: u16) -> u64 {
self
.paragraphs
.iter()
.map(|(padding, paragraph)| {
paragraph.height(width - Self::padding_width(&Line::raw(padding).into(), width))
})
.sum()
}
fn render_overlap(self, mut area: Rect, buf: &mut Buffer) -> (u16, u16) {
if area.height == 0 {
// Don't even bother
return (0, 0);
}
let mut scroll = self.scroll;
let mut actual_width = 0u16;
let mut actual_height = 0usize;
for (padding, paragraph) in self.paragraphs.into_iter().rev() {
let padding: Text<'_> = Line::raw(&padding).into();
let padding_width = Self::padding_width(&padding, area.width);
assert_eq!(
padding.height(),
1,
"Unexpected padding height: {} (padding={:?})",
padding.height(),
padding
);
// FIXME: paragraph.height() is expensive because it needs to run line-wrapping,
// and paragraph.render_overlap then needs to run it again twice.
let paragraph_height = paragraph.height(area.width - padding_width);
if paragraph_height <= scroll {
// Paragraph is under the viewport, don't render it
scroll -= paragraph_height;
} else {
let paragraph = paragraph.scroll(scroll);
let (actual_paragraph_width, actual_paragraph_height) = paragraph.render_overlap(
Rect {
x: area.x + padding_width,
width: area.width - padding_width,
..area
},
buf,
);
scroll = 0;
// Write the padding on each line the paragraph was rendered on
for y in (u16::max(
area.top(),
area.bottom().saturating_sub(actual_paragraph_height),
))..area.bottom()
{
Paragraph::new(padding.clone()).render(
Rect {
x: area.x,
y,
height: 1,
width: padding_width,
},
buf,
);
}
area.height = area.height.saturating_sub(actual_paragraph_height);
actual_height = actual_height.saturating_add(actual_paragraph_height.into());
actual_width = u16::max(actual_width, padding_width + actual_paragraph_width);
if area.height == 0 {
break;
}
}
}
(actual_width, actual_height as u16)
}
}

View File

@ -18,6 +18,8 @@ use enum_dispatch::enum_dispatch;
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::Widget; use ratatui::widgets::Widget;
mod bottom_aligned_container;
pub use bottom_aligned_container::BottomAlignedContainer;
mod bottom_aligned_paragraph; mod bottom_aligned_paragraph;
pub use bottom_aligned_paragraph::BottomAlignedParagraph; pub use bottom_aligned_paragraph::BottomAlignedParagraph;
@ -47,6 +49,7 @@ pub trait OverlappableWidget {
#[enum_dispatch(OverlappableWidget)] #[enum_dispatch(OverlappableWidget)]
pub enum BacklogItemWidget<'a> { pub enum BacklogItemWidget<'a> {
Paragraph(BottomAlignedParagraph<'a>), Paragraph(BottomAlignedParagraph<'a>),
Container(BottomAlignedContainer<'a>),
Divider(Divider<'a>), Divider(Divider<'a>),
Empty(EmptyWidget), Empty(EmptyWidget),
} }

View File

@ -64,7 +64,7 @@ fn test_single_item() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender = Prerender::new(); let prerender = Prerender::new();
let item = BufferItem { let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")), content: BufferItemContent::SimpleText(Text::raw("hello")),
prerender: &prerender, prerender: &prerender,
unique_id: None, unique_id: None,
}; };
@ -91,7 +91,7 @@ fn test_single_item_cached() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender = Prerender::new(); let prerender = Prerender::new();
let item = BufferItem { let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")), content: BufferItemContent::SimpleText(Text::raw("hello")),
prerender: &prerender, prerender: &prerender,
unique_id: None, unique_id: None,
}; };
@ -116,7 +116,7 @@ fn test_single_item_cached() {
assert_eq!(prerender.key(), Some(10)); assert_eq!(prerender.key(), Some(10));
let item = BufferItem { let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")), content: BufferItemContent::SimpleText(Text::raw("hello")),
prerender: &prerender, prerender: &prerender,
unique_id: None, unique_id: None,
}; };
@ -134,12 +134,12 @@ fn test_only_necessary_width() {
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")), content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw(":)")), content: BufferItemContent::SimpleText(Text::raw(":)")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -165,12 +165,12 @@ fn test_only_necessary_width() {
assert_eq!(prerender1.key(), Some(10)); assert_eq!(prerender1.key(), Some(10));
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")), content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw(":)")), content: BufferItemContent::SimpleText(Text::raw(":)")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -195,7 +195,7 @@ fn test_single_item_tight() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender = Prerender::new(); let prerender = Prerender::new();
let item = BufferItem { let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")), content: BufferItemContent::SimpleText(Text::raw("hello")),
prerender: &prerender, prerender: &prerender,
unique_id: None, unique_id: None,
}; };
@ -221,13 +221,13 @@ fn test_two_items() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -255,12 +255,12 @@ fn test_two_items_scroll() {
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: Some(456), unique_id: Some(456),
}; };
@ -283,12 +283,12 @@ fn test_two_items_scroll() {
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: Some(456), unique_id: Some(456),
}; };
@ -311,12 +311,12 @@ fn test_two_items_scroll() {
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: Some(456), unique_id: Some(456),
}; };
@ -342,13 +342,13 @@ fn test_two_items_multiline() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world\n!")), content: BufferItemContent::SimpleText(Text::raw("world\n!")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -374,13 +374,13 @@ fn test_two_items_tight() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -405,7 +405,7 @@ fn test_cache_moved() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
@ -428,13 +428,13 @@ fn test_cache_moved() {
// New item added at bottom // New item added at bottom
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")), content: BufferItemContent::SimpleText(Text::raw("hi")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")), content: BufferItemContent::SimpleText(Text::raw("world")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
@ -462,17 +462,17 @@ fn test_overflow_and_scroll() {
let prerender3 = Prerender::new(); let prerender3 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")), content: BufferItemContent::SimpleText(Text::raw("line1 x")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")), content: BufferItemContent::SimpleText(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
let item3 = BufferItem { let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")), content: BufferItemContent::SimpleText(Text::raw("line5 z")),
prerender: &prerender3, prerender: &prerender3,
unique_id: None, unique_id: None,
}; };
@ -498,17 +498,17 @@ fn test_overflow_and_scroll() {
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")), content: BufferItemContent::SimpleText(Text::raw("line1 x")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")), content: BufferItemContent::SimpleText(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
let item3 = BufferItem { let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")), content: BufferItemContent::SimpleText(Text::raw("line5 z")),
prerender: &prerender3, prerender: &prerender3,
unique_id: None, unique_id: None,
}; };
@ -534,17 +534,17 @@ fn test_overflow_and_scroll() {
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")), content: BufferItemContent::SimpleText(Text::raw("line1 x")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")), content: BufferItemContent::SimpleText(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
let item3 = BufferItem { let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")), content: BufferItemContent::SimpleText(Text::raw("line5 z")),
prerender: &prerender3, prerender: &prerender3,
unique_id: None, unique_id: None,
}; };
@ -569,17 +569,17 @@ fn test_overflow_and_scroll() {
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")), content: BufferItemContent::SimpleText(Text::raw("line1 x")),
prerender: &prerender1, prerender: &prerender1,
unique_id: None, unique_id: None,
}; };
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")), content: BufferItemContent::SimpleText(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2, prerender: &prerender2,
unique_id: None, unique_id: None,
}; };
let item3 = BufferItem { let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")), content: BufferItemContent::SimpleText(Text::raw("line5 z")),
prerender: &prerender3, prerender: &prerender3,
unique_id: None, unique_id: None,
}; };
@ -608,7 +608,7 @@ fn test_scrolledup_new_line() {
let mut bl = Backlog::new(config()); let mut bl = Backlog::new(config());
let prerender1 = Prerender::new(); let prerender1 = Prerender::new();
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")), content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
@ -632,7 +632,7 @@ fn test_scrolledup_new_line() {
// Scroll up one line // Scroll up one line
bl.scroll_up(1); bl.scroll_up(1);
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")), content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
@ -653,13 +653,13 @@ fn test_scrolledup_new_line() {
// New item added at bottom, displayed paragraph should not move up // New item added at bottom, displayed paragraph should not move up
let item1 = BufferItem { let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")), content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
prerender: &prerender1, prerender: &prerender1,
unique_id: Some(123), unique_id: Some(123),
}; };
let prerender2 = Prerender::new(); let prerender2 = Prerender::new();
let item2 = BufferItem { let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("!")), content: BufferItemContent::SimpleText(Text::raw("!")),
prerender: &prerender2, prerender: &prerender2,
unique_id: Some(456), unique_id: Some(456),
}; };