mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 02:41:31 -06:00
* UI: checkboxes draw a rounded-square box, not a radio-button circle
The theme's 6 px widget radius made egui's 14 px checkbox box a circle,
so View > Page display's Continuous scrolling and Show cover page looked
like two more layout radio buttons. On the light theme an unticked box
had no outline and the panel's fill, so it couldn't be seen.
Every checkbox (81 call sites) now goes through widgets::checkbox /
checkbox_without_text, which draws the box with a 3 px radius and a 1 px
border outline, keeping an outline the caller already set (dialogs).
clippy.toml disallows egui::Ui::checkbox and egui::Checkbox::{new,
without_text} so new checkboxes use the helper too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: radio buttons are outlined and filled, so they show on the light theme
On the light theme the field colour is the panels' white and widgets
have no outline, so an unselected radio button (and an unticked
checkbox) was invisible outside dialogs: View > Page display showed only
the selected dot.
Every radio button (79 call sites) now goes through widgets::radio /
radio_value, sharing the checkbox helper's styling: a 1 px border
outline, text_muted on hover, and on the light theme the plain fill
(hover) as dialogs already had, keeping what the caller set. The dark
theme keeps its fills. clippy.toml also disallows egui::Ui::radio,
egui::Ui::radio_value and egui::RadioButton::new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
100 lines
4.4 KiB
Rust
100 lines
4.4 KiB
Rust
//! Edit ▸ Advanced Search (⇧⌘F): the search in a side panel that lists every match with its
|
|
//! context, page by page; click a result to go to it. It shares the find bar's search.
|
|
|
|
use egui::text::LayoutJob;
|
|
use egui::{Color32, FontId, TextFormat};
|
|
|
|
use crate::canvas::DocView;
|
|
use crate::theme::{self, Tokens};
|
|
use crate::widgets;
|
|
|
|
/// Glyphs of context shown on each side of a match.
|
|
const CONTEXT: usize = 36;
|
|
|
|
pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: usize) {
|
|
crate::widgets::begin_dialog(ui);
|
|
let searched = view.texts.len() + view.text_failed.len();
|
|
let Some(find) = view.find.as_mut() else { return };
|
|
find.in_panel = true;
|
|
let l = ui.label(egui::RichText::new(tl!("What word or phrase would you like to search for?")).color(t.text_muted));
|
|
let r = ui
|
|
.add(crate::widgets::line(&mut find.query).id(egui::Id::new("search-panel-input")).hint_text(tl!("Search")).desired_width(f32::INFINITY))
|
|
.labelled_by(l.id);
|
|
if find.focus {
|
|
r.request_focus();
|
|
find.focus = false;
|
|
}
|
|
ui.horizontal(|ui| {
|
|
// Outlined boxes (the panel and the check-box fill are alike), as in dialogs.
|
|
let w = &mut ui.visuals_mut().widgets;
|
|
w.inactive.bg_stroke = egui::Stroke::new(1.0, t.border);
|
|
w.inactive.bg_fill = t.hover;
|
|
let a = ui.add(widgets::checkbox(&mut find.whole_words, tl!("Whole words only"))).changed();
|
|
let b = ui.add(widgets::checkbox(&mut find.case_sensitive, tl!("Case-sensitive"))).changed();
|
|
if a || b {
|
|
find.case_query.clear();
|
|
}
|
|
});
|
|
if find.query != find.case_query {
|
|
view.rerun_find();
|
|
}
|
|
let Some(find) = view.find.as_ref() else { return };
|
|
ui.add_space(6.0);
|
|
let status = if find.query.trim().is_empty() {
|
|
String::new()
|
|
} else if find.matches.is_empty() && searched < pages {
|
|
crate::i18n::fmt(tl!("Searching… {s} of {p} pages"), &[("s", &searched.to_string()), ("p", &pages.to_string())])
|
|
} else {
|
|
let n = find.matches.len();
|
|
let more = if searched < pages {
|
|
crate::i18n::fmt(tl!(" (searching… {s} of {p} pages)"), &[("s", &searched.to_string()), ("p", &pages.to_string())])
|
|
} else {
|
|
String::new()
|
|
};
|
|
// English keeps its singular; Chinese has no plural.
|
|
let count = if n == 1 { crate::i18n::fmt(tl!("1 instance"), &[]) } else { crate::i18n::fmt(tl!("{n} instances"), &[("n", &n.to_string())]) };
|
|
format!("{count}{more}")
|
|
};
|
|
ui.label(egui::RichText::new(status).font(theme::semibold(12.5)));
|
|
ui.add_space(4.0);
|
|
let mut go = None;
|
|
let mut last_page = None;
|
|
for (i, (p, range)) in find.matches.iter().enumerate().take(2000) {
|
|
if last_page != Some(*p) {
|
|
ui.add_space(4.0);
|
|
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Page {label}"), &[("label", &(p + 1).to_string())])).small().color(t.text_faint));
|
|
last_page = Some(*p);
|
|
}
|
|
let Some(text) = view.texts.get(p) else { continue };
|
|
let n = text.glyphs.len();
|
|
let before = text.text_of(range.start.saturating_sub(CONTEXT)..range.start.min(n));
|
|
let hit = text.text_of(range.start.min(n)..range.end.min(n));
|
|
let after = text.text_of(range.end.min(n)..(range.end + CONTEXT).min(n));
|
|
let flat = |s: String| s.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
let mut job = LayoutJob::default();
|
|
let font = FontId::new(12.5, egui::FontFamily::Proportional);
|
|
let plain = TextFormat { font_id: font.clone(), color: t.text_muted, ..Default::default() };
|
|
job.append(&format!("…{} ", flat(before)), 0.0, plain.clone());
|
|
job.append(
|
|
&flat(hit),
|
|
0.0,
|
|
TextFormat {
|
|
font_id: theme::semibold(12.5),
|
|
color: t.text,
|
|
background: Color32::from_rgba_unmultiplied(255, 214, 0, 90),
|
|
..Default::default()
|
|
},
|
|
);
|
|
job.append(&format!(" {}…", flat(after)), 0.0, plain);
|
|
job.wrap.max_width = ui.available_width();
|
|
let selected = find.current == Some(i);
|
|
let resp = ui.add(egui::Button::selectable(selected, job).wrap()).on_hover_text(tl!("Go to this result"));
|
|
if resp.clicked() {
|
|
go = Some(i);
|
|
}
|
|
}
|
|
if let Some(i) = go {
|
|
view.go_to_match(i);
|
|
}
|
|
}
|