Support STRIKEOUT, UNDERCURL, and all underline styles

* Add `Metadata` struct to pass bg, underline_color, and flags info via
   metadata.
 * Keep and `IndexSet` of `Metadata` info in Terminal.
 * Use `Metadata` info to render STRIKEOUT, UNDERCURL, and all underline
   styles via `BgRect`.

Signed-off-by: Mohammad AlSaleh <CE.Mohammad.AlSaleh@gmail.com>
This commit is contained in:
Mohammad AlSaleh 2024-01-11 23:22:39 +03:00 committed by Jeremy Soller
parent 7f37ede453
commit 60b55a076d
4 changed files with 206 additions and 25 deletions

1
Cargo.lock generated
View file

@ -871,6 +871,7 @@ dependencies = [
"fork", "fork",
"i18n-embed", "i18n-embed",
"i18n-embed-fl", "i18n-embed-fl",
"indexmap",
"lazy_static", "lazy_static",
"libcosmic", "libcosmic",
"log", "log",

View file

@ -9,6 +9,7 @@ edition = "2021"
alacritty_terminal = "0.20" alacritty_terminal = "0.20"
env_logger = "0.10" env_logger = "0.10"
lazy_static = "1" lazy_static = "1"
indexmap = "2"
log = "0.4" log = "0.4"
serde = { version = "1", features = ["serde_derive"] } serde = { version = "1", features = ["serde_derive"] }
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }

View file

