use std::collections::HashSet;
use std::sync::Arc;
use super::harvest;
const FLASH_SECS: f64 = 1.5;
const JUMP_FRAMES: u32 = 90;
#[derive(Debug, Clone, Default)]
pub(crate) struct LiveView {
pub active: bool,
pub query_changed: bool,
pub known: HashSet<String>,
pub visible: HashSet<String>,
}
impl LiveView {
pub fn active(&self) -> bool {
self.active
}
pub fn hides(&self, id: &str) -> bool {
self.active && self.known.contains(id) && !self.visible.contains(id)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Jump {
pub path: Vec<String>,
pub label: Option<String>,
frames: u32,
last_rect: Option<egui::Rect>,
}
impl Jump {
pub fn new(path: Vec<String>, label: Option<String>) -> Self {
Self {
path,
label,
frames: 0,
last_rect: None,
}
}
}
#[derive(Debug, Clone)]
struct Flash {
section: String,
on_header: bool,
offset: egui::Vec2,
size: egui::Vec2,
started: f64,
}
pub(crate) struct DrawnSection<'a> {
pub id: &'a str,
pub title: &'a str,
pub keywords: &'a [&'a str],
pub header: egui::Response,
pub body: Option<(egui::Id, egui::Rect)>,
pub fully_open: bool,
}
fn view_id() -> egui::Id {
egui::Id::new("settings_search_live_view")
}
fn jump_id() -> egui::Id {
view_id().with("jump")
}
fn flash_id() -> egui::Id {
view_id().with("flash")
}
pub(crate) fn set_live_view(ctx: &egui::Context, view: Arc<LiveView>) {
ctx.data_mut(|d| d.insert_temp(view_id(), view));
}
pub(crate) fn live_view(ctx: &egui::Context) -> Option<Arc<LiveView>> {
ctx.data(|d| d.get_temp::<Arc<LiveView>>(view_id()))
}
pub(crate) fn start_jump(ctx: &egui::Context, jump: Jump) {
if jump.label.is_some() {
ctx.enable_accesskit();
}
ctx.data_mut(|d| {
d.insert_temp(jump_id(), Some(jump));
d.insert_temp::<Option<Flash>>(flash_id(), None);
});
ctx.request_repaint();
}
pub(crate) fn pending_jump(ctx: &egui::Context) -> Option<Jump> {
ctx.data(|d| d.get_temp::<Option<Jump>>(jump_id()))
.flatten()
}
pub(crate) fn forces_open(ctx: &egui::Context, view: Option<&LiveView>, id: &str) -> bool {
view.is_some_and(|v| v.query_changed)
|| pending_jump(ctx).is_some_and(|j| j.path.iter().any(|p| p == id))
}
pub(crate) fn note_section(ui: &egui::Ui, drawn: DrawnSection<'_>) {
let ctx = ui.ctx();
if harvest::harvesting(ctx) {
if let Some((body, _)) = drawn.body {
harvest::record_section(
ctx,
drawn.id,
drawn.title,
drawn.keywords,
drawn.header.id,
body,
);
}
return;
}
if let Some(jump) = pending_jump(ctx)
&& jump.path.first().is_some_and(|p| p == drawn.id)
{
advance_jump(ui, jump, &drawn);
}
paint_flash(ui, &drawn);
}
fn advance_jump(ui: &egui::Ui, mut jump: Jump, drawn: &DrawnSection<'_>) {
let ctx = ui.ctx();
jump.frames += 1;
let header = drawn.header.rect;
let target = match (&jump.label, drawn.body) {
(None, _) => Some((header, true)),
(Some(label), Some((_, body))) if drawn.fully_open => {
find_caption(ctx, body, label).map(|r| (r, false))
}
_ => None,
};
let target = target.or_else(|| (jump.frames > JUMP_FRAMES).then_some((header, true)));
let Some((rect, on_header)) = target else {
store_jump(ctx, Some(jump));
ctx.request_repaint();
return;
};
let settled = jump
.last_rect
.is_some_and(|last| (last.min - rect.min).length() < 0.5)
|| jump.frames > JUMP_FRAMES;
if !settled {
jump.last_rect = Some(rect);
store_jump(ctx, Some(jump));
ctx.request_repaint();
return;
}
ui.scroll_to_rect(rect, Some(egui::Align::Center));
let anchor = if on_header {
header.min
} else {
drawn.body.map_or(header.min, |(_, b)| b.min)
};
let flash = Flash {
section: drawn.id.to_string(),
on_header,
offset: rect.min - anchor,
size: rect.size(),
started: ctx.input(|i| i.time),
};
ctx.data_mut(|d| d.insert_temp(flash_id(), Some(flash)));
store_jump(ctx, None);
ctx.disable_accesskit();
ctx.request_repaint();
}
fn store_jump(ctx: &egui::Context, jump: Option<Jump>) {
ctx.data_mut(|d| d.insert_temp(jump_id(), jump));
}
fn find_caption(ctx: &egui::Context, within: egui::Rect, label: &str) -> Option<egui::Rect> {
ctx.viewport(|vp| {
let state = vp.this_pass.accesskit_state.as_ref()?;
state
.nodes
.values()
.filter(|node| harvest::caption(node).as_deref() == Some(label))
.filter_map(|node| node.bounds())
.map(|b| {
egui::Rect::from_min_max(
egui::pos2(b.x0 as f32, b.y0 as f32),
egui::pos2(b.x1 as f32, b.y1 as f32),
)
})
.filter(|r| within.intersects(*r))
.min_by(|a, b| a.min.y.total_cmp(&b.min.y))
})
}
fn paint_flash(ui: &egui::Ui, drawn: &DrawnSection<'_>) {
let ctx = ui.ctx();
let Some(flash) = ctx
.data(|d| d.get_temp::<Option<Flash>>(flash_id()))
.flatten()
else {
return;
};
if flash.section != drawn.id {
return;
}
let age = ctx.input(|i| i.time) - flash.started;
if age > FLASH_SECS {
ctx.data_mut(|d| d.insert_temp::<Option<Flash>>(flash_id(), None));
return;
}
let anchor = if flash.on_header {
drawn.header.rect.min
} else {
match drawn.body {
Some((_, body)) => body.min,
None => return,
}
};
let fade = (1.0 - age / FLASH_SECS) as f32;
let rect = egui::Rect::from_min_size(anchor + flash.offset, flash.size).expand(3.0);
let color = egui::Color32::from_rgb(255, 193, 7).gamma_multiply(fade);
ui.painter().rect_stroke(
rect,
4.0,
egui::Stroke::new(2.0, color),
egui::StrokeKind::Outside,
);
ctx.request_repaint();
}
#[cfg(test)]
pub(crate) fn flashing(ctx: &egui::Context) -> Option<String> {
ctx.data(|d| d.get_temp::<Option<Flash>>(flash_id()))
.flatten()
.map(|f| f.section)
}