diff --git a/Cargo.lock b/Cargo.lock index 1d2c900658..895087f45b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,7 +534,6 @@ version = "0.1.12" dependencies = [ "arrayvec", "rio-proc-macros", - "unicode-normalization", ] [[package]] @@ -1363,8 +1362,23 @@ checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "gif", + "image-webp", "num-traits", "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", ] [[package]] @@ -2141,6 +2155,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.34.0" @@ -2375,6 +2395,7 @@ dependencies = [ "regex", "regex-automata 0.4.7", "rio-window", + "rustc-hash 2.0.0", "serde", "smallvec", "sugarloaf", @@ -3973,6 +3994,12 @@ dependencies = [ "syn 2.0.76", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -3981,3 +4008,12 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 6c54767079..16d54bd7b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ sugarloaf = { path = "sugarloaf", version = "0.1.12" } corcovado = { path = "corcovado", version = "0.1.12" } rio-config = { path = "rio-config", version = "0.1.12" } rio-proc-macros = { path = "rio-proc-macros", version = "0.1.12" } -copa = { path = "copa", default-features = true, version = "0.1.12" } +copa = { path = "copa", default-features = false, version = "0.1.12" } teletypewriter = { path = "teletypewriter", version = "0.1.12" } rio-backend = { path = "rio-backend", version = "0.1.12" } rio-window = { path = "rio-window", version = "0.1.12", default-features = false } @@ -41,7 +41,8 @@ raw-window-handle = { version = "0.6.2", features = ["std"] } parking_lot = { version = "0.12.3", features = ["nightly", "hardware-lock-elision"] } rustc-hash = "2.0.0" unicode-width = "0.1.13" -image_rs = { package = "image", version = "0.25.2", default-features = false, features = ["ico"] } +base64 = "0.22.1" +image_rs = { package = "image", version = "0.25.2", default-features = false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } regex = "1.10.5" bytemuck = { version = "1.17.0", features = [ "derive" ] } swash = "0.1.18" diff --git a/copa/Cargo.toml b/copa/Cargo.toml index 8c067c3e51..0bcc1800bb 100644 --- a/copa/Cargo.toml +++ b/copa/Cargo.toml @@ -13,9 +13,6 @@ edition = "2021" rio-proc-macros = { workspace = true } arrayvec = { version = "0.7.6", default-features = false, optional = true } -[dev-dependencies] -unicode-normalization = "0.1.22" - [features] default = ["no_std"] no_std = ["arrayvec"] diff --git a/copa/src/lib.rs b/copa/src/lib.rs index 69cdc2cf4f..bd96edbecf 100644 --- a/copa/src/lib.rs +++ b/copa/src/lib.rs @@ -657,35 +657,35 @@ mod tests { } } - #[test] - fn issue_191() { - use crate::std::string::{String, ToString}; - use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization}; - - // https://github.com/raphamorim/rio/issues/191 - // 한 isn't rendered correctly via printf '\xe1\x84\x92\xe1\x85\xa1\xe1\x86\xab\n' - let mut input = - std::str::from_utf8(b"\xe1\x84\x92\xe1\x85\xa1\xe1\x86\xab").unwrap(); - let mut dispatcher = Dispatcher::default(); - let mut parser = Parser::new(); - - let p = std::mem::take(&mut input).to_string(); - let normalized: String; - let text = if is_nfc_quick(p.chars()) == IsNormalized::Yes { - p.as_str() - } else { - normalized = p.as_str().nfc().collect(); - normalized.as_str() - }; - - let bytes: &[u8] = &text.bytes().collect::>(); - - for byte in bytes { - parser.advance(&mut dispatcher, *byte); - } - - assert_eq!(text, "한"); - } + // #[test] + // fn issue_191() { + // use crate::std::string::{String, ToString}; + // use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization}; + + // // https://github.com/raphamorim/rio/issues/191 + // // 한 isn't rendered correctly via printf '\xe1\x84\x92\xe1\x85\xa1\xe1\x86\xab\n' + // let mut input = + // std::str::from_utf8(b"\xe1\x84\x92\xe1\x85\xa1\xe1\x86\xab").unwrap(); + // let mut dispatcher = Dispatcher::default(); + // let mut parser = Parser::new(); + + // let p = std::mem::take(&mut input).to_string(); + // let normalized: String; + // let text = if is_nfc_quick(p.chars()) == IsNormalized::Yes { + // p.as_str() + // } else { + // normalized = p.as_str().nfc().collect(); + // normalized.as_str() + // }; + + // let bytes: &[u8] = &text.bytes().collect::>(); + + // for byte in bytes { + // parser.advance(&mut dispatcher, *byte); + // } + + // assert_eq!(text, "한"); + // } #[test] fn exceed_max_buffer_size() { diff --git a/docs/docs/features/index.md b/docs/docs/features/index.md index 3fcb84ef13..dbfaffabd6 100644 --- a/docs/docs/features/index.md +++ b/docs/docs/features/index.md @@ -9,7 +9,7 @@ Short introduction of Rio terminal features. - [Vi mode](/docs/features/vi-mode) - [Hyperlinks](/docs/features/hyperlinks) -- [iTerm image protocol](/docs/features/iterm-image-protocol) +- [iTerm2 image protocol](/docs/features/iterm2-image-protocol) - [Kitty keyboard protocol](/docs/features/kitty-keyboard-protocol) - [Rio is fast](/docs/features/rio-is-fast) - [Adaptive theme](/docs/features/adaptive-theme) diff --git a/docs/docs/features/iterm-image-protocol.md b/docs/docs/features/iterm2-image-protocol.md similarity index 70% rename from docs/docs/features/iterm-image-protocol.md rename to docs/docs/features/iterm2-image-protocol.md index c9cd792898..72ad0df6f3 100644 --- a/docs/docs/features/iterm-image-protocol.md +++ b/docs/docs/features/iterm2-image-protocol.md @@ -1,10 +1,8 @@ --- -title: 'iTerm Image Protocol' +title: 'iTerm2 Image Protocol' language: 'en' --- -**Note: iTerm Image Protocol is still under development** - Rio implements support for the iTerm2 inline image protocol. To render an image inline in your terminal, you can use `imgcat` provided as a script by iTerm2 in [iterm2.com/utilities/imgcat](https://iterm2.com/utilities/imgcat) or from other sources like: @@ -14,8 +12,6 @@ To render an image inline in your terminal, you can use `imgcat` provided as a s - [npmjs.com/package/imgcat](https://www.npmjs.com/package/imgcat) - ... and etecetera. -```bash -imgcat ./rio-logo.png -``` +![Demo iTerm2 image protocol](/assets/features/demo-iterm2-image-protocol.png) -More info about [iTerm image protocol](https://iterm2.com/documentation-images.html) +More info regarding [iTerm image protocol](https://iterm2.com/documentation-images.html) diff --git a/docs/docs/releases.md b/docs/docs/releases.md index 7c25f5e2c1..f93d93c45b 100644 --- a/docs/docs/releases.md +++ b/docs/docs/releases.md @@ -10,6 +10,7 @@ language: 'en' +- Support to iTerm2 image protocol. - Fix: Issue building rio for Void Linux [#656](https://github.com/raphamorim/rio/issues/656). - Fix: Adaptive theme doesn't appear to work correctly on macOS [#660](https://github.com/raphamorim/rio/issues/660). - Fix: Image background support to OpenGL targets. diff --git a/docs/static/assets/features/demo-iterm2-image-protocol.png b/docs/static/assets/features/demo-iterm2-image-protocol.png new file mode 100644 index 0000000000..36cbe8deba Binary files /dev/null and b/docs/static/assets/features/demo-iterm2-image-protocol.png differ diff --git a/frontends/rioterm/src/bindings/kitty_keyboard.rs b/frontends/rioterm/src/bindings/kitty_keyboard.rs index 237a74af48..43e3d46bce 100644 --- a/frontends/rioterm/src/bindings/kitty_keyboard.rs +++ b/frontends/rioterm/src/bindings/kitty_keyboard.rs @@ -15,14 +15,14 @@ pub fn build_key_sequence(key: &KeyEvent, mods: ModifiersState, mode: Mode) -> V let mut modifiers = mods.into(); let kitty_seq = mode.intersects( - Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC - | Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES - | Mode::KEYBOARD_REPORT_EVENT_TYPES, + Mode::REPORT_ALL_KEYS_AS_ESC + | Mode::DISAMBIGUATE_ESC_CODES + | Mode::REPORT_EVENT_TYPES, ); - let kitty_encode_all = mode.contains(Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC); + let kitty_encode_all = mode.contains(Mode::REPORT_ALL_KEYS_AS_ESC); // The default parameter is 1, so we can omit it. - let kitty_event_type = mode.contains(Mode::KEYBOARD_REPORT_EVENT_TYPES) + let kitty_event_type = mode.contains(Mode::REPORT_EVENT_TYPES) && (key.repeat || key.state == ElementState::Released); let context = SequenceBuilder { @@ -47,7 +47,7 @@ pub fn build_key_sequence(key: &KeyEvent, mods: ModifiersState, mode: Mode) -> V let text = key.text_with_all_modifiers(); let associated_text = text.filter(|text| { - mode.contains(Mode::KEYBOARD_REPORT_ASSOCIATED_TEXT) + mode.contains(Mode::REPORT_ASSOCIATED_TEXT) && key.state != ElementState::Released && !text.is_empty() && !is_control_character(text) @@ -143,7 +143,7 @@ impl SequenceBuilder { // NOTE: Base layouts are ignored, since winit doesn't expose this information // yet. - let payload = if self.mode.contains(Mode::KEYBOARD_REPORT_ALTERNATE_KEYS) + let payload = if self.mode.contains(Mode::REPORT_ALTERNATE_KEYS) && alternate_key_code != unicode_key_code { format!("{unicode_key_code}:{alternate_key_code}") diff --git a/frontends/rioterm/src/bindings/mod.rs b/frontends/rioterm/src/bindings/mod.rs index 1ea4d12e68..c97464bd05 100644 --- a/frontends/rioterm/src/bindings/mod.rs +++ b/frontends/rioterm/src/bindings/mod.rs @@ -174,11 +174,11 @@ impl BindingMode { binding_mode.set(BindingMode::SEARCH, search); binding_mode.set( BindingMode::DISAMBIGUATE_KEYS, - mode.contains(Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES), + mode.contains(Mode::DISAMBIGUATE_ESC_CODES), ); binding_mode.set( BindingMode::ALL_KEYS_AS_ESC, - mode.contains(Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC), + mode.contains(Mode::REPORT_ALL_KEYS_AS_ESC), ); binding_mode.set(BindingMode::VI, mode.contains(Mode::VI)); binding_mode diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 1d4e8f0234..213721919f 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -457,7 +457,7 @@ impl Screen<'_> { let mods = self.modifiers.state(); if is_kitty_keyboard_enabled && key.state == ElementState::Released { - if !mode.contains(Mode::KEYBOARD_REPORT_EVENT_TYPES) + if !mode.contains(Mode::REPORT_EVENT_TYPES) || mode.contains(Mode::VI) || self.search_active() { @@ -476,7 +476,7 @@ impl Screen<'_> { // NOTE: Echo the key back on release to follow kitty/foot behavior. When // KEYBOARD_REPORT_ALL_KEYS_AS_ESC is used, we build proper escapes for // the keys below. - _ if mode.contains(Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC) => { + _ if mode.contains(Mode::REPORT_ALL_KEYS_AS_ESC) => { crate::bindings::kitty_keyboard::build_key_sequence(key, mods, mode) .into() } @@ -533,10 +533,10 @@ impl Screen<'_> { // 1. No keyboard input protocol is enabled. // 2. Mode is KEYBOARD_DISAMBIGUATE_ESC_CODES, but we have text + empty or Shift // modifiers and the location of the key is not on the numpad, and it's not an `Esc`. - let write_legacy = !mode.contains(Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC) + let write_legacy = !mode.contains(Mode::REPORT_ALL_KEYS_AS_ESC) && !text.is_empty() - && (!mode.contains(Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES) - || (mode.contains(Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES) + && (!mode.contains(Mode::DISAMBIGUATE_ESC_CODES) + || (mode.contains(Mode::DISAMBIGUATE_ESC_CODES) && (mods.is_empty() || mods == ModifiersState::SHIFT) && key.location != KeyLocation::Numpad // Special case escape here. diff --git a/rio-backend/Cargo.toml b/rio-backend/Cargo.toml index 02dd2691e5..2d329e90a9 100644 --- a/rio-backend/Cargo.toml +++ b/rio-backend/Cargo.toml @@ -18,10 +18,11 @@ tracing = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] toml = "0.8.19" -base64 = "0.22.1" +base64 = { workspace = true } bitflags = { workspace = true } bytemuck = { workspace = true } corcovado = { workspace = true } +rustc-hash = { workspace = true } regex = { workspace = true } raw-window-handle = { workspace = true } copypasta = { version = "0.10.1", default-features = false } diff --git a/rio-backend/src/ansi/graphics.rs b/rio-backend/src/ansi/graphics.rs index 699a10d5b6..13ac24f9cc 100644 --- a/rio-backend/src/ansi/graphics.rs +++ b/rio-backend/src/ansi/graphics.rs @@ -2,6 +2,7 @@ // Alacritty is licensed under Apache 2.0 license. // https://github.com/alacritty/alacritty/pull/4763/files +use crate::ansi::sixel; use crate::config::colors::ColorRgb; use crate::crosswords::grid::Dimensions; use crate::sugarloaf::{GraphicData, GraphicId}; @@ -33,9 +34,6 @@ pub struct UpdateQueues { pub clear_subregions: Vec, } -/// Max allowed dimensions (width, height) for the graphic, in pixels. -pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096]; - #[derive(Clone, Debug)] pub struct TextureRef { /// Graphic identifier. @@ -127,7 +125,7 @@ pub enum TextureOperation { } /// Track changes in the grid to add or to remove graphics. -#[derive(Clone, Debug, Default)] +#[derive(Debug, Default)] pub struct Graphics { /// Last generated identifier. pub last_id: u64, @@ -146,6 +144,9 @@ pub struct Graphics { /// Cell width in pixels. pub cell_width: f32, + + /// Current Sixel parser. + pub sixel_parser: Option>, } impl Graphics { @@ -214,6 +215,7 @@ fn check_opaque_region() { color_type: ColorType::Rgb, pixels: vec![255; 10 * 10 * 3], is_opaque: true, + resize: None, }; assert!(graphic.is_filled(1, 1, 3, 3)); @@ -236,6 +238,7 @@ fn check_opaque_region() { height: 10, color_type: ColorType::Rgba, is_opaque: false, + resize: None, }; assert!(graphic.is_filled(0, 0, 3, 3)); diff --git a/rio-backend/src/ansi/iterm2_image_protocol.rs b/rio-backend/src/ansi/iterm2_image_protocol.rs new file mode 100644 index 0000000000..649a9f12cc --- /dev/null +++ b/rio-backend/src/ansi/iterm2_image_protocol.rs @@ -0,0 +1,193 @@ +// Implementation made by ayosec +// https://github.com/ayosec/alacritty/commit/661a64c2b35283c97bac71d29535393e909c7d19 +// This module implements support for the [iTerm2 images protocol](https://iterm2.com/documentation-images.html). +// +// iTerm2 uses the OSC 1337 for a many non-standard commands, but we only support +// adding inline graphics. +// +// This implementation also supports `width` and `height` parameters to resize the image. + +use sugarloaf::{GraphicData, GraphicId, ResizeCommand, ResizeParameter}; + +use rustc_hash::FxHashMap; +use std::str; + +use base64::engine::general_purpose::STANDARD as Base64; +use base64::Engine; + +/// Parse the OSC 1337 parameters to add a graphic to the grid. +pub fn parse(params: &[&[u8]]) -> Option { + let (params, contents) = param_values(params)?; + + if params.get("inline") != Some(&"1") { + return None; + } + + let buffer = match Base64.decode(contents) { + Ok(buffer) => buffer, + Err(err) => { + tracing::warn!("Can't decode base64 data: {}", err); + return None; + } + }; + + let image = match image_rs::load_from_memory(&buffer) { + Ok(image) => image, + Err(err) => { + tracing::warn!("Can't load image: {}", err); + return None; + } + }; + + let mut graphics = GraphicData::from_dynamic_image(GraphicId(0), image); + graphics.resize = resize_param(¶ms); + Some(graphics) +} + +/// Extract parameter values. +/// +/// The format defined by iTerm2 starts with a `File=` string, and the file +/// contents are specified after a `:`. +/// +/// ```notrust +/// ESC ] 1337 ; File = [arguments] : base-64 encoded file contents ^G +/// ``` +/// +/// This format is not expected by the parser in the `vte` crate. +/// +/// The `File=` string is found in the first parameter, and the file contents are +/// appended in the last one. We have to split these parameter to get the expected +/// data. +fn param_values<'a>( + params: &[&'a [u8]], +) -> Option<(FxHashMap<&'a str, &'a str>, &'a [u8])> { + let mut map = FxHashMap::default(); + let mut contents = None; + + for (index, mut param) in params.iter().skip(1).copied().enumerate() { + // First parameter should start with "File=" + if index == 0 { + if !param.starts_with(&b"File="[..]) { + return None; + } + + param = ¶m[5..]; + } + + if let Some(separator) = param.iter().position(|&b| b == b'=') { + let (key, mut value) = param.split_at(separator); + value = &value[1..]; + + // Last parameter has the file contents after the first ':'. + // Add 2 because we are skipping the first param. + if index + 2 == params.len() { + if let Some(separator) = value.iter().position(|&b| b == b':') { + let (a, b) = value.split_at(separator); + value = a; + contents = Some(&b[1..]); + } + } + + if let (Ok(key), Ok(value)) = (str::from_utf8(key), str::from_utf8(value)) { + map.insert(key, value); + } + } + } + + contents.map(|c| (map, c)) +} + +/// Compute the resize operation from the OSC parameters. +/// +/// Accepted formats: +/// +/// - N: N character cells. +/// - Npx: N pixels. +/// - N%: N percent of the window's width or height. +/// - auto: Computed from the original graphic size. +fn resize_param(params: &FxHashMap<&str, &str>) -> Option { + fn parse(value: Option<&str>) -> Option { + let value = match value { + None | Some("auto") => return Some(ResizeParameter::Auto), + Some(value) => value, + }; + + // Split the value after the first non-digit byte. + // If there is no unit, parse as number of cells. + let first_nondigit = value + .as_bytes() + .iter() + .position(|b: &u8| !b.is_ascii_digit()); + // .position(|b| !(b'0'..=b'9').contains(&b)); + let (number, unit) = match first_nondigit { + Some(position) => value.split_at(position), + None => return Some(ResizeParameter::Cells(str::parse(value).ok()?)), + }; + + match (str::parse(number), unit) { + (Ok(number), "%") => Some(ResizeParameter::WindowPercent(number)), + (Ok(number), "px") => Some(ResizeParameter::Pixels(number)), + _ => None, + } + } + + let width = parse(params.get(&"width").copied())?; + let height = parse(params.get(&"height").copied())?; + + let preserve_aspect_ratio = params.get(&"preserveAspectRatio") != Some(&"0"); + + Some(ResizeCommand { + width, + height, + preserve_aspect_ratio, + }) +} + +#[test] +fn parse_osc1337_parameters() { + let params = [ + b"1337".as_ref(), + b"File=name=ABCD".as_ref(), + b"size=3".as_ref(), + b"inline=1:AAAA".as_ref(), + ]; + + let (params, contents) = param_values(¶ms).unwrap(); + + assert_eq!(params["name"], "ABCD"); + assert_eq!(params["size"], "3"); + assert_eq!(params["inline"], "1"); + + assert_eq!(contents, b"AAAA".as_ref()) +} + +#[test] +fn parse_osc1337_single_parameter() { + let params = [b"1337".as_ref(), b"File=inline=1:AAAA".as_ref()]; + + let (params, contents) = param_values(¶ms).unwrap(); + + assert_eq!(params["inline"], "1"); + assert_eq!(contents, b"AAAA".as_ref()) +} + +#[test] +fn resize_params() { + use ResizeParameter::{Auto, Cells, Pixels, WindowPercent}; + + macro_rules! assert_resize { + ($param_width:expr, $param_height:expr, $width:expr, $height:expr) => { + let mut params = FxHashMap::default(); + params.insert("width", $param_width); + params.insert("height", $param_height); + + let resize = resize_param(¶ms).unwrap(); + assert_eq!(resize.width, $width); + assert_eq!(resize.height, $height); + }; + } + + assert_resize!("auto", "50%", Auto, WindowPercent(50)); + assert_resize!("10", "20", Cells(10), Cells(20)); + assert_resize!("10%", "50px", WindowPercent(10), Pixels(50)); +} diff --git a/rio-backend/src/ansi/mod.rs b/rio-backend/src/ansi/mod.rs index 784e1f9831..4fbf45dc90 100644 --- a/rio-backend/src/ansi/mod.rs +++ b/rio-backend/src/ansi/mod.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; pub mod charset; pub mod control; pub mod graphics; +pub mod iterm2_image_protocol; pub mod mode; pub mod sixel; diff --git a/rio-backend/src/ansi/mode.rs b/rio-backend/src/ansi/mode.rs index 0777790614..e79f3f42e5 100644 --- a/rio-backend/src/ansi/mode.rs +++ b/rio-backend/src/ansi/mode.rs @@ -1,8 +1,96 @@ -use tracing::warn; - -#[derive(Debug, Eq, PartialEq)] +/// Wrapper for the ANSI modes. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Mode { - /// ?1 + /// Known ANSI mode. + Named(NamedMode), + /// Unidentified publc mode. + Unknown(u16), +} + +impl Mode { + pub fn new(mode: u16) -> Self { + match mode { + 4 => Self::Named(NamedMode::Insert), + 20 => Self::Named(NamedMode::LineFeedNewLine), + _ => Self::Unknown(mode), + } + } + + /// Get the raw value of the mode. + pub fn raw(self) -> u16 { + match self { + Self::Named(named) => named as u16, + Self::Unknown(mode) => mode, + } + } +} + +impl From for Mode { + fn from(value: NamedMode) -> Self { + Self::Named(value) + } +} + +/// ANSI modes. +#[repr(u16)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum NamedMode { + /// IRM Insert Mode. + Insert = 4, + LineFeedNewLine = 20, +} + +/// Wrapper for the private DEC modes. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum PrivateMode { + /// Known private mode. + Named(NamedPrivateMode), + /// Unknown private mode. + Unknown(u16), +} + +impl PrivateMode { + pub fn new(mode: u16) -> Self { + match mode { + 1 => Self::Named(NamedPrivateMode::CursorKeys), + 3 => Self::Named(NamedPrivateMode::ColumnMode), + 6 => Self::Named(NamedPrivateMode::Origin), + 7 => Self::Named(NamedPrivateMode::LineWrap), + 12 => Self::Named(NamedPrivateMode::BlinkingCursor), + 25 => Self::Named(NamedPrivateMode::ShowCursor), + 1000 => Self::Named(NamedPrivateMode::ReportMouseClicks), + 1002 => Self::Named(NamedPrivateMode::ReportCellMouseMotion), + 1003 => Self::Named(NamedPrivateMode::ReportAllMouseMotion), + 1004 => Self::Named(NamedPrivateMode::ReportFocusInOut), + 1005 => Self::Named(NamedPrivateMode::Utf8Mouse), + 1006 => Self::Named(NamedPrivateMode::SgrMouse), + 1007 => Self::Named(NamedPrivateMode::AlternateScroll), + 1042 => Self::Named(NamedPrivateMode::UrgencyHints), + 1049 => Self::Named(NamedPrivateMode::SwapScreenAndSetRestoreCursor), + 2004 => Self::Named(NamedPrivateMode::BracketedPaste), + 2026 => Self::Named(NamedPrivateMode::SyncUpdate), + _ => Self::Unknown(mode), + } + } + + /// Get the raw value of the mode. + pub fn raw(self) -> u16 { + match self { + Self::Named(named) => named as u16, + Self::Unknown(mode) => mode, + } + } +} + +impl From for PrivateMode { + fn from(value: NamedPrivateMode) -> Self { + Self::Named(value) + } +} + +/// Private DEC modes. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum NamedPrivateMode { CursorKeys = 1, /// Select 80 or 132 columns per page (DECCOLM). /// @@ -15,96 +103,58 @@ pub enum Mode { /// * erases all data in page memory /// * resets DECLRMM to unavailable /// * clears data from the status line (if set to host-writable) - Column = 3, - /// IRM Insert Mode. - /// - /// NB should be part of non-private mode enum. - /// - /// * `CSI 4 h` change to insert mode - /// * `CSI 4 l` reset to replacement mode - Insert = 4, - /// ?6 + ColumnMode = 3, Origin = 6, - /// ?7 LineWrap = 7, - /// ?12 BlinkingCursor = 12, - /// 20 - /// - /// NB This is actually a private mode. We should consider adding a second - /// enumeration for public/private modesets. - LineFeedNewLine = 20, - /// ?25 ShowCursor = 25, - /// ?80 - SixelDisplay = 80, - /// ?1000 ReportMouseClicks = 1000, - /// ?1002 - ReportSquareMouseMotion = 1002, - /// ?1003 + ReportCellMouseMotion = 1002, ReportAllMouseMotion = 1003, - /// ?1004 ReportFocusInOut = 1004, - /// ?1005 Utf8Mouse = 1005, - /// ?1006 SgrMouse = 1006, - /// ?1007 AlternateScroll = 1007, - /// ?1042 UrgencyHints = 1042, - /// ?1049 SwapScreenAndSetRestoreCursor = 1049, - /// Use a private palette for each new graphic. - SixelPrivateColorRegisters = 1070, - /// ?2004 BracketedPaste = 2004, - /// Sixel scrolling leaves cursor to right of graphic. - SixelCursorToTheRight = 8452, + /// The mode is handled automatically by [`Processor`]. + SyncUpdate = 2026, } -impl Mode { - /// Create mode from a primitive. - pub fn from_primitive(intermediate: Option<&u8>, num: u16) -> Option { - let private = match intermediate { - Some(b'?') => true, - None => false, - _ => return None, - }; +/// Mode for clearing line. +/// +/// Relative to cursor. +#[derive(Debug)] +pub enum LineClearMode { + /// Clear right of cursor. + Right, + /// Clear left of cursor. + Left, + /// Clear entire line. + All, +} - if private { - Some(match num { - 1 => Mode::CursorKeys, - 3 => Mode::Column, - 6 => Mode::Origin, - 7 => Mode::LineWrap, - 12 => Mode::BlinkingCursor, - 25 => Mode::ShowCursor, - 80 => Mode::SixelDisplay, - 1000 => Mode::ReportMouseClicks, - 1002 => Mode::ReportSquareMouseMotion, - 1003 => Mode::ReportAllMouseMotion, - 1004 => Mode::ReportFocusInOut, - 1005 => Mode::Utf8Mouse, - 1006 => Mode::SgrMouse, - 1007 => Mode::AlternateScroll, - 1042 => Mode::UrgencyHints, - 1049 => Mode::SwapScreenAndSetRestoreCursor, - 1070 => Mode::SixelPrivateColorRegisters, - 2004 => Mode::BracketedPaste, - 8452 => Mode::SixelCursorToTheRight, - _ => { - warn!("[unimplemented] primitive mode: {}", num); - return None; - } - }) - } else { - Some(match num { - 4 => Mode::Insert, - 20 => Mode::LineFeedNewLine, - _ => return None, - }) - } - } +/// Mode for clearing terminal. +/// +/// Relative to cursor. +#[derive(Debug)] +pub enum ClearMode { + /// Clear below cursor. + Below, + /// Clear above cursor. + Above, + /// Clear entire terminal. + All, + /// Clear 'saved' lines (scrollback). + Saved, +} + +/// Mode for clearing tab stops. +#[derive(Debug)] +pub enum TabulationClearMode { + /// Clear stop under cursor. + Current, + /// Clear all stops. + All, } diff --git a/rio-backend/src/ansi/sixel.rs b/rio-backend/src/ansi/sixel.rs index e18a1bea57..05dd5e25ad 100644 --- a/rio-backend/src/ansi/sixel.rs +++ b/rio-backend/src/ansi/sixel.rs @@ -25,9 +25,8 @@ use std::cmp::max; use std::{fmt, mem}; -use crate::ansi::graphics::MAX_GRAPHIC_DIMENSIONS; use crate::config::colors::ColorRgb; -use sugarloaf::{ColorType, GraphicData, GraphicId}; +use sugarloaf::{ColorType, GraphicData, GraphicId, MAX_GRAPHIC_DIMENSIONS}; use copa::Params; use tracing::trace; @@ -48,16 +47,24 @@ const MAX_COMMAND_PARAMS: usize = 5; #[derive(Debug)] pub enum Error { /// Image dimensions are too big. - TooBigImage { width: usize, height: usize }, + TooBigImage { + width: usize, + height: usize, + }, /// A component in a color introducer is not valid. - InvalidColorComponent { register: u16, component_value: u16 }, + InvalidColorComponent { + register: u16, + component_value: u16, + }, /// The coordinate system to define the color register is not valid. InvalidColorCoordinateSystem { register: u16, coordinate_system: u16, }, + + NonExistentParser, } impl fmt::Display for Error { @@ -92,6 +99,10 @@ impl fmt::Display for Error { coordinate_system, register ) } + + Error::NonExistentParser => { + write!(fmt, "Parser does not exist",) + } } } } @@ -543,6 +554,7 @@ impl Parser { color_type: ColorType::Rgba, pixels: rgba_pixels, is_opaque, + resize: None, }; Ok((data, self.color_registers)) diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs index 3e0571bc50..784d8858b2 100644 --- a/rio-backend/src/crosswords/mod.rs +++ b/rio-backend/src/crosswords/mod.rs @@ -25,7 +25,9 @@ use crate::ansi::graphics::GraphicCell; use crate::ansi::graphics::Graphics; use crate::ansi::graphics::TextureRef; use crate::ansi::graphics::UpdateQueues; -use crate::ansi::graphics::MAX_GRAPHIC_DIMENSIONS; +use crate::ansi::mode::NamedMode; +use crate::ansi::mode::NamedPrivateMode; +use crate::ansi::mode::PrivateMode; use crate::ansi::sixel; use crate::ansi::{ mode::Mode as AnsiMode, ClearMode, CursorShape, KeyboardModes, @@ -57,7 +59,7 @@ use std::ops::{Index, IndexMut, Range}; use std::option::Option; use std::ptr; use std::sync::Arc; -use sugarloaf::GraphicData; +use sugarloaf::{GraphicData, MAX_GRAPHIC_DIMENSIONS}; use tracing::{debug, info, warn}; use unicode_width::UnicodeWidthChar; use vi_mode::{ViModeCursor, ViMotion}; @@ -73,40 +75,63 @@ const MAX_GRAPHICS_PER_CELL: usize = 20; bitflags! { #[derive(Debug, Copy, Clone)] pub struct Mode: u32 { - const NONE = 0; - const SHOW_CURSOR = 0b0000_0000_0000_0000_0000_0001; - const APP_CURSOR = 0b0000_0000_0000_0000_0000_0010; - const APP_KEYPAD = 0b0000_0000_0000_0000_0000_0100; - const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_0000_1000; - const BRACKETED_PASTE = 0b0000_0000_0000_0000_0001_0000; - const SGR_MOUSE = 0b0000_0000_0000_0000_0010_0000; - const MOUSE_MOTION = 0b0000_0000_0000_0000_0100_0000; - const LINE_WRAP = 0b0000_0000_0000_0000_1000_0000; - const LINE_FEED_NEW_LINE = 0b0000_0000_0000_0001_0000_0000; - const ORIGIN = 0b0000_0000_0000_0010_0000_0000; - const INSERT = 0b0000_0000_0000_0100_0000_0000; - const FOCUS_IN_OUT = 0b0000_0000_0000_1000_0000_0000; - const ALT_SCREEN = 0b0000_0000_0001_0000_0000_0000; - const MOUSE_DRAG = 0b0000_0000_0010_0000_0000_0000; - const MOUSE_MODE = 0b0000_0000_0010_0000_0100_1000; - const UTF8_MOUSE = 0b0000_0000_0100_0000_0000_0000; - const ALTERNATE_SCROLL = 0b0000_0000_1000_0000_0000_0000; - const VI = 0b0000_0001_0000_0000_0000_0000; - const URGENCY_HINTS = 0b0000_0010_0000_0000_0000_0000; - const KEYBOARD_DISAMBIGUATE_ESC_CODES = 0b0000_0100_0000_0000_0000_0000; - const KEYBOARD_REPORT_EVENT_TYPES = 0b0000_1000_0000_0000_0000_0000; - const KEYBOARD_REPORT_ALTERNATE_KEYS = 0b0001_0000_0000_0000_0000_0000; - const KEYBOARD_REPORT_ALL_KEYS_AS_ESC = 0b0010_0000_0000_0000_0000_0000; - const KEYBOARD_REPORT_ASSOCIATED_TEXT = 0b0100_0000_0000_0000_0000_0000; - const KEYBOARD_PROTOCOL = Self::KEYBOARD_DISAMBIGUATE_ESC_CODES.bits() - | Self::KEYBOARD_REPORT_EVENT_TYPES.bits() - | Self::KEYBOARD_REPORT_ALTERNATE_KEYS.bits() - | Self::KEYBOARD_REPORT_ALL_KEYS_AS_ESC.bits() - | Self::KEYBOARD_REPORT_ASSOCIATED_TEXT.bits(); + const NONE = 0; + const SHOW_CURSOR = 0b0000_0000_0000_0000_0000_0001; + const APP_CURSOR = 0b0000_0000_0000_0000_0000_0010; + const APP_KEYPAD = 0b0000_0000_0000_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_0000_1000; + const BRACKETED_PASTE = 0b0000_0000_0000_0000_0001_0000; + const SGR_MOUSE = 0b0000_0000_0000_0000_0010_0000; + const MOUSE_MOTION = 0b0000_0000_0000_0000_0100_0000; + const LINE_WRAP = 0b0000_0000_0000_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b0000_0000_0000_0001_0000_0000; + const ORIGIN = 0b0000_0000_0000_0010_0000_0000; + const INSERT = 0b0000_0000_0000_0100_0000_0000; + const FOCUS_IN_OUT = 0b0000_0000_0000_1000_0000_0000; + const ALT_SCREEN = 0b0000_0000_0001_0000_0000_0000; + const MOUSE_DRAG = 0b0000_0000_0010_0000_0000_0000; + const MOUSE_MODE = 0b0000_0000_0010_0000_0100_1000; + const UTF8_MOUSE = 0b0000_0000_0100_0000_0000_0000; + const ALTERNATE_SCROLL = 0b0000_0000_1000_0000_0000_0000; + const VI = 0b0000_0001_0000_0000_0000_0000; + const URGENCY_HINTS = 0b0000_0010_0000_0000_0000_0000; + const DISAMBIGUATE_ESC_CODES = 0b0000_0100_0000_0000_0000_0000; + const REPORT_EVENT_TYPES = 0b0000_1000_0000_0000_0000_0000; + const REPORT_ALTERNATE_KEYS = 0b0001_0000_0000_0000_0000_0000; + const REPORT_ALL_KEYS_AS_ESC = 0b0010_0000_0000_0000_0000_0000; + const REPORT_ASSOCIATED_TEXT = 0b0100_0000_0000_0000_0000_0000; + const KITTY_KEYBOARD_PROTOCOL = Self::DISAMBIGUATE_ESC_CODES.bits() + | Self::REPORT_EVENT_TYPES.bits() + | Self::REPORT_ALTERNATE_KEYS.bits() + | Self::REPORT_ALL_KEYS_AS_ESC.bits() + | Self::REPORT_ASSOCIATED_TEXT.bits(); + const ANY = u32::MAX; + const SIXEL_DISPLAY = 1 << 28; const SIXEL_PRIV_PALETTE = 1 << 29; const SIXEL_CURSOR_TO_THE_RIGHT = 1 << 31; - const ANY = u32::MAX; + } +} + +/// The state of the [`Mode`] and [`PrivateMode`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +enum ModeState { + /// The mode is not supported. + NotSupported = 0, + /// The mode is currently set. + Set = 1, + /// The mode is currently not set. + Reset = 2, +} + +impl From for ModeState { + fn from(value: bool) -> Self { + if value { + Self::Set + } else { + Self::Reset + } } } @@ -124,23 +149,23 @@ impl From for Mode { fn from(value: KeyboardModes) -> Self { let mut mode = Self::empty(); mode.set( - Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES, + Mode::DISAMBIGUATE_ESC_CODES, value.contains(KeyboardModes::DISAMBIGUATE_ESC_CODES), ); mode.set( - Mode::KEYBOARD_REPORT_EVENT_TYPES, + Mode::REPORT_EVENT_TYPES, value.contains(KeyboardModes::REPORT_EVENT_TYPES), ); mode.set( - Mode::KEYBOARD_REPORT_ALTERNATE_KEYS, + Mode::REPORT_ALTERNATE_KEYS, value.contains(KeyboardModes::REPORT_ALTERNATE_KEYS), ); mode.set( - Mode::KEYBOARD_REPORT_ALL_KEYS_AS_ESC, + Mode::REPORT_ALL_KEYS_AS_ESC, value.contains(KeyboardModes::REPORT_ALL_KEYS_AS_ESC), ); mode.set( - Mode::KEYBOARD_REPORT_ASSOCIATED_TEXT, + Mode::REPORT_ASSOCIATED_TEXT, value.contains(KeyboardModes::REPORT_ASSOCIATED_TEXT), ); mode @@ -355,7 +380,7 @@ const TITLE_STACK_MAX_DEPTH: usize = 4096; // Max size of the keyboard modes. const KEYBOARD_MODE_STACK_MAX_DEPTH: usize = 16384; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Crosswords where U: EventListener, @@ -1184,8 +1209,8 @@ impl Crosswords { #[inline] fn set_keyboard_mode(&mut self, mode: Mode, apply: KeyboardModesApplyBehavior) { // println!("{:?}", mode); - let active_mode = self.mode & Mode::KEYBOARD_PROTOCOL; - self.mode &= !Mode::KEYBOARD_PROTOCOL; + let active_mode = self.mode & Mode::KITTY_KEYBOARD_PROTOCOL; + self.mode &= !Mode::KITTY_KEYBOARD_PROTOCOL; let new_mode = match apply { KeyboardModesApplyBehavior::Replace => mode, KeyboardModesApplyBehavior::Union => active_mode.union(mode), @@ -1229,144 +1254,291 @@ impl Crosswords { impl Handler for Crosswords { #[inline] fn set_mode(&mut self, mode: AnsiMode) { + let mode = match mode { + AnsiMode::Named(mode) => mode, + AnsiMode::Unknown(mode) => { + debug!("Ignoring unknown mode {} in set_mode", mode); + return; + } + }; + + tracing::trace!("Setting public mode: {:?}", mode); match mode { - AnsiMode::UrgencyHints => self.mode.insert(Mode::URGENCY_HINTS), - AnsiMode::SwapScreenAndSetRestoreCursor => { + NamedMode::Insert => self.mode.insert(Mode::INSERT), + NamedMode::LineFeedNewLine => self.mode.insert(Mode::LINE_FEED_NEW_LINE), + } + } + + #[inline] + fn unset_mode(&mut self, mode: AnsiMode) { + let mode = match mode { + AnsiMode::Named(mode) => mode, + AnsiMode::Unknown(mode) => { + debug!("Ignoring unknown mode {} in unset_mode", mode); + return; + } + }; + + tracing::trace!("Setting public mode: {:?}", mode); + match mode { + NamedMode::Insert => { + self.mode.remove(Mode::INSERT); + self.mark_fully_damaged(); + } + NamedMode::LineFeedNewLine => self.mode.remove(Mode::LINE_FEED_NEW_LINE), + } + } + + #[inline] + fn report_mode(&mut self, mode: AnsiMode) { + tracing::trace!("Reporting mode {mode:?}"); + let state = match mode { + AnsiMode::Named(mode) => match mode { + NamedMode::Insert => self.mode.contains(Mode::INSERT).into(), + NamedMode::LineFeedNewLine => { + self.mode.contains(Mode::LINE_FEED_NEW_LINE).into() + } + }, + AnsiMode::Unknown(_) => ModeState::NotSupported, + }; + + self.event_proxy.send_event( + RioEvent::PtyWrite(format!("\x1b[{};{}$y", mode.raw(), state as u8,)), + self.window_id, + ); + } + + #[inline] + fn set_private_mode(&mut self, mode: PrivateMode) { + let mode = match mode { + PrivateMode::Named(mode) => mode, + + // SixelDisplay + PrivateMode::Unknown(80) => { + self.mode.insert(Mode::SIXEL_DISPLAY); + return; + } + + // SixelPrivateColorRegisters + PrivateMode::Unknown(1070) => { + self.mode.insert(Mode::SIXEL_PRIV_PALETTE); + return; + } + + // SixelCursorToTheRight + PrivateMode::Unknown(8452) => { + self.mode.insert(Mode::SIXEL_CURSOR_TO_THE_RIGHT); + return; + } + + PrivateMode::Unknown(mode) => { + debug!("Ignoring unknown mode {} in set_private_mode", mode); + return; + } + }; + + tracing::trace!("Setting private mode: {:?}", mode); + match mode { + NamedPrivateMode::UrgencyHints => self.mode.insert(Mode::URGENCY_HINTS), + NamedPrivateMode::SwapScreenAndSetRestoreCursor => { if !self.mode.contains(Mode::ALT_SCREEN) { self.swap_alt(); } } - AnsiMode::ShowCursor => self.mode.insert(Mode::SHOW_CURSOR), - AnsiMode::CursorKeys => self.mode.insert(Mode::APP_CURSOR), + NamedPrivateMode::ShowCursor => self.mode.insert(Mode::SHOW_CURSOR), + NamedPrivateMode::CursorKeys => self.mode.insert(Mode::APP_CURSOR), // Mouse protocols are mutually exclusive. - AnsiMode::ReportMouseClicks => { + NamedPrivateMode::ReportMouseClicks => { self.mode.remove(Mode::MOUSE_MODE); self.mode.insert(Mode::MOUSE_REPORT_CLICK); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportSquareMouseMotion => { + NamedPrivateMode::ReportCellMouseMotion => { self.mode.remove(Mode::MOUSE_MODE); self.mode.insert(Mode::MOUSE_DRAG); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportAllMouseMotion => { + NamedPrivateMode::ReportAllMouseMotion => { self.mode.remove(Mode::MOUSE_MODE); self.mode.insert(Mode::MOUSE_MOTION); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportFocusInOut => self.mode.insert(Mode::FOCUS_IN_OUT), - AnsiMode::BracketedPaste => self.mode.insert(Mode::BRACKETED_PASTE), + NamedPrivateMode::ReportFocusInOut => self.mode.insert(Mode::FOCUS_IN_OUT), + NamedPrivateMode::BracketedPaste => self.mode.insert(Mode::BRACKETED_PASTE), // Mouse encodings are mutually exclusive. - AnsiMode::SgrMouse => { + NamedPrivateMode::SgrMouse => { self.mode.remove(Mode::UTF8_MOUSE); self.mode.insert(Mode::SGR_MOUSE); } - AnsiMode::Utf8Mouse => { + NamedPrivateMode::Utf8Mouse => { self.mode.remove(Mode::SGR_MOUSE); self.mode.insert(Mode::UTF8_MOUSE); } - AnsiMode::AlternateScroll => self.mode.insert(Mode::ALTERNATE_SCROLL), - AnsiMode::LineWrap => self.mode.insert(Mode::LINE_WRAP), - AnsiMode::LineFeedNewLine => self.mode.insert(Mode::LINE_FEED_NEW_LINE), - AnsiMode::Origin => self.mode.insert(Mode::ORIGIN), - AnsiMode::Column => self.deccolm(), - AnsiMode::Insert => self.mode.insert(Mode::INSERT), - AnsiMode::BlinkingCursor => { + NamedPrivateMode::AlternateScroll => self.mode.insert(Mode::ALTERNATE_SCROLL), + NamedPrivateMode::LineWrap => self.mode.insert(Mode::LINE_WRAP), + NamedPrivateMode::Origin => self.mode.insert(Mode::ORIGIN), + NamedPrivateMode::ColumnMode => self.deccolm(), + NamedPrivateMode::BlinkingCursor => { self.blinking_cursor = true; self.event_proxy .send_event(RioEvent::CursorBlinkingChange, self.window_id); } - AnsiMode::SixelDisplay => self.mode.insert(Mode::SIXEL_DISPLAY), - AnsiMode::SixelPrivateColorRegisters => { - self.mode.insert(Mode::SIXEL_PRIV_PALETTE) - } - AnsiMode::SixelCursorToTheRight => { - self.mode.insert(Mode::SIXEL_CURSOR_TO_THE_RIGHT); - } + NamedPrivateMode::SyncUpdate => (), } } #[inline] - fn dynamic_color_sequence(&mut self, prefix: String, index: usize, terminator: &str) { - debug!( - "Requested write of escape sequence for color code {}: color[{}]", - prefix, index - ); + fn unset_private_mode(&mut self, mode: PrivateMode) { + let mode = match mode { + PrivateMode::Named(mode) => mode, - let terminator = terminator.to_owned(); - self.event_proxy.send_event( - RioEvent::ColorRequest( - index, - Arc::new(move |color| { - format!( - "\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}", - prefix, color.r, color.g, color.b, terminator - ) - }), - ), - self.window_id, - ); - } + // SixelDisplay + PrivateMode::Unknown(80) => { + self.mode.remove(Mode::SIXEL_DISPLAY); + return; + } - #[inline] - fn unset_mode(&mut self, mode: AnsiMode) { + // SixelPrivateColorRegisters + PrivateMode::Unknown(1070) => { + self.graphics.sixel_shared_palette = None; + self.mode.remove(Mode::SIXEL_PRIV_PALETTE); + return; + } + + // SixelCursorToTheRight + PrivateMode::Unknown(8452) => { + self.mode.remove(Mode::SIXEL_CURSOR_TO_THE_RIGHT); + return; + } + + PrivateMode::Unknown(mode) => { + debug!("Ignoring unknown mode {} in unset_private_mode", mode); + return; + } + }; + + tracing::trace!("Unsetting private mode: {:?}", mode); match mode { - AnsiMode::UrgencyHints => self.mode.remove(Mode::URGENCY_HINTS), - AnsiMode::SwapScreenAndSetRestoreCursor => { + NamedPrivateMode::UrgencyHints => self.mode.remove(Mode::URGENCY_HINTS), + NamedPrivateMode::SwapScreenAndSetRestoreCursor => { if self.mode.contains(Mode::ALT_SCREEN) { self.swap_alt(); } } - AnsiMode::ShowCursor => self.mode.remove(Mode::SHOW_CURSOR), - AnsiMode::CursorKeys => self.mode.remove(Mode::APP_CURSOR), - AnsiMode::ReportMouseClicks => { + NamedPrivateMode::ShowCursor => self.mode.remove(Mode::SHOW_CURSOR), + NamedPrivateMode::CursorKeys => self.mode.remove(Mode::APP_CURSOR), + NamedPrivateMode::ReportMouseClicks => { self.mode.remove(Mode::MOUSE_REPORT_CLICK); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportSquareMouseMotion => { + NamedPrivateMode::ReportCellMouseMotion => { self.mode.remove(Mode::MOUSE_DRAG); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportAllMouseMotion => { + NamedPrivateMode::ReportAllMouseMotion => { self.mode.remove(Mode::MOUSE_MOTION); self.event_proxy .send_event(RioEvent::MouseCursorDirty, self.window_id); } - AnsiMode::ReportFocusInOut => self.mode.remove(Mode::FOCUS_IN_OUT), - AnsiMode::BracketedPaste => self.mode.remove(Mode::BRACKETED_PASTE), - AnsiMode::SgrMouse => self.mode.remove(Mode::SGR_MOUSE), - AnsiMode::Utf8Mouse => self.mode.remove(Mode::UTF8_MOUSE), - AnsiMode::AlternateScroll => self.mode.remove(Mode::ALTERNATE_SCROLL), - AnsiMode::LineWrap => self.mode.remove(Mode::LINE_WRAP), - AnsiMode::LineFeedNewLine => self.mode.remove(Mode::LINE_FEED_NEW_LINE), - AnsiMode::Origin => self.mode.remove(Mode::ORIGIN), - AnsiMode::Column => self.deccolm(), - AnsiMode::Insert => { - self.mode.remove(Mode::INSERT); - self.mark_fully_damaged(); - } - AnsiMode::BlinkingCursor => { + NamedPrivateMode::ReportFocusInOut => self.mode.remove(Mode::FOCUS_IN_OUT), + NamedPrivateMode::BracketedPaste => self.mode.remove(Mode::BRACKETED_PASTE), + NamedPrivateMode::SgrMouse => self.mode.remove(Mode::SGR_MOUSE), + NamedPrivateMode::Utf8Mouse => self.mode.remove(Mode::UTF8_MOUSE), + NamedPrivateMode::AlternateScroll => self.mode.remove(Mode::ALTERNATE_SCROLL), + NamedPrivateMode::LineWrap => self.mode.remove(Mode::LINE_WRAP), + NamedPrivateMode::Origin => self.mode.remove(Mode::ORIGIN), + NamedPrivateMode::ColumnMode => self.deccolm(), + NamedPrivateMode::BlinkingCursor => { // TODO: Update it // self.blinking_cursor = false; // self.event_proxy - // .send_event(RioEvent::CursorBlinkingChange, self.window_id); - } - AnsiMode::SixelDisplay => self.mode.remove(Mode::SIXEL_DISPLAY), - AnsiMode::SixelPrivateColorRegisters => { - self.graphics.sixel_shared_palette = None; - self.mode.remove(Mode::SIXEL_PRIV_PALETTE); - } - AnsiMode::SixelCursorToTheRight => { - self.mode.remove(Mode::SIXEL_CURSOR_TO_THE_RIGHT) + // .send_event(RioEvent::CursorBlinkingChange, self.window_id); } + NamedPrivateMode::SyncUpdate => (), } } + #[inline] + fn report_private_mode(&mut self, mode: PrivateMode) { + tracing::info!("Reporting private mode {mode:?}"); + let state = match mode { + PrivateMode::Named(mode) => match mode { + NamedPrivateMode::CursorKeys => { + self.mode.contains(Mode::APP_CURSOR).into() + } + NamedPrivateMode::Origin => self.mode.contains(Mode::ORIGIN).into(), + NamedPrivateMode::LineWrap => self.mode.contains(Mode::LINE_WRAP).into(), + NamedPrivateMode::BlinkingCursor => self.blinking_cursor.into(), + NamedPrivateMode::ShowCursor => { + self.mode.contains(Mode::SHOW_CURSOR).into() + } + NamedPrivateMode::ReportMouseClicks => { + self.mode.contains(Mode::MOUSE_REPORT_CLICK).into() + } + NamedPrivateMode::ReportCellMouseMotion => { + self.mode.contains(Mode::MOUSE_DRAG).into() + } + NamedPrivateMode::ReportAllMouseMotion => { + self.mode.contains(Mode::MOUSE_MOTION).into() + } + NamedPrivateMode::ReportFocusInOut => { + self.mode.contains(Mode::FOCUS_IN_OUT).into() + } + NamedPrivateMode::Utf8Mouse => { + self.mode.contains(Mode::UTF8_MOUSE).into() + } + NamedPrivateMode::SgrMouse => self.mode.contains(Mode::SGR_MOUSE).into(), + NamedPrivateMode::AlternateScroll => { + self.mode.contains(Mode::ALTERNATE_SCROLL).into() + } + NamedPrivateMode::UrgencyHints => { + self.mode.contains(Mode::URGENCY_HINTS).into() + } + NamedPrivateMode::SwapScreenAndSetRestoreCursor => { + self.mode.contains(Mode::ALT_SCREEN).into() + } + NamedPrivateMode::BracketedPaste => { + self.mode.contains(Mode::BRACKETED_PASTE).into() + } + NamedPrivateMode::SyncUpdate => ModeState::Reset, + NamedPrivateMode::ColumnMode => ModeState::NotSupported, + }, + PrivateMode::Unknown(_) => ModeState::NotSupported, + }; + + self.event_proxy.send_event( + RioEvent::PtyWrite(format!("\x1b[?{};{}$y", mode.raw(), state as u8,)), + self.window_id, + ); + } + + #[inline] + fn dynamic_color_sequence(&mut self, prefix: String, index: usize, terminator: &str) { + debug!( + "Requested write of escape sequence for color code {}: color[{}]", + prefix, index + ); + + let terminator = terminator.to_owned(); + self.event_proxy.send_event( + RioEvent::ColorRequest( + index, + Arc::new(move |color| { + format!( + "\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}", + prefix, color.r, color.g, color.b, terminator + ) + }), + ), + self.window_id, + ); + } + #[inline] fn goto(&mut self, line: Line, col: Column) { let (y_offset, max_y) = if self.mode.contains(Mode::ORIGIN) { @@ -2400,9 +2572,41 @@ impl Handler for Crosswords { } #[inline] - fn start_sixel_graphic(&mut self, params: &Params) -> Option> { + fn sixel_graphic_start(&mut self, params: &Params) { let palette = self.graphics.sixel_shared_palette.take(); - Some(Box::new(sixel::Parser::new(params, palette))) + self.graphics.sixel_parser = Some(Box::new(sixel::Parser::new(params, palette))); + } + + #[inline] + fn is_sixel_graphic_active(&self) -> bool { + self.graphics.sixel_parser.is_some() + } + + #[inline] + fn sixel_graphic_put(&mut self, byte: u8) -> Result<(), sixel::Error> { + if let Some(parser) = &mut self.graphics.sixel_parser { + parser.put(byte) + } else { + Err(sixel::Error::NonExistentParser) + } + } + + #[inline] + fn sixel_graphic_reset(&mut self) { + self.graphics.sixel_parser = None; + } + + #[inline] + fn sixel_graphic_finish(&mut self) { + let parser = self.graphics.sixel_parser.take(); + if let Some(parser) = parser { + match parser.finish() { + Ok((graphic, palette)) => self.insert_graphic(graphic, Some(palette)), + Err(err) => tracing::warn!("Failed to parse Sixel data: {}", err), + } + } else { + tracing::warn!("Failed to sixel_graphic_finish"); + } } #[inline] @@ -2417,6 +2621,16 @@ impl Handler for Crosswords { } } + let graphic = match graphic.resized( + cell_width, + cell_height, + cell_width * self.grid.columns(), + cell_height * self.grid.screen_lines(), + ) { + Some(graphic) => graphic, + None => return, + }; + if graphic.width > MAX_GRAPHIC_DIMENSIONS[0] || graphic.height > MAX_GRAPHIC_DIMENSIONS[1] { diff --git a/rio-backend/src/performer/handler.rs b/rio-backend/src/performer/handler.rs index 55175af7d4..30f5d1c111 100644 --- a/rio-backend/src/performer/handler.rs +++ b/rio-backend/src/performer/handler.rs @@ -1,5 +1,6 @@ +use crate::ansi::iterm2_image_protocol; use crate::ansi::CursorShape; -use crate::ansi::{mode::Mode, sixel, KeyboardModes, KeyboardModesApplyBehavior}; +use crate::ansi::{sixel, KeyboardModes, KeyboardModesApplyBehavior}; use crate::config::colors::{AnsiColor, ColorRgb, NamedColor}; use crate::crosswords::pos::{CharsetIndex, Column, Line, StandardCharset}; use crate::crosswords::square::Hyperlink; @@ -13,7 +14,10 @@ use tracing::{debug, warn}; use crate::crosswords::attr::Attr; use crate::ansi::control::C0; -use crate::ansi::{ClearMode, LineClearMode, TabulationClearMode}; +use crate::ansi::{ + mode::{Mode, NamedPrivateMode, PrivateMode}, + ClearMode, LineClearMode, TabulationClearMode, +}; use std::fmt::Write; // https://vt100.net/emu/dec_ansi_parser @@ -25,16 +29,14 @@ const SYNC_UPDATE_TIMEOUT: Duration = Duration::from_millis(150); /// Maximum number of bytes read in one synchronized update (2MiB). const SYNC_BUFFER_SIZE: usize = 0x20_0000; -/// Number of bytes in the synchronized update DCS sequence before the passthrough parameters. -const SYNC_ESCAPE_START_LEN: usize = 5; +/// Number of bytes in the BSU/ESU CSI sequences. +const SYNC_ESCAPE_LEN: usize = 8; -/// Start of the DCS sequence for beginning synchronized updates. -const SYNC_START_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = - [b'\x1b', b'P', b'=', b'1', b's']; +/// BSU CSI sequence for beginning or extending synchronized updates. +const BSU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026h"; -/// Start of the DCS sequence for terminating synchronized updates. -const SYNC_END_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = - [b'\x1b', b'P', b'=', b'2', b's']; +/// ESU CSI sequence for terminating synchronized updates. +const ESU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026l"; fn xparse_color(color: &[u8]) -> Option { if !color.is_empty() && color[0] == b'#' { @@ -75,19 +77,6 @@ fn parse_rgb_color(color: &[u8]) -> Option { }) } -/// Pending DCS sequence. -#[derive(Debug)] -enum Dcs { - /// Begin of the synchronized update. - SyncStart, - - /// End of the synchronized update. - SyncEnd, - - /// Sixel data - SixelData(Box), -} - /// Parse colors in `#r(rrr)g(ggg)b(bbb)` format. fn parse_legacy_color(color: &[u8]) -> Option { let item_len = color.len() / 3; @@ -282,7 +271,19 @@ pub trait Handler { fn set_mode(&mut self, _mode: Mode) {} /// Unset mode. - fn unset_mode(&mut self, _: Mode) {} + fn unset_mode(&mut self, _mode: Mode) {} + + /// DECRPM - report mode. + fn report_mode(&mut self, _mode: Mode) {} + + /// Set private mode. + fn set_private_mode(&mut self, _mode: PrivateMode) {} + + /// Unset private mode. + fn unset_private_mode(&mut self, _mode: PrivateMode) {} + + /// DECRPM - report private mode. + fn report_private_mode(&mut self, _mode: PrivateMode) {} /// DECSTBM - Set the terminal scrolling region. fn set_scrolling_region(&mut self, _top: usize, _bottom: Option) {} @@ -342,9 +343,15 @@ pub trait Handler { fn graphics_attribute(&mut self, _: u16, _: u16) {} /// Create a parser for Sixel data. - fn start_sixel_graphic(&mut self, _params: &Params) -> Option> { - None + fn sixel_graphic_start(&mut self, _params: &Params) {} + fn is_sixel_graphic_active(&self) -> bool { + false + } + fn sixel_graphic_put(&mut self, _byte: u8) -> Result<(), sixel::Error> { + Ok(()) } + fn sixel_graphic_reset(&mut self) {} + fn sixel_graphic_finish(&mut self) {} /// Insert a new graphic item. fn insert_graphic(&mut self, _data: GraphicData, _palette: Option>) {} @@ -384,9 +391,6 @@ struct ProcessorState { /// State for synchronized terminal updates. sync_state: SyncState, - - /// DCS sequence waiting for termination. - dcs: Option, } #[derive(Debug)] @@ -396,9 +400,6 @@ struct SyncState { /// Bytes read during the synchronized update. buffer: Vec, - - /// Sync DCS waiting for termination sequence. - pending_dcs: Option, } impl Default for SyncState { @@ -406,7 +407,6 @@ impl Default for SyncState { Self { buffer: Vec::with_capacity(SYNC_BUFFER_SIZE), timeout: None, - pending_dcs: None, } } } @@ -450,6 +450,8 @@ impl ParserProcessor { self.parser.advance(&mut performer, byte); } + // Report that update ended, since we could end due to timeout. + handler.unset_private_mode(NamedPrivateMode::SyncUpdate.into()); // Resetting state after processing makes sure we don't interpret buffered sync escapes. self.state.sync_state.buffer.clear(); self.state.sync_state.timeout = None; @@ -475,48 +477,29 @@ impl ParserProcessor { { self.state.sync_state.buffer.push(byte); - // Handle sync DCS escape sequences. - match self.state.sync_state.pending_dcs { - Some(_) => self.advance_sync_dcs_end(handler, byte), - None => self.advance_sync_dcs_start(), - } + // Handle sync CSI escape sequences. + self.advance_sync_csi(handler); } - /// Find the start of sync DCS sequences. - fn advance_sync_dcs_start(&mut self) { + /// Handle BSU/ESU CSI sequences during synchronized update. + fn advance_sync_csi(&mut self, handler: &mut H) + where + H: Handler, + { // Get the last few bytes for comparison. let len = self.state.sync_state.buffer.len(); - let offset = len.saturating_sub(SYNC_ESCAPE_START_LEN); + let offset = len.saturating_sub(SYNC_ESCAPE_LEN); let end = &self.state.sync_state.buffer[offset..]; + // NOTE: It is technically legal to specify multiple private modes in the same + // escape, but we only allow EXACTLY `\e[?2026h`/`\e[?2026l` to keep the parser + // reasonable. + // // Check for extension/termination of the synchronized update. - if end == SYNC_START_ESCAPE_START { - self.state.sync_state.pending_dcs = Some(Dcs::SyncStart); - } else if end == SYNC_END_ESCAPE_START || len >= SYNC_BUFFER_SIZE - 1 { - self.state.sync_state.pending_dcs = Some(Dcs::SyncEnd); - } - } - - /// Parse the DCS termination sequence for synchronized updates. - fn advance_sync_dcs_end(&mut self, handler: &mut H, byte: u8) - where - H: Handler, - { - match byte { - // Ignore DCS passthrough characters. - 0x00..=0x17 | 0x19 | 0x1c..=0x7f | 0xa0..=0xff => (), - // Cancel the DCS sequence. - 0x18 | 0x1a | 0x80..=0x9f => self.state.sync_state.pending_dcs = None, - // Dispatch on ESC. - 0x1b => match self.state.sync_state.pending_dcs.take() { - Some(Dcs::SyncStart) => { - self.state.sync_state.timeout = - Some(Instant::now() + SYNC_UPDATE_TIMEOUT); - } - Some(Dcs::SyncEnd) => self.stop_sync(handler), - Some(Dcs::SixelData(_)) => (), - None => (), - }, + if end == BSU_CSI { + self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); + } else if end == ESU_CSI || len >= SYNC_BUFFER_SIZE - 1 { + self.stop_sync(handler); } } } @@ -568,8 +551,7 @@ impl copa::Perform for Performer<'_, U> { ) { match (action, intermediates) { ('q', []) => { - let parser = self.handler.start_sixel_graphic(params); - self.state.dcs = parser.map(Dcs::SixelData); + self.handler.sixel_graphic_start(params); } _ => debug!( "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", @@ -579,34 +561,22 @@ impl copa::Perform for Performer<'_, U> { } fn put(&mut self, byte: u8) { - debug!("[put] {byte:02x}"); - match self.state.dcs { - Some(Dcs::SixelData(ref mut parser)) => { - if let Err(err) = parser.put(byte) { - tracing::warn!("Failed to parse Sixel data: {}", err); - self.state.dcs = None; - } + if self.handler.is_sixel_graphic_active() { + if let Err(err) = self.handler.sixel_graphic_put(byte) { + tracing::warn!("Failed to parse Sixel data: {}", err); + self.handler.sixel_graphic_reset(); } - - _ => debug!("[unhandled put] byte={:?}", byte), + } else { + debug!("[unhandled put] byte={:?}", byte); } } #[inline] fn unhook(&mut self) { - match self.state.dcs.take() { - Some(Dcs::SyncStart) => { - self.state.sync_state.timeout = - Some(Instant::now() + SYNC_UPDATE_TIMEOUT); - } - Some(Dcs::SyncEnd) => (), - Some(Dcs::SixelData(parser)) => match parser.finish() { - Ok((graphic, palette)) => { - self.handler.insert_graphic(graphic, Some(palette)) - } - Err(err) => tracing::warn!("Failed to parse Sixel data: {}", err), - }, - _ => debug!("[unhandled unhook]"), + if self.handler.is_sixel_graphic_active() { + self.handler.sixel_graphic_finish(); + } else { + debug!("[unhandled dcs_unhook]"); } } @@ -804,37 +774,12 @@ impl copa::Perform for Performer<'_, U> { // OSC 1337 is not necessarily only used by iTerm2 protocol // OSC 1337 is equal to xterm OSC 50 - // b"1337" => { - // \x1b]1337;File=[arguments]:[base-64 encoded file contents]^G - // - // Example: - // printf "\x1b]1337;File=;size=234;width=100:aGVsbG8=\x07" - // - // leads to - // name: None, - // size: Some(234), - // width: 100, - // height: Automatic, - // preserve_aspect_ratio: true, - // inline: false, - // do_not_move_cursor: false, - // data: b"hello".to_vec(), - - // if params.len() >= 2 - // && params[1].len() >= 5 - // && params[1][0..5] == *b"File=" - // { - // let content = params[2].split(|&b| b == b':').collect::>(); - // if content.len() == 2 { - // let _arguments = content[0]; - - // let _base_64_file_content = content[1]; - // } - // // self.handler.set_cursor_shape(shape); - // // return; - // } - // unhandled(params); - // } + b"1337" => { + if let Some(graphic) = iterm2_image_protocol::parse(params) { + self.handler.insert_graphic(graphic, None); + } + } + _ => unhandled(params), } } @@ -914,19 +859,20 @@ impl copa::Perform for Performer<'_, U> { let x = next_param_or(1) as usize; handler.goto(Line(y - 1), Column(x - 1)); } - ('h', intermediates) => { + ('h', []) => { + for param in params_iter.map(|param| param[0]) { + handler.set_mode(Mode::new(param)) + } + } + ('h', [b'?']) => { for param in params_iter.map(|param| param[0]) { - let intermediate = intermediates.first(); - // Handle sync updates opaquely. - if intermediate == Some(&b'?') && param == 2026 { - // self.state.sync_state.timeout.set_timeout(SYNC_UPDATE_TIMEOUT); + if param == NamedPrivateMode::SyncUpdate as u16 { + self.state.sync_state.timeout = + Some(Instant::now() + SYNC_UPDATE_TIMEOUT); } - match Mode::from_primitive(intermediate, param) { - Some(mode) => handler.set_mode(mode), - None => csi_unhandled!(), - } + handler.set_private_mode(PrivateMode::new(param)) } } ('I', []) => handler.move_forward_tabs(next_param_or(1)), @@ -958,12 +904,14 @@ impl copa::Perform for Performer<'_, U> { handler.clear_line(mode); } ('L', []) => handler.insert_blank_lines(next_param_or(1) as usize), - ('l', intermediates) => { + ('l', []) => { for param in params_iter.map(|param| param[0]) { - match Mode::from_primitive(intermediates.first(), param) { - Some(mode) => handler.unset_mode(mode), - None => csi_unhandled!(), - } + handler.unset_mode(Mode::new(param)) + } + } + ('l', [b'?']) => { + for param in params_iter.map(|param| param[0]) { + handler.unset_private_mode(PrivateMode::new(param)) } } ('M', []) => handler.delete_lines(next_param_or(1) as usize), @@ -981,6 +929,14 @@ impl copa::Perform for Performer<'_, U> { } ('n', []) => handler.device_status(next_param_or(0) as usize), ('P', []) => handler.delete_chars(next_param_or(1) as usize), + ('p', [b'$']) => { + let mode = next_param_or(0); + handler.report_mode(Mode::new(mode)); + } + ('p', [b'?', b'$']) => { + let mode = next_param_or(0); + handler.report_private_mode(PrivateMode::new(mode)); + } ('q', [b' ']) => { // DECSCUSR (CSI Ps SP q) -- Set Cursor Style. let cursor_style_id = next_param_or(0); diff --git a/sugarloaf/src/components/core/image.rs b/sugarloaf/src/components/core/image.rs index a7b343c948..ed2ba026c6 100644 --- a/sugarloaf/src/components/core/image.rs +++ b/sugarloaf/src/components/core/image.rs @@ -9,7 +9,7 @@ use std::sync::Arc; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, - data: Data, + pub data: Data, } impl Handle { diff --git a/sugarloaf/src/lib.rs b/sugarloaf/src/lib.rs index 3bb56c152e..3b83795961 100644 --- a/sugarloaf/src/lib.rs +++ b/sugarloaf/src/lib.rs @@ -9,7 +9,10 @@ pub use font_introspector::{Stretch, Style, Weight}; pub use crate::sugarloaf::{ compositors::SugarCompositors, - graphics::{ColorType, Graphic, GraphicData, GraphicId, Graphics}, + graphics::{ + ColorType, Graphic, GraphicData, GraphicId, Graphics, ResizeCommand, + ResizeParameter, MAX_GRAPHIC_DIMENSIONS, + }, primitives::*, Sugarloaf, SugarloafErrors, SugarloafRenderer, SugarloafWindow, SugarloafWindowSize, SugarloafWithErrors, diff --git a/sugarloaf/src/sugarloaf/graphics.rs b/sugarloaf/src/sugarloaf/graphics.rs index f8ca0cdf80..3df813b84e 100644 --- a/sugarloaf/src/sugarloaf/graphics.rs +++ b/sugarloaf/src/sugarloaf/graphics.rs @@ -5,7 +5,12 @@ use crate::sugarloaf::types; use crate::sugarloaf::Handle; +use image_rs::DynamicImage; use rustc_hash::FxHashMap; +use std::cmp; + +/// Max allowed dimensions (width, height) for the graphic, in pixels. +pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096]; pub struct GraphicDataEntry { pub handle: Handle, @@ -117,6 +122,9 @@ pub struct GraphicData { /// Indicate if there are no transparent pixels. pub is_opaque: bool, + + /// Render graphic in a different size. + pub resize: Option, } impl GraphicData { @@ -157,4 +165,201 @@ impl GraphicData { true } + + pub fn from_dynamic_image(id: GraphicId, image: DynamicImage) -> Self { + let color_type; + let width; + let height; + let pixels; + + match image { + // Sugarloaf only accepts rgba8 now + // DynamicImage::ImageRgb8(image) => { + // color_type = ColorType::Rgb; + // width = image.width() as usize; + // height = image.height() as usize; + // pixels = image.into_raw(); + // } + DynamicImage::ImageRgba8(image) => { + color_type = ColorType::Rgba; + width = image.width() as usize; + height = image.height() as usize; + pixels = image.into_raw(); + } + + _ => { + // Non-RGB image. Convert it to RGBA. + let image = image.into_rgba8(); + color_type = ColorType::Rgba; + width = image.width() as usize; + height = image.height() as usize; + pixels = image.into_raw(); + } + } + + GraphicData { + id, + width, + height, + color_type, + pixels, + is_opaque: false, + resize: None, + } + } + + /// Resize the graphic according to the dimensions in the `resize` field. + pub fn resized( + self, + cell_width: usize, + cell_height: usize, + view_width: usize, + view_height: usize, + ) -> Option { + let resize = match self.resize { + Some(resize) => resize, + None => return Some(self), + }; + + if (resize.width == ResizeParameter::Auto + && resize.height == ResizeParameter::Auto) + || self.height == 0 + || self.width == 0 + { + return Some(self); + } + + let mut width = match resize.width { + ResizeParameter::Auto => 1, + ResizeParameter::Pixels(n) => n as usize, + ResizeParameter::Cells(n) => n as usize * cell_width, + ResizeParameter::WindowPercent(n) => n as usize * view_width / 100, + }; + + let mut height = match resize.height { + ResizeParameter::Auto => 1, + ResizeParameter::Pixels(n) => n as usize, + ResizeParameter::Cells(n) => n as usize * cell_height, + ResizeParameter::WindowPercent(n) => n as usize * view_height / 100, + }; + + if width == 0 || height == 0 { + return None; + } + + // Compute "auto" dimensions. + if resize.width == ResizeParameter::Auto { + width = self.width * height / self.height; + } + + if resize.height == ResizeParameter::Auto { + height = self.height * width / self.width; + } + + // Limit size to MAX_GRAPHIC_DIMENSIONS. + width = cmp::min(width, MAX_GRAPHIC_DIMENSIONS[0]); + height = cmp::min(height, MAX_GRAPHIC_DIMENSIONS[1]); + + tracing::trace!("Resize new graphic to width={}, height={}", width, height,); + + // Create a new DynamicImage to resize the graphic. + let dynimage = match self.color_type { + ColorType::Rgb => { + let buffer = image_rs::RgbImage::from_raw( + self.width as u32, + self.height as u32, + self.pixels, + )?; + DynamicImage::ImageRgb8(buffer) + } + + ColorType::Rgba => { + let buffer = image_rs::RgbaImage::from_raw( + self.width as u32, + self.height as u32, + self.pixels, + )?; + DynamicImage::ImageRgba8(buffer) + } + }; + + // Finally, use `resize` or `resize_exact` to make the new image. + let width = width as u32; + let height = height as u32; + // https://doc.servo.org/image/imageops/enum.FilterType.html + let filter = image_rs::imageops::FilterType::Triangle; + + let new_image = if resize.preserve_aspect_ratio { + dynimage.resize(width, height, filter) + } else { + dynimage.resize_exact(width, height, filter) + }; + + Some(Self::from_dynamic_image(self.id, new_image)) + } +} + +/// Unit to specify a dimension to resize the graphic. +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub enum ResizeParameter { + /// Dimension is computed from the original graphic dimensions. + Auto, + + /// Size is specified in number of grid cells. + Cells(u32), + + /// Size is specified in number pixels. + Pixels(u32), + + /// Size is specified in a percent of the window. + WindowPercent(u32), +} + +/// Dimensions to resize a graphic. +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +pub struct ResizeCommand { + pub width: ResizeParameter, + + pub height: ResizeParameter, + + pub preserve_aspect_ratio: bool, +} + +#[test] +fn check_opaque_region() { + let graphic = GraphicData { + id: GraphicId(0), + width: 10, + height: 10, + color_type: ColorType::Rgb, + pixels: vec![255; 10 * 10 * 3], + is_opaque: true, + resize: None, + }; + + assert!(graphic.is_filled(1, 1, 3, 3)); + assert!(!graphic.is_filled(8, 8, 10, 10)); + + let pixels = { + // Put a transparent 3x3 box inside the picture. + let mut data = vec![255; 10 * 10 * 4]; + for y in 3..6 { + let offset = y * 10 * 4; + data[offset..offset + 3 * 4].fill(0); + } + data + }; + + let graphic = GraphicData { + id: GraphicId(0), + pixels, + width: 10, + height: 10, + color_type: ColorType::Rgba, + is_opaque: false, + resize: None, + }; + + assert!(graphic.is_filled(0, 0, 3, 3)); + assert!(!graphic.is_filled(1, 1, 4, 4)); }