Preserve input focus across passive overlays

This commit is contained in:
2026-07-23 09:28:16 +02:00
parent a01c86e383
commit 08a5bd192c
3 changed files with 54 additions and 11 deletions

View File

@@ -2,7 +2,7 @@
RuDS is a native Rust blogging desktop application and the successor to bDS2. It manages local projects from authoring through preview, static-site generation, integrity checks, and publishing while preserving the existing bDS filesystem and SQLite formats. RuDS is a native Rust blogging desktop application and the successor to bDS2. It manages local projects from authoring through preview, static-site generation, integrity checks, and publishing while preserving the existing bDS filesystem and SQLite formats.
The desktop is a single-window application: closing its window persists UI state and exits RuDS. The desktop is a single-window application: closing its window persists UI state and exits RuDS. Background notifications remain unobtrusive and preserve keyboard focus while editing.
The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately. The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately.

View File

@@ -387,16 +387,17 @@ pub fn view<'a>(
overlays.push(modal::view(modal_state, locale, data_dir)); overlays.push(modal::view(modal_state, locale, data_dir));
} }
if overlays.is_empty() { workspace_with_overlays(base_layout, overlays)
base_layout }
} else {
let mut layers = vec![base_layout]; fn workspace_with_overlays<'a>(
layers.extend(overlays); base_layout: Element<'a, Message>,
stack(layers) overlays: Vec<Element<'a, Message>>,
.width(Length::Fill) ) -> Element<'a, Message> {
.height(Length::Fill) stack(std::iter::once(base_layout).chain(overlays))
.into() .width(Length::Fill)
} .height(Length::Fill)
.into()
} }
/// Route the content area based on the active tab type. /// Route the content area based on the active tab type.
@@ -711,6 +712,11 @@ fn loading_view<'a>(locale: UiLocale) -> Element<'a, Message> {
mod tests { mod tests {
use super::*; use super::*;
use crate::state::tabs::{Tab, TabType}; use crate::state::tabs::{Tab, TabType};
use iced::advanced::widget::Tree;
use iced::widget::text_input;
type TextInputState =
text_input::State<<iced::Renderer as iced::advanced::text::Renderer>::Paragraph>;
fn tab(id: &str, tab_type: TabType, title: &str) -> Tab { fn tab(id: &str, tab_type: TabType, title: &str) -> Tab {
Tab { Tab {
@@ -722,6 +728,38 @@ mod tests {
} }
} }
#[test]
fn overlays_preserve_the_focused_workspace_input() {
let base = || -> Element<'static, Message> {
text_input("Commit message", "draft")
.on_input(|_| Message::Noop)
.into()
};
let mut tree = Tree::new(workspace_with_overlays(base(), Vec::new()).as_widget());
tree.children[0]
.state
.downcast_mut::<TextInputState>()
.focus();
let with_toast = workspace_with_overlays(base(), vec![text("Done").into()]);
tree.diff(with_toast.as_widget());
assert!(
tree.children[0]
.state
.downcast_ref::<TextInputState>()
.is_focused()
);
let without_toast = workspace_with_overlays(base(), Vec::new());
tree.diff(without_toast.as_widget());
assert!(
tree.children[0]
.state
.downcast_ref::<TextInputState>()
.is_focused()
);
}
#[test] #[test]
fn style_tab_routes_to_real_editor_only_with_project_state() { fn style_tab_routes_to_real_editor_only_with_project_state() {
let empty_posts = HashMap::new(); let empty_posts = HashMap::new();

View File

@@ -210,6 +210,11 @@ invariant ActivityActiveHighlight {
btn.active = (active_view = btn.id and sidebar_visible) btn.active = (active_view = btn.id and sidebar_visible)
} }
invariant PassiveOverlayFocusStability {
-- Adding or removing passive overlays such as toasts and background-task
-- notifications never changes the keyboard-focused control.
}
-- ─── Git badge ──────────────────────────────────────────────── -- ─── Git badge ────────────────────────────────────────────────
-- Only the git activity button carries a badge. -- Only the git activity button carries a badge.