Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified img/block-code-points.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/block.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/code-points.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/demo-sequence.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/demo-social.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/inspector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/plane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/range.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/sequence-inspector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/sequence-normalization-result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/sequence-normalization.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/sequence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 83 additions & 6 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod workspace;

use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use unicode_segmentation::UnicodeSegmentation;

use crate::{
app::{AppState, View},
Expand Down Expand Up @@ -68,25 +69,101 @@ pub fn glyph_preview_request(area: Rect, state: &AppState) -> Option<GlyphPrevie
}

fn selectable_list_line(
mut line: Line<'static>,
line: Line<'static>,
selected: bool,
content_width: u16,
colors: SelectionColors,
) -> Line<'static> {
if !selected {
return line;
let line = padded_line(line, usize::from(content_width));
if selected {
line.style(colors.style())
} else {
line
}
}

fn padded_line_content_width(width: usize) -> usize {
width.saturating_sub(2)
}

let padding = usize::from(content_width).saturating_sub(line.width());
line.push_span(Span::raw(" ".repeat(padding)));
line.style(colors.style())
fn padded_line(mut line: Line<'static>, width: usize) -> Line<'static> {
let mut remaining = padded_line_content_width(width);
let mut spans = vec![Span::raw(" ".repeat(width.min(1)))];
for mut span in line.spans {
let mut end = 0;
let mut clipped = false;
for (index, grapheme) in span.content.grapheme_indices(true) {
let grapheme_width = Span::raw(grapheme).width();
if grapheme_width > remaining {
clipped = true;
break;
}
remaining -= grapheme_width;
end = index + grapheme.len();
}
if end < span.content.len() {
span.content = span.content[..end].to_owned().into();
}
spans.push(span);
if clipped {
break;
}
}
spans.push(Span::raw(" ".repeat(remaining + usize::from(width >= 2))));
line.spans = spans;
line
}

#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures;

#[test]
fn list_padding_survives_clipping_wide_and_combining_graphemes() {
use ratatui::{buffer::Buffer, style::Modifier, widgets::Widget};

let theme = crate::ui::theme::ColorTheme::default();
for selected in [false, true] {
let line = Line::from(vec![
Span::styled(
"AB",
ratatui::style::Style::new().add_modifier(Modifier::BOLD),
),
Span::raw("あe\u{0301}👩‍💻TAIL"),
]);
let line = selectable_list_line(line, selected, 8, theme.selection);
assert_eq!(line.to_string(), " ABあe\u{0301} ");
assert_eq!(line.width(), 8);

let area = Rect::new(0, 0, 8, 1);
let mut buffer = Buffer::empty(area);
line.render(area, &mut buffer);
assert_eq!(buffer[(0, 0)].symbol(), " ");
assert_eq!(buffer[(7, 0)].symbol(), " ");
assert!(buffer[(1, 0)].modifier.contains(Modifier::BOLD));
assert_eq!(buffer[(5, 0)].symbol(), "e\u{0301}");
if selected {
for x in [0, 1, 2, 3, 5, 6, 7] {
assert_eq!(buffer[(x, 0)].bg, theme.selection.bg);
}
}
}
}

#[test]
fn list_padding_fits_areas_narrower_than_the_padding() {
for width in 0..=2 {
let line = selectable_list_line(
Line::from("あABC"),
true,
width,
crate::ui::theme::ColorTheme::default().selection,
);
assert_eq!(line.to_string(), " ".repeat(usize::from(width)));
}
}

#[test]
fn normalization_result_uses_the_reference_pane_instead_of_a_glyph_preview() {
use crate::app::{Action, update};
Expand Down
54 changes: 46 additions & 8 deletions src/ui/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use ratatui::{
use crate::app::AppState;
use crate::inspector::InspectorField;
use crate::ui::{
key_value, layout,
key_value, layout, padded_line, padded_line_content_width,
scrollbar::{self, ViewportScrollbar},
theme::ColorTheme,
workspace,
Expand Down Expand Up @@ -113,7 +113,7 @@ struct DocumentBuilder {

impl DocumentBuilder {
fn new(width: usize, color_theme: &ColorTheme) -> Self {
let label_width = LABEL_WIDTH.min(width.saturating_sub(1));
let label_width = LABEL_WIDTH.min(padded_line_content_width(width).saturating_sub(1));
Self {
lines: Vec::new(),
width,
Expand All @@ -136,15 +136,24 @@ impl DocumentBuilder {
}

fn fields(&mut self, label: &str, values: impl IntoIterator<Item = String>, selected: bool) {
self.lines.extend(key_value::property_lines(
let selected_style = selected.then(|| self.color_theme.selection.style());
let lines = key_value::property_lines(
label,
values,
self.width,
padded_line_content_width(self.width),
self.label_width,
Style::new().fg(self.color_theme.muted),
Style::new(),
selected.then(|| self.color_theme.selection.style()),
));
selected_style,
);
self.lines.extend(lines.into_iter().map(|line| {
let line = padded_line(line, self.width);
if let Some(style) = selected_style {
line.style(style)
} else {
line
}
}));
}
}

Expand Down Expand Up @@ -187,7 +196,7 @@ mod tests {
assert!(text.iter().any(|line| line.contains("Na — Narrow")));
assert!(text.iter().any(|line| line.contains("L — Left To Right")));
assert!(text.iter().any(|line| line.contains("\\u0041")));
assert!(text.iter().any(|line| line.ends_with("None")));
assert!(text.iter().any(|line| line.trim_end().ends_with("None")));
}

#[test]
Expand All @@ -207,7 +216,7 @@ mod tests {
assert!(
aliases
.iter()
.any(|line| line.trim_start() == "NUL — abbreviation")
.any(|line| line.trim() == "NUL — abbreviation")
);
assert!(
decomposition
Expand All @@ -220,6 +229,35 @@ mod tests {
}));
}

#[test]
fn wrapped_properties_keep_padding_and_the_complete_value() {
use ratatui::{buffer::Buffer, widgets::Widget};

let theme = ColorTheme::default();
let value = "ABCDEFGHIあe\u{0301}JKLMNOPQRST";
for selected in [false, true] {
let mut builder = DocumentBuilder::new(12, &theme);
builder.fields("Value", [value.to_owned()], selected);
let text = builder.lines[1..]
.iter()
.map(|line| line.to_string().trim().to_owned())
.collect::<String>();
assert_eq!(text, value);

for line in builder.lines {
let area = Rect::new(0, 0, 12, 1);
let mut buffer = Buffer::empty(area);
line.render(area, &mut buffer);
for x in [0, 11] {
assert_eq!(buffer[(x, 0)].symbol(), " ");
if selected {
assert_eq!(buffer[(x, 0)].bg, theme.selection.bg);
}
}
}
}
}

#[test]
fn wraps_values_before_the_viewport_is_applied() {
let wide = InspectorDocument::for_code_point(CodePoint::new(0xd800).unwrap(), 96);
Expand Down
20 changes: 12 additions & 8 deletions src/ui/normalization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use ratatui::{
use crate::{
app::AppState,
normalization::TextMetrics,
ui::{selectable_list_line, theme::ColorTheme},
ui::{padded_line, padded_line_content_width, selectable_list_line, theme::ColorTheme},
unicode::{
CodePoint, GeneralCategory, UnicodeDatabase,
text::{NormalizationForm, TextAnalysis},
Expand Down Expand Up @@ -79,14 +79,18 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState, theme: &ColorThem
.max()
.unwrap()
.max(2);
lines.push(table_row(
"Form vs Original",
&format!(
"{:>cp_width$} {:>byte_width$} {:>gc_width$}",
"CP", "Bytes", "GC"
let table_width = padded_line_content_width(width);
lines.push(padded_line(
table_row(
"Form vs Original",
&format!(
"{:>cp_width$} {:>byte_width$} {:>gc_width$}",
"CP", "Bytes", "GC"
),
table_width,
Style::new().fg(theme.muted),
),
width,
Style::new().fg(theme.muted),
));
for (index, form) in NormalizationForm::ALL.into_iter().enumerate() {
let selected = normalization.form() == form;
Expand All @@ -102,7 +106,7 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState, theme: &ColorThem
metrics.code_points, metrics.bytes, metrics.graphemes
);
lines.push(selectable_list_line(
table_row(&left, &right, width, Style::default()),
table_row(&left, &right, table_width, Style::default()),
selected,
canvas.width,
theme.selection,
Expand Down
25 changes: 18 additions & 7 deletions src/ui/normalization_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{

fn original_width(original: &TextAnalysis) -> u16 {
// Padding, boundary gutter, CP position, spacing and the longest U+XXXXXX.
(17 + original.code_points().len().to_string().len()
(19 + original.code_points().len().to_string().len()
+ original.graphemes().len().to_string().len())
.max(24) as u16
}
Expand Down Expand Up @@ -113,16 +113,27 @@ mod tests {

#[test]
fn original_columns_fit_all_positions_and_scalar_notations() {
for source in ["A\u{0301}".to_owned(), "①".repeat(10_000)] {
for (source, expected_width, result_height) in [
("A\u{0301}".to_owned(), 24, 13),
("\u{10ffff}".repeat(10_000), 29, 8),
] {
let original = TextAnalysis::new(source);
let width = original_width(&original);
let required = 17
+ original.code_points().len().to_string().len()
+ original.graphemes().len().to_string().len();
assert!(usize::from(width) >= required);
assert_eq!(width, expected_width);
let panes = layout::normalization_result(Rect::new(0, 2, 60, 13), width);
assert_eq!(panes.result.height, 13);
assert_eq!(panes.result.height, result_height);
assert!(panes.result.width >= 32);
let content = workspace::primary_section(panes.original).content;
let gutter_width = original.graphemes().len().to_string().len() as u16 + 3;
let row = selectable_list_line(
Line::from(format!("{} U+10FFFF", original.code_points().len())),
true,
content.width.saturating_sub(gutter_width),
ColorTheme::default().selection,
);
assert!(row.to_string().contains("U+10FFFF"));
assert!(row.to_string().starts_with(' '));
assert!(row.to_string().ends_with(' '));
}
}
}
Loading
Loading