From 092284b062a2774cb039577b170ce9281a4025d0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Jun 2022 19:21:42 -0700 Subject: [PATCH] Fully functional background colors :D --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 + crates/terminal/print256color.sh | 96 ++++++ crates/terminal/src/terminal_element.rs | 391 +++++++++++++++--------- crates/terminal/truecolor.sh | 19 ++ 5 files changed, 359 insertions(+), 150 deletions(-) create mode 100755 crates/terminal/print256color.sh create mode 100755 crates/terminal/truecolor.sh diff --git a/Cargo.lock b/Cargo.lock index 1a59f23918..85f3fc3a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4880,6 +4880,7 @@ dependencies = [ "editor", "futures", "gpui", + "itertools", "mio-extras", "ordered-float", "project", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 175c741421..0bbc056922 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -20,6 +20,8 @@ smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" ordered-float = "2.1.1" +itertools = "0.10" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/print256color.sh b/crates/terminal/print256color.sh new file mode 100755 index 0000000000..99e3d8c9f9 --- /dev/null +++ b/crates/terminal/print256color.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Tom Hale, 2016. MIT Licence. +# Print out 256 colours, with each number printed in its corresponding colour +# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 + +set -eu # Fail on errors or undeclared variables + +printable_colours=256 + +# Return a colour that contrasts with the given colour +# Bash only does integer division, so keep it integral +function contrast_colour { + local r g b luminance + colour="$1" + + if (( colour < 16 )); then # Initial 16 ANSI colours + (( colour == 0 )) && printf "15" || printf "0" + return + fi + + # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 + if (( colour > 231 )); then # Greyscale ramp + (( colour < 244 )) && printf "15" || printf "0" + return + fi + + # All other colours: + # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] + # See http://stackoverflow.com/a/27165165/5353461 + + # r=$(( (colour-16) / 36 )) + g=$(( ((colour-16) % 36) / 6 )) + # b=$(( (colour-16) % 6 )) + + # If luminance is bright, print number in black, white otherwise. + # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 + (( g > 2)) && printf "0" || printf "15" + return + + # Uncomment the below for more precise luminance calculations + + # # Calculate percieved brightness + # # See https://www.w3.org/TR/AERT#color-contrast + # # and http://www.itu.int/rec/R-REC-BT.601 + # # Luminance is in range 0..5000 as each value is 0..5 + # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) + # (( $luminance > 2500 )) && printf "0" || printf "15" +} + +# Print a coloured block with the number of that colour +function print_colour { + local colour="$1" contrast + contrast=$(contrast_colour "$1") + printf "\e[48;5;%sm" "$colour" # Start block of colour + printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number + printf "\e[0m " # Reset colour +} + +# Starting at $1, print a run of $2 colours +function print_run { + local i + for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do + print_colour "$i" + done + printf " " +} + +# Print blocks of colours +function print_blocks { + local start="$1" i + local end="$2" # inclusive + local block_cols="$3" + local block_rows="$4" + local blocks_per_line="$5" + local block_length=$((block_cols * block_rows)) + + # Print sets of blocks + for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do + printf "\n" # Space before each set of blocks + # For each block row + for (( row = 0; row < block_rows; row++ )) do + # Print block columns for all blocks on the line + for (( block = 0; block < blocks_per_line; block++ )) do + print_run $(( i + (block * block_length) )) "$block_cols" + done + (( i += block_cols )) # Prepare to print the next row + printf "\n" + done + done +} + +print_run 0 16 # The first 16 colours are spread over the whole spectrum +printf "\n" +print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive +print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d81292d0c2..5124a9ea71 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -14,39 +14,76 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, MouseRegion, PaintContext, Quad, WeakViewHandle, + Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, }; +use itertools::Itertools; use ordered_float::OrderedFloat; use settings::Settings; -use std::rc::Rc; +use std::{iter, rc::Rc}; use theme::TerminalStyle; use crate::{Input, ScrollTerminal, Terminal}; +///Scrolling is unbearably sluggish by default. Alacritty supports a configurable +///Scroll multiplier that is set to 3 by default. This will be removed when I +///Implement scroll bars. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +///Used to display the grid as passed to Alacritty and the TTY. +///Useful for debugging inconsistencies between behavior and display #[cfg(debug_assertions)] const DEBUG_GRID: bool = false; +///The GPUI element that paints the terminal. pub struct TerminalEl { view: WeakViewHandle, } +///Represents a span of cells in a single line in the terminal's grid. +///This is used for drawing background rectangles +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct LineSpan { + start: i32, + end: i32, + line: usize, + color: Color, +} + +impl LineSpan { + ///Creates a new LineSpan. `start` must be <= `end`. + ///If `start` == `end`, then this span is considered to be over a + /// single cell + fn new(start: i32, end: i32, line: usize, color: Color) -> LineSpan { + debug_assert!(start <= end); + LineSpan { + start, + end, + line, + color, + } + } +} + +struct CellWidth(f32); +struct LineHeight(f32); + +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + lines: Vec, + line_height: LineHeight, + em_width: CellWidth, + cursor: Option<(RectF, Color)>, + cur_size: SizeInfo, + background_color: Color, + background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan +} + impl TerminalEl { pub fn new(view: WeakViewHandle) -> TerminalEl { TerminalEl { view } } } -pub struct LayoutState { - lines: Vec, - line_height: f32, - em_width: f32, - cursor: Option<(RectF, Color)>, - cur_size: SizeInfo, - background_color: Color, -} - impl Element for TerminalEl { type LayoutState = LayoutState; type PaintState = (); @@ -56,73 +93,56 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let view = self.view.upgrade(cx).unwrap(); - let size = constraint.max; - let settings = cx.global::(); - let editor_theme = &settings.theme.editor; - let font_cache = cx.font_cache(); - - //Set up text rendering - let text_style = TextStyle { - color: editor_theme.text_color, - font_family_id: settings.buffer_font_family, - font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), - font_id: font_cache - .select_font(settings.buffer_font_family, &Default::default()) - .unwrap(), - font_size: settings.buffer_font_size, - font_properties: Default::default(), - underline: Default::default(), - }; - - let line_height = font_cache.line_height(text_style.font_size); - let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); - - let new_size = SizeInfo::new( - size.x() - cell_width, - size.y(), - cell_width, - line_height, - 0., - 0., - false, + //Settings immutably borrows cx here for the settings and font cache + //and we need to modify the cx to resize the terminal. So instead of + //storing Settings or the font_cache(), we toss them ASAP and then reborrow later + let text_style = make_text_style(cx.font_cache(), cx.global::()); + let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); + let cell_width = CellWidth( + cx.font_cache() + .em_advance(text_style.font_id, text_style.font_size), ); - view.update(cx.app, |view, _cx| { - view.set_size(new_size); - }); + let view_handle = self.view.upgrade(cx).unwrap(); - let settings = cx.global::(); - let terminal_theme = &settings.theme.terminal; - let term = view.read(cx).term.lock(); + //Tell the view our new size. Requires a mutable borrow of cx and the view + let cur_size = make_new_size(constraint, &cell_width, &line_height); + //Note that set_size locks and mutates the terminal. + //TODO: Would be nice to lock once for the whole of layout + view_handle.update(cx.app, |view, _cx| view.set_size(cur_size)); + //Now that we're done with the mutable portion, grab the immutable settings and view again + let terminal_theme = &(cx.global::()).theme.terminal; + let term = view_handle.read(cx).term.lock(); let content = term.renderable_content(); - let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + //And we're off! Begin layouting + let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme); + let backgrounds = chunks + .iter() + .filter(|(_, _, line_span)| line_span != &LineSpan::default()) + .map(|(_, _, line_span)| *line_span) + .collect(); let shaped_lines = layout_highlighted_chunks( - chunks.iter().map(|(text, style)| (text.as_str(), *style)), + chunks + .iter() + .map(|(text, style, _)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, - &cx.font_cache, + cx.font_cache(), usize::MAX, line_count, ); - let cursor_line = content.cursor.point.line.0 + content.display_offset as i32; - let mut cursor = None; - if let Some(layout_line) = cursor_line - .try_into() - .ok() - .and_then(|cursor_line: usize| shaped_lines.get(cursor_line)) - { - let cursor_x = layout_line.x_for_index(content.cursor.point.column.0); - cursor = Some(( - RectF::new( - vec2f(cursor_x, cursor_line as f32 * line_height), - vec2f(cell_width, line_height), - ), - terminal_theme.cursor, - )); - } + let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); + + let cursor = make_cursor_rect( + content.cursor.point, + &shaped_lines, + content.display_offset, + &line_height, + &cell_width, + ) + .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); ( constraint.max, @@ -131,7 +151,8 @@ impl Element for TerminalEl { line_height, em_width: cell_width, cursor, - cur_size: new_size, + cur_size, + background_rects, background_color: terminal_theme.background, }, ) @@ -148,44 +169,47 @@ impl Element for TerminalEl { cx.scene.push_mouse_region(MouseRegion { view_id: self.view.id(), - discriminant: None, - bounds: visible_bounds, - hover: None, mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), - click: None, - right_mouse_down: None, - right_click: None, - drag: None, - mouse_down_out: None, - right_mouse_down_out: None, + bounds: visible_bounds, + ..Default::default() }); - //Background + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + + //Start us off with a nice simple background color cx.scene.push_quad(Quad { - bounds: visible_bounds, + bounds: RectF::new(bounds.origin(), bounds.size()), background: Some(layout.background_color), border: Default::default(), corner_radius: 0., }); - let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding + //Draw cell backgrounds + for background_rect in &layout.background_rects { + let new_origin = origin + background_rect.0.origin(); + cx.scene.push_quad(Quad { + bounds: RectF::new(new_origin, background_rect.0.size()), + background: Some(background_rect.1), + border: Default::default(), + corner_radius: 0., + }) + } - let mut line_origin = origin; + //Draw text + let mut line_origin = origin.clone(); for line in &layout.lines { - let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height)); - + let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0)); if boundaries.intersects(visible_bounds) { - line.paint(line_origin, visible_bounds, layout.line_height, cx); + line.paint(line_origin, visible_bounds, layout.line_height.0, cx); } - line_origin.set_y(boundaries.max_y()); } + //Draw cursor if let Some((c, color)) = layout.cursor { let new_origin = origin + c.origin(); - let new_cursor = RectF::new(new_origin, c.size()); cx.scene.push_quad(Quad { - bounds: new_cursor, + bounds: RectF::new(new_origin, c.size()), background: Some(color), border: Default::default(), corner_radius: 0., @@ -215,7 +239,7 @@ impl Element for TerminalEl { } => { if visible_bounds.contains_point(*position) { let vertical_scroll = - (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); true } else { @@ -249,67 +273,134 @@ impl Element for TerminalEl { } } +fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: settings.buffer_font_family, + font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), + font_id: font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(), + font_size: settings.buffer_font_size, + font_properties: Default::default(), + underline: Default::default(), + } +} + +fn make_new_size( + constraint: SizeConstraint, + cell_width: &CellWidth, + line_height: &LineHeight, +) -> SizeInfo { + SizeInfo::new( + constraint.max.x() - cell_width.0, + constraint.max.y(), + cell_width.0, + line_height.0, + 0., + 0., + false, + ) +} + pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, -) -> (Vec<(String, Option)>, usize) { - let mut lines: Vec<(String, Option)> = vec![]; - let mut last_line = 0; - let mut line_count = 1; - let mut cur_chunk = String::new(); - - let mut cur_highlight = HighlightStyle { - color: Some(Color::white()), - ..Default::default() - }; - - for cell in grid_iterator { - let Indexed { - point: Point { line, .. }, - cell: Cell { - c, fg, flags, .. // TODO: Add bg and flags - }, //TODO: Learn what 'CellExtra does' - } = cell; - - let new_highlight = make_style_from_cell(fg, flags, theme); - - if line != last_line { +) -> (Vec<(String, Option, LineSpan)>, usize) { + let mut line_count: usize = 0; + let lines = grid_iterator.group_by(|i| i.point.line.0); + let result = lines + .into_iter() + .map(|(_, line)| { line_count += 1; - cur_chunk.push('\n'); - last_line = line.0; - } + let mut col_index = 0; - if new_highlight != cur_highlight { - lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); - cur_chunk.clear(); - cur_highlight = new_highlight; - } - cur_chunk.push(*c) - } - lines.push((cur_chunk, Some(cur_highlight))); - (lines, line_count) -} - -fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle { - let fg = Some(alac_color_to_gpui_color(fg, style)); - let underline = if flags.contains(Flags::UNDERLINE) { - Some(Underline { - color: fg, - squiggly: false, - thickness: OrderedFloat(1.), + let chunks = line.group_by(|i| cell_style(&i, theme)); + chunks + .into_iter() + .map(|(style, fragment)| { + let str_fragment = fragment.map(|indexed| indexed.c).collect::(); + let start = col_index; + let end = start + str_fragment.len() as i32; + col_index = end; + ( + str_fragment, + Some(style.0), + LineSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index + ) + }) + .chain(iter::once(("\n".to_string(), None, Default::default()))) + .collect::, LineSpan)>>() }) - } else { - None - }; - HighlightStyle { - color: fg, - underline, - ..Default::default() - } + .flatten() + .collect::, LineSpan)>>(); + (result, line_count) } -fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color { - match allac_color { +fn make_background_rects( + backgrounds: Vec, + shaped_lines: &Vec, + line_height: &LineHeight, +) -> Vec<(RectF, Color)> { + backgrounds + .into_iter() + .map(|line_span| { + let line = shaped_lines + .get(line_span.line) + .expect("Background line_num did not correspond to a line number"); + let x = line.x_for_index(line_span.start as usize); + let width = line.x_for_index(line_span.end as usize) - x; + ( + RectF::new( + vec2f(x, line_span.line as f32 * line_height.0), + vec2f(width, line_height.0), + ), + line_span.color, + ) + }) + .collect::>() +} + +fn make_cursor_rect( + cursor_point: Point, + shaped_lines: &Vec, + display_offset: usize, + line_height: &LineHeight, + cell_width: &CellWidth, +) -> Option { + let cursor_line = cursor_point.line.0 as usize + display_offset; + shaped_lines.get(cursor_line).map(|layout_line| { + let cursor_x = layout_line.x_for_index(cursor_point.column.0); + RectF::new( + vec2f(cursor_x, cursor_line as f32 * line_height.0), + vec2f(cell_width.0, line_height.0), + ) + }) +} + +fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) { + let flags = indexed.cell.flags; + let fg = Some(alac_color_to_gpui_color(&indexed.cell.fg, style)); + let bg = alac_color_to_gpui_color(&indexed.cell.bg, style); + + let underline = flags.contains(Flags::UNDERLINE).then(|| Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }); + + ( + HighlightStyle { + color: fg, + underline, + ..Default::default() + }, + bg, + ) +} + +fn alac_color_to_gpui_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { + match alac_color { alacritty_terminal::ansi::Color::Named(n) => match n { alacritty_terminal::ansi::NamedColor::Black => style.black, alacritty_terminal::ansi::NamedColor::Red => style.red, @@ -341,7 +432,7 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> C alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, }, //Theme defined - alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness } } @@ -366,14 +457,14 @@ pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { 15 => style.bright_white, 16..=231 => { let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components - let step = (u8::MAX as f32 / 5.).round() as u8; //Split the GPUI range into 5 chunks - Color::new(r * step, g * step, b * step, 1) //Map the rgb components to GPUI's range + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the 256 channel range into 5 chunks + Color::new(r * step, g * step, b * step, u8::MAX) //Map the [0, 5] rgb components to the [0, 256] channel range } //Grayscale from black to white, 0 to 24 232..=255 => { - let i = 24 - (index - 232); //Align index to 24..0 - let step = (u8::MAX as f32 / 24.).round() as u8; //Split the 256 range grayscale into 24 chunks - Color::new(i * step, i * step, i * step, 1) //Map the rgb components to GPUI's range + let i = index - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the [0,256] range grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the rgb components to the grayscale range } } } @@ -400,10 +491,10 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex let width = layout.cur_size.width(); let height = layout.cur_size.height(); //Alacritty uses 'as usize', so shall we. - for col in 0..(width / layout.em_width).round() as usize { + for col in 0..(width / layout.em_width.0).round() as usize { cx.scene.push_quad(Quad { bounds: RectF::new( - bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.), + bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), vec2f(1., height), ), background: Some(Color::green()), @@ -411,10 +502,10 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex corner_radius: 0., }); } - for row in 0..((height / layout.line_height) + 1.0).round() as usize { + for row in 0..((height / layout.line_height.0) + 1.0).round() as usize { cx.scene.push_quad(Quad { bounds: RectF::new( - bounds.origin() + vec2f(layout.em_width, row as f32 * layout.line_height), + bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), vec2f(width, 1.), ), background: Some(Color::green()), diff --git a/crates/terminal/truecolor.sh b/crates/terminal/truecolor.sh new file mode 100755 index 0000000000..14e5d81308 --- /dev/null +++ b/crates/terminal/truecolor.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copied from: https://unix.stackexchange.com/a/696756 +# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 + +awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ + s="/\\"; + total_cols=term_cols*term_lines; + for (colnum = 0; colnum255) g = 510-g; + printf "\033[48;2;%d;%d;%dm", r,g,b; + printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; + printf "%s\033[0m", substr(s,colnum%2+1,1); + if (colnum%term_cols==term_cols) printf "\n"; + } + printf "\n"; +}' \ No newline at end of file