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.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,208 @@
|
||||
//! Find in the whole document, pages in batches (PDF-01).
|
||||
|
||||
use ely_gpui_component::{
|
||||
editor::FindWidget,
|
||||
forms::{InputEvent, TextInput},
|
||||
primitives::Severity,
|
||||
};
|
||||
use ely_office::Tabs as _;
|
||||
use gpui::{
|
||||
AnyElement, AppContext as _, Context, Entity, Focusable as _, IntoElement, ParentElement as _,
|
||||
Styled as _, Subscription, Window, div,
|
||||
};
|
||||
|
||||
use crate::view::PdfView;
|
||||
use crate::viewer::Viewer;
|
||||
|
||||
/// Pages searched in one worker call.
|
||||
const BATCH: usize = 20;
|
||||
|
||||
/// The find box; its hits live in the tab's view.
|
||||
pub struct Find {
|
||||
/// Each find box is new; late answers check it.
|
||||
pub id: u64,
|
||||
pub tab: u64,
|
||||
pub query: Entity<TextInput>,
|
||||
/// Pages searched so far.
|
||||
pub done: usize,
|
||||
/// Counts searches; an older batch is dropped.
|
||||
pub round: u64,
|
||||
/// The view's version searched; an edit starts over.
|
||||
pub version: u64,
|
||||
_query: Subscription,
|
||||
}
|
||||
|
||||
/// A quad's bounding box: x0, y0, x1, y1.
|
||||
pub fn bounds(quad: &[f32; 8]) -> [f32; 4] {
|
||||
let xs = [quad[0], quad[2], quad[4], quad[6]];
|
||||
let ys = [quad[1], quad[3], quad[5], quad[7]];
|
||||
let min = |v: [f32; 4]| v.into_iter().fold(f32::MAX, f32::min);
|
||||
let max = |v: [f32; 4]| v.into_iter().fold(f32::MIN, f32::max);
|
||||
[min(xs), min(ys), max(xs), max(ys)]
|
||||
}
|
||||
|
||||
impl Viewer {
|
||||
/// The view of tab `id`.
|
||||
fn view_of(&self, id: u64) -> Option<Entity<PdfView>> {
|
||||
self.tab(id).map(|tab| tab.view.clone())
|
||||
}
|
||||
|
||||
pub fn open_find(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.core.active else {
|
||||
let text = Self::text(cx, "command.blocked.no_document", &[]);
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
if let Some(find) = self.find.as_ref().filter(|find| find.tab == tab) {
|
||||
return window.focus(&find.query.focus_handle(cx), cx);
|
||||
}
|
||||
self.drop_find(cx);
|
||||
let query = cx.new(|cx| TextInput::new(window, cx));
|
||||
let subscription = cx.subscribe(&query, |viewer, _, event, cx| match event {
|
||||
InputEvent::Changed => viewer.search(cx),
|
||||
InputEvent::Submit => viewer.step(true, cx),
|
||||
_ => {}
|
||||
});
|
||||
window.focus(&query.focus_handle(cx), cx);
|
||||
self.finds += 1;
|
||||
let id = self.finds;
|
||||
self.find = Some(Find { id, tab, query, done: 0, round: 0, version: 0, _query: subscription });
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Closes the find box; its marks go.
|
||||
pub fn drop_find(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(view) = self.find.take().and_then(|find| self.view_of(find.tab)) {
|
||||
view.update(cx, |view, cx| {
|
||||
(view.found, view.chosen) = (Vec::new(), None);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts over for the query now typed.
|
||||
pub fn search(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(view) = self.find.as_ref().and_then(|find| self.view_of(find.tab)) else {
|
||||
return;
|
||||
};
|
||||
let version = view.read(cx).version;
|
||||
view.update(cx, |view, _| (view.found, view.chosen) = (Vec::new(), None));
|
||||
if let Some(find) = self.find.as_mut() {
|
||||
(find.round, find.done, find.version) = (find.round + 1, 0, version);
|
||||
}
|
||||
self.search_more(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Searches the next batch of pages.
|
||||
fn search_more(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(find) = self.find.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let needle = find.query.read(cx).text().to_string();
|
||||
let (finder, id, round, from) = (find.id, find.tab, find.round, find.done);
|
||||
let Some(view) = self.view_of(id) else {
|
||||
return;
|
||||
};
|
||||
let (doc, pages) = (view.read(cx).doc, view.read(cx).sizes.len());
|
||||
if needle.is_empty() || from >= pages {
|
||||
return;
|
||||
}
|
||||
let to = (from + BATCH).min(pages);
|
||||
let this = cx.weak_entity();
|
||||
view.update(cx, |view, cx| {
|
||||
view.read(
|
||||
move |worker| {
|
||||
(from..to)
|
||||
.map(|page| Ok(worker.search(doc, page, &needle)?.into_iter().map(move |quad| (page, quad))))
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.map(|pages| pages.into_iter().flatten().collect::<Vec<_>>())
|
||||
},
|
||||
move |view, found, cx| {
|
||||
let live = move |viewer: &Viewer| viewer.find.as_ref().is_some_and(|find| (find.id, find.round) == (finder, round));
|
||||
if !this.upgrade().is_some_and(|viewer| live(viewer.read(cx))) {
|
||||
return;
|
||||
}
|
||||
let hits = match found {
|
||||
Ok(hits) => hits,
|
||||
Err(error) => return view.fail("searching", &error, cx),
|
||||
};
|
||||
view.found.extend(hits);
|
||||
let first = view.chosen.is_none() && !view.found.is_empty();
|
||||
cx.notify();
|
||||
// The viewer reads this view; wait until it is free.
|
||||
cx.defer(move |cx| {
|
||||
let Some(viewer) = this.upgrade() else {
|
||||
return;
|
||||
};
|
||||
viewer.update(cx, |viewer, cx| {
|
||||
if !live(viewer) {
|
||||
return;
|
||||
}
|
||||
if let Some(find) = viewer.find.as_mut() {
|
||||
find.done = to;
|
||||
}
|
||||
if first {
|
||||
viewer.step(true, cx);
|
||||
}
|
||||
viewer.search_more(cx);
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Goes to the next or previous hit.
|
||||
pub fn step(&mut self, forward: bool, cx: &mut Context<Self>) {
|
||||
let Some(find) = self.find.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(view) = self.view_of(find.tab) else {
|
||||
return;
|
||||
};
|
||||
view.update(cx, |view, cx| {
|
||||
let count = view.found.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let at = match (view.chosen, forward) {
|
||||
(None, _) => 0,
|
||||
(Some(at), true) => (at + 1) % count,
|
||||
(Some(at), false) => (at + count - 1) % count,
|
||||
};
|
||||
view.chosen = Some(at);
|
||||
let (page, y) = (view.found[at].0, bounds(&view.found[at].1[0])[1]);
|
||||
view.show(page, y, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The find box over the pages, while open.
|
||||
pub fn find_bar(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let find = self.find.as_ref().filter(|find| Some(find.tab) == self.core.active)?;
|
||||
let view = self.view_of(find.tab)?;
|
||||
let (chosen, count) = (view.read(cx).chosen, view.read(cx).found.len());
|
||||
let (step, close) = (cx.entity(), cx.entity());
|
||||
let widget = FindWidget::new("pdf-find", &find.query, chosen, count)
|
||||
.on_step(move |forward, _, cx| step.update(cx, |viewer, cx| viewer.step(forward, cx)))
|
||||
.on_close(move |window, cx| {
|
||||
close.update(cx, |viewer, cx| {
|
||||
viewer.drop_find(cx);
|
||||
window.focus(&view.read(cx).focus.clone(), cx);
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
Some(div().flex().justify_end().px_3().py_1().child(widget).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_quad_bounds_its_four_corners() {
|
||||
let quad = [10.0, 20.0, 30.0, 20.0, 10.0, 32.0, 30.0, 32.0];
|
||||
assert_eq!(super::bounds(&quad), [10.0, 20.0, 30.0, 32.0]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user