Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ fn keybind(v: &str) -> Option<Keybind> {
}

fn path() -> Option<PathBuf> {
// Tests use the defaults, whatever the person running them has configured.
if cfg!(test) {
return None;
}
let base = std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("litty/config"))
}
Expand Down
60 changes: 58 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ struct Win {
find: Option<String>,
/// A short message in the corner ("saved ~/Desktop/…cast") and when it goes away.
flash: Option<(String, Instant)>,
/// Hidden (a background native tab, minimised, covered): pixel buffers are freed meanwhile.
occluded: bool,
/// When to let macOS reclaim the spare surfaces of a window that stopped drawing.
trim_at: Option<Instant>,
/// Several lines waiting for Enter before they are pasted into a shell: (bytes, lines).
pending_paste: Option<(Vec<u8>, usize)>,
/// The link under the mouse when the right-click menu opened.
Expand All @@ -388,6 +392,8 @@ const PAD_PT: f32 = 10.0;
/// Minimum time between frames. Drawing holds the grid lock, so capping it keeps the parser
/// thread fed during floods like `cat bigfile`.
const FRAME: Duration = Duration::from_millis(15);
/// A window that stopped drawing for this long gives its spare surfaces back (macOS).
const TRIM_AFTER: Duration = Duration::from_millis(250);
const BLINK: Duration = Duration::from_millis(530);
const DOUBLE_CLICK: Duration = Duration::from_millis(400);
/// Longest a synchronized update (mode 2026) may hold the screen back if the program never ends it.
Expand Down Expand Up @@ -947,7 +953,11 @@ impl Win {
/// Rebuild fonts for the current point size / display scale, then re-layout.
fn rebuild(&mut self) {
let pad = config::get().padding.unwrap_or(PAD_PT);
self.renderer = Some(Renderer::new(self.font_pt * self.scale, (pad * self.scale) as usize));
let mut renderer = Renderer::new(self.font_pt * self.scale, (pad * self.scale) as usize);
if self.occluded {
renderer.release();
}
self.renderer = Some(renderer);
if let (Some(size), Some(r), Some(p)) = (self.window.as_ref().map(|w| w.inner_size()), &mut self.renderer, &mut self.presenter) {
p.resize(size.width as usize, size.height as usize);
r.resize(size.width as usize, size.height as usize);
Expand All @@ -956,6 +966,26 @@ impl Win {
}

fn redraw(&mut self) {
if self.occluded {
// Nothing to paint, but the (native) tab title and OSC 52 copies still apply.
let (mut title, mut clip) = (None, None);
let tab = &self.tabs[self.active];
for pane in &tab.panes {
pane.pending.store(false, Ordering::SeqCst);
let mut t = pane.term.lock().unwrap();
clip = clip.or(t.grid.clip.take());
if pane.id == tab.active {
title = t.grid.title.take();
}
}
if let (Some(title), Some(w)) = (title, &self.window) {
w.set_title(&title);
}
if let Some(clip) = clip {
clipboard_set(&clip);
}
return;
}
// Titles for the tab bar, taken before locking any pane (never hold two locks).
let titles: Vec<String> = if self.tabs.len() > 1 {
self.tabs
Expand Down Expand Up @@ -1006,6 +1036,9 @@ impl Win {
}
let damage = r.take_damage();
presenter.present(&r.fb, damage);
if cfg!(target_os = "macos") {
self.trim_at = Some(Instant::now() + TRIM_AFTER);
}
if let Some(title) = title {
win.set_title(&title);
}
Expand Down Expand Up @@ -2288,6 +2321,8 @@ impl Win {
flash: None,
pending_paste: None,
link: None,
occluded: false,
trim_at: None,
#[cfg(not(target_os = "macos"))]
menu: None,
};
Expand All @@ -2314,7 +2349,12 @@ impl Win {

/// Blink the cursor, and return when this window next needs waking (a postponed frame or a blink).
fn tick(&mut self, now: Instant) -> Option<Instant> {
let mut deadline = self.frame_deadline;
if let (Some(at), Some(p)) = (self.trim_at, &mut self.presenter)
&& now >= at
{
self.trim_at = p.trim().then_some(now + TRIM_AFTER);
}
let mut deadline = self.frame_deadline.into_iter().chain(self.trim_at).min();
if let Some((_, until)) = self.flash {
if now >= until {
self.flash = None;
Expand Down Expand Up @@ -2356,6 +2396,22 @@ impl Win {
self.redraw();
}
}
WindowEvent::Occluded(hidden) if hidden != self.occluded => {
self.occluded = hidden;
if let (Some(p), Some(r)) = (&mut self.presenter, &mut self.renderer) {
if hidden {
p.release();
r.release();
self.trim_at = None;
} else {
r.restore();
for pane in self.tabs.iter().flat_map(|t| &t.panes) {
pane.term.lock().unwrap().grid.dirty.fill(true);
}
self.redraw_soon();
}
}
}
WindowEvent::ModifiersChanged(m) => self.mods = m.state(),
WindowEvent::Focused(f) => {
self.focused = f;
Expand Down
53 changes: 51 additions & 2 deletions src/present.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ mod other {
}
}

/// Nothing to free: softbuffer keeps one buffer.
pub fn release(&mut self) {}

pub fn trim(&mut self) -> bool {
false
}

pub fn present(&mut self, fb: &[u32], damage: Option<(usize, usize)>) {
if damage.is_none() {
return;
Expand All @@ -52,7 +59,7 @@ mod mac {
use objc2::runtime::{AnyObject, Bool};
use objc2_core_foundation::{CFDictionary, CFRetained, CGPoint, CGRect, CGSize};
use objc2_foundation::{NSDictionary, NSNumber, NSString, ns_string};
use objc2_io_surface::{IOSurfaceLockOptions, IOSurfaceRef};
use objc2_io_surface::{IOSurfaceLockOptions, IOSurfacePurgeabilityState, IOSurfaceRef};
use objc2_quartz_core::{CALayer, CATransaction, kCAGravityTopLeft};
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};

Expand All @@ -67,6 +74,9 @@ mod mac {
surfaces: Vec<CFRetained<IOSurfaceRef>>,
/// Per surface: rows that changed since it was last written.
stale: Vec<Option<(usize, usize)>>,
/// Per surface: marked purgeable while not needed, so macOS may take its memory back
/// (and doesn't count it against litty).
volatile: Vec<bool>,
shown: usize,
size: (usize, usize),
/// False while the background is see-through (config `background-opacity`).
Expand All @@ -86,18 +96,47 @@ mod mac {
layer.setOpaque(true);
layer.setContentsGravity(unsafe { kCAGravityTopLeft });
root.addSublayer(&layer);
Presenter { window: window.clone(), layer, surfaces: Vec::new(), stale: Vec::new(), shown: 0, size: (0, 0), opaque: true }
Presenter { window: window.clone(), layer, surfaces: Vec::new(), stale: Vec::new(), volatile: Vec::new(), shown: 0, size: (0, 0), opaque: true }
}

pub fn resize(&mut self, w: usize, h: usize) {
self.size = (w, h);
self.surfaces.clear();
self.stale.clear();
self.volatile.clear();
let scale = self.window.scale_factor();
self.layer.setContentsScale(scale);
self.layer.setBounds(CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(w as f64 / scale, h as f64 / scale)));
}

/// The window is hidden: drop every surface (the next frame makes new ones).
pub fn release(&mut self) {
self.surfaces.clear();
self.stale.clear();
self.volatile.clear();
// SAFETY: no contents is a valid value.
unsafe { self.layer.setContents(None) };
}

/// Mark surfaces that aren't shown and that the compositor let go of as purgeable. Returns
/// true while one is still in use, so the caller looks again shortly.
pub fn trim(&mut self) -> bool {
let mut busy = false;
for (i, s) in self.surfaces.iter().enumerate() {
if i == self.shown || self.volatile[i] {
continue;
}
if s.is_in_use() {
busy = true;
} else {
// SAFETY: a valid surface; the old state isn't needed.
unsafe { s.set_purgeable(IOSurfacePurgeabilityState::PurgeableVolatile.0, std::ptr::null_mut()) };
self.volatile[i] = true;
}
}
busy
}

fn new_surface(&self) -> Option<CFRetained<IOSurfaceRef>> {
let num = |n: usize| NSNumber::new_usize(n);
let keys = [ns_string!("IOSurfaceWidth"), ns_string!("IOSurfaceHeight"), ns_string!("IOSurfaceBytesPerElement"), ns_string!("IOSurfacePixelFormat")];
Expand Down Expand Up @@ -126,11 +165,21 @@ mod mac {
let Some(s) = self.new_surface() else { return };
self.surfaces.push(s);
self.stale.push(Some((0, self.size.1)));
self.volatile.push(false);
n
}
None => (self.shown + 1) % n,
};
let s = &self.surfaces[i];
if std::mem::take(&mut self.volatile[i]) {
let mut old = 0;
// SAFETY: a valid surface and a place for its previous state.
unsafe { s.set_purgeable(IOSurfacePurgeabilityState::PurgeableNonVolatile.0, &mut old) };
if old == IOSurfacePurgeabilityState::PurgeableEmpty.0 {
// macOS took the pixels back: write the whole frame.
self.stale[i] = Some((0, self.size.1));
}
}
let (w, stride) = (self.size.0, s.bytes_per_row() / 4);
let (r0, r1) = self.stale[i].take().unwrap_or((0, 0));
// Below full opacity the background colour becomes see-through (premultiplied alpha).
Expand Down
17 changes: 17 additions & 0 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ pub struct Renderer {
tmp: Vec<Cell>,
/// Rows (first, last + 1) whose pixels changed since the last `take_damage`.
damage: Option<(usize, usize)>,
/// The window is hidden and its framebuffer freed (see `release`).
released: bool,
/// Glyphs are only drawn inside this (x0, y0, x1, y1): one that overhangs its cell would leave
/// pixels behind that no later repaint of the row erases.
clip: [usize; 4],
Expand All @@ -201,6 +203,7 @@ impl Renderer {
bar_h: 0,
tmp: Vec::new(),
damage: None,
released: false,
clip: [0; 4],
}
}
Expand All @@ -213,11 +216,25 @@ impl Renderer {
pub fn resize(&mut self, w: usize, h: usize) {
self.w = w;
self.h = h;
if self.released {
return;
}
self.fb = vec![def_bg(); w * h];
self.damage = Some((0, h));
self.clip = [0, 0, w, h];
}

/// The window is hidden: free the framebuffer (a Retina window's is tens of MB). Nothing may
/// be drawn until `restore`, which starts over with a blank one.
pub fn release(&mut self) {
(self.released, self.fb) = (true, Vec::new());
}

pub fn restore(&mut self) {
self.released = false;
self.resize(self.w, self.h);
}

/// The area panes are laid out in: the window minus padding and the tab bar.
pub fn area(&self) -> Rect {
Rect {
Expand Down
Loading