T21b and T21c: Documents, reading and knowledge
- DocumentViewer: pages the app drew on a desk, the page under the view's middle counted, zoom steps and fit, thumbnails, notes pinned where a press lands, and the owner's FindWidget over the pages, hits lit and the current one revealed - PageThumbnailList, EpubReader (chapters, type size, progress), PrintPreview - MarkdownRenderer feeds TableOfContents through sections and outline - Footnote, TooltipTerm, Glossary, ReadingProgress, ZenMode - Backlinks, Favorites, TrashBin, PageCover, PageIcon, TemplatePicker, VersionHistory with PageHistoryDiff - editor::FindWidget shows the replace fold and option toggles only with handlers - block editor tests move to documents/blocks - icons: a-arrow-down, a-arrow-up - gallery: reading and knowledge sections; scripted find, changes and zen
This commit is contained in:
32 files changed
+4269
-411
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1 @@
|
||||
[{"page":0,"words":[["On",72,77.0,33.0,16.0],["Lift",110.5,75.5,40.5,17.5],["Notes",72,108.0,23.5,6.5],["on",97.0,110.0,10.5,4.5],["color",109.5,107.5,21.0,7.0],["for",132.0,107.5,14.5,9.0],["Ely",146.0,107.5,14.0,9.0],["GPUI",161.5,108.0,23.5,6.5],["Component",186.0,108.0,47.5,8.5],["A",72,139.0,11.0,9.0],["color",85.5,138.0,35.5,10.0],["and",124.5,138.0,25.5,10.0],["its",153.0,138.0,16.0,10.0],["lift",172.5,138.0,19.5,10.0],["A",72,164.5,8.5,8.0],["lift",82.0,164.0,13.5,8.5],["blends",98.0,164.0,33.0,8.5],["a",133.0,167.0,5.5,5.5],["color",141.0,164.0,24.5,8.5],["toward",168.0,164.0,34.5,8.5],["white.",205.0,164.0,30.5,8.5],["Half",237.5,164.0,22.0,8.5],["a",261.0,167.0,5.5,5.5],["lift",269.0,164.0,13.5,8.5],["sits",285.0,164.5,17.0,8.0],["midway",304.5,164.0,38.5,11.0],["between",345.0,164.0,41.5,8.5],["the",388.5,164.0,16.0,8.5],["color",407.0,164.0,24.5,8.5],["and",434.0,164.0,18.5,8.5],["white,",455.0,164.0,30.5,11.0],["and",487.5,164.0,18.5,8.5],["a",508.5,167.0,5.5,5.5],["full",516.5,164.0,16.0,8.5],["lift",72,180.0,13.5,8.5],["is",88.0,180.5,8.0,8.0],["white",98.5,180.0,27.5,8.5],["itself.",128.0,180.0,27.0,8.5],["The",157.5,180.0,19.0,8.5],["blend",179.0,180.0,28.0,8.5],["happens",209.0,180.0,42.0,11.0],["in",253.5,180.5,9.5,8.0],["gamma",265.5,183.0,35.5,8.0],["space,",303.5,183.0,30.5,8.0],["where",336.5,180.0,30.5,8.5],["a",369.0,183.0,5.5,5.5],["small",377.0,180.0,26.0,8.5],["lift",405.5,180.0,13.5,8.5],["moves",421.5,183.0,31.5,5.5],["the",455.5,180.0,16.0,8.5],["eye",474.0,183.0,16.5,8.0],["as",493.0,183.0,10.5,5.5],["much",506.0,180.0,27.5,8.5],["as",72,199.0,10.5,5.5],["a",85.0,199.0,5.5,5.5],["large",93.0,196.0,24.0,11.0],["one",119.5,199.0,18.0,5.5],["moves",140.0,199.0,31.5,5.5],["the",174.0,196.0,16.0,8.5],["meter,",192.5,197.5,32.0,9.5],["so",227.0,199.0,11.0,5.5],["each",240.5,196.0,22.5,8.5],["step",265.5,197.5,21.0,9.5],["reads",289.0,196.0,27.0,8.5],["as",318.5,199.0,10.5,5.5],["even.",331.5,199.0,26.0,5.5],["Every",72,218.5,28.5,10.5],["accent",102.5,219.5,31.5,7.0],["in",136.5,218.5,9.5,8.0],["the",148.5,218.0,16.0,8.5],["library",167.0,218.0,32.5,11.0],["is",201.5,218.5,8.0,8.0],["a",212.0,221.0,5.5,5.5],["base",220.0,218.0,22.5,8.5],["color",244.5,218.0,24.5,8.5],["and",271.5,218.0,18.5,8.5],["a",292.5,221.0,5.5,5.5],["lift.",300.5,218.0,16.5,8.5],["The",319.5,218.0,19.0,8.5],["base",341.0,218.0,22.5,8.5],["carries",365.5,218.5,33.0,8.0],["the",401.0,218.0,16.0,8.5],["hue;",419.5,218.0,22.0,11.0],["the",444.0,218.0,16.0,8.5],["lift",462.5,218.0,13.5,8.5],["carries",478.5,218.5,33.0,8.0],["how",514.0,218.0,21.0,8.5],["loud",72,234.0,22.0,8.5],["the",96.5,234.0,16.0,8.5],["color",115.0,234.0,24.5,8.5],["is.",142.0,234.5,11.0,8.0],["Hover,",155.5,234.5,33.5,10.5],["press",191.5,237.0,26.5,8.0],["and",220.5,234.0,18.5,8.5],["focus",241.5,234.0,26.0,8.5],["are",270.0,237.0,15.5,5.5],["all",288.0,234.0,11.5,8.5],["lifts",302.0,234.0,18.5,8.5],["of",323.0,234.0,10.5,8.5],["the",335.0,234.0,16.0,8.5],["same",353.5,237.0,25.5,5.5],["accent,",381.5,235.5,34.5,9.5],["so",418.5,237.0,11.0,5.5],["a",432.0,237.0,5.5,5.5],["theme",440.0,234.0,31.0,8.5],["changes",473.5,234.0,39.5,11.0],["its",515.5,234.5,12.0,8.0],["hue",72,250.0,18.5,8.5],["in",93.0,250.5,9.5,8.0],["one",105.0,253.0,18.0,5.5],["place",125.5,250.0,25.5,11.0],["and",153.5,250.0,18.5,8.5],["its",174.5,250.5,12.0,8.0],["states",189.0,251.5,29.0,7.0],["follow.",220.5,250.0,32.5,8.5],["Tokens",72,280.0,48.5,10.0],["in",124.0,280.0,13.5,10.0],["one",141.0,283.5,25.0,6.5],["place",169.5,280.0,35.0,13.0],["Components",72,306.5,62.5,10.5],["never",137.0,309.0,27.5,5.5],["write",167.0,306.5,25.5,8.0],["a",194.5,309.0,5.5,5.5],["color,",202.5,306.0,27.5,11.0],["a",232.5,309.0,5.5,5.5],["size",240.5,306.5,18.5,8.0],["or",261.5,309.0,10.5,5.5],["a",274.5,309.0,5.5,5.5],["radius.",282.5,306.0,34.0,8.5],["They",319.0,306.0,25.0,11.0],["read",346.0,306.0,22.0,8.5],["tokens",370.5,306.0,33.0,8.5],["from",406.0,306.0,23.5,8.5],["the",432.0,306.0,16.0,8.5],["theme:",450.5,306.0,34.5,8.5],["the",487.5,306.0,16.0,8.5],["palette",506.0,306.0,34.0,11.0],["for",72,322.0,14.0,8.5],["color,",88.5,322.0,27.5,11.0],["the",118.5,322.0,16.0,8.5],["scale",137.0,322.0,24.0,8.5],["for",163.5,322.0,14.0,8.5],["text,",180.0,323.5,22.0,9.5],["the",204.5,322.0,16.0,8.5],["radii",223.0,322.0,22.5,8.5],["for",248.0,322.0,14.0,8.5],["corners.",264.5,325.0,40.0,5.5],["A",307.0,322.5,8.5,8.0],["token",317.0,322.0,28.0,8.5],["is",347.5,322.5,8.0,8.0],["a",358.0,325.0,5.5,5.5],["promise",366.0,322.5,40.0,10.5],["that",408.5,322.0,20.0,8.5],["the",431.0,322.0,16.0,8.5],["same",449.5,325.0,25.5,5.5],["thing",477.5,322.0,25.5,11.0],["looks",505.5,322.0,26.0,8.5],["the",72,338.0,16.0,8.5],["same",90.5,341.0,25.5,5.5],["everywhere,",118.5,338.0,59.5,11.0],["in",180.5,338.5,9.5,8.0],["light",192.5,338.0,22.0,11.0],["and",217.0,338.0,18.5,8.5],["in",238.0,338.5,9.5,8.0],["dark.",250.0,338.0,25.5,8.5],["The",72,360.0,19.0,8.5],["palette",93.5,360.0,34.0,11.0],["has",130.0,36Line truncated
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,390 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_gpui_component::{
|
||||
charts::NetworkGraph,
|
||||
data_display::{PropertyGrid, PropertyGroup},
|
||||
documents::{
|
||||
Backlink, Backlinks, Favorite, Favorites, PageCover, PageIcon, Template, TemplatePicker,
|
||||
TrashBin, Trashed, Version, VersionHistory,
|
||||
},
|
||||
forms::{Checkbox, Choice, DatePicker, Select, TagInput},
|
||||
lists::{Tree, TreeNode},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
use gpui::{App, FontWeight, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{Timestamp, ToSpan};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
const COVERS: [&str; 3] = [
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/atrium.jpg"
|
||||
),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/dunes.jpg"
|
||||
),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/atrium-olive.jpg"
|
||||
),
|
||||
];
|
||||
|
||||
fn favorite(key: &str, icon: &str, title: &str) -> Favorite {
|
||||
Favorite {
|
||||
key: key.to_string().into(),
|
||||
icon: icon.to_string().into(),
|
||||
title: title.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A page linking here, the link on the word "lift".
|
||||
fn backlink(icon: &str, page: &str, context: &str) -> Backlink {
|
||||
let at = context.find("lift").expect("the context names the page");
|
||||
Backlink {
|
||||
icon: icon.to_string().into(),
|
||||
page: page.to_string().into(),
|
||||
context: context.to_string().into(),
|
||||
mention: at..at + "lift".len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pages() -> Vec<TreeNode> {
|
||||
vec![
|
||||
TreeNode::new("handbook", "📘 Handbook").children([
|
||||
TreeNode::new("color", "🎨 Color").children([
|
||||
TreeNode::new("lift", "☀️ Lift"),
|
||||
TreeNode::new("dark", "🌙 Dark theme"),
|
||||
]),
|
||||
TreeNode::new("type", "🔤 Type"),
|
||||
TreeNode::new("motion", "🌀 Motion"),
|
||||
]),
|
||||
TreeNode::new("projects", "🗂 Projects").children([
|
||||
TreeNode::new("site", "🌐 Site"),
|
||||
TreeNode::new("release", "🚀 Release 1.0"),
|
||||
]),
|
||||
TreeNode::new("journal", "📓 Journal"),
|
||||
]
|
||||
}
|
||||
|
||||
fn templates() -> Rc<Vec<Template>> {
|
||||
let template =
|
||||
|name: &str, icon: &str, category: &str, description: &str, markdown: &str| Template {
|
||||
name: name.to_string().into(),
|
||||
icon: icon.to_string().into(),
|
||||
category: category.to_string().into(),
|
||||
description: description.to_string().into(),
|
||||
markdown: markdown.to_string().into(),
|
||||
};
|
||||
Rc::new(vec![
|
||||
template(
|
||||
"Meeting notes",
|
||||
"🗒",
|
||||
"Team",
|
||||
"Agenda, notes and actions",
|
||||
"# Meeting notes\n\n**Date:** \n\n## Agenda\n\n- \n\n## Actions\n\n- [ ] ",
|
||||
),
|
||||
template(
|
||||
"Retrospective",
|
||||
"🔁",
|
||||
"Team",
|
||||
"What went well, what to change",
|
||||
"# Retrospective\n\n## Went well\n\n- \n\n## To change\n\n- ",
|
||||
),
|
||||
template(
|
||||
"Roadmap",
|
||||
"🗺",
|
||||
"Product",
|
||||
"The quarters ahead",
|
||||
"# Roadmap\n\n| Quarter | Goal |\n|:--|:--|\n| Q1 | |\n| Q2 | |",
|
||||
),
|
||||
template(
|
||||
"Design review",
|
||||
"🎨",
|
||||
"Product",
|
||||
"A change and its questions",
|
||||
"# Design review\n\n> What problem does this solve?\n\n## Options\n\n1. \n2. ",
|
||||
),
|
||||
template(
|
||||
"Reading list",
|
||||
"📚",
|
||||
"Personal",
|
||||
"Books and where you are",
|
||||
"# Reading list\n\n- [ ] \n- [ ] ",
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn versions(now: Timestamp) -> Vec<Version> {
|
||||
let version = |hours: i64, author: &str, markdown: &str| Version {
|
||||
at: now - hours.hours(),
|
||||
author: author.to_string().into(),
|
||||
markdown: markdown.to_string().into(),
|
||||
};
|
||||
vec![
|
||||
version(
|
||||
1,
|
||||
"Ada",
|
||||
"# Lift\n\nA lift blends every color toward white, gently and in gamma space. Half a lift sits midway.",
|
||||
),
|
||||
version(
|
||||
26,
|
||||
"Grace",
|
||||
"# Lift\n\nA lift blends a color toward white in gamma space. Half a lift sits midway.",
|
||||
),
|
||||
version(74, "Ada", "# Lift\n\nA lift blends a color toward white."),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn knowledge(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let favorites = keep(
|
||||
"know-favorites",
|
||||
|| {
|
||||
vec![
|
||||
favorite("lift", "☀️", "Lift"),
|
||||
favorite("release", "🚀", "Release 1.0"),
|
||||
favorite("journal", "📓", "Journal"),
|
||||
]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now_favorites = favorites.read(cx).clone();
|
||||
let cover = keep(
|
||||
"know-cover",
|
||||
|| Some(SharedString::from(COVERS[0])),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let icon = keep("know-icon", || SharedString::from("☀️"), window, cx);
|
||||
let status = keep("know-status", || SharedString::from("Drafting"), window, cx);
|
||||
let done = keep("know-done", || false, window, cx);
|
||||
let (now_cover, now_icon, now_status, now_done) = (
|
||||
cover.read(cx).clone(),
|
||||
icon.read(cx).clone(),
|
||||
status.read(cx).clone(),
|
||||
*done.read(cx),
|
||||
);
|
||||
let trash = keep(
|
||||
"know-trash",
|
||||
|| {
|
||||
let now = Timestamp::now();
|
||||
vec![
|
||||
Trashed {
|
||||
key: "old-palette".into(),
|
||||
icon: "🎨".into(),
|
||||
title: "Old palette".into(),
|
||||
place: "Handbook".into(),
|
||||
deleted: now - 3.hours(),
|
||||
},
|
||||
Trashed {
|
||||
key: "draft".into(),
|
||||
icon: "📝".into(),
|
||||
title: "Launch draft".into(),
|
||||
place: "Projects".into(),
|
||||
deleted: now - 50.hours(),
|
||||
},
|
||||
]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now_trash = trash.read(cx).clone();
|
||||
let picked_version = keep("know-version", || 0usize, window, cx);
|
||||
let now_version = *picked_version.read(cx);
|
||||
let history = keep("know-history", || versions(Timestamp::now()), window, cx)
|
||||
.read(cx)
|
||||
.clone();
|
||||
let theme = cx.theme();
|
||||
let (unpinned, moved, restored, deleted, emptied) = (
|
||||
favorites.clone(),
|
||||
favorites.clone(),
|
||||
trash.clone(),
|
||||
trash.clone(),
|
||||
trash,
|
||||
);
|
||||
let graph = [
|
||||
"Lift",
|
||||
"Dark theme",
|
||||
"Color",
|
||||
"Type",
|
||||
"Motion",
|
||||
"Handbook",
|
||||
"Release 1.0",
|
||||
"Site",
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(NetworkGraph::new("know-graph"), |graph, (ix, name)| {
|
||||
graph.node(*name, if ix < 5 { 0 } else { 1 })
|
||||
});
|
||||
let graph = [
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(1, 2),
|
||||
(2, 5),
|
||||
(3, 5),
|
||||
(4, 5),
|
||||
(0, 4),
|
||||
(6, 5),
|
||||
(6, 7),
|
||||
(7, 0),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(graph, |graph, (a, b)| graph.edge(a, b));
|
||||
div()
|
||||
.child(
|
||||
section(
|
||||
"PageTree / Favorites / Pinned",
|
||||
"Pages kept close at hand above the tree of every page: favorites open with a press, drag to reorder and let go by their star. The tree is lists::Tree, a page's emoji leading its label.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(300.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.p_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(
|
||||
Favorites::new("know-favorites", now_favorites)
|
||||
.on_open(|key, _, _| log::info!("gallery: open {key}"))
|
||||
.on_unpin(move |key, _, cx| {
|
||||
let next = unpinned.read(cx).iter().filter(|page| page.key != *key).cloned().collect();
|
||||
set(&unpinned, next, cx);
|
||||
})
|
||||
.on_move(move |from, to, _, cx| {
|
||||
let mut next = moved.read(cx).clone();
|
||||
let page = next.remove(from);
|
||||
next.insert(to, page);
|
||||
set(&moved, next, cx);
|
||||
}),
|
||||
)
|
||||
.child(div().h(px(220.)).child(Tree::new("know-tree", pages())
|
||||
.open(["handbook", "color"])
|
||||
.selected(["lift"])
|
||||
.size_full())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"PageCover / PageIcon / PageProperties",
|
||||
"A page's head: its cover, its icon set large over the cover's edge, its title, and its properties. Hover the cover to change or remove it, the icon to pick another. The properties are data_display::PropertyGrid, a field's editor in each row.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(760.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.overflow_hidden()
|
||||
.children(now_cover.map(|source| {
|
||||
let cover = cover.clone();
|
||||
PageCover::new("know-cover", source).choices(COVERS, move |next, _, cx| set(&cover, next, cx))
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.px_8()
|
||||
.pb_6()
|
||||
.child(div().mt_neg_8().child(PageIcon::new("know-icon", now_icon).on_change(move |next, _, cx| set(&icon, next.clone(), cx))))
|
||||
.child(div().text_size(theme.text_size(TextSize::Xxl)).font_weight(FontWeight::SEMIBOLD).child("Lift"))
|
||||
.child(
|
||||
PropertyGrid::new("know-properties").group(
|
||||
PropertyGroup::new("Properties")
|
||||
.row(
|
||||
"Status",
|
||||
Select::new("know-status", ["Idea", "Drafting", "Review", "Done"].map(|name| Choice::new(name, name)))
|
||||
.selected(now_status)
|
||||
.on_change(move |next, _, cx| set(&status, next.clone(), cx)),
|
||||
)
|
||||
.row("Due", DatePicker::new("know-due", Some(jiff::civil::date(2026, 10, 12))))
|
||||
.row("Tags", TagInput::new("know-tags", ["color", "theme"]))
|
||||
.row("Published", Checkbox::new("know-published", now_done).on_change(move |on, _, cx| set(&done, on, cx))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"Backlinks / GraphView",
|
||||
"The pages that link here, each with the words around its link; and every page as a node, its links as edges. GraphView is charts::NetworkGraph.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(360.)).child(
|
||||
Backlinks::new(
|
||||
"know-backlinks",
|
||||
[
|
||||
backlink("🌙", "Dark theme", "Accents in dark carry more lift than in light, so they read the same."),
|
||||
backlink("🎨", "Color", "Every accent is a base color and a lift."),
|
||||
backlink("🌀", "Motion", "A lift of motion is a step on the duration scale."),
|
||||
],
|
||||
)
|
||||
.on_open(|ix, _, _| log::info!("gallery: backlink {ix}")),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(360.)).h(px(280.)).child(graph)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"TemplatePicker",
|
||||
"Pages to start from: found by name, kept by category, the one in view shown as it will read; Use takes it.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(760.)).h(px(400.)).child(TemplatePicker::new("know-templates", templates()).on_use(|ix, _, _| log::info!("gallery: template {ix}")))),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"TrashBin",
|
||||
"Deleted pages, newest first, with where each lived and when it went: restore one, delete one for good or empty it all, each final step asked twice.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(560.)).child(
|
||||
TrashBin::new("know-trash", now_trash)
|
||||
.on_restore(move |key, _, cx| {
|
||||
let next = restored.read(cx).iter().filter(|page| page.key != *key).cloned().collect();
|
||||
set(&restored, next, cx);
|
||||
})
|
||||
.on_delete(move |key, _, cx| {
|
||||
let next = deleted.read(cx).iter().filter(|page| page.key != *key).cloned().collect();
|
||||
set(&deleted, next, cx);
|
||||
})
|
||||
.on_empty(move |_, cx| set(&emptied, Vec::new(), cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"VersionHistory / PageHistoryDiff",
|
||||
"A page's versions by time and author: read one, or see word by word what it changed from the one before, added words washed and removed ones struck; restore any but the current.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
probe("know-versions", div().w(px(760.)).h(px(320.)).child(
|
||||
VersionHistory::new("know-versions", history, now_version)
|
||||
.on_select(move |ix, _, cx| set(&picked_version, ix, cx))
|
||||
.on_restore(|ix, _, _| log::info!("gallery: restore version {ix}")),
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
mod editing;
|
||||
mod knowledge;
|
||||
mod reading;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
@@ -58,8 +60,31 @@ const SCRIPT: &[Step] = &[
|
||||
Step::HoverAt("doc-blocks", 120.0, 20.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("handle"),
|
||||
Step::DownAt("read-viewer", 440.0, 58.0),
|
||||
Step::UpAt("read-viewer", 440.0, 58.0),
|
||||
Step::Key("cmd-a"),
|
||||
Step::Type("lift"),
|
||||
Step::Wait(200),
|
||||
Step::DownAt("read-viewer", 786.0, 58.0),
|
||||
Step::UpAt("read-viewer", 786.0, 58.0),
|
||||
Step::Wait(400),
|
||||
Step::Shot("find"),
|
||||
Step::DownAt("know-versions", 330.0, 22.0),
|
||||
Step::UpAt("know-versions", 330.0, 22.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("changes"),
|
||||
Step::DownAt("read-zen", 60.0, 24.0),
|
||||
Step::UpAt("read-zen", 60.0, 24.0),
|
||||
Step::Wait(500),
|
||||
Step::Shot("zen"),
|
||||
Step::Key("escape"),
|
||||
Step::Wait(300),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div().child(editing::editing(window, cx)).into_any_element()
|
||||
div()
|
||||
.child(editing::editing(window, cx))
|
||||
.child(reading::reading(window, cx))
|
||||
.child(knowledge::knowledge(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use ely_gpui_component::{
|
||||
documents::{
|
||||
Chapter, DocPage, DocumentViewer, EpubReader, Footnote, Glossary, MarkdownRenderer,
|
||||
PageHit, PageNote, PrintPreview, PrintSettings, ReadingProgress, RichTextEditor,
|
||||
TooltipTerm, ZenMode, markdown_highlights, outline,
|
||||
},
|
||||
editor::FindWidget,
|
||||
forms::TextInput,
|
||||
navigation::{Sections, TableOfContents},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, Entity, InteractiveElement, IntoElement, ParentElement, ScrollHandle,
|
||||
StatefulInteractiveElement, Styled, Window, div, point, px, size,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
const PAGES: [&str; 2] = [
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/pages/lift-1.png"
|
||||
),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/pages/lift-2.png"
|
||||
),
|
||||
];
|
||||
const SLIDES: [&str; 2] = [
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/pages/slide-1.png"
|
||||
),
|
||||
concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/pages/slide-2.png"
|
||||
),
|
||||
];
|
||||
const WORDS: &str = include_str!("../../assets/pages/lift.json");
|
||||
|
||||
const GUIDE: &str = "# Lift\n\nA lift blends a color toward white.\n\n## In light\n\nPaper white under ink near black; accents at half a lift.\n\n## In dark\n\nWarm charcoal under off-white text; accents lift further, so they read with the same weight.\n\n### Surfaces\n\nA card sits one lift above the page, a menu one above the card.\n\n## In motion\n\nHover, press and focus are lifts of the accent, eased on one scale of durations.\n\n## Closing\n\nWhatever the hue, a step of lift means the same step of emphasis.";
|
||||
|
||||
/// The words the app found on its pages, with their boxes in points.
|
||||
#[derive(Deserialize)]
|
||||
struct PageWords {
|
||||
page: usize,
|
||||
words: Vec<(String, f32, f32, f32, f32)>,
|
||||
}
|
||||
|
||||
/// Every word on the pages that holds `query`, as a host's text layer would find them.
|
||||
fn search(query: &str) -> Vec<PageHit> {
|
||||
let query = query.trim().to_lowercase();
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let pages: Vec<PageWords> = serde_json::from_str(WORDS).expect("the page words parse");
|
||||
let mut hits = Vec::new();
|
||||
for page in pages {
|
||||
for (word, x, y, width, height) in page.words {
|
||||
if word.to_lowercase().contains(&query) {
|
||||
hits.push(PageHit {
|
||||
page: page.page,
|
||||
bounds: Bounds::new(point(x, y), size(width, height)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
hits
|
||||
}
|
||||
|
||||
fn pages(paths: &[&'static str], width: f32, height: f32) -> Rc<Vec<DocPage>> {
|
||||
Rc::new(
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| DocPage {
|
||||
source: (*path).into(),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn field(
|
||||
key: &'static str,
|
||||
hint: &'static str,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<TextInput> {
|
||||
window.use_keyed_state(key, cx, move |window, cx| {
|
||||
TextInput::new(window, cx).placeholder(hint)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reading(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let scroll = keep("read-scroll", ScrollHandle::new, window, cx)
|
||||
.read(cx)
|
||||
.clone();
|
||||
let sections = window.use_keyed_state("read-sections", cx, {
|
||||
let scroll = scroll.clone();
|
||||
move |_, _| Sections::new(scroll)
|
||||
});
|
||||
let toc = outline(GUIDE).into_iter().fold(
|
||||
TableOfContents::new("read-toc", §ions),
|
||||
|toc, (value, text, depth)| toc.entry(value, text, depth),
|
||||
);
|
||||
let query = field("read-query", "Find in document", window, cx);
|
||||
let hits = search(query.read(cx).text());
|
||||
let current = keep("read-hit", || None::<usize>, window, cx);
|
||||
let now_hit = current.read(cx).filter(|hit| *hit < hits.len());
|
||||
let total = hits.len();
|
||||
let find =
|
||||
FindWidget::new("read-find", &query, now_hit, total).on_step(move |forward, _, cx| {
|
||||
let next = match current.read(cx).filter(|hit| *hit < total) {
|
||||
Some(hit) if forward => (hit + 1) % total,
|
||||
Some(hit) => (hit + total - 1) % total,
|
||||
None if forward => 0,
|
||||
None => total - 1,
|
||||
};
|
||||
set(¤t, Some(next), cx)
|
||||
});
|
||||
let zoom = keep("read-zoom", || 0.75_f32, window, cx);
|
||||
let notes = keep(
|
||||
"read-notes",
|
||||
|| {
|
||||
vec![PageNote {
|
||||
page: 0,
|
||||
at: point(470.0, 180.0),
|
||||
text: "Gamma, not linear light.".into(),
|
||||
}]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let (now_zoom, now_notes) = (*zoom.read(cx), notes.read(cx).clone());
|
||||
let slide_zoom = keep("read-slide-zoom", || 0.5_f32, window, cx);
|
||||
let now_slide_zoom = *slide_zoom.read(cx);
|
||||
let print = keep("read-print", PrintSettings::default, window, cx);
|
||||
let now_print = *print.read(cx);
|
||||
let zen = keep("read-zen", || false, window, cx);
|
||||
let now_zen = *zen.read(cx);
|
||||
let draft = window.use_keyed_state("read-draft", cx, |window, cx| {
|
||||
let mut draft = TextInput::new(window, cx).multi_line(6, 18).highlighter(markdown_highlights);
|
||||
draft.set_text("# A quiet page\n\nEverything else folds away while you write. Press Escape to come back.", cx);
|
||||
draft
|
||||
});
|
||||
let theme = cx.theme();
|
||||
let chapters = vec![
|
||||
Chapter { title: "A color and its lift".into(), markdown: "# A color and its lift\n\nA lift blends a color toward white. Half a lift sits midway between the color and white, and a full lift is white itself.\n\nEvery accent in the library is a base color and a lift. The base carries the hue; the lift carries how loud the color is.".into() },
|
||||
Chapter { title: "Tokens in one place".into(), markdown: "# Tokens in one place\n\nComponents never write a color, a size or a radius. They read tokens from the theme.\n\nThe palette has two sides. Neither is the other inverted.".into() },
|
||||
Chapter { title: "Motion that settles".into(), markdown: "# Motion that settles\n\nMotion in the library is short and settles without bounce. When reduced motion is on, every repeating motion holds still.".into() },
|
||||
];
|
||||
div()
|
||||
.child(
|
||||
section(
|
||||
"MarkdownRenderer / TableOfContents / DocumentOutline / ReadingProgress",
|
||||
"Markdown as a document, its headings marked for a table of contents beside it: the section in view is lit as it scrolls, and a press brings a section to the top. The bar above fills as the text scrolls. DocumentOutline is the same TableOfContents.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(760.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(ReadingProgress::new(&scroll))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.h(px(320.))
|
||||
.child(div().w(px(200.)).child(toc))
|
||||
.child(
|
||||
div()
|
||||
.id("read-guide")
|
||||
.flex_1()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&scroll)
|
||||
.child(MarkdownRenderer::new("read-guide-text", GUIDE).sections(§ions)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"DocumentViewer / PDFViewer / PageThumbnailList",
|
||||
"Pages the app drew, on a desk: the page in view and its count, zoom by steps or to the width, the pages as thumbnails beside, a find bar whose hits light on the page, and notes pinned where a press puts them. The app reads its PDF; the viewer shows it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"read-viewer",
|
||||
div().w(px(840.)).h(px(520.)).child(
|
||||
DocumentViewer::new("read-viewer", "on-lift.pdf", pages(&PAGES, 612.0, 792.0))
|
||||
.zoom(now_zoom, move |next, _, cx| set(&zoom, next, cx))
|
||||
.thumbnails()
|
||||
.find(find, hits, now_hit)
|
||||
.notes(now_notes, move |page, at, _, cx| {
|
||||
let mut next = notes.read(cx).clone();
|
||||
next.push(PageNote { page, at, text: "A new note.".into() });
|
||||
set(¬es, next, cx);
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"OfficePreview",
|
||||
"A slide deck, a sheet or a document the app turned into pages, shown by the same viewer.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(760.)).h(px(420.)).child(
|
||||
DocumentViewer::new("read-slides", "release.pptx", pages(&SLIDES, 960.0, 540.0))
|
||||
.zoom(now_slide_zoom, move |next, _, cx| set(&slide_zoom, next, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"EpubReader",
|
||||
"A book a chapter at a time, in a column sized for reading: how far through the chapter, the chapters listed, the type a step larger or smaller, and the next chapter a press away.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(760.)).h(px(420.)).child(EpubReader::new("read-book", "On Lift", chapters))),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"Footnote / Glossary / TooltipTerm",
|
||||
"A note's number in running text, the note a rest of the pointer away; a term underlined in dashes that explains itself; and every term in order under its letter.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(560.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_baseline()
|
||||
.child("A ")
|
||||
.child(TooltipTerm::new("read-lift", "lift", "How far a color is blended toward white, from none to all of it."))
|
||||
.child(" blends in gamma space")
|
||||
.child(Footnote::new("read-note", 1, "Where a small step moves the eye as much as a large one moves the meter."))
|
||||
.child(", so each step reads as even."),
|
||||
)
|
||||
.child(Glossary::new([
|
||||
("Lift", "How far a color is blended toward white."),
|
||||
("Accent", "A base color and a lift: the hue and how loud it is."),
|
||||
("Token", "A value every component reads from the theme."),
|
||||
("Leading", "The height of a line of text."),
|
||||
])),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"FocusMode / ZenMode",
|
||||
"Zen mode folds the tools away and sets the text in a quiet column; Escape brings them back.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"read-zen",
|
||||
div()
|
||||
.w(px(760.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
ZenMode::new("read-zen", now_zen)
|
||||
.on_exit({
|
||||
let zen = zen.clone();
|
||||
move |_, cx| set(&zen, false, cx)
|
||||
})
|
||||
.chrome(
|
||||
div().p_2().flex().child(
|
||||
ely_gpui_component::buttons::Button::new("read-zen-on", "Focus on the text")
|
||||
.on_click(move |_, _, cx| set(&zen, true, cx)),
|
||||
),
|
||||
)
|
||||
.child(div().p_2().child(RichTextEditor::new("read-draft", &draft))),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"PrintPreview",
|
||||
"The pages the app laid out for the settings, small on a desk, with paper, turn, margins and copies beside; each change asks the app for new pages.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(840.)).h(px(440.)).child(PrintPreview::new(
|
||||
"read-print",
|
||||
pages(&PAGES, 612.0, 792.0),
|
||||
now_print,
|
||||
move |next, _, cx| set(&print, next, cx),
|
||||
|settings, _, _| log::info!("gallery: print {settings:?}"),
|
||||
|_, _| log::info!("gallery: print cancelled"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user