edit/src/framebuffer.rs
2025-04-09 16:55:13 +02:00

838 lines
31 KiB
Rust

use crate::helpers::{self, CoordType, Point, Rect, Size};
use crate::ucd;
use std::fmt::Write;
use std::ops::{BitOr, BitXor};
use std::slice::ChunksExact;
#[derive(Clone, Copy)]
pub enum IndexedColor {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Background,
Foreground,
}
pub const INDEXED_COLORS_COUNT: usize = 18;
pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
0xff000000, 0xff212cbe, 0xff3aae3f, 0xff4a9abe, 0xffbe4d20, 0xffbe54bb, 0xffb2a700, 0xffbebebe,
0xff808080, 0xff303eff, 0xff51ea58, 0xff44c9ff, 0xffff6a2f, 0xffff74fc, 0xfff0e100, 0xffffffff,
0xff000000, 0xffffffff,
];
pub struct Framebuffer {
indexed_colors: [u32; INDEXED_COLORS_COUNT],
buffers: [Buffer; 2],
frame_counter: usize,
auto_colors: [u32; 2], // [dark, light]
}
impl Framebuffer {
pub fn new() -> Self {
Self {
indexed_colors: DEFAULT_THEME,
buffers: Default::default(),
frame_counter: 0,
auto_colors: [0, 0],
}
}
pub fn set_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
self.indexed_colors = colors;
self.auto_colors = [
self.indexed_colors[IndexedColor::Black as usize],
self.indexed_colors[IndexedColor::BrightWhite as usize],
];
if !Bitmap::quick_is_dark(self.auto_colors[0]) {
self.auto_colors.swap(0, 1);
}
}
pub fn reset(&mut self, size: Size) {
if size != self.buffers[0].bg_bitmap.size {
for buffer in &mut self.buffers {
buffer.text = LineBuffer::new(size);
buffer.bg_bitmap = Bitmap::new(size);
buffer.fg_bitmap = Bitmap::new(size);
buffer.attributes = AttributeBuffer::new(size);
}
let front = &mut self.buffers[self.frame_counter & 1];
// Trigger a full redraw. (Yes, it's a hack.)
front.fg_bitmap.fill(1);
// Trigger a cursor update as well, just to be sure.
front.cursor = Cursor::new_invalid();
}
self.frame_counter = self.frame_counter.wrapping_add(1);
let back = &mut self.buffers[self.frame_counter & 1];
let bg = self.indexed_colors[IndexedColor::Background as usize];
let fg = self.indexed_colors[IndexedColor::Foreground as usize];
back.text.fill_whitespace();
back.bg_bitmap.fill(bg);
back.fg_bitmap.fill(fg);
back.attributes.reset();
back.cursor = Cursor::new_disabled();
}
/// Replaces text contents in a single line of the framebuffer.
/// All coordinates are in viewport coordinates.
/// Assumes that all tabs have been replaced with spaces.
///
/// TODO: This function is ripe for performance improvements.
pub fn replace_text(
&mut self,
y: CoordType,
origin_x: CoordType,
clip_right: CoordType,
text: &str,
) -> Rect {
let back = &mut self.buffers[self.frame_counter & 1];
back.text.replace_text(y, origin_x, clip_right, text)
}
pub fn draw_scrollbar(
&mut self,
clip_rect: Rect,
track: Rect,
content_offset: CoordType,
content_height: CoordType,
) -> CoordType {
let track_clipped = track.intersect(clip_rect);
if track_clipped.is_empty() {
return 0;
}
let viewport_height = track.height();
// The content height is at least the viewport height.
let content_height = content_height.max(viewport_height);
// No need to draw a scrollbar if the content fits in the viewport.
let content_offset_max = content_height - viewport_height;
if content_offset_max == 0 {
return 0;
}
// The content offset must be at least one viewport height from the bottom.
// You don't want to scroll past the end after all...
let content_offset = content_offset.clamp(0, content_offset_max);
// In order to increase the visual resolution of the scrollbar,
// we'll use 1/8th blocks to represent the thumb.
// First, scale the offsets to get that 1/8th resolution.
let viewport_height = viewport_height as i64 * 8;
let content_offset_max = content_offset_max as i64 * 8;
let content_offset = content_offset as i64 * 8;
let content_height = content_height as i64 * 8;
// The proportional thumb height (0-1) is the fraction of viewport and
// content height. The taller the content, the smaller the thumb:
// = viewport_height / content_height
// We then scale that to the viewport height to get the height in 1/8th units.
// = viewport_height * viewport_height / content_height
// We add content_height/2 to round the integer division, which results in a numerator of:
// = viewport_height * viewport_height + content_height / 2
let numerator = viewport_height * viewport_height + content_height / 2;
let thumb_height = numerator / content_height;
// Ensure the thumb has a minimum size of 1 row.
let thumb_height = thumb_height.max(8);
// The proportional thumb top position (0-1) is:
// = content_offset / content_offset_max
// The maximum thumb top position is the viewport height minus the thumb height:
// = viewport_height - thumb_height
// To get the thumb top position in 1/8th units, we multiply both:
// = (viewport_height - thumb_height) * content_offset / content_offset_max
// We add content_offset_max/2 to round the integer division, which results in a numerator of:
// = (viewport_height - thumb_height) * content_offset + content_offset_max / 2
let numerator = (viewport_height - thumb_height) * content_offset + content_offset_max / 2;
let thumb_top = numerator / content_offset_max;
// The thumb bottom position is the thumb top position plus the thumb height.
let thumb_bottom = thumb_top + thumb_height;
// Shift to absolute coordinates.
let thumb_top = thumb_top + track.top as i64 * 8;
let thumb_bottom = thumb_bottom + track.top as i64 * 8;
// Clamp to the visible area.
let thumb_top = thumb_top.max(track_clipped.top as i64 * 8);
let thumb_bottom = thumb_bottom.min(track_clipped.bottom as i64 * 8);
// Calculate the height of the top/bottom cell of the thumb.
let top_fract = (thumb_top % 8) as CoordType;
let bottom_fract = (thumb_bottom % 8) as CoordType;
// Shift to absolute coordinates.
let thumb_top = ((thumb_top + 7) / 8) as CoordType;
let thumb_bottom = (thumb_bottom / 8) as CoordType;
self.blend_bg(track_clipped, self.indexed(IndexedColor::BrightBlack));
self.blend_fg(track_clipped, self.indexed(IndexedColor::BrightWhite));
// Draw the full blocks.
for y in thumb_top..thumb_bottom {
self.replace_text(y, track_clipped.left, track_clipped.right, "");
}
// Draw the top/bottom cell of the thumb.
// U+2581 to U+2588, 1/8th block to 8/8th block elements glyphs: ▁▂▃▄▅▆▇█
// In UTF8: E2 96 81 to E2 96 88
let mut fract_buf = [0xE2, 0x96, 0x88];
if top_fract != 0 {
fract_buf[2] = (0x88 - top_fract) as u8;
self.replace_text(
thumb_top - 1,
track_clipped.left,
track_clipped.right,
unsafe { std::str::from_utf8_unchecked(&fract_buf) },
);
}
if bottom_fract != 0 {
fract_buf[2] = (0x88 - bottom_fract) as u8;
let rect = self.replace_text(
thumb_bottom,
track_clipped.left,
track_clipped.right,
unsafe { std::str::from_utf8_unchecked(&fract_buf) },
);
self.blend_bg(rect, self.indexed(IndexedColor::BrightWhite));
self.blend_fg(rect, self.indexed(IndexedColor::BrightBlack));
}
((thumb_height + 4) / 8) as CoordType
}
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
self.indexed_colors[index as usize]
}
#[inline]
pub fn indexed_alpha(&self, index: IndexedColor, alpha: u8) -> u32 {
self.indexed_colors[index as usize] & 0x00ffffff | (alpha as u32) << 24
}
pub fn contrasted(&self, color: u32) -> u32 {
self.auto_colors[Bitmap::quick_is_dark(color) as usize]
}
pub fn blend_bg(&mut self, target: Rect, bg: u32) {
let back = &mut self.buffers[self.frame_counter & 1];
back.bg_bitmap.blend(target, bg);
}
pub fn blend_fg(&mut self, target: Rect, fg: u32) {
let back = &mut self.buffers[self.frame_counter & 1];
back.fg_bitmap.blend(target, fg);
}
pub fn reverse(&mut self, target: Rect) {
let back = &mut self.buffers[self.frame_counter & 1];
let target = target.intersect(back.bg_bitmap.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = back.bg_bitmap.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
let bg = &mut back.bg_bitmap.data[beg..end];
let fg = &mut back.fg_bitmap.data[beg..end];
bg.swap_with_slice(fg);
}
}
pub fn replace_attr(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
let back = &mut self.buffers[self.frame_counter & 1];
back.attributes.replace(target, mask, attr);
}
pub fn flip_attr(&mut self, target: Rect, attr: Attributes) {
let back = &mut self.buffers[self.frame_counter & 1];
back.attributes.flip(target, attr);
}
pub fn set_cursor(&mut self, pos: Point, overtype: bool) {
let back = &mut self.buffers[self.frame_counter & 1];
back.cursor.pos = pos;
back.cursor.overtype = overtype;
}
pub fn render(&mut self) -> String {
let idx = self.frame_counter & 1;
let (back, front) = unsafe {
let ptr = self.buffers.as_mut_ptr();
let back = &mut *ptr.add(idx);
let front = &*ptr.add(1 - idx);
(back, front)
};
let mut front_lines = front.text.lines.iter(); // hahaha
let mut front_bgs = front.bg_bitmap.iter();
let mut front_fgs = front.fg_bitmap.iter();
let mut front_attrs = front.attributes.iter();
let mut back_lines = back.text.lines.iter();
let mut back_bgs = back.bg_bitmap.iter();
let mut back_fgs = back.fg_bitmap.iter();
let mut back_attrs = back.attributes.iter();
let mut result = String::with_capacity(256);
let mut last_bg = self.indexed(IndexedColor::Background);
let mut last_fg = self.indexed(IndexedColor::Foreground);
let mut last_attr = Attributes::None;
for y in 0..front.text.size.height {
// SAFETY: The only thing that changes the size of these containers,
// is the reset() method and it always resets front/back to the same size.
let front_line = unsafe { front_lines.next().unwrap_unchecked() };
let front_bg = unsafe { front_bgs.next().unwrap_unchecked() };
let front_fg = unsafe { front_fgs.next().unwrap_unchecked() };
let front_attr = unsafe { front_attrs.next().unwrap_unchecked() };
let back_line = unsafe { back_lines.next().unwrap_unchecked() };
let back_bg = unsafe { back_bgs.next().unwrap_unchecked() };
let back_fg = unsafe { back_fgs.next().unwrap_unchecked() };
let back_attr = unsafe { back_attrs.next().unwrap_unchecked() };
// TODO: Ideally, we should properly diff the contents and so if
// only parts of a line change, we should only update those parts.
if front_line == back_line
&& front_bg == back_bg
&& front_fg == back_fg
&& front_attr == back_attr
{
continue;
}
let line_bytes = back_line.as_bytes();
let mut cfg = ucd::MeasurementConfig::new(&line_bytes);
let mut chunk_end = 0;
if result.is_empty() {
result.push_str("\x1b[m");
}
_ = write!(result, "\x1b[{};1H", y + 1);
while {
let bg = back_bg[chunk_end];
let fg = back_fg[chunk_end];
let attr = back_attr[chunk_end];
// Chunk into runs of the same color.
while {
chunk_end += 1;
chunk_end < back_bg.len()
&& back_bg[chunk_end] == bg
&& back_fg[chunk_end] == fg
&& back_attr[chunk_end] == attr
} {}
if last_bg != bg {
last_bg = bg;
if bg == self.indexed_colors[IndexedColor::Background as usize] {
result.push_str("\x1b[49m");
} else {
_ = write!(
result,
"\x1b[48;2;{};{};{}m",
bg & 0xff,
(bg >> 8) & 0xff,
(bg >> 16) & 0xff
);
}
}
if last_fg != fg {
last_fg = fg;
if fg == self.indexed_colors[IndexedColor::Foreground as usize] {
result.push_str("\x1b[39m");
} else {
_ = write!(
result,
"\x1b[38;2;{};{};{}m",
fg & 0xff,
(fg >> 8) & 0xff,
(fg >> 16) & 0xff
);
}
}
if last_attr != attr {
let diff = last_attr ^ attr;
if diff.underlined() {
if attr.underlined() {
result.push_str("\x1b[4m");
} else {
result.push_str("\x1b[24m");
}
}
last_attr = attr;
}
let beg = cfg.cursor().offset;
let end = cfg
.goto_visual(Point {
x: chunk_end as CoordType,
y: 0,
})
.offset;
result.push_str(&back_line[beg..end]);
chunk_end < back_bg.len()
} {}
}
// If the cursor has changed since the last frame we naturally need to update it,
// but this also applies if the code above wrote to the screen,
// as it uses CUP sequences to reposition the cursor for writing.
if !result.is_empty() || back.cursor != front.cursor {
if back.cursor.pos.x >= 0 && back.cursor.pos.y >= 0 {
// CUP to the cursor position.
// DECSCUSR to set the cursor style.
// DECTCEM to show the cursor.
_ = write!(
result,
"\x1b[{};{}H\x1b[{} q\x1b[?25h",
back.cursor.pos.y + 1,
back.cursor.pos.x + 1,
if back.cursor.overtype { 1 } else { 5 }
);
} else {
// DECTCEM to hide the cursor.
result.push_str("\x1b[?25l");
}
}
result
}
}
pub fn alpha_blend(dst: u32, src: u32) -> u32 {
Bitmap::alpha_blend(dst, src)
}
#[derive(Default)]
struct Buffer {
text: LineBuffer,
bg_bitmap: Bitmap,
fg_bitmap: Bitmap,
attributes: AttributeBuffer,
cursor: Cursor,
}
#[derive(Default)]
struct LineBuffer {
lines: Vec<String>,
size: Size,
}
impl LineBuffer {
fn new(size: Size) -> Self {
Self {
lines: vec![String::new(); size.height as usize],
size,
}
}
fn fill_whitespace(&mut self) {
let width = self.size.width as usize;
for l in &mut self.lines {
l.clear();
l.reserve(width + width / 2);
helpers::string_append_repeat(l, ' ', width);
}
}
/// Replaces text contents in a single line of the framebuffer.
/// All coordinates are in viewport coordinates.
/// Assumes that all tabs have been replaced with spaces.
///
/// TODO: This function is ripe for performance improvements.
pub fn replace_text(
&mut self,
y: CoordType,
origin_x: CoordType,
clip_right: CoordType,
text: &str,
) -> Rect {
let Some(line) = self.lines.get_mut(y as usize) else {
return Rect::default();
};
let bytes = text.as_bytes();
let clip_right = clip_right.clamp(0, self.size.width);
let layout_width = clip_right - origin_x;
// Can't insert text that can't fit or is empty.
if layout_width <= 0 || bytes.is_empty() {
return Rect::default();
}
let mut cfg = ucd::MeasurementConfig::new(&bytes);
// Check if the text intersects with the left edge of the framebuffer
// and figure out the parts that are inside.
let mut left = origin_x;
if left < 0 {
let cursor = cfg.goto_visual(Point { x: -left, y: 0 });
left += cursor.visual_pos.x;
if left < 0 && cursor.offset < text.len() {
// `-left` must've intersected a wide glyph and since goto_visual stops _before_ reaching the target,
// we stoped before the wide glyph and thus must step forward to the next glyph.
let cursor = cfg.goto_logical(Point {
x: cursor.logical_pos.x + 1,
y: 0,
});
left += cursor.visual_pos.x;
}
}
// If the text still starts outside the framebuffer, we must've ran out of text above.
// Otherwise, if it starts outside the right edge to begin with, we can't insert it anyway.
if left < 0 || left >= clip_right {
return Rect::default();
}
// Measure the width of the new text (= `res_new.visual_target.x`).
let res_new = cfg.goto_visual(Point {
x: layout_width,
y: 0,
});
// Figure out at which byte offset the new text gets inserted.
let right = left + res_new.visual_pos.x;
let line_bytes = line.as_bytes();
let mut cfg_old = ucd::MeasurementConfig::new(&line_bytes);
let res_old_beg = cfg_old.goto_visual(Point { x: left, y: 0 });
let mut res_old_end = cfg_old.goto_visual(Point { x: right, y: 0 });
// Since the goto functions will always stop short of the target position,
// we need to manually step beyond it if we intersect with a wide glyph.
if res_old_end.visual_pos.x < right {
res_old_end = cfg_old.goto_logical(Point {
x: res_old_end.logical_pos.x + 1,
y: 0,
});
}
// If we intersect a wide glyph, we need to pad the new text with spaces.
let mut str_new = &text[..res_new.offset];
let mut str_buf = String::new();
let overlap_beg = left - res_old_beg.visual_pos.x;
let overlap_end = res_old_end.visual_pos.x - right;
if overlap_beg > 0 || overlap_end > 0 {
if overlap_beg > 0 {
helpers::string_append_repeat(&mut str_buf, ' ', overlap_beg as usize);
}
str_buf.push_str(str_new);
if overlap_end > 0 {
helpers::string_append_repeat(&mut str_buf, ' ', overlap_end as usize);
}
str_new = &str_buf;
}
(*line).replace_range(res_old_beg.offset..res_old_end.offset, str_new);
Rect {
left,
top: y,
right,
bottom: y + 1,
}
}
}
#[derive(Default)]
struct Bitmap {
data: Vec<u32>,
size: Size,
}
impl Bitmap {
fn new(size: Size) -> Self {
Self {
data: vec![0; (size.width * size.height) as usize],
size,
}
}
fn fill(&mut self, color: u32) {
self.data.fill(color);
}
fn blend(&mut self, target: Rect, color: u32) {
if (color & 0xff000000) == 0x00000000 {
return;
}
let target = target.intersect(self.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = self.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
let data = &mut self.data[beg..end];
if (color & 0xff000000) == 0xff000000 {
data.fill(color);
} else {
let end = data.len();
let mut off = 0;
while {
let c = data[off];
// Chunk into runs of the same color, so that we only call alpha_blend once per run.
let chunk_beg = off;
while {
off += 1;
off < end && data[off] == c
} {}
let chunk_end = off;
data[chunk_beg..chunk_end].fill(Self::alpha_blend(c, color));
off < end
} {}
}
}
}
fn alpha_blend(dst: u32, src: u32) -> u32 {
let src_a = (src >> 24) as f32 * (1.0 / 255.0);
let src_b = Self::srgb_to_linear(src >> 16);
let src_g = Self::srgb_to_linear(src >> 8);
let src_r = Self::srgb_to_linear(src);
let dst_a = (dst >> 24) as f32 * (1.0 / 255.0);
let dst_b = Self::srgb_to_linear(dst >> 16);
let dst_g = Self::srgb_to_linear(dst >> 8);
let dst_r = Self::srgb_to_linear(dst);
let out_a = src_a + dst_a * (1.0 - src_a);
// The formula is technically:
// (src_bgr * src_a + dst_bgr * dst_a * (1 - src_a)) / out_a
// but we can merge the division of out_a with the two preceding terms.
// This saves us a bunch of operations that cannot be optimized away otherwise.
let out_a_inv = 1.0 / out_a;
let src_mul = src_a * out_a_inv;
let dst_mul = dst_a * (1.0 - src_a) * out_a_inv;
let out_b = src_b * src_mul + dst_b * dst_mul;
let out_g = src_g * src_mul + dst_g * dst_mul;
let out_r = src_r * src_mul + dst_r * dst_mul;
let out_b = Self::linear_to_srgb(out_b);
let out_g = Self::linear_to_srgb(out_g);
let out_r = Self::linear_to_srgb(out_r);
(((out_a * 255.0f32) as u32) << 24) | (out_b << 16) | (out_g << 8) | out_r
}
fn srgb_to_linear(c: u32) -> f32 {
// Generated using:
// ```rs
// let fc = c as f32 / 255.0;
// if fc <= 0.04045 {
// fc / 12.92
// } else {
// ((fc + 0.055) / 1.055).powf(2.4)
// }
// ```
// I'd love to use hex floats, but for some reason Rust maintainers decided against it...
#[rustfmt::skip]
#[allow(clippy::excessive_precision)]
const LUT: [f32; 256] = [
0.0000000000, 0.0003035270, 0.0006070540, 0.0009105810, 0.0012141080, 0.0015176350, 0.0018211619, 0.0021246888, 0.0024282159, 0.0027317430, 0.0030352699, 0.0033465356, 0.0036765069, 0.0040247170, 0.0043914421, 0.0047769533,
0.0051815170, 0.0056053917, 0.0060488326, 0.0065120910, 0.0069954102, 0.0074990317, 0.0080231922, 0.0085681248, 0.0091340570, 0.0097212177, 0.0103298230, 0.0109600937, 0.0116122449, 0.0122864870, 0.0129830306, 0.0137020806,
0.0144438436, 0.0152085144, 0.0159962922, 0.0168073755, 0.0176419523, 0.0185002182, 0.0193823613, 0.0202885624, 0.0212190095, 0.0221738834, 0.0231533647, 0.0241576303, 0.0251868572, 0.0262412224, 0.0273208916, 0.0284260381,
0.0295568332, 0.0307134409, 0.0318960287, 0.0331047624, 0.0343398079, 0.0356013142, 0.0368894450, 0.0382043645, 0.0395462364, 0.0409151986, 0.0423114114, 0.0437350273, 0.0451862030, 0.0466650836, 0.0481718220, 0.0497065634,
0.0512694679, 0.0528606549, 0.0544802807, 0.0561284944, 0.0578054339, 0.0595112406, 0.0612460710, 0.0630100295, 0.0648032799, 0.0666259527, 0.0684781820, 0.0703601092, 0.0722718611, 0.0742135793, 0.0761853904, 0.0781874284,
0.0802198276, 0.0822827145, 0.0843762159, 0.0865004659, 0.0886556059, 0.0908417329, 0.0930589810, 0.0953074843, 0.0975873619, 0.0998987406, 0.1022417471, 0.1046164930, 0.1070231125, 0.1094617173, 0.1119324341, 0.1144353822,
0.1169706732, 0.1195384338, 0.1221387982, 0.1247718409, 0.1274376959, 0.1301364899, 0.1328683347, 0.1356333494, 0.1384316236, 0.1412633061, 0.1441284865, 0.1470272839, 0.1499598026, 0.1529261619, 0.1559264660, 0.1589608639,
0.1620294005, 0.1651322246, 0.1682693958, 0.1714410931, 0.1746473908, 0.1778884083, 0.1811642349, 0.1844749898, 0.1878207624, 0.1912016720, 0.1946178079, 0.1980693042, 0.2015562356, 0.2050787061, 0.2086368501, 0.2122307271,
0.2158605307, 0.2195262313, 0.2232279778, 0.2269658893, 0.2307400703, 0.2345506549, 0.2383976579, 0.2422811985, 0.2462013960, 0.2501583695, 0.2541521788, 0.2581829131, 0.2622507215, 0.2663556635, 0.2704978585, 0.2746773660,
0.2788943350, 0.2831487954, 0.2874408960, 0.2917706966, 0.2961383164, 0.3005438447, 0.3049873710, 0.3094689548, 0.3139887452, 0.3185468316, 0.3231432438, 0.3277781308, 0.3324515820, 0.3371636569, 0.3419144452, 0.3467040956,
0.3515326977, 0.3564002514, 0.3613068759, 0.3662526906, 0.3712377846, 0.3762622178, 0.3813261092, 0.3864295185, 0.3915725648, 0.3967553079, 0.4019778669, 0.4072403014, 0.4125427008, 0.4178851545, 0.4232677519, 0.4286905527,
0.4341537058, 0.4396572411, 0.4452012479, 0.4507858455, 0.4564110637, 0.4620770514, 0.4677838385, 0.4735315442, 0.4793202281, 0.4851499796, 0.4910208881, 0.4969330430, 0.5028865933, 0.5088814497, 0.5149177909, 0.5209956765,
0.5271152258, 0.5332764983, 0.5394796133, 0.5457245708, 0.5520114899, 0.5583404899, 0.5647116303, 0.5711249113, 0.5775805116, 0.5840784907, 0.5906189084, 0.5972018838, 0.6038274169, 0.6104956269, 0.6172066331, 0.6239604354,
0.6307572126, 0.6375969648, 0.6444797516, 0.6514056921, 0.6583748460, 0.6653873324, 0.6724432111, 0.6795425415, 0.6866854429, 0.6938719153, 0.7011020184, 0.7083759308, 0.7156936526, 0.7230552435, 0.7304608822, 0.7379105687,
0.7454043627, 0.7529423237, 0.7605246305, 0.7681512833, 0.7758223414, 0.7835379243, 0.7912980318, 0.7991028428, 0.8069523573, 0.8148466945, 0.8227858543, 0.8307699561, 0.8387991190, 0.8468732834, 0.8549926877, 0.8631572723,
0.8713672161, 0.8796223402, 0.8879231811, 0.8962693810, 0.9046613574, 0.9130986929, 0.9215820432, 0.9301108718, 0.9386858940, 0.9473065734, 0.9559735060, 0.9646862745, 0.9734454751, 0.9822505713, 0.9911022186, 1.0000000000,
];
LUT[(c & 0xff) as usize]
}
fn linear_to_srgb(c: f32) -> u32 {
if c <= 0.0031308f32 {
(c * 12.92f32 * 255.0f32) as u32
} else {
((1.055f32 * c.powf(1.0f32 / 2.4f32) - 0.055f32) * 255.0f32) as u32
}
}
fn quick_is_dark(c: u32) -> bool {
let r = c & 0xff;
let g = (c >> 8) & 0xff;
let b = (c >> 16) & 0xff;
// Rough approximation of the sRGB luminance Y = 0.2126 R + 0.7152 G + 0.0722 B.
let l = r * 3 + g * 10 + b;
l < 128 * 14
}
/// Iterates over each row in the bitmap.
fn iter(&self) -> ChunksExact<u32> {
self.data.chunks_exact(self.size.width as usize)
}
}
#[repr(transparent)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Attributes(u8);
#[allow(non_upper_case_globals)] // Mimics an enum, but it's actually a bitfield. Allows simple diffing.
impl Attributes {
pub const None: Attributes = Attributes(0);
pub const Underlined: Attributes = Attributes(0b1);
pub const All: Attributes = Attributes(0b1);
pub const fn underlined(self) -> bool {
self.0 & Self::Underlined.0 != 0
}
}
impl BitOr for Attributes {
type Output = Attributes;
fn bitor(self, rhs: Self) -> Self::Output {
Attributes(self.0 | rhs.0)
}
}
impl BitXor for Attributes {
type Output = Attributes;
fn bitxor(self, rhs: Self) -> Self::Output {
Attributes(self.0 ^ rhs.0)
}
}
#[derive(Default)]
struct AttributeBuffer {
data: Vec<Attributes>,
size: Size,
}
impl AttributeBuffer {
fn new(size: Size) -> Self {
Self {
data: vec![Default::default(); (size.width * size.height) as usize],
size,
}
}
fn reset(&mut self) {
self.data.fill(Default::default());
}
fn replace(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
let target = target.intersect(self.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = self.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
for a in &mut self.data[beg..end] {
*a = Attributes(a.0 & !mask.0 | attr.0);
}
}
}
fn flip(&mut self, target: Rect, attr: Attributes) {
let target = target.intersect(self.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = self.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
for a in &mut self.data[beg..end] {
*a = Attributes(a.0 ^ attr.0);
}
}
}
/// Iterates over each row in the bitmap.
fn iter(&self) -> ChunksExact<Attributes> {
self.data.chunks_exact(self.size.width as usize)
}
}
#[derive(Default, PartialEq, Eq)]
struct Cursor {
pos: Point,
overtype: bool,
}
impl Cursor {
const fn new_invalid() -> Self {
Self {
pos: Point::MIN,
overtype: false,
}
}
const fn new_disabled() -> Self {
Self {
pos: Point { x: -1, y: -1 },
overtype: false,
}
}
}