R8.1: document content keeps the file's colours in a dark app

Pages, cells, slides and PDF pages paint as the file says in any
appearance; only the chrome around them follows dark mode. The
kernel keeps Ely's one light scheme, sent once a view, and MuPDF
no longer inverts. Reviews now run gpt-6.1-sol at xhigh.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude committed 2026-10-18 19:42:13 -04:00
1 parent d4cb274749
commit 6f1eebb4b5
42 files changed
+204 -209

No files matched your search

+3 -3
View File
@@ -36,7 +36,7 @@ fn fraction(key: &TileKey, size: (f32, f32)) -> [f32; 4] {
impl PdfView {
/// The pages in view and the tiles each needs.
pub fn showing(&self, sf: f32, dark: bool) -> Vec<Showing> {
pub fn showing(&self, sf: f32) -> Vec<Showing> {
let laid = self.laid_out();
let (room_w, room_h) = (f32::from(self.room.size.width), f32::from(self.room.size.height));
let (left, top) = (-f32::from(self.scroll.offset().x), -f32::from(self.scroll.offset().y));
@@ -50,7 +50,7 @@ impl PdfView {
let keys = ys
.flat_map(|ty| xs.clone().map(move |tx| (tx, ty)))
.map(|(tx, ty)| {
let key = TileKey { page: place.page, scale, rotate: self.rotate, dark, tx, ty };
let key = TileKey { page: place.page, scale, rotate: self.rotate, tx, ty };
(key, [tx * TILE, ty * TILE, TILE.min(dw - tx * TILE), TILE.min(dh - ty * TILE)])
})
.collect();
@@ -123,7 +123,7 @@ mod tests {
#[test]
fn a_tile_keeps_its_place_as_a_fraction_of_its_page() {
let key = TileKey { page: 0, scale: 2000, rotate: 0, dark: false, tx: 1, ty: 0 };
let key = TileKey { page: 0, scale: 2000, rotate: 0, tx: 1, ty: 0 };
// A 612 by 792 page at 2x: 1224 by 1584.
assert_eq!(fraction(&key, (612.0, 792.0)), [512.0 / 1224.0, 0.0, 512.0 / 1224.0, 512.0 / 1584.0]);
let turned = TileKey { rotate: 90, ..key };
+5 -9
View File
@@ -16,14 +16,13 @@ use crate::view::{PdfView, Reported};
/// A tile's side in device pixels.
pub const TILE: i32 = 512;
/// A page tile at one scale, turn and theme.
/// A page tile at one scale and turn.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TileKey {
pub page: usize,
/// Device pixels a point, times a thousand.
pub scale: u32,
pub rotate: i32,
pub dark: bool,
pub tx: i32,
pub ty: i32,
}
@@ -123,17 +122,14 @@ impl PdfView {
/// Pages in view and their tiles; missing ones asked.
pub fn page_view(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let (sf, dark) = (window.scale_factor(), cx.theme().is_dark());
let (sunken, paper) = {
let colors = &cx.theme().colors;
(colors.sunken, if dark { colors.surface } else { colors.paper })
};
let sf = window.scale_factor();
let (sunken, paper) = (cx.theme().colors.sunken, gpui::white());
let laid = self.laid_out();
if let Some((x, y)) = self.go_to.take() {
self.scroll.set_offset(point(px(-x), px(-y)));
}
let top = -f32::from(self.scroll.offset().y);
let showing = self.showing(sf, dark);
let showing = self.showing(sf);
// The current page: first showing more than a sliver.
if let Some(first) = showing.iter().map(|s| s.place).find(|p| p.y + p.h > top + GAP * 3.0) {
let paired = showing.iter().any(|s| s.place.page == self.current) && first.page / 2 == self.current / 2;
@@ -266,7 +262,7 @@ impl PdfView {
self.painting = true;
let (doc, scale, version) = (self.doc, key.scale as f32 / 1000.0, self.version);
self.off(
move |worker| worker.render(doc, key.page, (scale, key.rotate), rect, key.dark),
move |worker| worker.render(doc, key.page, (scale, key.rotate), rect),
move |view, painted, cx| {
view.painting = false;
if view.version != version {
+13 -13
View File
@@ -100,11 +100,11 @@ impl PdfView {
}
/// Asks for the first missing picture in `rows`.
fn ask_thumb(&mut self, rows: Range<usize>, (sf, dark): (f32, bool), cx: &mut Context<Self>) {
fn ask_thumb(&mut self, rows: Range<usize>, sf: f32, cx: &mut Context<Self>) {
if self.thumbing {
return;
}
let due = |page: &usize| !self.thumbs.contains_key(&(*page, dark)) || self.thumbs_stale.contains(&(*page, dark));
let due = |page: &usize| !self.thumbs.contains_key(page) || self.thumbs_stale.contains(page);
let Some(page) = rows.clone().find(|page| due(page) && !self.thumbs_failed.contains(page)) else {
return;
};
@@ -114,7 +114,7 @@ impl PdfView {
let (doc, version) = (self.doc, self.version);
self.thumbing = true;
self.off(
move |worker| worker.render(doc, page, (scale, 0), tile, dark),
move |worker| worker.render(doc, page, (scale, 0), tile),
move |view, painted, cx| {
view.thumbing = false;
if view.version != version {
@@ -128,8 +128,8 @@ impl PdfView {
});
match painted.map(|bgra| crate::pages::picture((tile[2] as u32, tile[3] as u32), bgra)) {
Ok(Some(image)) => {
view.thumbs_stale.remove(&(page, dark));
if let Some(old) = view.thumbs.insert((page, dark), image) {
view.thumbs_stale.remove(&page);
if let Some(old) = view.thumbs.insert(page, image) {
view.retired.push(old);
}
}
@@ -149,18 +149,18 @@ impl PdfView {
impl PdfView {
/// The picture a row draws; an old one until repainted.
pub fn picture(&self, page: usize, dark: bool) -> Option<Arc<RenderImage>> {
self.thumbs.get(&(page, dark)).cloned()
pub fn picture(&self, page: usize) -> Option<Arc<RenderImage>> {
self.thumbs.get(&page).cloned()
}
}
/// One row: the page number and its picture.
fn row(view: &Entity<PdfView>, page: usize, dark: bool, cx: &App) -> gpui::AnyElement {
fn row(view: &Entity<PdfView>, page: usize, cx: &App) -> gpui::AnyElement {
let read = view.read(cx);
let colors = &cx.theme().colors;
let (w, h) = read.sizes[page];
let picked = read.picked.contains(&page);
let picture = read.picture(page, dark);
let picture = read.picture(page);
let count = read.sizes.len();
let label = Viewer::text(cx, "pdf.page.label", &[("n", &(page + 1).to_string())]);
let (clicked, pressed, dropped) = (view.clone(), view.clone(), view.clone());
@@ -210,7 +210,7 @@ fn row(view: &Entity<PdfView>, page: usize, dark: bool, cx: &App) -> gpui::AnyEl
.h(px(height))
.border_1()
.border_color(if picked { colors.focus } else { colors.border })
.bg(if dark { colors.surface } else { colors.paper })
.bg(gpui::white())
.when_some(picture, |frame, picture| frame.child(img(picture).size_full())),
)
.child(div().text_xs().text_color(colors.fg_subtle).child((page + 1).to_string()))
@@ -258,12 +258,12 @@ impl Render for Side {
Pane::Notes => crate::notelist::list(&view, cx),
Pane::Files => crate::attach::list(&view, cx),
Pane::Pages => {
let shown = (window.scale_factor(), cx.theme().is_dark());
let sf = window.scale_factor();
let count = view.read(cx).sizes.len();
let rows = view.clone();
let list = uniform_list("pdf-thumbs", count, move |range, _, cx| {
rows.update(cx, |view, cx| view.ask_thumb(range.clone(), shown, cx));
range.map(|page| row(&rows, page, shown.1, cx)).collect()
rows.update(cx, |view, cx| view.ask_thumb(range.clone(), sf, cx));
range.map(|page| row(&rows, page, cx)).collect()
})
.size_full();
div()
+8 -9
View File
@@ -188,13 +188,13 @@ fn tiles_beside_the_view_are_neither_asked_nor_kept() {
/// Flags any notify leaving a shown page with nothing drawn.
fn never_bare(cx: &mut VisualTestContext, view: &Entity<PdfView>) -> (gpui::Subscription, std::rc::Rc<std::cell::Cell<bool>>) {
let (sf, dark) = cx.update(|window, app| (window.scale_factor(), ely_gpui_component::theme::ActiveTheme::theme(app).is_dark()));
let sf = cx.update(|window, _| window.scale_factor());
let bare = std::rc::Rc::new(std::cell::Cell::new(false));
let seen = bare.clone();
let watch = cx.update(|_, app| {
app.observe(view, move |view, app| {
let read = view.read(app);
if read.showing(sf, dark).iter().any(|page| read.pieces(page).is_empty()) {
if read.showing(sf).iter().any(|page| read.pieces(page).is_empty()) {
seen.set(true);
}
})
@@ -248,10 +248,10 @@ fn a_new_scale_keeps_the_old_tiles_until_new_ones_land() {
cx.simulate_resize(gpui::size(px(700.), px(800.)));
// The new size lays out before any new tile lands.
cx.run_until_parked();
let (sf, dark) = cx.update(|window, app| (window.scale_factor(), ely_gpui_component::theme::ActiveTheme::theme(app).is_dark()));
let sf = cx.update(|window, _| window.scale_factor());
let now = cx.update(|_, app| {
let read = view.read(app);
read.showing(sf, dark).iter().map(|page| read.pieces(page).len()).collect::<Vec<_>>()
read.showing(sf).iter().map(|page| read.pieces(page).len()).collect::<Vec<_>>()
});
assert!(now.iter().all(|count| *count > 0), "drawn right after the resize: {now:?}");
settle(cx, |app| view.read(app).laid_out().scale != scale && repainted(view.read(app)));
@@ -315,7 +315,7 @@ fn old_tiles_are_painted_and_freed_with_their_tab() {
let shown = cx.update(|window, app| {
let read = view.read(app);
let room = read.room;
read.showing(window.scale_factor(), false).iter().map(|s| {
read.showing(window.scale_factor()).iter().map(|s| {
let (left, top) = (-f32::from(read.scroll.offset().x), -f32::from(read.scroll.offset().y));
let w = (s.place.x + s.place.w).min(left + f32::from(room.size.width)) - s.place.x.max(left);
let h = (s.place.y + s.place.h).min(top + f32::from(room.size.height)) - s.place.y.max(top);
@@ -333,9 +333,8 @@ fn an_edit_keeps_page_pictures_until_they_repaint() {
let mut cx = TestAppContext::single();
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "one.pdf", &[(612.0, 792.0, &["Hello"])])], true);
let view = shown(cx, &viewer);
let dark = cx.update(|_, app| ely_gpui_component::theme::ActiveTheme::theme(app).is_dark());
settle(cx, |app| view.read(app).picture(0, dark).is_some() && repainted(view.read(app)));
let old = cx.update(|_, app| view.read(app).picture(0, dark).expect("a picture"));
settle(cx, |app| view.read(app).picture(0).is_some() && repainted(view.read(app)));
let old = cx.update(|_, app| view.read(app).picture(0).expect("a picture"));
// A picture on its way holds back the repaint.
cx.update(|_, app| view.update(app, |view, _| view.thumbing = true));
cx.update(|_, app| view.update(app, |view, cx| view.annotate("adding a note", vec![note()], cx)));
@@ -349,7 +348,7 @@ fn an_edit_keeps_page_pictures_until_they_repaint() {
cx.notify()
}));
settle(cx, |app| view.read(app).thumbs_stale.is_empty());
let new = cx.update(|_, app| view.read(app).picture(0, dark).expect("a picture"));
let new = cx.update(|_, app| view.read(app).picture(0).expect("a picture"));
assert!(!std::sync::Arc::ptr_eq(&new, &old), "the new picture replaces it");
super::frames(cx);
assert!(cx.update(|window, _| window.has_image_atlas_entry(&new)), "and is drawn");
+1 -1
View File
@@ -158,7 +158,7 @@ fn scanned(dir: &std::path::Path) -> std::path::PathBuf {
let doc = worker.open(&source).expect("opens").doc;
let s: f32 = 200.0 / 72.0;
let (w, h) = ((612.0 * s).ceil() as i32, (792.0 * s).ceil() as i32);
let bgra = worker.render(doc, 0, (s, 0), [0, 0, w, h], false).expect("draws").bgra;
let bgra = worker.render(doc, 0, (s, 0), [0, 0, w, h]).expect("draws").bgra;
let gray: Vec<u8> = bgra.chunks_exact(4).map(|p| ((u16::from(p[0]) + u16::from(p[1]) + u16::from(p[2])) / 3) as u8).collect();
let path = dir.join("scan.pdf");
std::fs::write(&path, scan((612.0, 792.0), (w as u32, h as u32), &gray)).expect("writes");
+19 -1
View File
@@ -1,6 +1,6 @@
//! Password prompts, zoomed pairs, jumps across (C11.1).
use ely_office::{Dialog, Dialogs as _, Tabs as _};
use ely_office::{Dialog, Dialogs as _, Opening as _, Tabs as _};
use gpui::{Entity, Modifiers, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualTestContext, point, px};
use super::{file, frames, settle, shown, windowed};
@@ -144,3 +144,21 @@ fn ctrl_and_a_wheel_notch_step_the_zoom_once() {
frames(cx);
assert_eq!(read(cx).0, Zoom::Scale(crate::keys::stepped(1.0, false)));
}
#[test]
fn a_dark_app_shows_the_page_as_the_file_paints_it() {
use ely_gpui_component::theme::{Mode, Theme};
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let (viewer, cx) = windowed(&mut cx, dir.path(), &[], true);
cx.update(|_, app| Theme::set_mode_now(Mode::Dark, app));
let path = file(dir.path(), "one.pdf", &[(612.0, 792.0, &["alpha"])]);
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.open_file(path, cx)));
settle(cx, |app| viewer.read(app).active_view().is_some());
let view = shown(cx, &viewer);
let corner = |app: &gpui::App| view.read(app).tiles.iter().find(|(key, _)| key.tx == 0 && key.ty == 0).map(|(_, image)| image.clone());
settle(cx, |app| corner(app).is_some());
let tile = cx.update(|_, app| corner(app)).expect("the first tile");
let bytes = tile.as_bytes(0).expect("its pixels");
assert_eq!(&bytes[..3], &[255, 255, 255], "the paper stays white");
}
+3 -3
View File
@@ -87,11 +87,11 @@ pub struct PdfView {
pub saved: String,
/// Saves so far: a read from before one is stale.
pub saves: u64,
/// Page pictures for the side panel, by page and darkness.
pub thumbs: HashMap<(usize, bool), Arc<RenderImage>>,
/// Page pictures for the side panel, by page.
pub thumbs: HashMap<usize, Arc<RenderImage>>,
pub thumbs_failed: HashSet<usize>,
/// Pictures from before an edit; shown until repainted.
pub thumbs_stale: HashSet<(usize, bool)>,
pub thumbs_stale: HashSet<usize>,
/// A picture is on its way from the worker.
pub thumbing: bool,
/// Pages picked in the side panel, and the anchor.
+4 -4
View File
@@ -100,12 +100,12 @@ impl Host for Deck {
view.turn_parts();
});
}
// Pictures follow the kernel's light or dark scheme.
let dark = view.read(cx).dark();
// Pictures before the scheme landed are repainted.
let lit = view.read(cx).lit();
if let Some(tab) = self.tab_mut(id)
&& tab.extra.dark != dark
&& tab.extra.lit != lit
{
tab.extra.dark = dark;
tab.extra.lit = lit;
self.repaint_all(id, cx);
}
self.offer_menu(id, view, cx);
+2 -2
View File
@@ -50,8 +50,8 @@ pub struct Extra {
pub painting: bool,
/// The parts are masters.
pub masters: bool,
/// The scheme the pictures were painted with.
pub dark: Option<bool>,
/// The pictures were painted with Ely's scheme.
pub lit: bool,
/// The view was fitted to the slide once.
pub fitted: bool,
}
+1 -1
View File
@@ -267,7 +267,7 @@ impl Deck {
let range = output.pages();
let (w, h) = tab.extra.size;
let size = (PICTURE, (f64::from(PICTURE) * h as f64 / w.max(1) as f64).round() as u32);
let (copy, light) = (tab.work.join("output.odp"), ely_lok::scheme(false));
let (copy, light) = (tab.work.join("output.odp"), ely_lok::SCHEME);
let (shown, opened) = (folder.display().to_string(), folder.clone());
log::info!("show: writes slide pictures");
self.read_in(
+2 -2
View File
@@ -252,7 +252,7 @@ impl Deck {
// A show starts from black; effects play on the page.
let behind = match &preview.playing {
Playing::Transition { .. } => gpui::black(),
Playing::Effects { .. } => ely_lok::paper(cx),
Playing::Effects { .. } => ely_lok::paper(),
};
let content = match &preview.playing {
Playing::Effects {
@@ -266,7 +266,7 @@ impl Deck {
} else {
(t / preview.length) as f32
};
let paper = ely_lok::paper(cx);
let paper = ely_lok::paper();
// Exits are not read; the old slide shows everything.
let from = (from.as_deref())
.map(|from| compose::slide(from, |_| true, frame, paper).into_any_element());
+1 -1
View File
@@ -159,7 +159,7 @@ impl Render for Presenter {
let big = stage::fit(show.slide, (room.width * 0.6, room.height * 0.8));
let small = stage::fit(show.slide, (room.width * 0.3, room.height * 0.35));
let next = match show.upcoming() {
Some(part) => stage::still(show, part, small, cx),
Some(part) => stage::still(show, part, small),
None => div().w(small.0).h(small.1).into_any_element(),
};
let notes = show.part().map(|part| show.slides[part].notes.clone());
+3 -3
View File
@@ -27,7 +27,7 @@ pub fn stage(show: &Show, (w, h): (Pixels, Pixels), cx: &App) -> AnyElement {
let Some(stack) = show.stack(part) else {
return black((w, h));
};
let paper = ely_lok::paper(cx);
let paper = ely_lok::paper();
let t = show.entered.elapsed().as_secs_f64();
let slide = &show.slides[part];
let shown = match &show.leaving {
@@ -76,9 +76,9 @@ pub fn stage(show: &Show, (w, h): (Pixels, Pixels), cx: &App) -> AnyElement {
}
/// A slide as it first shows, still.
pub fn still(show: &Show, part: usize, size: (Pixels, Pixels), cx: &App) -> AnyElement {
pub fn still(show: &Show, part: usize, size: (Pixels, Pixels)) -> AnyElement {
match show.stack(part) {
Some(stack) => compose::slide(stack, |visible| visible, size, ely_lok::paper(cx))
Some(stack) => compose::slide(stack, |visible| visible, size, ely_lok::paper())
.into_any_element(),
None => black(size),
}
+4 -7
View File
@@ -59,16 +59,14 @@ pub fn discard(worker: Arc<Mutex<Worker>>, doc: Doc, copy: PathBuf, cx: &App) {
fn copy(
worker: &mut Worker,
doc: &Doc,
(copy, scheme): (&std::path::Path, Option<&'static str>),
copy: &std::path::Path,
) -> io::Result<(usize, Doc, Vec<ShowSlide>)> {
let current = doc.sheets(worker)?.current;
let url = ely_lok::file_url(copy);
doc.save_as(worker, &url, "odp")?;
let shown = worker.load(&url)?;
let read = |worker: &mut Worker| -> io::Result<Vec<ShowSlide>> {
if let Some(scheme) = scheme {
worker.run(&shown, "view.theme", &[scheme])?;
}
worker.run(&shown, "view.theme", &[ely_lok::SCHEME])?;
ely_lok::show::slide_show(&worker.values(&shown, ely_lok::show::SLIDE_SHOW)?)
};
match read(worker) {
@@ -161,14 +159,13 @@ impl Deck {
self.show_starting = false;
return;
};
let scheme = tab.view.read(cx).dark().map(ely_lok::scheme);
let path = tab.work.join(format!("show-{}.odp", self.shows));
self.shows += 1;
let into = path.clone();
let worker = self.core.worker.clone();
self.read_in(
id,
move |worker, doc| copy(worker, doc, (&into, scheme)),
move |worker, doc| copy(worker, doc, &into),
move |deck, read, _, cx| {
deck.show_starting = false;
let (current, doc, slides) = match read {
@@ -382,7 +379,7 @@ mod tests {
let deck = deck.read(app);
let tab = deck.active_tab().expect("a tab");
let mut worker = deck.core.worker.lock().expect("the worker lock");
let (_, doc, slides) = super::copy(&mut worker, &tab.doc, (&path, None)).expect("a copy");
let (_, doc, slides) = super::copy(&mut worker, &tab.doc, &path).expect("a copy");
super::Starting { tab: tab.id, slides, first: 0, doc, copy: path.clone(), audience: 99, presenter: false }
});
update(&mut cx, &deck, |deck, cx| {