//! One document input route for links, selection and reader controls. use super::*; use crate::renderer::InputModifiers; fn banner_height(app: &AppWindow) -> f32 { if app.get_remote_images_blocked() && !app.get_text_mode() && !app.get_source_mode() && !app.global::().get_reader_mode() { 60.0 } else { 0.0 } } fn repaint_reader(app: &AppWindow, renderer: &Rc>, gpu: bool) { repaint_reader_inner(app, renderer, gpu, false); } fn repaint_reader_inner( app: &AppWindow, renderer: &Rc>, gpu: bool, incremental: bool, ) { if !gpu { let (width, height) = email_viewport_size(app); let result = if incremental { renderer .borrow_mut() .render_cpu_scroll_step(width, height, app.window().scale_factor()) } else { renderer .borrow_mut() .render_cpu_if_needed(width, height, app.window().scale_factor()) }; match result { Ok(Some(frame)) => apply_cpu_frame(app, frame), Ok(None) => {} Err(error) => { app.global::().set_notice(error.into()); app.global::().set_reader_mode(false); app.set_text_mode(true); } } } sync_reader_metadata(app, renderer); if !renderer.borrow().selection_active() { update_email_selection(app, renderer); } app.window().request_redraw(); } fn schedule_scroll_frame( app: &AppWindow, renderer: &Rc>, gpu: bool, pending: &Rc>, ) { if pending.replace(true) { return; } let weak = app.as_weak(); let renderer = Rc::downgrade(renderer); let pending = pending.clone(); Timer::single_shot(Duration::from_millis(16), move || { pending.set(false); if let (Some(app), Some(renderer)) = (weak.upgrade(), renderer.upgrade()) { repaint_reader_inner(&app, &renderer, gpu, true); if !gpu && renderer.borrow().needs_repaint() { schedule_scroll_frame(&app, &renderer, gpu, &pending); } } }); } pub(super) fn register_renderer_input_callbacks( app: &AppWindow, renderer: &Rc>, gpu: bool, opener: ExternalUrlOpener, ) { let weak = app.as_weak(); let appearance_renderer = renderer.clone(); let appearance_pending = Rc::new(Cell::new(false)); app.global::().on_refresh(move || { if appearance_pending.replace(true) { return; } let weak = weak.clone(); let renderer = Rc::downgrade(&appearance_renderer); let pending = appearance_pending.clone(); Timer::single_shot(Duration::ZERO, move || { pending.set(false); let (Some(app), Some(renderer)) = (weak.upgrade(), renderer.upgrade()) else { return; }; let colors = crate::message_appearance::EmailAppearance::from_app(&app); if renderer.borrow_mut().set_appearance(colors) { repaint_reader(&app, &renderer, gpu); } }); }); let weak = app.as_weak(); let r = renderer.clone(); app.on_open_email_link(move |url| { let Some(app) = weak.upgrade() else { return; }; if url.starts_with('#') { if let Some(y) = r.borrow().fragment_y(&url) { let y = y + banner_height(&app); app.set_email_scroll_y(-y); app.invoke_email_scroll(y, app.get_email_viewport_height()); } return; } if flectar_mail_core::mailto::is_mailto(&url) { crate::mailto_links::open(&app, "message", [url.as_str()]); return; } if let Err(error) = open_email_link(&opener, &url) { app.set_render_status(UiMessage::detail("Could not open link: {}", error)); } }); let r = renderer.clone(); app.global::() .on_link_at(move |x, y| r.borrow().link_at(x, y).unwrap_or_default().into()); let weak = app.as_weak(); let r = renderer.clone(); let pending = Rc::new(Cell::new(false)); app.on_email_pointer_event(move |x, y, kind, control, shift, alt, meta| { let changed = r.borrow_mut().handle_pointer_event( x, y, &kind, InputModifiers::new(control, shift, alt, meta), ); let activation = r.borrow_mut().take_activation(); let Some(app) = weak.upgrade() else { return; }; if let Some(url) = activation { app.invoke_open_email_link(url.into()); } if kind == "up" || kind == "cancel" { update_email_selection(&app, &r); } if changed && !pending.replace(true) { let weak = app.as_weak(); let r = Rc::downgrade(&r); let pending = pending.clone(); Timer::single_shot(Duration::from_millis(16), move || { pending.set(false); if let (Some(app), Some(r)) = (weak.upgrade(), r.upgrade()) { repaint_reader(&app, &r, gpu); } }); } }); let weak = app.as_weak(); let r = renderer.clone(); let scroll_pending = Rc::new(Cell::new(false)); app.on_email_scroll(move |y, height| { let Some(app) = weak.upgrade() else { return; }; let dirty = r .borrow_mut() .set_visible_region((y - banner_height(&app)).max(0.0), height); if dirty { schedule_scroll_frame(&app, &r, gpu, &scroll_pending); } }); let weak = app.as_weak(); let r = renderer.clone(); app.on_email_key_event(move |text, pressed, repeat, control, shift, alt, meta| { let copied = r.borrow_mut().handle_key_event( &text, pressed, repeat, InputModifiers::new(control, shift, alt, meta), ); if let Some(app) = weak.upgrade() { if let Some(text) = copied { copy_text(&app, text); } if r.borrow().needs_repaint() { let (_, caret) = r.borrow().selection_carets(); if caret.valid { let top = (-app.get_email_scroll_y() - banner_height(&app)).max(0.0); let height = app.get_email_viewport_height(); let y = if caret.y < top { caret.y } else if caret.y + caret.height > top + height { caret.y + caret.height - height } else { top }; app.set_email_scroll_y(-y - banner_height(&app)); r.borrow_mut().set_visible_region(y, height); } repaint_reader(&app, &r, gpu); } } }); let weak = app.as_weak(); let r = renderer.clone(); app.on_select_email_text(move || { r.borrow_mut().select_all(); if let Some(app) = weak.upgrade() { repaint_reader(&app, &r, gpu); } }); let weak = app.as_weak(); app.on_copy_email_status(move |status| { if let Some(app) = weak.upgrade() { match status.as_str() { "Copied HTML source" => { app.set_render_status(UiMessage::plain("Copied HTML source")) } "Copied selected text" => { app.set_render_status(UiMessage::plain("Copied selected text")) } "Copied email text" => app.set_render_status(UiMessage::plain("Copied email text")), "Copied link" => app.set_render_status(UiMessage::plain("Copied link")), _ => {} } } }); let weak = app.as_weak(); let r = renderer.clone(); app.global::() .on_find(move |query, direction| { let (index, count, y) = r.borrow_mut().find(&query, direction); if let Some(app) = weak.upgrade() { app.global::() .set_find_status(format!("{index} / {count}").into()); if let Some(y) = y { app.set_email_scroll_y(-y - banner_height(&app)); r.borrow_mut() .set_visible_region(y, app.get_email_viewport_height()); } repaint_reader(&app, &r, gpu); } }); let weak = app.as_weak(); let r = renderer.clone(); app.global::().on_command(move |command| { let Some(app) = weak.upgrade() else { return; }; let reader = app.global::(); let requested_zoom = crate::preview_controls::zoom(&command, r.borrow().zoom, 0.5, 3.0); let command = if command.starts_with("zoom-set:") { "zoom-set" } else { command.as_str() }; match command { "viewport" => repaint_reader(&app, &r, gpu), "zoom-set" | "zoom-in" | "zoom-out" | "zoom-reset" | "fit" => { let old = r.borrow().zoom; let zoom = match command { "zoom-set" => requested_zoom, "zoom-in" => old * 1.2, "zoom-out" => old / 1.2, "fit" => old / reader.get_width_ratio().max(1.0), _ => 1.0, } .clamp(0.5, 3.0); r.borrow_mut().set_auto_fit(command == "fit"); reader.set_auto_fit(command == "fit"); r.borrow_mut().set_zoom(zoom); reader.set_zoom(zoom); reader.set_zoom_revision(reader.get_zoom_revision().wrapping_add(1)); let banner = banner_height(&app); let scroll = -app.get_email_scroll_y(); let scroll = if scroll < banner { scroll } else { banner + (scroll - banner) * zoom / old }; app.set_email_scroll_y(-scroll); r.borrow_mut().set_visible_region( (scroll - banner).max(0.0), app.get_email_viewport_height(), ); repaint_reader(&app, &r, gpu); } "clear-selection" => { r.borrow_mut().clear_selection(); repaint_reader(&app, &r, gpu); } "copy-rich" => { let renderer = r.borrow(); let text = renderer.selected_text().unwrap_or_default(); let html = renderer.selected_html(); if !crate::reader_clipboard::set_html(&html, &text) { copy_text(&app, text); } } "copy-all" => copy_text(&app, r.borrow().plain_text()), "page-up" | "page-down" | "up" | "down" | "home" | "end" => { let height = app.get_email_viewport_height(); let current = -app.get_email_scroll_y(); let bottom = (app.get_email_scroll_document_height() - height).max(0.0); let y = match command { "page-up" => current - height * 0.85, "page-down" => current + height * 0.85, "up" => current - 40.0, "down" => current + 40.0, "home" => 0.0, _ => bottom, } .clamp(0.0, bottom); app.set_email_scroll_y(-y); r.borrow_mut() .set_visible_region((y - banner_height(&app)).max(0.0), height); repaint_reader(&app, &r, gpu); } _ => {} } }); } pub(super) fn copy_text(app: &AppWindow, text: String) { app.set_clipboard_request(text.into()); app.set_clipboard_request_id(app.get_clipboard_request_id().wrapping_add(1)); }