pub mod assistant;
pub mod command_palette;
pub mod custom_cursor;
pub mod island;
pub mod scrollbar;
pub mod search;
pub mod trail_cursor;
pub mod utils;
use crate::context::renderable::TerminalSnapshot;
use rio_backend::crosswords::LineDamage;
use rio_backend::event::TerminalDamage;
use taffy::NodeId;
use crate::ansi::CursorShape;
use crate::context::renderable::{Cursor, PendingUpdate, RenderableContent};
use crate::context::ContextManager;
use crate::crosswords::grid::row::Row;
use crate::crosswords::pos::{Column, Line, Pos};
use crate::crosswords::square::{Square, Wide};
use crate::crosswords::style::{Style as CellStyle, StyleFlags, StyleSet};
use rio_backend::config::colors::term::TermColors;
use rio_backend::config::colors::{
term::{List, DIM_FACTOR},
AnsiColor, ColorArray, Colors, NamedColor,
};
use rio_backend::config::navigation::Navigation;
use rio_backend::config::Config;
use rio_backend::event::EventProxy;
use rio_backend::sugarloaf::font_introspector::Attributes;
use rio_backend::sugarloaf::{
drawable_character, is_private_user_area, CursorKind, Graphic, SpanStyle,
SpanStyleDecoration, Stretch, Style, SugarCursor, Sugarloaf, UnderlineInfo,
UnderlineShape, Weight,
};
use std::collections::BTreeSet;
use std::ops::RangeInclusive;
pub struct Renderer {
is_vi_mode_enabled: bool,
is_game_mode_enabled: bool,
draw_bold_text_with_light_colors: bool,
use_drawable_chars: bool,
pub named_colors: Colors,
pub colors: List,
pub navigation: Navigation,
pub margin: rio_backend::config::layout::Margin,
pub island: Option<island::Island>,
pub command_palette: command_palette::CommandPalette,
unfocused_split_opacity: f32,
unfocused_split_fill: Option<ColorArray>,
last_active: Option<NodeId>,
pub config_has_blinking_enabled: bool,
pub config_blinking_interval: u64,
ignore_selection_fg_color: bool,
pub search: search::SearchOverlay,
pub assistant: assistant::AssistantOverlay,
pub scrollbar: scrollbar::Scrollbar,
#[allow(unused)]
pub option_as_alt: String,
#[allow(unused)]
pub macos_use_unified_titlebar: bool,
pub dynamic_background: ([f32; 4], wgpu::Color, bool),
pub custom_mouse_cursor: bool,
pub trail_cursor_enabled: bool,
pub trail_cursor: trail_cursor::TrailCursor,
}
fn styles_are_compatible_for_shaping(a: &SpanStyle, b: &SpanStyle) -> bool {
if a.pua_constraint.is_some() || b.pua_constraint.is_some() {
return false;
}
a.font_id == b.font_id
&& a.color == b.color
&& a.font_attrs == b.font_attrs
&& a.decoration == b.decoration
&& a.decoration_color == b.decoration_color
&& a.cursor == b.cursor
&& a.media == b.media
&& a.drawable_char == b.drawable_char
&& a.font_vars == b.font_vars
&& a.width == b.width
}
#[inline]
fn is_powerline(c: char) -> bool {
('\u{E0B0}'..='\u{E0D7}').contains(&c)
}
fn pua_constraint_width(row: &Row<Square>, col: usize, cols: usize) -> f32 {
if col + 1 >= cols {
return 1.0;
}
if col > 0 {
let prev = row.inner[col - 1].c();
if is_private_user_area(&prev) && !is_powerline(prev) {
return 1.0;
}
}
let next = &row.inner[col + 1];
if next.c() == '\0' || next.c() == ' ' || matches!(next.wide(), Wide::Spacer) {
return 2.0;
}
1.0
}
impl Renderer {
pub fn new(config: &Config) -> Renderer {
let colors = List::from(&config.colors);
let named_colors = config.colors;
let mut dynamic_background =
(named_colors.background.0, named_colors.background.1, false);
if config.window.opacity < 1. {
dynamic_background.1.a = config.window.opacity as f64;
dynamic_background.2 = true;
} else if config.window.background_image.is_some() {
dynamic_background.1 = wgpu::Color::TRANSPARENT;
dynamic_background.2 = true;
}
let island = if config.navigation.is_enabled() {
Some(island::Island::new(
named_colors.tabs,
named_colors.tabs_active,
named_colors.tab_border,
config.navigation.hide_if_single,
))
} else {
None
};
Renderer {
unfocused_split_opacity: config.navigation.unfocused_split_opacity,
unfocused_split_fill: config.navigation.unfocused_split_fill,
last_active: None,
use_drawable_chars: config.fonts.use_drawable_chars,
draw_bold_text_with_light_colors: config.draw_bold_text_with_light_colors,
macos_use_unified_titlebar: config.window.macos_use_unified_titlebar,
config_blinking_interval: config.cursor.blinking_interval.clamp(350, 1200),
option_as_alt: config.option_as_alt.to_lowercase(),
is_vi_mode_enabled: false,
config_has_blinking_enabled: config.cursor.blinking,
ignore_selection_fg_color: config.ignore_selection_fg_color,
colors,
navigation: config.navigation.clone(),
margin: config.margin,
island,
command_palette: {
let mut palette = command_palette::CommandPalette::new();
palette.has_adaptive_theme = config.adaptive_colors.is_some();
palette
},
named_colors,
dynamic_background,
search: search::SearchOverlay::default(),
assistant: assistant::AssistantOverlay::default(),
scrollbar: scrollbar::Scrollbar::new(config.enable_scroll_bar),
is_game_mode_enabled: config.renderer.strategy.is_game(),
custom_mouse_cursor: config.effects.custom_mouse_cursor,
trail_cursor_enabled: config.effects.trail_cursor,
trail_cursor: trail_cursor::TrailCursor::new(),
}
}
#[inline]
pub fn set_active_search(&mut self, active_search: Option<String>) {
self.search.set_active_search(active_search);
}
#[inline]
fn create_style(
&mut self,
square: &Square,
cell_style: &CellStyle,
term_colors: &TermColors,
) -> (SpanStyle, char) {
let flags = cell_style.flags;
let mut foreground_color = self.compute_color(&cell_style.fg, flags, term_colors);
let mut background_color = self.compute_bg_color(cell_style, term_colors);
let content = if square.c() == '\t' || flags.contains(StyleFlags::HIDDEN) {
' '
} else {
square.c()
};
let font_attrs = match (
flags.contains(StyleFlags::ITALIC),
flags.contains(StyleFlags::ITALIC) && flags.contains(StyleFlags::BOLD),
flags.contains(StyleFlags::BOLD),
) {
(true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
(_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
(_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
_ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
};
if flags.contains(StyleFlags::INVERSE) {
std::mem::swap(&mut background_color, &mut foreground_color);
}
let background_color = if self.dynamic_background.2
&& background_color[0] == self.dynamic_background.0[0]
&& background_color[1] == self.dynamic_background.0[1]
&& background_color[2] == self.dynamic_background.0[2]
{
None
} else {
Some(background_color)
};
let (decoration, decoration_color) =
self.compute_decoration(cell_style, term_colors);
(
SpanStyle {
color: foreground_color,
background_color,
font_attrs: font_attrs.into(),
decoration,
decoration_color,
..SpanStyle::default()
},
content,
)
}
#[inline]
fn compute_decoration(
&self,
cell_style: &CellStyle,
term_colors: &TermColors,
) -> (Option<SpanStyleDecoration>, Option<[f32; 4]>) {
let mut decoration = None;
let mut decoration_color = None;
if cell_style.flags.contains(StyleFlags::UNDERLINE) {
decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Regular,
}));
} else if cell_style.flags.contains(StyleFlags::STRIKEOUT) {
decoration = Some(SpanStyleDecoration::Strikethrough);
} else if cell_style.flags.contains(StyleFlags::DOUBLE_UNDERLINE) {
decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: true,
shape: UnderlineShape::Regular,
}));
} else if cell_style.flags.contains(StyleFlags::DOTTED_UNDERLINE) {
decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Dotted,
}));
} else if cell_style.flags.contains(StyleFlags::DASHED_UNDERLINE) {
decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Dashed,
}));
} else if cell_style.flags.contains(StyleFlags::UNDERCURL) {
decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Curly,
}));
}
if decoration.is_some() {
if let Some(color) = cell_style.underline_color {
decoration_color =
Some(self.compute_color(&color, cell_style.flags, term_colors));
}
};
(decoration, decoration_color)
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn is_position_in_hint_matches(
matches: &[rio_backend::crosswords::search::Match],
pos: Pos,
) -> bool {
matches.iter().any(|m| m.contains(&pos))
}
#[allow(clippy::too_many_arguments)]
fn create_line(
&mut self,
sugarloaf: &mut Sugarloaf,
row: &Row<Square>,
style_set: &StyleSet,
extras_table: &rio_backend::crosswords::grid::ExtrasTable,
has_cursor: bool,
line_opt: Option<usize>,
line: Line,
renderable_content: &RenderableContent,
hint_matches: Option<&[rio_backend::crosswords::search::Match]>,
focused_match: &Option<RangeInclusive<Pos>>,
term_colors: &TermColors,
is_active: bool,
) {
let cursor = &renderable_content.cursor;
let selection_range = renderable_content.selection_range;
let columns: usize = row.len();
let mut content = String::with_capacity(columns);
let mut last_style = SpanStyle::default();
let mut font_lookups = Vec::new();
let mut styles_and_chars = Vec::with_capacity(columns);
let mut cached_style_id: Option<crate::crosswords::style::StyleId> = None;
let mut cached_style: CellStyle = CellStyle::default();
let mut cached_bg_only: u64 = u64::MAX;
for column in 0..columns {
let square = &row.inner[column];
if matches!(square.wide(), Wide::Spacer) {
continue;
}
use crate::crosswords::square::ContentTag;
match square.content_tag() {
ContentTag::BgPalette => {
let idx = square.bg_palette_index();
let key = 0x0100_0000 | idx as u64;
if cached_bg_only != key {
cached_style = CellStyle {
bg: AnsiColor::Indexed(idx),
..CellStyle::default()
};
cached_bg_only = key;
cached_style_id = None;
}
}
ContentTag::BgRgb => {
let (r, g, b) = square.bg_rgb();
let key =
0x0200_0000 | ((r as u64) << 16) | ((g as u64) << 8) | b as u64;
if cached_bg_only != key {
cached_style =
CellStyle {
bg: AnsiColor::Spec(
rio_backend::config::colors::ColorRgb { r, g, b },
),
..CellStyle::default()
};
cached_bg_only = key;
cached_style_id = None;
}
}
ContentTag::Codepoint => {
let sid = square.style_id();
if Some(sid) != cached_style_id {
cached_style = style_set.get(sid);
cached_style_id = Some(sid);
cached_bg_only = u64::MAX;
}
}
}
let cell_style = &cached_style;
let (mut style, mut square_content) =
if has_cursor && column == cursor.state.pos.col {
self.create_cursor_style(
square,
cell_style,
cursor,
is_active,
term_colors,
)
} else {
self.create_style(square, cell_style, term_colors)
};
if let Some(ref range) = selection_range {
let pos = Pos::new(line, Column(column));
if range.contains(pos) {
style.color = if self.ignore_selection_fg_color {
self.compute_color(&cell_style.fg, cell_style.flags, term_colors)
} else {
self.named_colors.selection_foreground
};
style.background_color = Some(self.named_colors.selection_background);
}
}
if square_content == '\0' {
if square.has_graphics() {
if let Some(eid) = square.extras_id() {
if let Some(gc) = extras_table
.get(eid)
.and_then(|e| e.graphic.as_ref())
.and_then(|g| g.first())
{
style.media = Some(Graphic {
id: gc.texture.id,
offset_x: gc.offset_x,
offset_y: gc.offset_y,
});
}
}
}
styles_and_chars.push((style, square_content, column));
continue;
}
let should_underline = {
if let Some(highlighted_hint) = &renderable_content.highlighted_hint {
let current_pos = Pos::new(line, Column(column));
highlighted_hint.start <= current_pos
&& current_pos <= highlighted_hint.end
} else {
false
}
};
if should_underline {
style.decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Regular,
}));
}
if selection_range.is_none() {
if let Some(matches) = hint_matches {
let pos = Pos::new(line, Column(column));
if Self::is_position_in_hint_matches(matches, pos) {
let is_focused =
focused_match.as_ref().is_some_and(|fm| fm.contains(&pos));
if is_focused {
style.color =
self.named_colors.search_focused_match_foreground;
style.background_color =
Some(self.named_colors.search_focused_match_background);
} else {
style.color = self.named_colors.search_match_foreground;
style.background_color =
Some(self.named_colors.search_match_background);
}
}
}
}
if let Some(hint_label) = self.find_hint_label_at_position(
renderable_content,
Pos::new(line, Column(column)),
) {
if let Some(label_char) = hint_label.label.first() {
square_content = *label_char;
}
if hint_label.is_first {
style.color = self.named_colors.hint_foreground;
style.background_color = Some(self.named_colors.hint_background);
} else {
style.color = self.named_colors.hint_foreground;
let mut dimmed_bg = self.named_colors.hint_background;
dimmed_bg[0] *= 0.8;
dimmed_bg[1] *= 0.8;
dimmed_bg[2] *= 0.8;
style.background_color = Some(dimmed_bg);
}
use rio_backend::sugarloaf::font_introspector::{Attributes, Weight};
let current_attrs = style.font_attrs;
style.font_attrs = Attributes::new(
current_attrs.stretch(),
Weight::BOLD,
current_attrs.style(),
);
}
if square_content == '\u{10EEEE}' {
square_content = ' ';
}
if square.has_graphics() {
if let Some(eid) = square.extras_id() {
if let Some(gc) = extras_table
.get(eid)
.and_then(|e| e.graphic.as_ref())
.and_then(|g| g.first())
{
style.media = Some(Graphic {
id: gc.texture.id,
offset_x: gc.offset_x,
offset_y: gc.offset_y,
});
}
}
}
if self.use_drawable_chars {
if let Some(character) = drawable_character(square_content) {
style.drawable_char = Some(character);
}
}
let has_drawable_char = style.drawable_char.is_some();
if !has_drawable_char {
if let Some(cached) =
sugarloaf.try_glyph_cached(square_content, style.font_attrs)
{
style.font_id = cached.font_id;
style.width = cached.width;
if cached.is_pua {
style.pua_constraint =
Some(pua_constraint_width(row, column, columns));
}
} else {
font_lookups.push((
styles_and_chars.len(),
square_content,
style.font_attrs,
));
}
}
styles_and_chars.push((style, square_content, column));
}
if !font_lookups.is_empty() {
let queries: Vec<(char, Attributes)> = font_lookups
.iter()
.map(|&(_, ch, attrs)| (ch, attrs))
.collect();
let resolved = sugarloaf.resolve_glyphs_batch(&queries);
for ((style_index, _, _), glyph) in font_lookups.iter().zip(resolved.iter()) {
let column = styles_and_chars[*style_index].2;
let style = &mut styles_and_chars[*style_index].0;
style.font_id = glyph.font_id;
style.width = glyph.width;
if glyph.is_pua {
style.pua_constraint =
Some(pua_constraint_width(row, column, columns));
}
}
}
let builder = sugarloaf.content();
let mut pending_blank_width: f32 = 0.0;
let mut pending_blank_style = SpanStyle::default();
for (style, square_content, column) in styles_and_chars {
let has_media = style.media.is_some();
let is_blank =
(square_content == '\0' || square_content == ' ') && !has_media;
if pending_blank_width > 0.0
&& (!is_blank
|| style.background_color != pending_blank_style.background_color
|| style.cursor != pending_blank_style.cursor
|| style.decoration != pending_blank_style.decoration
|| style.decoration_color != pending_blank_style.decoration_color)
{
let mut rect_style = pending_blank_style;
rect_style.width = pending_blank_width;
if let Some(line) = line_opt {
builder.add_span_as_rect_on_line(line, rect_style);
} else {
builder.add_span_as_rect(rect_style);
}
pending_blank_width = 0.0;
}
if style.drawable_char.is_some() {
if !content.is_empty() {
if let Some(line) = line_opt {
builder.add_span_on_line(line, &content, last_style);
} else {
builder.add_span(&content, last_style);
}
content.clear();
}
last_style = style;
content.push(' '); } else if is_blank {
if !content.is_empty() {
if let Some(line) = line_opt {
builder.add_span_on_line(line, &content, last_style);
} else {
builder.add_span(&content, last_style);
}
content.clear();
}
if pending_blank_width == 0.0 {
pending_blank_style = style;
}
pending_blank_width += 1.0;
last_style = style;
} else {
if !styles_are_compatible_for_shaping(&last_style, &style)
|| last_style.background_color != style.background_color
{
if !content.is_empty() {
if let Some(line) = line_opt {
builder.add_span_on_line(line, &content, last_style);
} else {
builder.add_span(&content, last_style);
}
content.clear();
}
last_style = style;
}
let push_char = if has_media && square_content == '\0' {
' '
} else {
square_content
};
content.push(push_char);
}
if column == (columns - 1) {
if !content.is_empty() {
if let Some(line) = line_opt {
builder.add_span_on_line(line, &content, last_style);
} else {
builder.add_span(&content, last_style);
}
}
if pending_blank_width > 0.0 {
let mut rect_style = pending_blank_style;
rect_style.width = pending_blank_width;
if let Some(line) = line_opt {
builder.add_span_as_rect_on_line(line, rect_style);
} else {
builder.add_span_as_rect(rect_style);
}
}
break;
}
}
if let Some(line) = line_opt {
builder.build_line(line);
} else {
builder.new_line();
}
}
#[inline]
fn compute_color(
&self,
color: &AnsiColor,
flags: StyleFlags,
term_colors: &TermColors,
) -> ColorArray {
let dim = flags.contains(StyleFlags::DIM);
let bold = flags.contains(StyleFlags::BOLD);
match color {
AnsiColor::Named(ansi) => {
match (self.draw_bold_text_with_light_colors, dim, bold) {
(_, true, true)
if ansi == &NamedColor::Foreground
&& self.named_colors.light_foreground.is_none() =>
{
self.color(NamedColor::DimForeground as usize, term_colors)
}
(true, false, true) => {
self.color(ansi.to_light() as usize, term_colors)
}
(_, true, false) | (false, true, true) => {
self.color(ansi.to_dim() as usize, term_colors)
}
_ => self.color(*ansi as usize, term_colors),
}
}
AnsiColor::Spec(rgb) => {
if !dim {
rgb.to_arr()
} else {
rgb.to_arr_with_dim()
}
}
AnsiColor::Indexed(index) => {
let index = match (dim, index) {
(true, 8..=15) => *index as usize - 8,
(true, 0..=7) => NamedColor::DimBlack as usize + *index as usize,
_ => *index as usize,
};
self.color(index, term_colors)
}
}
}
#[inline]
fn compute_bg_color(
&self,
cell_style: &CellStyle,
term_colors: &TermColors,
) -> ColorArray {
let dim = cell_style.flags.contains(StyleFlags::DIM);
let bold = cell_style.flags.contains(StyleFlags::BOLD);
match cell_style.bg {
AnsiColor::Named(ansi) => self.color(ansi as usize, term_colors),
AnsiColor::Spec(rgb) => {
if dim {
(&(rgb * DIM_FACTOR)).into()
} else {
(&rgb).into()
}
}
AnsiColor::Indexed(idx) => {
let idx = match (self.draw_bold_text_with_light_colors, dim, bold, idx) {
(true, false, true, 0..=7) => idx as usize + 8,
(false, true, false, 8..=15) => idx as usize - 8,
(false, true, false, 0..=7) => {
NamedColor::DimBlack as usize + idx as usize
}
_ => idx as usize,
};
self.color(idx, term_colors)
}
}
}
#[inline]
fn create_cursor_style(
&self,
square: &Square,
cell_style: &CellStyle,
cursor: &Cursor,
is_active: bool,
term_colors: &TermColors,
) -> (SpanStyle, char) {
let flags = cell_style.flags;
let font_attrs = match (
flags.contains(StyleFlags::ITALIC),
flags.contains(StyleFlags::ITALIC) && flags.contains(StyleFlags::BOLD),
flags.contains(StyleFlags::BOLD),
) {
(true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
(_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
(_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
_ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
};
let mut color = self.compute_color(&cell_style.fg, flags, term_colors);
let mut background_color = self.compute_bg_color(cell_style, term_colors);
let content = if cursor.is_ime_enabled {
cursor.content
} else if square.c() == '\0' {
' '
} else {
square.c()
};
if flags.contains(StyleFlags::INVERSE) {
std::mem::swap(&mut background_color, &mut color);
}
let has_dynamic_background = self.dynamic_background.2
&& background_color[0] == self.dynamic_background.0[0]
&& background_color[1] == self.dynamic_background.0[1]
&& background_color[2] == self.dynamic_background.0[2];
let background_color = if has_dynamic_background
&& (cursor.state.content != CursorShape::Block && is_active)
{
None
} else {
Some(background_color)
};
match (
cursor.is_ime_enabled,
(cursor.state.content == CursorShape::Block || !is_active),
) {
(_, true) => {
color = self.named_colors.background.0;
}
(true, false) => {
color = self.named_colors.foreground;
}
(false, false) => {}
}
let mut style = SpanStyle {
color,
background_color,
font_attrs: font_attrs.into(),
..SpanStyle::default()
};
let cursor_color = if !self.is_vi_mode_enabled {
term_colors[NamedColor::Cursor].unwrap_or(self.named_colors.cursor)
} else {
self.named_colors.vi_cursor
};
let (decoration, decoration_color) =
self.compute_decoration(cell_style, term_colors);
style.decoration = decoration;
style.decoration_color = decoration_color;
match cursor.state.content {
CursorShape::Underline => {
style.decoration = Some(SpanStyleDecoration::Underline(UnderlineInfo {
is_doubled: false,
shape: UnderlineShape::Regular,
}));
style.decoration_color = Some(cursor_color);
}
CursorShape::Block => {
style.cursor = Some(SugarCursor {
kind: CursorKind::Block,
color: cursor_color,
order: 0,
});
}
CursorShape::Beam => {
style.cursor = Some(SugarCursor {
kind: CursorKind::Caret,
color: cursor_color,
order: 0,
});
}
CursorShape::Hidden => {}
}
if !is_active {
style.decoration = None;
style.cursor = Some(SugarCursor {
kind: CursorKind::HollowBlock,
color: cursor_color,
order: 0,
});
}
(style, content)
}
#[inline]
pub fn set_vi_mode(&mut self, is_vi_mode_enabled: bool) {
self.is_vi_mode_enabled = is_vi_mode_enabled;
}
#[inline]
pub fn color(&self, color: usize, term_colors: &TermColors) -> ColorArray {
term_colors[color].unwrap_or(self.colors[color])
}
#[inline]
pub fn run(
&mut self,
sugarloaf: &mut Sugarloaf,
context_manager: &mut ContextManager<EventProxy>,
focused_match: &Option<RangeInclusive<Pos>>,
) -> Option<crate::context::renderable::WindowUpdate> {
let grid = context_manager.current_grid_mut();
let active_key = grid.current;
let grid_scaled_margin = grid.get_scaled_margin();
let mut has_active_changed = false;
if self.last_active != Some(active_key) {
has_active_changed = true;
self.last_active = Some(active_key);
}
if self.scrollbar.is_enabled() {
self.scrollbar.clear_panel_states();
for grid_context in grid.contexts_mut().values() {
let panel_rect = grid_context.layout_rect;
let ctx = grid_context.context();
let terminal = ctx.terminal.lock();
self.scrollbar
.push_panel_state(scrollbar::PanelScrollState {
rich_text_id: ctx.rich_text_id,
panel_rect,
display_offset: terminal.display_offset(),
history_size: terminal.history_size(),
screen_lines: terminal.screen_lines(),
});
}
}
for (key, grid_context) in grid.contexts_mut().iter_mut() {
let is_active = &active_key == key;
let panel_rect = grid_context.layout_rect;
let context = grid_context.context_mut();
let mut has_ime = false;
if let Some(preedit) = context.ime.preedit() {
if let Some(content) = preedit.text.chars().next() {
context.renderable_content.cursor.content = content;
context.renderable_content.cursor.is_ime_enabled = true;
has_ime = true;
}
}
if !has_ime {
context.renderable_content.cursor.is_ime_enabled = false;
context.renderable_content.cursor.content =
context.renderable_content.cursor.content_ref;
}
let force_full_damage = has_active_changed || self.is_game_mode_enabled;
if !context.renderable_content.pending_update.is_dirty() && !force_full_damage
{
continue;
}
let ui_terminal_damage = context
.renderable_content
.pending_update
.take_terminal_damage();
let _ui_damage = context.renderable_content.pending_update.take_ui_damage();
context.renderable_content.pending_update.reset();
let terminal_snapshot = {
let mut terminal = context.terminal.lock();
terminal.damage_event_in_flight = false;
let pty_damage = terminal.peek_damage_event();
let damage = if force_full_damage {
TerminalDamage::Full
} else {
match (ui_terminal_damage, pty_damage) {
(Some(ui), Some(pty)) => {
PendingUpdate::merge_terminal_damages(ui, pty)
}
(Some(d), None) | (None, Some(d)) => d,
(None, None) => {
drop(terminal);
continue;
}
}
};
terminal.reset_damage();
let snapshot = TerminalSnapshot {
colors: terminal.colors,
display_offset: terminal.display_offset(),
blinking_cursor: terminal.blinking_cursor,
visible_rows: terminal.visible_rows(),
style_set: terminal.grid.style_set.clone(),
extras_table: terminal.grid.extras_table.clone(),
cursor: terminal.cursor(),
damage,
columns: terminal.columns(),
screen_lines: terminal.screen_lines(),
history_size: terminal.history_size(),
kitty_virtual_placements: terminal
.graphics
.kitty_virtual_placements
.clone(),
kitty_images: terminal.graphics.kitty_images.clone(),
kitty_placements: {
let mut placements: Vec<_> = terminal
.graphics
.kitty_placements
.values()
.filter(|p| {
terminal.graphics.kitty_images.contains_key(&p.image_id)
})
.cloned()
.collect();
placements.sort_by_key(|p| p.z_index);
placements
},
kitty_graphics_dirty: terminal.graphics.kitty_graphics_dirty,
};
terminal.graphics.kitty_graphics_dirty = false;
drop(terminal);
snapshot
};
if !terminal_snapshot.kitty_placements.is_empty() {
let line_height = sugarloaf.style().line_height;
let content = sugarloaf.content();
content.sel(context.rich_text_id);
content.clear_image_overlays();
let layout = context.dimension;
let cell_width = layout.dimension.width;
let cell_height = layout.dimension.height * line_height;
let origin_x = panel_rect[0] + grid_scaled_margin.left;
let origin_y = panel_rect[1] + grid_scaled_margin.top;
let history_size = terminal_snapshot.history_size as i64;
let display_offset = terminal_snapshot.display_offset as i64;
let screen_lines = terminal_snapshot.screen_lines as i64;
for p in &terminal_snapshot.kitty_placements {
let screen_row = p.dest_row - (history_size - display_offset);
let image_bottom_row = screen_row + p.rows as i64;
if image_bottom_row <= 0 || screen_row >= screen_lines {
continue;
}
content.push_image_overlay(rio_backend::sugarloaf::GraphicOverlay {
image_id: p.image_id,
x: origin_x + p.dest_col as f32 * cell_width,
y: origin_y + screen_row as f32 * cell_height,
width: p.pixel_width as f32,
height: p.pixel_height as f32,
z_index: p.z_index,
});
}
} else if terminal_snapshot.kitty_graphics_dirty {
let content = sugarloaf.content();
content.sel(context.rich_text_id);
content.clear_image_overlays();
}
let hint_matches = context.renderable_content.hint_matches.as_deref();
context.renderable_content.cursor.state = terminal_snapshot.cursor.clone();
let mut specific_lines: Option<BTreeSet<LineDamage>> = None;
if !force_full_damage {
match &terminal_snapshot.damage {
TerminalDamage::Noop => {
continue;
}
TerminalDamage::Full => {
}
TerminalDamage::Partial(lines) => {
if !lines.is_empty() {
specific_lines = Some(lines.clone());
}
}
TerminalDamage::CursorOnly => {
specific_lines = Some(
[LineDamage {
line: *context.renderable_content.cursor.state.pos.row
as usize,
damaged: true,
}]
.into_iter()
.collect(),
);
}
}
}
let rich_text_id = context.rich_text_id;
let mut is_cursor_visible =
context.renderable_content.cursor.state.is_visible();
context.renderable_content.has_blinking_enabled =
terminal_snapshot.blinking_cursor;
if terminal_snapshot.blinking_cursor {
let has_selection = context.renderable_content.selection_range.is_some();
if !has_selection {
let mut should_blink = true;
if let Some(last_typing_time) = context.renderable_content.last_typing
{
if last_typing_time.elapsed() < std::time::Duration::from_secs(1)
{
should_blink = false;
}
}
if should_blink {
let now = std::time::Instant::now();
let should_toggle = if let Some(last_blink) =
context.renderable_content.last_blink_toggle
{
now.duration_since(last_blink).as_millis()
>= self.config_blinking_interval as u128
} else {
context.renderable_content.is_blinking_cursor_visible = true;
context.renderable_content.last_blink_toggle = Some(now);
false };
if should_toggle {
context.renderable_content.is_blinking_cursor_visible =
!context.renderable_content.is_blinking_cursor_visible;
context.renderable_content.last_blink_toggle = Some(now);
}
} else {
context.renderable_content.is_blinking_cursor_visible = true;
context.renderable_content.last_blink_toggle = None;
}
} else {
context.renderable_content.is_blinking_cursor_visible = true;
context.renderable_content.last_blink_toggle = None;
}
is_cursor_visible = context.renderable_content.is_blinking_cursor_visible;
}
if !is_active && context.renderable_content.cursor.state.is_visible() {
is_cursor_visible = true;
}
match specific_lines {
None => {
sugarloaf.content().sel(rich_text_id).clear();
for (i, row) in terminal_snapshot.visible_rows.iter().enumerate() {
let has_cursor = is_cursor_visible
&& context.renderable_content.cursor.state.pos.row == i;
self.create_line(
sugarloaf,
row,
&terminal_snapshot.style_set,
&terminal_snapshot.extras_table,
has_cursor,
None,
Line((i as i32) - terminal_snapshot.display_offset as i32),
&context.renderable_content,
hint_matches,
focused_match,
&terminal_snapshot.colors,
is_active,
);
}
sugarloaf.content().build();
}
Some(lines) => {
sugarloaf.content().sel(rich_text_id);
for line in lines {
let line = line.line;
let has_cursor = is_cursor_visible
&& context.renderable_content.cursor.state.pos.row == line;
sugarloaf.content().clear_line(line);
if let Some(visible_row) =
terminal_snapshot.visible_rows.get(line)
{
self.create_line(
sugarloaf,
visible_row,
&terminal_snapshot.style_set,
&terminal_snapshot.extras_table,
has_cursor,
Some(line),
Line(
(line as i32)
- terminal_snapshot.display_offset as i32,
),
&context.renderable_content,
hint_matches,
focused_match,
&terminal_snapshot.colors,
is_active,
);
}
}
}
}
}
let window_size = sugarloaf.window_size();
let scale_factor = sugarloaf.scale_factor();
if self.unfocused_split_opacity < 1.0 {
let tint = self
.unfocused_split_fill
.unwrap_or(self.dynamic_background.0);
let dim_color = [
tint[0],
tint[1],
tint[2],
1.0 - self.unfocused_split_opacity,
];
for (key, grid_context) in grid.contexts_mut().iter() {
if &active_key == key {
continue;
}
let panel_rect = grid_context.layout_rect;
let x = (panel_rect[0] + grid_scaled_margin.left) / scale_factor;
let y = (panel_rect[1] + grid_scaled_margin.top) / scale_factor;
let w = panel_rect[2] / scale_factor;
let h = panel_rect[3] / scale_factor;
sugarloaf.rect(None, x, y, w, h, dim_color, -0.05, 3);
}
}
if let Some(island) = &mut self.island {
island.render(
sugarloaf,
(window_size.width, window_size.height, scale_factor),
context_manager,
);
}
self.assistant.render(
sugarloaf,
(window_size.width, window_size.height, scale_factor),
);
self.search.render(
sugarloaf,
(window_size.width, window_size.height, scale_factor),
);
self.command_palette.render(
sugarloaf,
(window_size.width, window_size.height, scale_factor),
);
let grid_scaled_margin_sb = context_manager.get_current_grid_scaled_margin();
let grid_margin_sb = (grid_scaled_margin_sb.left, grid_scaled_margin_sb.top);
let panel_count = self.scrollbar.panel_states().len();
for i in 0..panel_count {
let state = self.scrollbar.panel_states()[i];
self.scrollbar.render(
sugarloaf,
state.panel_rect,
scale_factor,
state.display_offset,
state.history_size,
state.screen_lines,
state.rich_text_id,
grid_margin_sb,
);
}
let grid_scaled_margin = context_manager.get_current_grid_scaled_margin();
for border_object in context_manager.get_panel_borders() {
match border_object {
rio_backend::sugarloaf::Object::Quad(quad) => {
let x = (quad.x + grid_scaled_margin.left) / scale_factor;
let y = (quad.y + grid_scaled_margin.top) / scale_factor;
let width = quad.width / scale_factor;
let height = quad.height / scale_factor;
let corner_radii = [
quad.corner_radii.top_left / scale_factor,
quad.corner_radii.top_right / scale_factor,
quad.corner_radii.bottom_right / scale_factor,
quad.corner_radii.bottom_left / scale_factor,
];
let border_widths = [
quad.border_widths.top / scale_factor,
quad.border_widths.right / scale_factor,
quad.border_widths.bottom / scale_factor,
quad.border_widths.left / scale_factor,
];
sugarloaf.quad(
None,
x,
y,
width,
height,
quad.background_color,
corner_radii,
border_widths,
quad.border_color,
quad.border_style as i32,
-0.1, 1, );
}
rio_backend::sugarloaf::Object::Rect(rect) => {
let x = (rect.x + grid_scaled_margin.left) / scale_factor;
let y = (rect.y + grid_scaled_margin.top) / scale_factor;
let width = rect.width / scale_factor;
let height = rect.height / scale_factor;
sugarloaf.rect(None, x, y, width, height, rect.color, -0.1, 1);
}
_ => {}
}
}
let current_context = context_manager.current_grid_mut().current_mut();
let window_update = if let Some(bg_state) =
current_context.renderable_content.background.take()
{
use crate::context::renderable::BackgroundState;
match bg_state {
BackgroundState::Set(color) => {
sugarloaf.set_background_color(Some(color));
}
BackgroundState::Reset => {
sugarloaf.set_background_color(None);
}
}
Some(crate::context::renderable::WindowUpdate::Background(
bg_state,
))
} else {
None
};
window_update
}
#[inline]
pub fn needs_redraw(&mut self) -> bool {
if self.trail_cursor.is_animating() {
return true;
}
if self.scrollbar.needs_redraw() {
return true;
}
if let Some(island) = &self.island {
island.needs_redraw()
} else {
false
}
}
fn find_hint_label_at_position<'a>(
&self,
renderable_content: &'a RenderableContent,
pos: Pos,
) -> Option<&'a crate::context::renderable::HintLabel> {
renderable_content
.hint_labels
.iter()
.find(|label| label.position == pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rio_backend::crosswords::pos::{Column, Line, Pos};
#[test]
fn test_is_position_in_hint_matches() {
let matches = vec![
Pos::new(Line(0), Column(0))..=Pos::new(Line(0), Column(4)),
Pos::new(Line(1), Column(5))..=Pos::new(Line(1), Column(9)),
Pos::new(Line(5), Column(10))..=Pos::new(Line(5), Column(15)),
];
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(0), Column(0))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(0), Column(2))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(0), Column(4))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(1), Column(5))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(1), Column(7))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(1), Column(9))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(5), Column(10))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(5), Column(15))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(0), Column(5))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(1), Column(4))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(1), Column(10))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(0))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(5), Column(9))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(5), Column(16))
));
}
#[test]
fn test_empty_hint_matches() {
let matches: Vec<rio_backend::crosswords::search::Match> = vec![];
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(0), Column(0))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(10), Column(20))
));
}
#[test]
fn test_single_character_match() {
let matches = vec![Pos::new(Line(3), Column(7))..=Pos::new(Line(3), Column(7))];
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(3), Column(7))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(3), Column(6))
));
assert!(!Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(3), Column(8))
));
}
#[test]
fn test_overlapping_matches() {
let matches = vec![
Pos::new(Line(2), Column(5))..=Pos::new(Line(2), Column(10)),
Pos::new(Line(2), Column(8))..=Pos::new(Line(2), Column(12)),
];
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(8))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(9))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(10))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(5))
));
assert!(Renderer::is_position_in_hint_matches(
&matches,
Pos::new(Line(2), Column(12))
));
}
fn make_row(chars: &[char], cols: usize) -> Row<Square> {
let mut row = Row::<Square>::new(cols);
for (i, &ch) in chars.iter().enumerate() {
row[Column(i)].set_c(ch);
}
row
}
const ICON: char = '\u{F115}';
#[test]
fn test_pua_icon_then_nothing() {
let row = make_row(&[ICON], 10);
assert_eq!(pua_constraint_width(&row, 0, 10), 2.0);
}
#[test]
fn test_pua_icon_followed_by_char() {
let row = make_row(&[ICON, 'a'], 10);
assert_eq!(pua_constraint_width(&row, 0, 10), 1.0);
}
#[test]
fn test_pua_icon_followed_by_space() {
let row = make_row(&[ICON, ' ', 'a'], 10);
assert_eq!(pua_constraint_width(&row, 0, 10), 2.0);
}
#[test]
fn test_pua_two_icons() {
let row = make_row(&[ICON, ICON], 10);
assert_eq!(pua_constraint_width(&row, 0, 10), 1.0);
assert_eq!(pua_constraint_width(&row, 1, 10), 1.0);
}
#[test]
fn test_pua_icon_at_end_of_row() {
let row = make_row(&[' ', ' ', ICON], 3);
assert_eq!(pua_constraint_width(&row, 2, 3), 1.0);
}
#[test]
fn test_pua_icon_space_icon() {
let row = make_row(&[ICON, ' ', ICON], 4);
assert_eq!(pua_constraint_width(&row, 0, 4), 2.0);
assert_eq!(pua_constraint_width(&row, 2, 4), 2.0);
}
#[test]
fn test_pua_char_then_icon_then_nothing() {
let row = make_row(&['z', ICON], 4);
assert_eq!(pua_constraint_width(&row, 1, 4), 2.0);
}
#[test]
fn test_pua_char_then_icon_then_space() {
let row = make_row(&['z', ICON, ' '], 4);
assert_eq!(pua_constraint_width(&row, 1, 4), 2.0);
}
#[test]
fn test_pua_icon_followed_by_no_break_space() {
let row = make_row(&[ICON, '\u{00A0}', 'z'], 10);
assert_eq!(pua_constraint_width(&row, 0, 10), 1.0);
}
const POWERLINE: char = '\u{E0B0}';
#[test]
fn test_pua_icon_then_powerline() {
let row = make_row(&[ICON, POWERLINE], 4);
assert_eq!(pua_constraint_width(&row, 0, 4), 1.0);
}
#[test]
fn test_pua_powerline_then_icon() {
let row = make_row(&[POWERLINE, ICON], 4);
assert_eq!(pua_constraint_width(&row, 1, 4), 2.0);
}
#[test]
fn test_pua_powerline_then_nothing() {
let row = make_row(&[POWERLINE], 4);
assert_eq!(pua_constraint_width(&row, 0, 4), 2.0);
}
#[test]
fn test_pua_powerline_then_space() {
let row = make_row(&[POWERLINE, ' ', 'z'], 4);
assert_eq!(pua_constraint_width(&row, 0, 4), 2.0);
}
}