@ -19,6 +19,7 @@ use cosmic::{iced::advanced::graphics::text::font_system, widget::segmented_butt
use cosmic_text::{ use cosmic_text::{
Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Wrap, Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Wrap,
}; };
use indexmap::IndexSet;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
@ -132,6 +133,28 @@ fn linear_color(color: cosmic_text::Color) -> cosmic_text::Color {
) )
} }
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Metadata {
pub bg: cosmic_text::Color,
pub underline_color: cosmic_text::Color,
pub flags: Flags,
}
impl Metadata {
fn new(bg: cosmic_text::Color, underline_color: cosmic_text::Color) -> Self {
let flags = Flags::empty();
Self { bg, underline_color, flags }
}
fn with_underline_color(self, underline_color: cosmic_text::Color) -> Self {
Self { underline_color, ..self }
}
fn with_flags(self, flags: Flags) -> Self {
Self { flags, ..self }
}
}
pub struct Terminal { pub struct Terminal {
default_attrs: Attrs<'static>, default_attrs: Attrs<'static>,
buffer: Arc<Buffer>, buffer: Arc<Buffer>,
@ -146,6 +169,7 @@ pub struct Terminal {
pub needs_update: bool, pub needs_update: bool,
search_regex_opt: Option<RegexSearch>, search_regex_opt: Option<RegexSearch>,
search_value: String, search_value: String,
pub metadata_set: IndexSet<Metadata>,
} }
impl Terminal { impl Terminal {
@ -165,13 +189,22 @@ impl Terminal {
let use_bright_bold = app_config.use_bright_bold; let use_bright_bold = app_config.use_bright_bold;
let metrics = Metrics::new(14.0, 20.0); let metrics = Metrics::new(14.0, 20.0);
let default_bg = convert_color(&colors, Color::Named(NamedColor::Background));
let default_fg = convert_color(&colors, Color::Named(NamedColor::Foreground));
let mut metadata_set = IndexSet::new();
let default_metada = Metadata::new(default_bg, default_fg);
let (default_metada_idx, _) = metadata_set.insert_full(default_metada);
//TODO: set color to default fg //TODO: set color to default fg
let default_attrs = Attrs::new() let default_attrs = Attrs::new()
.family(Family::Monospace) .family(Family::Monospace)
.weight(Weight(font_weight)) .weight(Weight(font_weight))
.stretch(font_stretch) .stretch(font_stretch)
.color(convert_color(&colors, Color::Named(NamedColor::Foreground))) .color(default_fg)
.metadata(convert_color(&colors, Color::Named(NamedColor::Background)).0 as usize); .metadata(default_metada_idx);
let mut buffer = Buffer::new_empty(metrics); let mut buffer = Buffer::new_empty(metrics);
let (cell_width, cell_height) = { let (cell_width, cell_height) = {
@ -219,6 +252,7 @@ impl Terminal {
needs_update: true, needs_update: true,
search_regex_opt: None, search_regex_opt: None,
search_value: String::new(), search_value: String::new(),
metadata_set,
} }
} }
@ -490,14 +524,19 @@ impl Terminal {
} }
} }
if changed { if changed {
self.metadata_set.clear();
let default_bg = convert_color(&colors, Color::Named(NamedColor::Background));
let default_fg = convert_color(&colors, Color::Named(NamedColor::Foreground));
let default_metadata = Metadata::new(default_bg, default_fg);
let (default_metadata_idx, _) = self.metadata_set.insert_full(default_metadata);
self.default_attrs = Attrs::new() self.default_attrs = Attrs::new()
.family(Family::Monospace) .family(Family::Monospace)
.weight(Weight(config.font_weight)) .weight(Weight(config.font_weight))
.stretch(config.typed_font_stretch()) .stretch(config.typed_font_stretch())
.color(convert_color(&colors, Color::Named(NamedColor::Foreground))) .color(default_fg)
.metadata( .metadata(default_metadata_idx);
convert_color(&colors, Color::Named(NamedColor::Background)).0 as usize,
);
update = true; update = true;
} }
} }
@ -544,6 +583,9 @@ impl Terminal {
let instant = Instant::now(); let instant = Instant::now();
// Only keep default
self.metadata_set.truncate(1);
//TODO: is redraw needed after all events? //TODO: is redraw needed after all events?
//TODO: use LineDamageBounds //TODO: use LineDamageBounds
{ {
@ -641,8 +683,16 @@ impl Terminal {
// Convert foreground to linear // Convert foreground to linear
attrs = attrs.color(linear_color(fg)); attrs = attrs.color(linear_color(fg));
// Use metadata as background color
attrs = attrs.metadata(bg.0 as usize); let underline_color = indexed.cell.underline_color()
.map(|c| convert_color(&self.colors, c))
.unwrap_or(fg);
let metadata = Metadata::new(bg, fg)
.with_flags(indexed.cell.flags)
.with_underline_color(underline_color);
let (meta_idx, _) = self.metadata_set.insert_full(metadata);
attrs = attrs.metadata(meta_idx);
//TODO: more flags //TODO: more flags
if indexed.cell.flags.contains(Flags::BOLD) { if indexed.cell.flags.contains(Flags::BOLD) {
attrs = attrs.weight(self.bold_font_weight); attrs = attrs.weight(self.bold_font_weight);

View file

@ -3,7 +3,7 @@
use alacritty_terminal::{ use alacritty_terminal::{
index::{Column as TermColumn, Point as TermPoint, Side as TermSide}, index::{Column as TermColumn, Point as TermPoint, Side as TermSide},
selection::{Selection, SelectionType}, selection::{Selection, SelectionType},
term::TermMode, term::{cell::Flags, TermMode},
}; };
use cosmic::{ use cosmic::{
cosmic_theme::palette::{blend::Compose, WithAlpha}, cosmic_theme::palette::{blend::Compose, WithAlpha},
@ -30,6 +30,7 @@ use cosmic::{
Renderer, Renderer,
}; };
use cosmic_text::LayoutGlyph; use cosmic_text::LayoutGlyph;
use indexmap::IndexSet;
use std::{ use std::{
cell::Cell, cell::Cell,
cmp, cmp,
@ -37,7 +38,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crate::{Terminal, TerminalScroll}; use crate::{Terminal, TerminalScroll, terminal::Metadata};
pub struct TerminalBox<'a, Message> { pub struct TerminalBox<'a, Message> {
terminal: &'a Mutex<Terminal>, terminal: &'a Mutex<Terminal>,
@ -250,7 +251,9 @@ where
// Render default background // Render default background
{ {
let background_color = cosmic_text::Color(terminal.default_attrs().metadata as u32); let meta = &terminal.metadata_set[terminal.default_attrs().metadata];
let background_color = meta.bg;
renderer.fill_quad( renderer.fill_quad(
Quad { Quad {
bounds: Rectangle::new( bounds: Rectangle::new(
@ -273,17 +276,19 @@ where
// Render cell backgrounds that do not match default // Render cell backgrounds that do not match default
terminal.with_buffer(|buffer| { terminal.with_buffer(|buffer| {
for run in buffer.layout_runs() { for run in buffer.layout_runs() {
struct BgRect { struct BgRect<'a> {
default_metadata: usize, default_metadata: usize,
metadata: usize, metadata: usize,
glyph_font_size: f32,
start_x: f32, start_x: f32,
end_x: f32, end_x: f32,
line_height: f32, line_height: f32,
line_top: f32, line_top: f32,
view_position: Point, view_position: Point,
metadata_set: &'a IndexSet<Metadata>,
} }
impl BgRect { impl<'a> BgRect<'a> {
fn update<Renderer: renderer::Renderer>( fn update<Renderer: renderer::Renderer>(
&mut self, &mut self,
glyph: &LayoutGlyph, glyph: &LayoutGlyph,
@ -294,6 +299,7 @@ where
} else { } else {
self.fill(renderer); self.fill(renderer);
self.metadata = glyph.metadata; self.metadata = glyph.metadata;
self.glyph_font_size = glyph.font_size;
self.start_x = glyph.x; self.start_x = glyph.x;
self.end_x = glyph.x + glyph.w; self.end_x = glyph.x + glyph.w;
} }
@ -304,36 +310,159 @@ where
return; return;
} }
let color = cosmic_text::Color(self.metadata as u32); let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
self.view_position + Vector::new(self.start_x, self.line_top),
Size::new(self.end_x - self.start_x, self.line_height),
),
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Color::new( Color::new(
color.r() as f32 / 255.0, color.r() as f32 / 255.0,
color.g() as f32 / 255.0, color.g() as f32 / 255.0,
color.b() as f32 / 255.0, color.b() as f32 / 255.0,
color.a() as f32 / 255.0, color.a() as f32 / 255.0,
), )
};
macro_rules! mk_pos_offset {
($x_offset:expr, $bottom_offset:expr) => {
Vector::new(self.start_x + $x_offset, self.line_top + self.line_height - $bottom_offset)
};
}
macro_rules! mk_quad {
($pos_offset:expr, $style_line_height:expr, $width:expr) => {
Quad {
bounds: Rectangle::new(
self.view_position + $pos_offset,
Size::new($width, $style_line_height),
),
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
}
};
($pos_offset:expr, $style_line_height:expr) => {
mk_quad!($pos_offset, $style_line_height, self.end_x - self.start_x)
};
}
let metadata = &self.metadata_set[self.metadata];
let color = metadata.bg;
renderer.fill_quad(
mk_quad!(mk_pos_offset!(0.0, self.line_height), self.line_height),
cosmic_text_to_iced_color(color),
); );
if !metadata.flags.is_empty() {
let style_line_height = (self.glyph_font_size / 10.0).max(2.0).min(16.0);
let line_color = cosmic_text_to_iced_color(metadata.underline_color);
if metadata.flags.contains(Flags::STRIKEOUT) {
let bottom_offset = (self.line_height - style_line_height) / 2.0;
let pos_offset = mk_pos_offset!(0.0, bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height);
renderer.fill_quad(underline_quad, line_color);
}
if metadata.flags.contains(Flags::UNDERLINE) {
let bottom_offset = style_line_height * 2.0;
let pos_offset = mk_pos_offset!(0.0, bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height);
renderer.fill_quad(underline_quad, line_color);
}
if metadata.flags.contains(Flags::DOUBLE_UNDERLINE) {
let style_line_height = style_line_height / 2.0;
let gap = style_line_height.max(1.5);
let bottom_offset = (style_line_height + gap) * 2.0;
let pos_offset1 = mk_pos_offset!(0.0, bottom_offset);
let underline1_quad = mk_quad!(pos_offset1, style_line_height);
let pos_offset2 = mk_pos_offset!(0.0, bottom_offset/2.0);
let underline2_quad = mk_quad!(pos_offset2, style_line_height);
renderer.fill_quad(underline1_quad, line_color);
renderer.fill_quad(underline2_quad, line_color);
}
if metadata.flags.contains(Flags::DOTTED_UNDERLINE) {
let bottom_offset = style_line_height * 2.0;
let full_width = self.end_x - self.start_x;
let mut accu_width = 0.0;
let mut dot_width = 2.0f32.min(full_width - accu_width);
while accu_width < full_width {
dot_width = dot_width.min(full_width - accu_width);
let pos_offset = mk_pos_offset!(accu_width, bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height, dot_width);
renderer.fill_quad(underline_quad, line_color);
accu_width += 2.0 * dot_width;
}
}
if metadata.flags.contains(Flags::DASHED_UNDERLINE) {
let bottom_offset = style_line_height * 2.0;
let full_width = self.end_x - self.start_x;
let mut accu_width = 0.0;
let mut dash_width = 6.0f32.min(full_width - accu_width);
let gap_width = dash_width / 2.0;
// gap-width dash first
let pos_offset = mk_pos_offset!(accu_width, bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height, gap_width);
renderer.fill_quad(underline_quad, line_color);
accu_width += gap_width * 2.0;
while accu_width < full_width {
dash_width = dash_width.min(full_width - accu_width);
let pos_offset = mk_pos_offset!(accu_width, bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height, dash_width);
renderer.fill_quad(underline_quad, line_color);
accu_width += dash_width + gap_width;
}
}
if metadata.flags.contains(Flags::UNDERCURL) {
let style_line_height = style_line_height.floor();
let bottom_offset = style_line_height * 1.5;
let full_width = self.end_x - self.start_x;
let mut accu_width = 0.0;
let mut dot_width = 1.0f32.min(full_width - accu_width);
while accu_width < full_width {
dot_width = dot_width.min(full_width - accu_width);
let dot_bottom_offset = match accu_width as u32 % 8 {
3 | 4 | 5 => bottom_offset + style_line_height,
2 | 6 => bottom_offset + 2.0 * style_line_height / 3.0,
1 | 7 => bottom_offset + 1.0 * style_line_height / 3.0,
_ => bottom_offset,
};
let pos_offset = mk_pos_offset!(accu_width, dot_bottom_offset);
let underline_quad = mk_quad!(pos_offset, style_line_height, dot_width);
renderer.fill_quad(underline_quad, line_color);
accu_width += dot_width;
}
}
}
} }
} }
let default_metadata = terminal.default_attrs().metadata; let default_metadata = terminal.default_attrs().metadata;
let metadata_set = &terminal.metadata_set;
let mut bg_rect = BgRect { let mut bg_rect = BgRect {
default_metadata, default_metadata,
metadata: default_metadata, metadata: default_metadata,
glyph_font_size: 0.0,
start_x: 0.0, start_x: 0.0,
end_x: 0.0, end_x: 0.0,
line_height: buffer.metrics().line_height, line_height: buffer.metrics().line_height,
line_top: run.line_top, line_top: run.line_top,
view_position, view_position,
metadata_set,
}; };
for glyph in run.glyphs.iter() { for glyph in run.glyphs.iter() {
bg_rect.update(glyph, renderer); bg_rect.update(glyph, renderer);