diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c334dbae21..7142960c4f9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch Debug (Windows)", + "name": "Launch edit (Windows)", "preLaunchTask": "rust: cargo build", "type": "cppvsdbg", "request": "launch", @@ -14,7 +14,7 @@ ], }, { - "name": "Launch Debug (GDB)", + "name": "Launch edit (GDB, Linux)", "preLaunchTask": "rust: cargo build", "type": "cppdbg", "request": "launch", @@ -27,15 +27,28 @@ ], }, { - "name": "Launch Debug (LLDB)", + // NOTE for macOS: In order for this task to work you have to: + // 1. Run the "Fix externalConsole on macOS" task once + // 2. Add the following to your VS Code settings: + // "lldb-dap.environment": { + // "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" + // } + "name": "Launch edit (lldb-dap, macOS)", "preLaunchTask": "rust: cargo build", - "type": "lldb", + "type": "lldb-dap", "request": "launch", "program": "${workspaceFolder}/target/debug/edit", "cwd": "${workspaceFolder}", "args": [ "${workspaceFolder}/crates/edit/src/bin/edit/main.rs" ], - } + }, + { + // This is a workaround for https://github.com/microsoft/vscode-cpptools/issues/5079 + "name": "Fix externalConsole on macOS", + "type": "node-terminal", + "request": "launch", + "command": "osascript -e 'tell application \"Terminal\"\ndo script \"echo hello\"\nend tell'" + }, ] } diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index 90c47b6c882..f96ceb7d531 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -15,6 +15,7 @@ name = "lib" harness = false [features] +# Display editor latency in the top-right corner debug-latency = [] [dependencies] diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index b374e0ade4c..7e02ec82b41 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -12,8 +12,6 @@ mod localization; mod state; use std::borrow::Cow; -#[cfg(feature = "debug-latency")] -use std::fmt::Write; use std::path::{Path, PathBuf}; use std::time::Duration; use std::{env, process}; @@ -23,7 +21,7 @@ use draw_filepicker::*; use draw_menubar::*; use draw_statusbar::*; use edit::framebuffer::{self, IndexedColor}; -use edit::helpers::{CoordType, KIBI, MEBI, MetricFormatter, Rect, Size}; +use edit::helpers::*; use edit::input::{self, kbmod, vk}; use edit::oklab::StraightRgba; use edit::tui::*; @@ -178,6 +176,8 @@ fn run() -> apperr::Result<()> { #[cfg(feature = "debug-latency")] { + use std::fmt::Write as _; + // Print the number of passes and latency in the top right corner. let time_end = std::time::Instant::now(); let status = time_end - time_beg; diff --git a/crates/edit/src/framebuffer.rs b/crates/edit/src/framebuffer.rs index b02f867b95f..f2225e15edd 100644 --- a/crates/edit/src/framebuffer.rs +++ b/crates/edit/src/framebuffer.rs @@ -107,6 +107,8 @@ pub struct Framebuffer { /// of the palette as [dark, light], unless the palette is recognized /// as a light them, in which case it swaps them. auto_colors: [StraightRgba; 2], + /// Above this lightness value, we consider a color to be "light". + auto_color_threshold: f32, /// A cache table for previously contrasted colors. /// See: contrast_colors: [Cell<(StraightRgba, StraightRgba)>; CACHE_TABLE_SIZE], @@ -125,6 +127,7 @@ impl Framebuffer { DEFAULT_THEME[IndexedColor::Black as usize], DEFAULT_THEME[IndexedColor::BrightWhite as usize], ], + auto_color_threshold: 0.5, contrast_colors: [const { Cell::new((StraightRgba::zero(), StraightRgba::zero())) }; CACHE_TABLE_SIZE], background_fill: DEFAULT_THEME[IndexedColor::Background as usize], @@ -145,7 +148,17 @@ impl Framebuffer { self.indexed_colors[IndexedColor::Black as usize], self.indexed_colors[IndexedColor::BrightWhite as usize], ]; - if !Self::is_dark(self.auto_colors[0]) { + + // It's not guaranteed that Black is actually dark and BrightWhite light (vice versa for a light theme). + // Such is the case with macOS 26's "Clear Dark" theme (and probably a lot other themes). + // Its black is #35424C (l=0.3716; oof!) and bright white is #E5EFF5 (l=0.9464). + // If we have a color such as #43698A (l=0.5065), which is l>0.5 ("light") and need a contrasting color, + // we need that to be #E5EFF5, even though that's also l>0.5. With a midpoint of 0.659, we get that right. + let lightness = self.auto_colors.map(|c| c.as_oklab().lightness()); + self.auto_color_threshold = (lightness[0] + lightness[1]) * 0.5; + + // Ensure [0] is dark and [1] is light. + if lightness[0] > lightness[1] { self.auto_colors.swap(0, 1); } } @@ -346,15 +359,12 @@ impl Framebuffer { #[cold] fn contrasted_slow(&self, color: StraightRgba) -> StraightRgba { let idx = (color.to_ne() as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT; - let contrast = self.auto_colors[Self::is_dark(color) as usize]; + let is_dark = color.as_oklab().lightness() < self.auto_color_threshold; + let contrast = self.auto_colors[is_dark as usize]; self.contrast_colors[idx].set((color, contrast)); contrast } - fn is_dark(color: StraightRgba) -> bool { - color.as_oklab().lightness() < 0.5 - } - /// Blends the given sRGB color onto the background bitmap. /// /// TODO: The current approach blends foreground/background independently, diff --git a/crates/edit/src/helpers.rs b/crates/edit/src/helpers.rs index bd433588881..40590733da9 100644 --- a/crates/edit/src/helpers.rs +++ b/crates/edit/src/helpers.rs @@ -82,6 +82,9 @@ pub struct Size { } impl Size { + pub const MIN: Self = Self { width: 0, height: 0 }; + pub const MAX: Self = Self { width: CoordType::MAX, height: CoordType::MAX }; + pub fn as_rect(&self) -> Rect { Rect { left: 0, top: 0, right: self.width, bottom: self.height } } diff --git a/crates/edit/src/sys/unix.rs b/crates/edit/src/sys/unix.rs index 71b9edc7d74..07514325d5c 100644 --- a/crates/edit/src/sys/unix.rs +++ b/crates/edit/src/sys/unix.rs @@ -11,7 +11,7 @@ use std::fs::File; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; -use std::ptr::{self, NonNull, null_mut}; +use std::ptr::{NonNull, null_mut}; use std::{thread, time}; use stdext::arena::{Arena, ArenaString, scratch_arena}; @@ -203,7 +203,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option ArenaString<'a> { } } + #[must_use] + pub fn from_iter>(arena: &'a Arena, iter: T) -> Self { + let mut s = Self::new_in(arena); + s.extend(iter); + s + } + /// It's empty. pub fn is_empty(&self) -> bool { self.vec.is_empty() @@ -284,6 +291,18 @@ impl fmt::Write for ArenaString<'_> { } } +impl Extend for ArenaString<'_> { + fn extend>(&mut self, iter: I) { + let iterator = iter.into_iter(); + let (lower_bound, _) = iterator.size_hint(); + self.reserve(lower_bound); + iterator.for_each(move |c| self.push(c)); + } + + // TODO: This is where I'd put `extend_one` and `extend_reserve` impls, *but as always*, + // essential stdlib functions are unstable and that means we can't have them. +} + #[macro_export] macro_rules! arena_format { ($arena:expr, $($arg:tt)*) => {{