From 0dd5b56dfe6bfbf2c8bcb32a7b54013ffd6459f6 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 19 Oct 2026 12:17:59 +0000 Subject: [PATCH] Task panes: draw them with craft_ui::panes; Styles docks left or right or floats (#1756) The nine task panes (SmartArt Text Pane, Navigation, Clipboard, Styles, Style Inspector, Accessibility, Alt Text, Restrict Editing, Comments) are drawn by craft_ui::panes with the Columns policy: a title bar with a menu and close button, a resize edge that also takes arrow keys, and F6 to move between panes and the document. Panes open from the same view flags, in the same order and at the same default widths; close runs each pane's existing command. The Styles pane can dock left or right or float through the pane menu or the new command view.stylesPaneDock. ui.json keeps its position and float rect; unknown values read as the defaults. --- Cargo.lock | 2 +- crates/engine/src/cmd/view.rs | 14 ++ crates/engine/src/lib.rs | 3 +- crates/engine/src/session.rs | 37 ++++ crates/engine/src/tests.rs | 25 +++ crates/ui-egui/Cargo.toml | 2 +- crates/ui-egui/src/a11y_pane.rs | 8 - crates/ui-egui/src/i18n/bg.tsv | 8 + crates/ui-egui/src/i18n/cs.tsv | 8 + crates/ui-egui/src/i18n/de.tsv | 8 + crates/ui-egui/src/i18n/es.tsv | 8 + crates/ui-egui/src/i18n/et.tsv | 8 + crates/ui-egui/src/i18n/fr.tsv | 8 + crates/ui-egui/src/i18n/hr.tsv | 8 + crates/ui-egui/src/i18n/hu.tsv | 8 + crates/ui-egui/src/i18n/it.tsv | 8 + crates/ui-egui/src/i18n/ja.tsv | 8 + crates/ui-egui/src/i18n/ko.tsv | 8 + crates/ui-egui/src/i18n/pl.tsv | 8 + crates/ui-egui/src/i18n/pt-br.tsv | 8 + crates/ui-egui/src/i18n/ru.tsv | 8 + crates/ui-egui/src/i18n/sr-latn.tsv | 8 + crates/ui-egui/src/i18n/sr.tsv | 8 + crates/ui-egui/src/i18n/sv.tsv | 8 + crates/ui-egui/src/i18n/tr.tsv | 8 + crates/ui-egui/src/i18n/uk.tsv | 8 + crates/ui-egui/src/i18n/vi.tsv | 8 + crates/ui-egui/src/i18n/zh-hans.tsv | 8 + crates/ui-egui/src/i18n/zh-hant.tsv | 8 + crates/ui-egui/src/lib.rs | 90 ++++++++- crates/ui-egui/src/panes.rs | 289 +++++++++++++++++----------- 31 files changed, 522 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8154b2041..ded460c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d#d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" +source = "git+https://github.com/aiongg/craft-libs?rev=c94ab4d313832ea9a84da721a6d3e7a7838dacef#c94ab4d313832ea9a84da721a6d3e7a7838dacef" dependencies = [ "egui", ] diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index ff2989913..eea11db59 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -91,6 +91,10 @@ pub fn specs() -> Vec { .params(r#"{"value"?: "light" | "dark" | "system"; without it, toggles light and dark}"#) .pure(), CommandSpec::new("view.stylesPane", "Styles Pane", "Home › Styles", |s, v| toggle(s, v, |x| &mut x.styles_pane)).pure(), + // Where the Styles pane sits: docked right or left, or floating (#1756). No `dock` reads it. + CommandSpec::new("view.stylesPaneDock", "Styles Pane Position", "Home › Styles", styles_pane_dock) + .params(r#"{"dock"?: "left" | "right" | "float"}"#) + .pure(), CommandSpec::new("view.commentsPane", "Comments Pane", "Review › Comments", |s, v| toggle(s, v, |x| &mut x.comments_pane)).pure(), CommandSpec::new("view.accessibilityPane", "Accessibility Pane", "Review › Accessibility", |s, v| { toggle(s, v, |x| &mut x.accessibility_pane) @@ -158,6 +162,16 @@ fn mode(s: &mut Session, m: ViewMode, read: bool) -> CmdResult { Ok(json!({"mode": m, "readMode": read})) } +fn styles_pane_dock(s: &mut Session, v: &Value) -> CmdResult { + if let Some(d) = v.get("dock") { + let Some(dock) = d.as_str().and_then(crate::StylesPaneDock::parse) else { + return Err(CmdError::Params("dock must be left, right or float".into())); + }; + s.view.styles_pane_dock = dock; + } + Ok(json!({"dock": s.view.styles_pane_dock.name()})) +} + fn read_column_width(s: &mut Session, v: &Value) -> CmdResult { let width = match v.get("value").and_then(Value::as_str) { Some("auto") => None, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0158a6054..32e43e3f9 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -28,7 +28,8 @@ pub use io::Password; pub use keymap::KeyMap; pub use ribbon::RibbonLayout; pub use session::{ - CellRange, ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, UNDO_LIST_MAX, ViewState, blank_document, set_system_locale, + CellRange, ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, StylesPaneDock, UNDO_LIST_MAX, ViewState, blank_document, + set_system_locale, }; pub use wordcraft_doc as doc; pub use wordcraft_geom::Region; diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index 671a53cb2..93fe4fcff 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -131,6 +131,10 @@ pub struct ViewState { pub table_gridlines: bool, pub nav_pane: bool, pub styles_pane: bool, + /// Where the Styles pane sits: docked right (the default) or left, or floating (#1756). + /// Kept in the user's preferences, not the document. + #[serde(default)] + pub styles_pane_dock: StylesPaneDock, /// Style Inspector pane (Home › Styles). #[serde(default)] pub style_inspector: bool, @@ -204,6 +208,38 @@ pub struct ViewState { pub full_screen: bool, } +/// Where the Styles pane sits (`view.stylesPaneDock`, #1756). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum StylesPaneDock { + #[default] + Right, + Left, + /// A window of its own over the document, which can be moved and resized. + #[serde(rename = "float")] + Floating, +} + +impl StylesPaneDock { + /// `left`, `right` or `float`; anything else is `None`. + pub fn parse(name: &str) -> Option { + match name { + "right" => Some(Self::Right), + "left" => Some(Self::Left), + "float" => Some(Self::Floating), + _ => None, + } + } + + pub fn name(self) -> &'static str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Floating => "float", + } + } +} + fn on() -> bool { true } @@ -240,6 +276,7 @@ impl Default for ViewState { table_gridlines: true, nav_pane: false, styles_pane: false, + styles_pane_dock: StylesPaneDock::Right, style_inspector: false, comments_pane: false, clipboard_pane: false, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4031e230a..f45b1656c 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3882,6 +3882,31 @@ fn styles_apply_pane_is_not_an_edit() { assert!(s.view.styles_pane); } +/// #1756: the Styles pane docks on the right (the default), on the left, or floats. +#[test] +fn styles_pane_docks_left_right_or_floats() { + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Hello"})); + s.dirty = false; + let undo = s.undo_labels(); + assert_eq!(run(&mut s, "view.state", json!({}))["stylesPaneDock"], "right"); + assert_eq!(run(&mut s, "view.stylesPaneDock", json!({})), json!({"dock": "right"}), "no dock reads it"); + for dock in ["left", "float", "right"] { + assert_eq!(run(&mut s, "view.stylesPaneDock", json!({"dock": dock})), json!({"dock": dock})); + assert_eq!(run(&mut s, "view.stylesPaneDock", json!({})), json!({"dock": dock})); + assert_eq!(run(&mut s, "view.state", json!({}))["stylesPaneDock"], dock); + } + run(&mut s, "view.stylesPaneDock", json!({"dock": "left"})); + for junk in [json!({"dock": "top"}), json!({"dock": 1}), json!({"dock": null}), json!({"dock": ["left"]}), json!({"dock": "LEFT "})] { + assert!(s.run("view.stylesPaneDock", &junk).is_err(), "{junk} is an error"); + assert_eq!(run(&mut s, "view.stylesPaneDock", json!({})), json!({"dock": "left"}), "{junk} changes nothing"); + } + // A view setting: no undo step, the document isn't changed, and the pane's visibility stays. + assert_eq!(s.undo_labels(), undo); + assert!(!s.dirty); + assert!(!s.view.styles_pane); +} + #[test] fn styles_apply_rejects_bad_style_params() { let mut s = s(); diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 48ed41219..d7762b82c 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -9,7 +9,7 @@ description = "WordCraft egui front end (Word-style ribbon UI); swappable: nothi workspace = true [dependencies] -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" } +craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "c94ab4d313832ea9a84da721a6d3e7a7838dacef" } wordcraft-geom = { workspace = true } wordcraft-doc = { workspace = true } unicode-bidi = { workspace = true } diff --git a/crates/ui-egui/src/a11y_pane.rs b/crates/ui-egui/src/a11y_pane.rs index 25d2c82c6..425af8c6c 100644 --- a/crates/ui-egui/src/a11y_pane.rs +++ b/crates/ui-egui/src/a11y_pane.rs @@ -10,10 +10,6 @@ use crate::theme::{regular, semibold}; /// Review › Check Accessibility: the findings for the document as it is now. pub fn accessibility(app: &mut WordApp, ui: &mut Ui) { - if crate::panes::header(ui, "Accessibility") { - let _ = app.run("view.accessibilityPane", json!({"value": false})); - return; - } let issues = issues(app, ui); if issues.is_empty() { ui.label(egui::RichText::new(tl!("No accessibility issues found.")).weak()); @@ -90,10 +86,6 @@ fn go_to(app: &mut WordApp, issue: &Value) { /// Alt Text: the selected picture's, shape's, chart's or group's description and decorative mark, /// or the title and description of the table the caret is in. pub fn alt_text(app: &mut WordApp, ui: &mut Ui) { - if crate::panes::header(ui, "Alt Text") { - let _ = app.run("view.altTextPane", json!({"value": false})); - return; - } if let Some((pos, obj)) = wordcraft_engine::cmd::objects::selected(&app.session) { let decorative = obj.is_decorative(); ui.label(egui::RichText::new(tl!("Describe what this object shows, for people who can't see it.")).weak()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index af0a3a314..f62fcc659 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2687,3 +2687,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Въведете размер в произволна единица, например 5 cm или 300 px. Другата страна се променя съответно, така че картината запазва пропорциите си. Import… Импортиране… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Положение на панела със стилове + Dock Left Закачане вляво + Dock Right Закачане вдясно + Float Откачане + Pane Options Опции на панела + Move Pane Преместване на панела + Resize Pane Преоразмеряване на панела diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 041f03be1..280765cc7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2690,3 +2690,11 @@ preview Title Ukázka Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Zadejte velikost v libovolné jednotce, například 5 cm nebo 300 px. Druhá strana se přizpůsobí, takže obrázek zachová své proporce. Import… Importovat… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Umístění podokna stylů + Dock Left Ukotvit vlevo + Dock Right Ukotvit vpravo + Float Plovoucí + Pane Options Možnosti podokna + Move Pane Přesunout podokno + Resize Pane Změnit velikost podokna diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index eeb70c65e..b75c438cf 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2287,3 +2287,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Geben Sie eine Größe in einer beliebigen Einheit ein, z. B. 5 cm oder 300 px. Die andere Seite passt sich an, sodass das Bild seine Proportionen behält. Import… Importieren… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Position des Formatvorlagenbereichs + Dock Left Links andocken + Dock Right Rechts andocken + Float Unverankert + Pane Options Bereichsoptionen + Move Pane Bereich verschieben + Resize Pane Bereichsgröße ändern diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 19ef8fcf5..6e0e94e02 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2654,3 +2654,11 @@ preview Title Título Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Escribe un tamaño en cualquier unidad, como 5 cm o 300 px. El otro lado se ajusta, así que la imagen conserva sus proporciones. Import… Importar… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Posición del panel de estilos + Dock Left Acoplar a la izquierda + Dock Right Acoplar a la derecha + Float Flotante + Pane Options Opciones del panel + Move Pane Mover panel + Resize Pane Cambiar tamaño del panel diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index 0aa9e52c1..aeec5279f 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -2322,3 +2322,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Sisesta suurus mis tahes ühikus, näiteks 5 cm või 300 px. Teine külg muutub kaasa, nii et pilt säilitab oma proportsioonid. Import… Impordi… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Stiilipaani asukoht + Dock Left Dokki vasakule + Dock Right Dokki paremale + Float Ujuv + Pane Options Paani suvandid + Move Pane Teisalda paan + Resize Pane Muuda paani suurust diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 44c9cb937..f4d289be7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2700,3 +2700,11 @@ template Letter Lettre Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Tapez une taille dans n'importe quelle unité, par exemple 5 cm ou 300 px. L'autre côté suit, et l'image garde ses proportions. Import… Importer… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Position du volet Styles + Dock Left Ancrer à gauche + Dock Right Ancrer à droite + Float Flottant + Pane Options Options du volet + Move Pane Déplacer le volet + Resize Pane Redimensionner le volet diff --git a/crates/ui-egui/src/i18n/hr.tsv b/crates/ui-egui/src/i18n/hr.tsv index a80b38d0b..29ebe3479 100644 --- a/crates/ui-egui/src/i18n/hr.tsv +++ b/crates/ui-egui/src/i18n/hr.tsv @@ -2698,3 +2698,11 @@ preview Title Naslov Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Upišite veličinu u bilo kojoj jedinici, npr. 5 cm ili 300 px. Druga se strana prilagođava pa slika zadržava svoje proporcije. Import… Uvezi… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Položaj okna stilova + Dock Left Usidri lijevo + Dock Right Usidri desno + Float Plutajuće + Pane Options Mogućnosti okna + Move Pane Premjesti okno + Resize Pane Promijeni veličinu okna diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 0998d7d86..c15b031f7 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2694,3 +2694,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Írj be egy méretet bármilyen mértékegységben, például 5 cm vagy 300 px. A másik oldal követi, így a kép megtartja az arányait. Import… Importálás… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Stílusok panel helye + Dock Left Rögzítés balra + Dock Right Rögzítés jobbra + Float Lebegtetés + Pane Options Panel beállításai + Move Pane Panel áthelyezése + Resize Pane Panel átméretezése diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 314f7cada..a85cf1d4e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2707,3 +2707,11 @@ preview Title Esempio Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Scrivi una dimensione in qualsiasi unità, ad esempio 5 cm o 300 px. L'altro lato si adegua, così l'immagine mantiene le proporzioni. Import… Importa… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Posizione del riquadro Stili + Dock Left Ancora a sinistra + Dock Right Ancora a destra + Float Mobile + Pane Options Opzioni riquadro + Move Pane Sposta riquadro + Resize Pane Ridimensiona riquadro diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index d633eac51..00cd791af 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2288,3 +2288,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. 5 cm や 300 px など、任意の単位でサイズを入力します。もう一方の辺も合わせて変わるため、図の縦横比は保たれます。 Import… インポート… +# Task panes and the Styles pane position (#1756) + Styles Pane Position スタイル ウィンドウの位置 + Dock Left 左にドッキング + Dock Right 右にドッキング + Float フローティング + Pane Options ウィンドウ オプション + Move Pane ウィンドウの移動 + Resize Pane ウィンドウのサイズ変更 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 7e50812e0..49b47bd02 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2542,3 +2542,11 @@ preview Title 제목 Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. 5 cm 또는 300 px처럼 원하는 단위로 크기를 입력하세요. 다른 쪽도 함께 바뀌어 그림의 비율이 유지됩니다. Import… 가져오기… +# Task panes and the Styles pane position (#1756) + Styles Pane Position 스타일 창 위치 + Dock Left 왼쪽에 고정 + Dock Right 오른쪽에 고정 + Float 떠 있음 + Pane Options 창 옵션 + Move Pane 창 이동 + Resize Pane 창 크기 조정 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 784182eb7..f2be90935 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2703,3 +2703,11 @@ preview Title Tytuł Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Wpisz rozmiar w dowolnej jednostce, np. 5 cm lub 300 px. Druga strona dopasuje się, więc obraz zachowa proporcje. Import… Importuj… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Położenie okienka stylów + Dock Left Zadokuj po lewej + Dock Right Zadokuj po prawej + Float Pływające + Pane Options Opcje okienka + Move Pane Przenieś okienko + Resize Pane Zmień rozmiar okienka diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 1a1038e1a..dd6f449c4 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2300,3 +2300,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Digite um tamanho em qualquer unidade, como 5 cm ou 300 px. O outro lado acompanha, e a imagem mantém as proporções. Import… Importar… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Posição do painel de estilos + Dock Left Encaixar à esquerda + Dock Right Encaixar à direita + Float Flutuante + Pane Options Opções do painel + Move Pane Mover painel + Resize Pane Redimensionar painel diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7880999f9..3e0041660 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2696,3 +2696,11 @@ preview Title Пример Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Введите размер в любых единицах, например 5 cm или 300 px. Другая сторона изменится вместе с ней, и рисунок сохранит пропорции. Import… Импорт… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Положение области стилей + Dock Left Закрепить слева + Dock Right Закрепить справа + Float Открепить + Pane Options Параметры области + Move Pane Переместить область + Resize Pane Изменить размер области diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 8e0a9136c..3a7fe5f91 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -2298,3 +2298,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Unesite veličinu u bilo kojoj jedinici, na primer 5 cm ili 300 px. Druga strana se prilagođava, pa slika zadržava svoje proporcije. Import… Uvezi… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Položaj okna stilova + Dock Left Usidri levo + Dock Right Usidri desno + Float Plutajuće + Pane Options Opcije okna + Move Pane Premesti okno + Resize Pane Promeni veličinu okna diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index c55fa2c7e..7e99069f3 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -2302,3 +2302,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Унесите величину у било којој јединици, на пример 5 cm или 300 px. Друга страна се прилагођава, па слика задржава своје пропорције. Import… Увези… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Положај окна стилова + Dock Left Усидри лево + Dock Right Усидри десно + Float Плутајуће + Pane Options Опције окна + Move Pane Премести окно + Resize Pane Промени величину окна diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 50cab37aa..88d593509 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2713,3 +2713,11 @@ preview Title Titel Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Skriv en storlek i valfri enhet, till exempel 5 cm eller 300 px. Den andra sidan följer med, så att bilden behåller sina proportioner. Import… Importera… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Stilpanelens placering + Dock Left Docka till vänster + Dock Right Docka till höger + Float Flytande + Pane Options Panelalternativ + Move Pane Flytta panel + Resize Pane Ändra storlek på panel diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e7771f20a..971f4914d 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2695,3 +2695,11 @@ preview Title Örnek Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Herhangi bir birimde bir boyut yazın, örneğin 5 cm veya 300 px. Diğer kenar da buna uyar, böylece resim oranlarını korur. Import… İçe aktar… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Stiller Bölmesi Konumu + Dock Left Sola Yerleştir + Dock Right Sağa Yerleştir + Float Serbest Bırak + Pane Options Bölme Seçenekleri + Move Pane Bölmeyi Taşı + Resize Pane Bölmeyi Yeniden Boyutlandır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f32758595..01895fb26 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2300,3 +2300,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Введіть розмір у будь-яких одиницях, наприклад 5 cm або 300 px. Інша сторона зміниться разом із нею, тож зображення збереже пропорції. Import… Імпортувати… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Положення панелі стилів + Dock Left Закріпити ліворуч + Dock Right Закріпити праворуч + Float Відкріпити + Pane Options Параметри панелі + Move Pane Перемістити панель + Resize Pane Змінити розмір панелі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 6616368ac..2b5ba02ce 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2738,3 +2738,11 @@ template Letter Thư Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. Nhập kích thước theo đơn vị bất kỳ, ví dụ 5 cm hoặc 300 px. Cạnh còn lại thay đổi theo, nên ảnh giữ nguyên tỷ lệ. Import… Nhập… +# Task panes and the Styles pane position (#1756) + Styles Pane Position Vị trí khung kiểu + Dock Left Neo bên trái + Dock Right Neo bên phải + Float Nổi + Pane Options Tùy chọn khung + Move Pane Di chuyển khung + Resize Pane Đổi kích thước khung diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d733ca8f0..ae23a8e6a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2300,3 +2300,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. 可用任意单位输入大小,例如 5 cm 或 300 px。另一边随之调整,图片保持原有比例。 Import… 导入… +# Task panes and the Styles pane position (#1756) + Styles Pane Position 样式窗格位置 + Dock Left 停靠在左侧 + Dock Right 停靠在右侧 + Float 浮动 + Pane Options 窗格选项 + Move Pane 移动窗格 + Resize Pane 调整窗格大小 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index a8bc708ea..de6f92368 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2289,3 +2289,11 @@ Type a size in any unit, such as 5 cm or 300 px. The other side follows, so the picture keeps its proportions. 可用任何單位輸入大小,例如 5 cm 或 300 px。另一邊隨之調整,圖片保持原有比例。 Import… 匯入… +# Task panes and the Styles pane position (#1756) + Styles Pane Position 樣式窗格位置 + Dock Left 停駐在左側 + Dock Right 停駐在右側 + Float 浮動 + Pane Options 窗格選項 + Move Pane 移動窗格 + Resize Pane 調整窗格大小 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 220131df8..4e8f1463f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -271,6 +271,15 @@ pub struct UiState { /// View › Navigation Pane: open or closed as the person left it, kept between runs (#2526). /// The session owns it while running (see [`WordApp::prefs`]). pub nav_pane: bool, + /// Where the Styles pane sits (#1756). The session owns it while running (see [`WordApp::prefs`]). + #[serde(deserialize_with = "lenient_styles_pane_dock")] + pub styles_pane_dock: wordcraft_engine::StylesPaneDock, + /// The floating Styles pane's `[x, y, width, height]`, also kept while it is docked. + #[serde(deserialize_with = "lenient_float_rect")] + pub styles_pane_float: Option<[f32; 4]>, + /// The docked Styles pane's width this run; like the other panes' widths, not kept between runs. + #[serde(skip)] + pub styles_pane_width: Option, pub show_discord: bool, /// User name (File › Options) for comments and tracked changes; empty keeps the default. pub author: String, @@ -373,6 +382,9 @@ impl Default for UiState { legacy_dark: None, nav_tab: "headings".into(), nav_pane: false, + styles_pane_dock: wordcraft_engine::StylesPaneDock::Right, + styles_pane_float: None, + styles_pane_width: None, show_discord: true, author: String::new(), window: None, @@ -459,6 +471,21 @@ pub fn recent_fonts_from<'a>(names: impl IntoIterator) -> Vec>(d: D) -> Result { + let v = Value::deserialize(d)?; + Ok(v.as_str().and_then(wordcraft_engine::StylesPaneDock::parse).unwrap_or_default()) +} + +/// Anything but four finite numbers reads as no saved rect; `craft_ui::panes` brings a saved one +/// back inside the window. +fn lenient_float_rect<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { + let v = Value::deserialize(d)?; + let Some([x, y, w, h]) = v.as_array().map(Vec::as_slice) else { return Ok(None) }; + let n = |v: &Value| v.as_f64().map(|n| n as f32).filter(|n| n.is_finite()); + Ok(n(x).zip(n(y)).zip(n(w)).zip(n(h)).map(|(((x, y), w), h)| [x, y, w, h])) +} + /// `recentFonts` as saved, through [`recent_fonts_from`]: an entry that isn't a name is dropped, /// not the whole list. fn lenient_recent_fonts<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { @@ -762,6 +789,7 @@ impl WordApp { ui.read_aloud_skip_citations = self.session.read_aloud.skip_citations; ui.dark_page = self.session.view.dark_mode; ui.nav_pane = self.session.view.nav_pane; + ui.styles_pane_dock = self.session.view.styles_pane_dock; ui.keyboard = self.session.keymap.clone(); ui.ribbon = self.session.ribbon.clone(); ui.ink_pens = self.session.view.draw.pens.clone(); @@ -807,6 +835,7 @@ impl WordApp { self.session.read_aloud.skip_citations = self.ui.read_aloud_skip_citations; self.session.view.dark_mode = self.ui.dark_page; self.session.view.nav_pane = self.ui.nav_pane; + self.session.view.styles_pane_dock = self.ui.styles_pane_dock; // Keys for commands this version doesn't have are dropped. self.session.keymap = std::mem::take(&mut self.ui.keyboard); let reg = self.session.registry.clone(); @@ -6613,6 +6642,62 @@ mod tests { assert!(!old.session.view.nav_pane, "no saved value: closed"); } + /// Task panes: two open panes on one edge sit side by side, the first outermost; a pane's × + /// runs its command; the Styles pane's place and float rect survive a restart (#1756). + #[test] + fn task_panes_share_an_edge_close_and_keep_the_styles_place() { + use egui_kittest::kittest::Queryable; + let bars = |h: &egui_kittest::Harness<'_, WordApp>| { + let mut bars: Vec = h.get_all_by_label("Move Pane").map(|n| n.rect()).collect(); + bars.sort_by(|a, b| a.left().total_cmp(&b.left())); + bars + }; + // A title bar's ▾ and × sit right of the bar, on its row. + fn click_button(h: &egui_kittest::Harness<'_, WordApp>, bar: egui::Rect, label: &str) { + let on_row = |r: egui::Rect| r.center().y > bar.top() && r.center().y < bar.bottom() && r.left() >= bar.right() - 1.0; + let node = h.get_all_by_label(label).filter(|n| on_row(n.rect())).min_by(|a, b| a.rect().left().total_cmp(&b.rect().left())); + node.unwrap_or_else(|| panic!("no {label} beside the title bar")).click(); + } + let mut a = app(); + a.run("view.stylesPane", json!({"value": true})).unwrap(); + a.run("view.commentsPane", json!({"value": true})).unwrap(); + let mut h = harness(a); + h.run_steps(4); + let [comments, styles] = bars(&h)[..] else { panic!("two pane title bars") }; + assert!(comments.right() <= styles.left() + 1.0, "side by side"); + assert!(comments.left() > 720.0, "both on the right"); + // Default widths 290 and 250: Styles, opened first, is the outer one. + assert!((comments.width() - styles.width() - 40.0).abs() < 2.0, "Styles is outermost: {comments:?} {styles:?}"); + + click_button(&h, styles, "Close"); + h.run_steps(3); + assert!(!h.state().session.view.styles_pane, "× ran view.stylesPane"); + assert!(h.state().session.view.comments_pane); + assert_eq!(bars(&h).len(), 1); + + h.state_mut().run("view.stylesPane", json!({"value": true})).unwrap(); + h.run_steps(3); + let styles = *bars(&h).last().unwrap(); + click_button(&h, styles, "Pane Options"); + h.run_steps(2); + h.get_by_label("Float").click(); + h.run_steps(3); + assert_eq!(h.state().session.view.styles_pane_dock, wordcraft_engine::StylesPaneDock::Floating); + let rect = h.state().ui.styles_pane_float.expect("the float rect is kept"); + + let saved = serde_json::to_vec(&h.state().prefs()).unwrap(); + let mut next = app(); + next.apply_prefs(serde_json::from_slice(&saved).unwrap()); + assert_eq!(next.session.view.styles_pane_dock, wordcraft_engine::StylesPaneDock::Floating); + assert_eq!(next.ui.styles_pane_float, Some(rect)); + + let mut junk = app(); + junk.apply_prefs(serde_json::from_str(r#"{"stylesPaneDock": "top", "stylesPaneFloat": [1, "x", 3, 4], "navPane": true}"#).unwrap()); + assert_eq!(junk.session.view.styles_pane_dock, wordcraft_engine::StylesPaneDock::Right, "junk reads as the default"); + assert_eq!(junk.ui.styles_pane_float, None); + assert!(junk.session.view.nav_pane, "the rest of the file still loads"); + } + #[test] fn prefs_without_user_name_keep_default() { let mut a = app(); @@ -7890,13 +7975,14 @@ mod tests { h.step(); assert!(h.state().session.view.focus_mode); assert!(!shown(&h, "Ruler") && !shown(&h, "Focus"), "the ribbon is hidden"); - assert!(!shown(&h, "Navigation"), "and so is the Navigation pane"); + // The pane's title bar: the only pane open is Navigation. + assert!(!shown(&h, "Move Pane"), "and so is the Navigation pane"); h.event(egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }); h.step(); h.step(); assert!(!h.state().session.view.focus_mode, "Escape leaves Focus"); assert!(shown(&h, "Ruler"), "and the ribbon is back"); - assert!(shown(&h, "Navigation"), "with the pane"); + assert!(shown(&h, "Move Pane"), "with the pane"); // Immersive Reader is Read Mode, not Focus: its status bar stays. h.state_mut().run("view.immersive", json!({})).unwrap(); h.step(); diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index c600daaea..673a472e1 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -1,116 +1,198 @@ -//! Side panes: Navigation (headings, pages, search results), Clipboard, Styles, Style Inspector, +//! Task panes: Navigation (headings, pages, search results), Clipboard, Styles, Style Inspector, //! Accessibility and Alt Text (see `a11y_pane`), Restrict Editing ([`crate::dialogs_review`]), Comments, -//! and the SmartArt Text Pane. +//! and the SmartArt Text Pane. `craft_ui::panes` draws them around the document, with their title +//! bars, menus and resize edges; the session's view flags say which are open. use std::collections::BTreeMap; +use craft_ui::panes::{Edge, PaneContent, PaneHost, PaneLabels, PaneRequest, PaneState, Place, SidePolicy}; use egui::{Stroke, Ui, vec2}; use serde_json::{Value, json}; use wordcraft_doc::{Pos, StoryRef}; +use wordcraft_engine::StylesPaneDock; use crate::WordApp; use crate::theme::{Tokens, regular, semibold}; -pub fn show(app: &mut WordApp, ui: &mut Ui) { - let t = Tokens::get(ui.ctx()); - // SmartArt Design › Text Pane, while a SmartArt graphic WordCraft can edit is selected. - if app.session.view.smart_art_pane && wordcraft_engine::cmd::smart_art::selected_smart_art(&app.session).is_some() { - egui::Panel::left("smart_art_pane") - .default_size(250.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| { - if header(ui, "Text Pane") { - let _ = app.run("smartArt.textPane", json!({"value": false})); - return; - } - crate::dialogs_smart_art::text_pane(app, ui); - }); +/// The task panes, in the order they open: each edge's first pane is outermost. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Pane { + /// SmartArt Design › Text Pane, while a SmartArt graphic WordCraft can edit is selected. + SmartArtText, + Navigation, + Clipboard, + Styles, + StyleInspector, + Accessibility, + AltText, + RestrictEditing, + Comments, +} + +impl Pane { + fn title(self) -> &'static str { + match self { + Self::SmartArtText => "Text Pane", + Self::Navigation => "Navigation", + Self::Clipboard => "Clipboard", + Self::Styles => "Styles", + Self::StyleInspector => "Style Inspector", + Self::Accessibility => "Accessibility", + Self::AltText => "Alt Text", + Self::RestrictEditing => "Restrict Editing", + Self::Comments => "Comments", + } } - if app.session.view.nav_pane { - egui::Panel::left("nav_pane") - .default_size(260.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| nav(app, ui)); + + fn default_state(self) -> PaneState { + let (edge, width) = match self { + Self::SmartArtText => (Edge::Start, 250.0), + Self::Navigation => (Edge::Start, 260.0), + Self::Clipboard => (Edge::Start, 250.0), + Self::Styles => (Edge::End, 250.0), + Self::StyleInspector => (Edge::End, 240.0), + Self::Accessibility => (Edge::End, 280.0), + Self::AltText => (Edge::End, 260.0), + Self::RestrictEditing => (Edge::End, 270.0), + Self::Comments => (Edge::End, 290.0), + }; + PaneState::docked(edge, width) } - if app.session.view.clipboard_pane { - egui::Panel::left("clipboard_pane") - .default_size(250.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| clipboard(app, ui)); - } - if app.session.view.styles_pane { - egui::Panel::right("styles_pane") - .default_size(250.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| styles(app, ui)); - } - if app.session.view.style_inspector { - egui::Panel::right("style_inspector") - .default_size(240.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| inspector(app, ui)); - } - if app.session.view.accessibility_pane { - egui::Panel::right("accessibility_pane") - .default_size(280.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| crate::a11y_pane::accessibility(app, ui)); - } - if app.session.view.alt_text_pane { - egui::Panel::right("alt_text_pane") - .default_size(260.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| crate::a11y_pane::alt_text(app, ui)); - } - if app.restrict.open { - egui::Panel::right("restrict_pane") - .default_size(270.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| { - if header(ui, "Restrict Editing") { - app.restrict.open = false; - return; - } - crate::dialogs_review::restrict_pane(app, ui); - }); - } - if app.session.view.comments_pane { - egui::Panel::right("comments_pane") - .default_size(290.0) - .resizable(true) - .frame(egui::Frame::NONE.fill(t.panel).inner_margin(10).stroke(Stroke::new(1.0, t.border))) - .show(ui, |ui| comments(app, ui)); + + /// The command that closes the pane; Restrict Editing has none and closes in [`close`]. + fn close_command(self) -> Option<&'static str> { + match self { + Self::SmartArtText => Some("smartArt.textPane"), + Self::Navigation => Some("view.navigationPane"), + Self::Clipboard => Some("edit.clipboardPane"), + Self::Styles => Some("view.stylesPane"), + Self::StyleInspector => Some("styles.inspector"), + Self::Accessibility => Some("view.accessibilityPane"), + Self::AltText => Some("view.altTextPane"), + Self::Comments => Some("view.commentsPane"), + Self::RestrictEditing => None, + } } } -pub(crate) fn header(ui: &mut Ui, title: &str) -> bool { - let mut close = false; - ui.horizontal(|ui| { - ui.label(egui::RichText::new(tl!(title)).font(semibold(15.0))); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Drawn, not the "✕" character: the interface fonts have no glyph for it. - if crate::widgets::icon_button(ui, "close", tl!("Close")).clicked() { - close = true; - } - }); - }); - ui.add_space(4.0); - close +/// The panes open now, from the session's view flags. +pub fn open(app: &WordApp) -> Vec { + let v = &app.session.view; + let smart_art = v.smart_art_pane && wordcraft_engine::cmd::smart_art::selected_smart_art(&app.session).is_some(); + [ + (Pane::SmartArtText, smart_art), + (Pane::Navigation, v.nav_pane), + (Pane::Clipboard, v.clipboard_pane), + (Pane::Styles, v.styles_pane), + (Pane::StyleInspector, v.style_inspector), + (Pane::Accessibility, v.accessibility_pane), + (Pane::AltText, v.alt_text_pane), + (Pane::RestrictEditing, app.restrict.open), + (Pane::Comments, v.comments_pane), + ] + .into_iter() + .filter_map(|(pane, shown)| shown.then_some(pane)) + .collect() +} + +/// The Styles pane's place and size, from `view.stylesPaneDock` and the UI preferences (#1756). +/// Other panes keep theirs in the host for the session. +fn styles_state(app: &WordApp) -> PaneState { + let place = match app.session.view.styles_pane_dock { + StylesPaneDock::Right => Place::Docked(Edge::End), + StylesPaneDock::Left => Place::Docked(Edge::Start), + StylesPaneDock::Floating => Place::Floating, + }; + PaneState { place, size: app.ui.styles_pane_width.or(Pane::Styles.default_state().size), float_rect: app.ui.styles_pane_float } +} + +fn labels() -> PaneLabels { + PaneLabels { + menu: tl!("Pane Options").into(), + move_start: tl!("Dock Left").into(), + move_end: tl!("Dock Right").into(), + float: tl!("Float").into(), + close: tl!("Close").into(), + move_pane: tl!("Move Pane").into(), + resize: tl!("Resize Pane").into(), + ..PaneLabels::default() + } +} + +struct Content<'a> { + app: &'a mut WordApp, +} + +impl PaneContent for Content<'_> { + fn title(&self, pane: &Pane) -> String { + tl!(pane.title()).into() + } + + fn body(&mut self, ui: &mut Ui, pane: &Pane) { + let app = &mut *self.app; + match pane { + Pane::SmartArtText => crate::dialogs_smart_art::text_pane(app, ui), + Pane::Navigation => nav(app, ui), + Pane::Clipboard => clipboard(app, ui), + Pane::Styles => styles(app, ui), + Pane::StyleInspector => inspector(app, ui), + Pane::Accessibility => crate::a11y_pane::accessibility(app, ui), + Pane::AltText => crate::a11y_pane::alt_text(app, ui), + Pane::RestrictEditing => crate::dialogs_review::restrict_pane(app, ui), + Pane::Comments => comments(app, ui), + } + } + + fn default_state(&self, pane: &Pane) -> PaneState { + pane.default_state() + } +} + +/// Draw the open panes around the document. Call before the canvas's `CentralPanel`. +pub fn show(app: &mut WordApp, ui: &mut Ui) { + let t = Tokens::get(ui.ctx()); + let open = open(app); + let styles = styles_state(app); + let out = PaneHost::new("task_panes") + .policy(SidePolicy::Columns) + .labels(labels()) + .frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.0, t.border))) + .body_margin(egui::Margin::same(10)) + .show(ui, &open, |pane| (*pane == Pane::Styles).then_some(styles), &mut Content { app }); + for request in out.requests { + apply(app, request); + } +} + +/// Run a request from the pane host through the pane's commands and the UI preferences. +pub(crate) fn apply(app: &mut WordApp, request: PaneRequest) { + match request { + PaneRequest::Close(pane) => close(app, pane), + PaneRequest::Place(Pane::Styles, place) => { + let dock = match place { + Place::Docked(Edge::Start) => "left", + Place::Docked(Edge::End) => "right", + Place::Floating => "float", + Place::Docked(Edge::Bottom) => return, + }; + let _ = app.run("view.stylesPaneDock", json!({"dock": dock})); + } + PaneRequest::Resize(Pane::Styles, width) => app.ui.styles_pane_width = Some(width), + PaneRequest::FloatRect(Pane::Styles, rect) => app.ui.styles_pane_float = Some(rect), + PaneRequest::Place(..) | PaneRequest::Resize(..) | PaneRequest::FloatRect(..) | PaneRequest::Focus(_) => {} + } +} + +fn close(app: &mut WordApp, pane: Pane) { + match pane.close_command() { + Some(id) => { + let _ = app.run(id, json!({"value": false})); + } + None => app.restrict.open = false, + } } fn nav(app: &mut WordApp, ui: &mut Ui) { - if header(ui, "Navigation") { - let _ = app.run("view.navigationPane", json!({"value": false})); - return; - } let qid = egui::Id::new("nav_query"); let mut q = ui.data(|d| d.get_temp::(qid)).unwrap_or_default(); // Ctrl+F with text selected searches for it, as Word does (a selection within one line). @@ -234,10 +316,6 @@ fn nav_heading(ui: &mut Ui, level: u8, text: &str, selected: bool) -> egui::Resp /// Items collected by Copy and Cut, newest first; clicking one pastes it at the caret. fn clipboard(app: &mut WordApp, ui: &mut Ui) { - if header(ui, "Clipboard") { - let _ = app.run("edit.clipboardPane", json!({"value": false})); - return; - } let items: Vec = app.session.clip_history.items().iter().map(|i| i.preview()).collect(); ui.horizontal(|ui| { if ui.add_enabled(!items.is_empty(), egui::Button::new(tl!("Paste All"))).clicked() { @@ -280,10 +358,6 @@ fn clipboard(app: &mut WordApp, ui: &mut Ui) { } fn styles(app: &mut WordApp, ui: &mut Ui) { - if header(ui, "Styles") { - let _ = app.run("view.stylesPane", json!({"value": false})); - return; - } let st = app.session.run("format.state", &json!({})).unwrap_or_default(); let cur = st.get("style").and_then(Value::as_str).unwrap_or("Normal").to_string(); let current_name = app @@ -294,7 +368,8 @@ fn styles(app: &mut WordApp, ui: &mut Ui) { .map(crate::i18n::style_name) .unwrap_or_else(|| st.get("styleName").and_then(Value::as_str).unwrap_or("").to_string()); ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Current style: {style}"), &[("style", ¤t_name)])).small()); - ui.horizontal(|ui| { + // Wrapped: the host gives the pane a fixed width, and the row is wider than its default. + ui.horizontal_wrapped(|ui| { if ui.button(tl!("New Style…")).clicked() { app.dialog = crate::dialogs::Dialog::open("newStyle", app); } @@ -355,10 +430,6 @@ fn styles(app: &mut WordApp, ui: &mut Ui) { } fn comments(app: &mut WordApp, ui: &mut Ui) { - if header(ui, "Comments") { - let _ = app.run("view.commentsPane", json!({"value": false})); - return; - } if crate::widgets::icon_text_button(ui, "newComment", tl!("New Comment"), regular(12.5)).clicked() { let _ = app.run("review.newComment", json!({"text": ""})); } @@ -590,10 +661,6 @@ pub(crate) fn flush_comment_drafts(app: &mut WordApp) -> Result<(), String> { /// Style Inspector: the paragraph and character levels at the caret, each with its style and the /// direct formatting on top, and a button to clear each level. fn inspector(app: &mut WordApp, ui: &mut Ui) { - if header(ui, "Style Inspector") { - let _ = app.run("styles.inspector", json!({"value": false})); - return; - } // Read-only, so call it directly: running the command every frame would pay for the command // machinery and consume per-command state (a pending `join_next_undo`) just to draw the pane. let r = wordcraft_engine::cmd::inspector::inspect(&app.session);