T26a Media: images

A new chapter, src/media. ImageViewer fits a picture to its stage and
zooms by the document steps, by keys, buttons and Cmd-scroll about the
pointer; a drag through gpui's drag, a scroll or the arrows pan it once it
overhangs; R turns it a quarter at a time. gpui paints pictures without a
transform, so media::turn turns the decoded frames off the main thread,
an asset keyed by the picture and the turn; `image` 0.25, which gpui
already decodes with, is now a dependency. ImageThumbnail shows a picture
in its own shape with a corner label and a ring when chosen, and is a Tab
stop only with a handler. Lightbox / Gallery Viewer points to
overlays::Lightbox, which the gallery opens from the thumbnails.

T26 splits into T26a–e. AGENTS.md: the drag rule says drags that pan,
turn or slide go through gpui's drag; pictures turn by their pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 15:34:45 -04:00
1 parent 4678773383
commit f8a51a5352
14 files changed
+1171 -7

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
use std::path::{Path, PathBuf};
use ely_gpui_component::{
media::{ImageThumbnail, ImageViewer},
overlays::{Lightbox, Slide},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, row, section, set},
};
/// The gallery's pictures: a file, its width over height, a caption, and a corner label.
const PICTURES: [(&str, f32, &str, &str); 5] = [
(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/atrium.jpg"
),
1.5,
"An atrium, noon",
"",
),
(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/dunes.jpg"
),
1.5,
"Dunes at first light",
"",
),
(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/atrium-olive.jpg"
),
1.5,
"An olive tree by the stair",
"HDR",
),
(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/dunes-grass.jpg"
),
1.5,
"Grass on the ridge",
"",
),
(
concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/gallery/assets/dunes-square.jpg"
),
1.0,
"The long shadow",
"+2",
),
];
pub fn viewer(cx: &mut App) -> impl IntoElement + use<> {
section(
"ImageViewer",
"A picture to look at closely. It fits at first; the buttons, plus and minus, or Cmd-scroll zoom it, and 0 and 1 go back to fit and actual size, as a double press does. Drag it or use the arrows once it overhangs; R turns it.",
cx,
)
.child(probe(
"media-viewer",
div().w(px(640.)).h(px(440.)).child(ImageViewer::new("media-viewer", PathBuf::from(PICTURES[0].0))),
))
}
pub fn thumbnails(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let shown = keep("media-shown", || None::<usize>, window, cx);
let opened = keep("media-last", || 0usize, window, cx);
let last = *opened.read(cx);
let thumbs = PICTURES
.iter()
.enumerate()
.map(|(ix, (path, ratio, _, label))| {
let (open, mark) = (shown.clone(), opened.clone());
let thumb = ImageThumbnail::new(
SharedString::from(format!("media-thumb-{ix}")),
Path::new(*path),
*ratio,
)
.selected(ix == last)
.on_click(move |_, cx| {
set(&mark, ix, cx);
set(&open, Some(ix), cx);
});
let thumb = if label.is_empty() {
thumb
} else {
thumb.label(*label)
};
div().w(px(96. * ratio + 6.)).child(thumb)
});
let overlay = (*shown.read(cx)).map(|at| {
let (step, close) = (shown.clone(), shown.clone());
Lightbox::new(
"media-lightbox",
PICTURES.map(|(path, _, caption, _)| Slide::new(Path::new(path), caption)),
at,
move |_, cx| set(&close, None, cx),
)
.on_step(move |to, _, cx| set(&step, Some(to), cx))
});
section(
"ImageThumbnail / Lightbox → overlays::Lightbox",
"Pictures small, each in its own shape, with a label when one helps. The one last opened keeps a ring; a press or Enter opens the lightbox, which steps through them all.",
cx,
)
.child(probe("media-thumbs", row().items_end().children(thumbs)))
.children(overlay)
}
+40
View File
@@ -0,0 +1,40 @@
mod images;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 26,
slug: "media",
title: "Media",
summary: "Pictures, video and sound: looking closely, cropping and marking up, playing, and the controls and devices around them.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::DownAt("media-viewer", 320.0, 240.0),
Step::UpAt("media-viewer", 320.0, 240.0),
Step::Key("="),
Step::Key("="),
Step::Wait(300),
Step::Shot("zoomed"),
Step::Key("r"),
Step::Wait(500),
Step::Shot("turned"),
Step::DownAt("media-thumbs", 237.0, 50.0),
Step::UpAt("media-thumbs", 237.0, 50.0),
Step::Wait(500),
Step::Shot("lightbox"),
Step::Key("escape"),
Step::Wait(300),
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(images::viewer(cx))
.child(images::thumbnails(window, cx))
.into_any_element()
}
+2
View File
@@ -14,6 +14,7 @@ mod generative;
mod git;
mod layout;
mod lists;
mod media;
mod menus;
mod motion;
mod navigation;
@@ -65,6 +66,7 @@ pub const ALL: &[Page] = &[
chat::PAGE,
agent::PAGE,
generative::PAGE,
media::PAGE,
theme::PAGE,
];