Add '> ' prefix before each line of a blockquote, even after line-wrapping
This commit is contained in:
@ -89,7 +89,7 @@ impl Buffer for LogBuffer {
|
||||
.iter()
|
||||
.rev()
|
||||
.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);
|
||||
Text::raw(line)
|
||||
})),
|
||||
|
@ -22,6 +22,7 @@ use futures::StreamExt;
|
||||
use matrix_sdk::async_trait;
|
||||
use nonempty::NonEmpty;
|
||||
use ratatui::text::Text;
|
||||
use smallvec::SmallVec;
|
||||
use sorted_vec::SortedVec;
|
||||
|
||||
use crate::widgets::Prerender;
|
||||
@ -92,7 +93,9 @@ impl PartialOrd for BufferSortKey {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BufferItemContent<'buf> {
|
||||
Text(Text<'buf>),
|
||||
SimpleText(Text<'buf>),
|
||||
/// Pairs of `(padding, content)`
|
||||
Text(Vec<(String, Text<'buf>)>),
|
||||
Divider(Text<'buf>),
|
||||
Empty,
|
||||
}
|
||||
|
@ -58,11 +58,17 @@ use crate::widgets::Prerender;
|
||||
/// Like [`BufferItemContent`] but owned.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OwnedBufferItemContent {
|
||||
Text {
|
||||
SimpleText {
|
||||
event_id: Option<OwnedEventId>,
|
||||
is_message: bool,
|
||||
text: Text<'static>,
|
||||
},
|
||||
Text {
|
||||
event_id: Option<OwnedEventId>,
|
||||
is_message: bool,
|
||||
/// `(padding, content)` pairs
|
||||
text: Vec<(String, Text<'static>)>,
|
||||
},
|
||||
Divider(String),
|
||||
Empty,
|
||||
}
|
||||
@ -71,19 +77,32 @@ impl DynamicUsage for OwnedBufferItemContent {
|
||||
fn dynamic_usage(&self) -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
+ match self {
|
||||
OwnedBufferItemContent::Text { text, .. } => {
|
||||
OwnedBufferItemContent::SimpleText { text, .. } => {
|
||||
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::Empty => 0,
|
||||
}
|
||||
}
|
||||
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
|
||||
let (min, max) = match self {
|
||||
OwnedBufferItemContent::Text { text, .. } => {
|
||||
OwnedBufferItemContent::SimpleText { text, .. } => {
|
||||
let area = text.width() * text.height();
|
||||
(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::Empty => (0, Some(0)),
|
||||
};
|
||||
@ -175,22 +194,22 @@ impl SingleClientRoomBuffer {
|
||||
|
||||
// Like `format!()` but returns OwnedBufferItemContent::Text, with is_message=false
|
||||
macro_rules! text {
|
||||
($($tokens:tt)*) => {
|
||||
($prefix: expr, $($tokens:tt)*) => {
|
||||
OwnedBufferItemContent::Text {
|
||||
event_id: event.event_id().map(ToOwned::to_owned),
|
||||
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
|
||||
macro_rules! msg {
|
||||
($($tokens:tt)*) => {
|
||||
($prefix: expr, $($tokens:tt)*) => {
|
||||
OwnedBufferItemContent::Text {
|
||||
event_id: event.event_id().map(ToOwned::to_owned),
|
||||
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",
|
||||
formatted.format
|
||||
);
|
||||
msg!(" <{}> {}", sender, formatted.body)
|
||||
msg!(" ", "<{}> {}", sender, formatted.body)
|
||||
},
|
||||
MessageType::Text(TextMessageEventContent { body, .. }) => {
|
||||
msg!(" <{}> {}", sender, escape_html(body))
|
||||
msg!(" ", "<{}> {}", sender, escape_html(body))
|
||||
},
|
||||
_ =>
|
||||
// Fallback to text body
|
||||
{
|
||||
msg!(
|
||||
" <{}> {}",
|
||||
" ",
|
||||
"<{}> {}",
|
||||
sender,
|
||||
escape_html(&message.body().replace('\n', "\n "))
|
||||
)
|
||||
},
|
||||
},
|
||||
RedactedMessage => msg!("xx <{}> [redacted]", sender),
|
||||
RedactedMessage => msg!("xx ", "<{}> [redacted]", sender),
|
||||
Sticker(sticker) => msg!(
|
||||
"st <{}> {}",
|
||||
"st ",
|
||||
"<{}> {}",
|
||||
sender,
|
||||
escape_html(&sticker.content().body)
|
||||
),
|
||||
UnableToDecrypt(_) => text!("xx <{}> [unable to decrypt]", sender),
|
||||
UnableToDecrypt(_) => text!("xx ", "<{}> [unable to decrypt]", sender),
|
||||
MembershipChange(change) => {
|
||||
use matrix_sdk_ui::timeline::MembershipChange::*;
|
||||
if change.user_id() == event.sender() {
|
||||
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 {
|
||||
None => text!("--- {} made no discernable changes to themselves", sender),
|
||||
Error => text!(
|
||||
"xxx {} made a change to themselves that made matrix-sdk-ui error",
|
||||
None => text!(
|
||||
"--- ",
|
||||
"{} made no discernable changes to themselves",
|
||||
sender
|
||||
),
|
||||
Joined => text!("--> {} joined", sender),
|
||||
Left => text!("<-- {} left", sender),
|
||||
Banned => text!("-x- {} banned themselves", sender),
|
||||
Unbanned => text!("-x- {} unbanned themselves", sender),
|
||||
Kicked => text!("<!- {} kicked themselves", sender),
|
||||
Invited => text!("-o- {} invited themselves", sender),
|
||||
KickedAndBanned => text!("<!x {} kicked and banned themselves", sender),
|
||||
InvitationAccepted => text!("-o> {} accepted an invite", 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),
|
||||
Error => text!(
|
||||
"xxx ",
|
||||
"{} made a change to themselves that made matrix-sdk-ui error",
|
||||
sender
|
||||
),
|
||||
Joined => text!("--> ", "{} joined", sender),
|
||||
Left => text!("<-- ", "{} left", sender),
|
||||
Banned => text!("-x- ", "{} banned themselves", sender),
|
||||
Unbanned => text!("-x- ", "{} unbanned themselves", sender),
|
||||
Kicked => text!("<!- ", "{} kicked themselves", sender),
|
||||
Invited => text!("-o- ", "{} invited themselves", sender),
|
||||
KickedAndBanned => text!("<!x ", "{} kicked and banned themselves", sender),
|
||||
InvitationAccepted => text!("-o> ", "{} accepted an invite", 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!(
|
||||
"xxx {} made a change matrix-sdk-ui does not support yet",
|
||||
"xxx ",
|
||||
"{} made a change matrix-sdk-ui does not support yet",
|
||||
sender
|
||||
),
|
||||
}
|
||||
} else if change.user_id() == "" {
|
||||
let Some(change_kind) = change.change() else {
|
||||
return text!("--- {} made incomprehensible changes", sender);
|
||||
return text!("--- ", "{} made incomprehensible changes", sender);
|
||||
};
|
||||
match change_kind {
|
||||
None => text!("--- {} made no discernable changes", sender),
|
||||
Error => text!("xxx {} made a change that made matrix-sdk-ui error", sender),
|
||||
None => text!("--- ", "{} made no discernable changes", sender),
|
||||
Error => text!(
|
||||
"xxx ",
|
||||
"{} made a change that made matrix-sdk-ui error",
|
||||
sender
|
||||
),
|
||||
Joined | Left | Banned | Unbanned | Kicked | Invited | KickedAndBanned
|
||||
| InvitationAccepted | InvitationRejected | InvitationRevoked | Knocked
|
||||
| KnockAccepted | KnockRetracted | KnockDenied => {
|
||||
text!("--> {} made a non-sensical change: {:?}", sender, change)
|
||||
text!(
|
||||
"--> ",
|
||||
"{} made a non-sensical change: {:?}",
|
||||
sender,
|
||||
change
|
||||
)
|
||||
},
|
||||
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
|
||||
),
|
||||
}
|
||||
@ -297,35 +338,48 @@ impl SingleClientRoomBuffer {
|
||||
);
|
||||
let target = markup_colored_by_mxid(&target, &target);
|
||||
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 {
|
||||
None => text!("--- {} made no discernable changes to {}", sender, target),
|
||||
None => text!(
|
||||
"--- ",
|
||||
"{} made no discernable changes to {}",
|
||||
sender,
|
||||
target
|
||||
),
|
||||
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,
|
||||
target
|
||||
),
|
||||
Joined | Left => text!(
|
||||
"--> {} made a non-sensical change to {}: {:?}",
|
||||
"--> ",
|
||||
"{} made a non-sensical change to {}: {:?}",
|
||||
sender,
|
||||
target,
|
||||
change
|
||||
),
|
||||
Banned => text!("-x- {} banned {}", sender, target),
|
||||
Unbanned => text!("-x- {} unbanned {}", sender, target),
|
||||
Kicked => text!("<!- {} kicked {}", sender, target),
|
||||
Invited => text!("-o- {} invited {}", sender, target),
|
||||
KickedAndBanned => text!("<!x {} kicked and banned {}", sender, target),
|
||||
InvitationAccepted => text!("-o> {} accepted an invite to {}", sender, target),
|
||||
InvitationRejected => text!("-ox {} rejected an invite to {}", sender, target),
|
||||
InvitationRevoked => text!("--x {} revoked an invite to {}", sender, target),
|
||||
Knocked => text!("-?> {} made {} knock", sender, target),
|
||||
KnockAccepted => text!("-?o {} accepted {}'s knock", sender, target),
|
||||
KnockRetracted => text!("-?x {} retracted {}'s knock", sender, target),
|
||||
KnockDenied => text!("-?x {} denied {}'s knock", sender, target),
|
||||
Banned => text!("-x- ", "{} banned {}", sender, target),
|
||||
Unbanned => text!("-x- ", "{} unbanned {}", sender, target),
|
||||
Kicked => text!("<!- ", "{} kicked {}", sender, target),
|
||||
Invited => text!("-o- ", "{} invited {}", sender, target),
|
||||
KickedAndBanned => text!("<!x ", "{} kicked and banned {}", sender, target),
|
||||
InvitationAccepted => text!("-o> ", "{} accepted an invite to {}", sender, target),
|
||||
InvitationRejected => text!("-ox ", "{} rejected an invite to {}", sender, target),
|
||||
InvitationRevoked => text!("--x ", "{} revoked an invite to {}", sender, target),
|
||||
Knocked => text!("-?> ", "{} made {} knock", sender, target),
|
||||
KnockAccepted => text!("-?o ", "{} accepted {}'s knock", sender, target),
|
||||
KnockRetracted => text!("-?x ", "{} retracted {}'s knock", sender, target),
|
||||
KnockDenied => text!("-?x ", "{} denied {}'s knock", sender, target),
|
||||
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,
|
||||
target
|
||||
),
|
||||
@ -333,13 +387,14 @@ impl SingleClientRoomBuffer {
|
||||
}
|
||||
},
|
||||
|
||||
ProfileChange(_) => text!("--- {} updated their profile", sender),
|
||||
ProfileChange(_) => text!("--- ", "{} updated their profile", sender),
|
||||
OtherState(state) => {
|
||||
if state.state_key() == "" {
|
||||
text!("--- {} changed the room: {:?}", sender, state.content())
|
||||
text!("--- ", "{} changed the room: {:?}", sender, state.content())
|
||||
} else {
|
||||
text!(
|
||||
"--- {} changed {}: {:?}",
|
||||
"--- ",
|
||||
"{} changed {}: {:?}",
|
||||
sender,
|
||||
escape_html(state.state_key()),
|
||||
state.content() // TODO: escape html
|
||||
@ -347,7 +402,8 @@ impl SingleClientRoomBuffer {
|
||||
}
|
||||
},
|
||||
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,
|
||||
escape_html(&event_type.to_string()),
|
||||
error // TODO: escape html
|
||||
@ -358,14 +414,15 @@ impl SingleClientRoomBuffer {
|
||||
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,
|
||||
escape_html(&event_type.to_string()),
|
||||
escape_html(state_key),
|
||||
error // TODO: escape html
|
||||
)
|
||||
},
|
||||
Poll(_) => text!("-?- {} acted on a poll", sender),
|
||||
Poll(_) => text!("-?- ", "{} acted on a poll", sender),
|
||||
}
|
||||
},
|
||||
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => {
|
||||
@ -492,7 +549,7 @@ impl RoomBuffer {
|
||||
.map(|item| {
|
||||
(
|
||||
None,
|
||||
OwnedBufferItemContent::Text {
|
||||
OwnedBufferItemContent::SimpleText {
|
||||
event_id: None,
|
||||
is_message: false,
|
||||
text: Text::raw(format!("Initial item: {:#?}", item)),
|
||||
@ -638,6 +695,9 @@ impl Buffer for RoomBuffer {
|
||||
.rev()
|
||||
.map(|(line_id, line, prerender)| BufferItem {
|
||||
content: match line {
|
||||
OwnedBufferItemContent::SimpleText { text, .. } => {
|
||||
BufferItemContent::SimpleText(text.clone())
|
||||
},
|
||||
OwnedBufferItemContent::Text { text, .. } => BufferItemContent::Text(text.clone()),
|
||||
OwnedBufferItemContent::Divider(text) => BufferItemContent::Divider(Text::raw(text)),
|
||||
OwnedBufferItemContent::Empty => BufferItemContent::Empty,
|
||||
|
@ -26,7 +26,8 @@ use crate::components::Action;
|
||||
use crate::config::{Config, ScrollAmount};
|
||||
use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
|
||||
use crate::widgets::{
|
||||
BacklogItemWidget, BottomAlignedParagraph, Divider, EmptyWidget, OverlappableWidget,
|
||||
BacklogItemWidget, BottomAlignedContainer, BottomAlignedParagraph, Divider, EmptyWidget,
|
||||
OverlappableWidget,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
@ -128,7 +129,17 @@ impl Backlog {
|
||||
|
||||
fn build_widget<'a>(&self, content: BufferItemContent<'a>, scroll: u64) -> BacklogItemWidget<'a> {
|
||||
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) => {
|
||||
if scroll == 0 {
|
||||
Divider::new(Paragraph::new(text).alignment(Alignment::Center)).into()
|
||||
|
@ -95,23 +95,25 @@ fn format_tree(
|
||||
config: &Config,
|
||||
tree: Rc<Node>,
|
||||
state: &mut FormatState,
|
||||
text: &mut Text<'static>,
|
||||
text: &mut Vec<(String, Text<'static>)>,
|
||||
mut previous_sibling_is_block: bool,
|
||||
) -> bool {
|
||||
use markup5ever_rcdom::NodeData::*;
|
||||
let mut state = match &tree.data {
|
||||
Document | Doctype { .. } | Comment { .. } | ProcessingInstruction { .. } => state.to_owned(),
|
||||
Text { contents } => {
|
||||
NodeData::Document
|
||||
| NodeData::Doctype { .. }
|
||||
| NodeData::Comment { .. }
|
||||
| NodeData::ProcessingInstruction { .. } => state.to_owned(),
|
||||
NodeData::Text { contents } => {
|
||||
let s: String = contents.clone().into_inner().into();
|
||||
let s = s.replace('\n', ""); // Lines are insignificant in HTML
|
||||
if previous_sibling_is_block && !s.is_empty() {
|
||||
text.lines.push(Line {
|
||||
spans: vec![Span::styled(state.padding.to_owned(), state.style)],
|
||||
alignment: None,
|
||||
});
|
||||
text.push((state.padding.clone(), Text::raw("")));
|
||||
previous_sibling_is_block = false;
|
||||
}
|
||||
text
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.1
|
||||
.lines
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
@ -119,7 +121,7 @@ fn format_tree(
|
||||
.push(Span::styled(s, state.style));
|
||||
state.to_owned()
|
||||
},
|
||||
Element {
|
||||
NodeData::Element {
|
||||
name: QualName {
|
||||
ns: ns!(html),
|
||||
local: name,
|
||||
@ -173,7 +175,7 @@ fn format_tree(
|
||||
state
|
||||
},
|
||||
local_name!("br") => {
|
||||
text.lines.push(Line::raw(state.padding.to_owned()));
|
||||
text.push((state.padding.clone(), Text::raw("")));
|
||||
state.to_owned()
|
||||
},
|
||||
local_name!("p") => {
|
||||
@ -202,7 +204,7 @@ fn format_tree(
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|child| match &child.data {
|
||||
Element {
|
||||
NodeData::Element {
|
||||
name:
|
||||
QualName {
|
||||
ns: ns!(html),
|
||||
@ -228,11 +230,8 @@ fn format_tree(
|
||||
ListState::None => state.to_owned(),
|
||||
ListState::Unordered => {
|
||||
let mut line = Line::default();
|
||||
line
|
||||
.spans
|
||||
.push(Span::styled(state.padding.to_owned(), state.style));
|
||||
line.spans.push(Span::styled("* ", state.style));
|
||||
text.lines.push(line);
|
||||
text.push((state.padding.to_owned(), line.into()));
|
||||
FormatState {
|
||||
padding: state.padding.to_owned() + " ",
|
||||
..state.to_owned()
|
||||
@ -243,9 +242,6 @@ fn format_tree(
|
||||
digits,
|
||||
} => {
|
||||
let mut line = Line::default();
|
||||
line
|
||||
.spans
|
||||
.push(Span::styled(state.padding.to_owned(), state.style));
|
||||
*counter += 1;
|
||||
line
|
||||
.spans
|
||||
@ -256,7 +252,7 @@ fn format_tree(
|
||||
state.style,
|
||||
));
|
||||
}
|
||||
text.lines.push(line);
|
||||
text.push((state.padding.to_owned(), line.into()));
|
||||
FormatState {
|
||||
padding: state.padding.to_owned() + " " + &" ".repeat(digits.into()),
|
||||
..state.to_owned()
|
||||
@ -278,7 +274,7 @@ fn format_tree(
|
||||
},
|
||||
_ => 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() {
|
||||
@ -292,9 +288,12 @@ fn format_tree(
|
||||
}
|
||||
|
||||
match &tree.data {
|
||||
Document | Doctype { .. } | Comment { .. } | ProcessingInstruction { .. } => {},
|
||||
Text { .. } => {},
|
||||
Element {
|
||||
NodeData::Document
|
||||
| NodeData::Doctype { .. }
|
||||
| NodeData::Comment { .. }
|
||||
| NodeData::ProcessingInstruction { .. } => {},
|
||||
NodeData::Text { .. } => {},
|
||||
NodeData::Element {
|
||||
name: QualName {
|
||||
ns: ns!(html),
|
||||
local: name,
|
||||
@ -308,13 +307,16 @@ fn format_tree(
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Element { .. } => {},
|
||||
NodeData::Element { .. } => {},
|
||||
}
|
||||
|
||||
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(
|
||||
RcDom::default(),
|
||||
Default::default(),
|
||||
@ -323,11 +325,12 @@ pub fn format_html(config: &Config, s: &str) -> Text<'static> {
|
||||
)
|
||||
.one(s)
|
||||
.document;
|
||||
let prefix = Text::raw(prefix);
|
||||
let mut state = FormatState {
|
||||
padding: " ".to_owned(),
|
||||
padding: " ".repeat(prefix.width() + 1), // TODO: make +1 configurable
|
||||
..Default::default()
|
||||
};
|
||||
let mut text = Text::raw("");
|
||||
let mut text = vec![("".to_owned(), prefix)];
|
||||
format_tree(config, tree, &mut state, &mut text, false);
|
||||
text
|
||||
}
|
||||
|
133
src/widgets/bottom_aligned_container.rs
Normal file
133
src/widgets/bottom_aligned_container.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ use enum_dispatch::enum_dispatch;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
mod bottom_aligned_container;
|
||||
pub use bottom_aligned_container::BottomAlignedContainer;
|
||||
mod bottom_aligned_paragraph;
|
||||
pub use bottom_aligned_paragraph::BottomAlignedParagraph;
|
||||
|
||||
@ -47,6 +49,7 @@ pub trait OverlappableWidget {
|
||||
#[enum_dispatch(OverlappableWidget)]
|
||||
pub enum BacklogItemWidget<'a> {
|
||||
Paragraph(BottomAlignedParagraph<'a>),
|
||||
Container(BottomAlignedContainer<'a>),
|
||||
Divider(Divider<'a>),
|
||||
Empty(EmptyWidget),
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ fn test_single_item() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender = Prerender::new();
|
||||
let item = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hello")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hello")),
|
||||
prerender: &prerender,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -91,7 +91,7 @@ fn test_single_item_cached() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender = Prerender::new();
|
||||
let item = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hello")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hello")),
|
||||
prerender: &prerender,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -116,7 +116,7 @@ fn test_single_item_cached() {
|
||||
assert_eq!(prerender.key(), Some(10));
|
||||
|
||||
let item = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hello")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hello")),
|
||||
prerender: &prerender,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -134,12 +134,12 @@ fn test_only_necessary_width() {
|
||||
let prerender1 = Prerender::new();
|
||||
let prerender2 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi\nworld")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw(":)")),
|
||||
content: BufferItemContent::SimpleText(Text::raw(":)")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -165,12 +165,12 @@ fn test_only_necessary_width() {
|
||||
assert_eq!(prerender1.key(), Some(10));
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi\nworld")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw(":)")),
|
||||
content: BufferItemContent::SimpleText(Text::raw(":)")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -195,7 +195,7 @@ fn test_single_item_tight() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender = Prerender::new();
|
||||
let item = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hello")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hello")),
|
||||
prerender: &prerender,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -221,13 +221,13 @@ fn test_two_items() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender1 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let prerender2 = Prerender::new();
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -255,12 +255,12 @@ fn test_two_items_scroll() {
|
||||
let prerender2 = Prerender::new();
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: Some(456),
|
||||
};
|
||||
@ -283,12 +283,12 @@ fn test_two_items_scroll() {
|
||||
bl.scroll_up(1);
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: Some(456),
|
||||
};
|
||||
@ -311,12 +311,12 @@ fn test_two_items_scroll() {
|
||||
bl.scroll_up(1);
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: Some(456),
|
||||
};
|
||||
@ -342,13 +342,13 @@ fn test_two_items_multiline() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender1 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let prerender2 = Prerender::new();
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world\n!")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world\n!")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -374,13 +374,13 @@ fn test_two_items_tight() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender1 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let prerender2 = Prerender::new();
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -405,7 +405,7 @@ fn test_cache_moved() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender1 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -428,13 +428,13 @@ fn test_cache_moved() {
|
||||
|
||||
// New item added at bottom
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
let prerender2 = Prerender::new();
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("world")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("world")),
|
||||
prerender: &prerender2,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -462,17 +462,17 @@ fn test_overflow_and_scroll() {
|
||||
let prerender3 = Prerender::new();
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line1 x")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line1 x")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
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,
|
||||
unique_id: None,
|
||||
};
|
||||
let item3 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line5 z")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line5 z")),
|
||||
prerender: &prerender3,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -498,17 +498,17 @@ fn test_overflow_and_scroll() {
|
||||
bl.scroll_up(1);
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line1 x")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line1 x")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
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,
|
||||
unique_id: None,
|
||||
};
|
||||
let item3 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line5 z")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line5 z")),
|
||||
prerender: &prerender3,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -534,17 +534,17 @@ fn test_overflow_and_scroll() {
|
||||
bl.scroll_up(1);
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line1 x")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line1 x")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
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,
|
||||
unique_id: None,
|
||||
};
|
||||
let item3 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line5 z")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line5 z")),
|
||||
prerender: &prerender3,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -569,17 +569,17 @@ fn test_overflow_and_scroll() {
|
||||
bl.scroll_up(1);
|
||||
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line1 x")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line1 x")),
|
||||
prerender: &prerender1,
|
||||
unique_id: None,
|
||||
};
|
||||
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,
|
||||
unique_id: None,
|
||||
};
|
||||
let item3 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("line5 z")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("line5 z")),
|
||||
prerender: &prerender3,
|
||||
unique_id: None,
|
||||
};
|
||||
@ -608,7 +608,7 @@ fn test_scrolledup_new_line() {
|
||||
let mut bl = Backlog::new(config());
|
||||
let prerender1 = Prerender::new();
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi\nworld")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
@ -632,7 +632,7 @@ fn test_scrolledup_new_line() {
|
||||
// Scroll up one line
|
||||
bl.scroll_up(1);
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi\nworld")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
@ -653,13 +653,13 @@ fn test_scrolledup_new_line() {
|
||||
|
||||
// New item added at bottom, displayed paragraph should not move up
|
||||
let item1 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("hi\nworld")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("hi\nworld")),
|
||||
prerender: &prerender1,
|
||||
unique_id: Some(123),
|
||||
};
|
||||
let prerender2 = Prerender::new();
|
||||
let item2 = BufferItem {
|
||||
content: BufferItemContent::Text(Text::raw("!")),
|
||||
content: BufferItemContent::SimpleText(Text::raw("!")),
|
||||
prerender: &prerender2,
|
||||
unique_id: Some(456),
|
||||
};
|
||||
|
Reference in New Issue
Block a user