Files
ElyOffice/apps/ely-pdf/src/notelist.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

87 lines
3.2 KiB
Rust

//! The annotation list in the side panel (PDF-05).
use ely_gpui_component::{
feedback::EmptyState,
lists::{ListItem, SelectableList},
primitives::IconName,
};
use gpui::{
AnyElement, App, Entity, InteractiveElement as _, IntoElement, ParentElement as _, StatefulInteractiveElement as _,
Styled as _, div,
};
use crate::annotate::Pick;
use crate::content::Aim;
use crate::notes::kind_label;
use crate::view::PdfView;
use crate::viewer::Viewer;
fn key(page: usize, id: u64) -> String {
format!("{page}:{id}")
}
fn parse(key: &str) -> Option<(usize, u64)> {
let (page, id) = key.split_once(':')?;
Some((page.parse().ok()?, id.parse().ok()?))
}
/// Every annotation, page by page; unread pages are asked.
pub fn list(view: &Entity<PdfView>, cx: &mut App) -> AnyElement {
let count = view.read(cx).sizes.len();
view.update(cx, |view, cx| view.ask_notes(0..count, cx));
let read = view.read(cx);
let selected = match read.aim {
Some(Aim::Note(Pick::Annot { page, id })) => Some(key(page, id)),
_ => None,
};
let mut rows = Vec::new();
for (page, marks) in (0..count).filter_map(|page| Some((page, read.notes.get(&page)?))) {
for annot in &marks.annots {
let place = Viewer::text(cx, "pdf.page.label", &[("n", &(page + 1).to_string())]);
let mut item = ListItem::new(("pdf-note", annot.id as usize), kind_label(cx, &annot.kind)).detail(place);
if let Some(first) = annot.contents.lines().find(|line| !line.trim().is_empty()) {
item = item.description(first.to_string());
}
rows.push((key(page, annot.id), item));
}
}
let all_read = (0..count).all(|page| read.notes.contains_key(&page));
if rows.is_empty() && all_read {
return EmptyState::new("pdf-notes-none", IconName::File, Viewer::text(cx, "pdf.notes.none", &[])).into_any_element();
}
let total = rows.len();
let picked = view.clone();
let list = (rows.into_iter().enumerate())
.fold(SelectableList::new("pdf-notes"), |list, (at, (key, item))| list.row(key, item.place(at + 1, total)))
.selected(selected)
.label(Viewer::text(cx, "pdf.side.notes", &[]))
.on_change(move |keys, _, cx| {
let Some((page, id)) = keys.first().and_then(|key| parse(key)) else {
return;
};
picked.update(cx, |view, cx| view.pick_note(page, id, cx))
});
div().id("pdf-notes-scroll").size_full().overflow_y_scroll().child(list).into_any_element()
}
impl PdfView {
/// Picks annotation `id` and shows where it is.
pub fn pick_note(&mut self, page: usize, id: u64, cx: &mut gpui::Context<Self>) {
// Gone with an edit since the list was drawn.
let Some(top) = self.notes.get(&page).and_then(|m| m.annots.iter().find(|a| a.id == id)).map(|a| a.rect[1]) else {
return;
};
self.aim = Some(Aim::Note(Pick::Annot { page, id }));
self.show(page, top, cx);
}
}
#[cfg(test)]
mod tests {
#[test]
fn row_keys_name_page_and_object() {
assert_eq!(super::parse(&super::key(3, 41)), Some((3, 41)));
assert_eq!(super::parse("3"), None);
}
}