mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 12:20:12 -06:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42c9eaab3 | ||
|
|
12869b8e02 | ||
|
|
8548b33be1 | ||
|
|
abd5a6e957 | ||
|
|
8dac5c7802 |
+3
-1
File diff suppressed because one or more lines are too long
@@ -92,6 +92,7 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
if drag.drag_started() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
}
|
||||
crate::chrome::note_title_drag(ui.ctx(), band);
|
||||
// The chevron's stroke starts at the content edge.
|
||||
let c = if rtl {
|
||||
pos2(band.max.x - 26.0, band.center().y)
|
||||
|
||||
@@ -281,6 +281,24 @@ fn autosave_knob_x(rtl: bool, on: bool, r: Rect) -> f32 {
|
||||
crate::rtl::lead_x(rtl != on, r, 8.0)
|
||||
}
|
||||
|
||||
/// The `ctx.data` key for the part of the title bar that drags this document window.
|
||||
fn title_drag_id(ctx: &egui::Context) -> egui::Id {
|
||||
egui::Id::unique(("title_drag_rect", ctx.viewport_id()))
|
||||
}
|
||||
|
||||
/// Notes `rect` as the part of the title bar that drags the window in this frame: behind a modal
|
||||
/// dialog its backdrop has the pointer there, and moves the window instead ([`title_drag_rect`]).
|
||||
pub(crate) fn note_title_drag(ctx: &egui::Context, rect: Rect) {
|
||||
let (key, pass) = (title_drag_id(ctx), ctx.cumulative_pass_nr());
|
||||
ctx.data_mut(|d| d.insert_temp(key, (rect, pass)));
|
||||
}
|
||||
|
||||
/// The part of the title bar that drags the window, if it was drawn in this frame.
|
||||
pub(crate) fn title_drag_rect(ctx: &egui::Context) -> Option<Rect> {
|
||||
let (key, pass) = (title_drag_id(ctx), ctx.cumulative_pass_nr());
|
||||
ctx.data(|d| d.get_temp::<(Rect, u64)>(key)).filter(|(_, p)| *p == pass).map(|(r, _)| r)
|
||||
}
|
||||
|
||||
/// Draws the title bar with window dragging, AutoSave, quick access, account, community and search
|
||||
/// controls, and the centred clickable document title.
|
||||
pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
|
||||
@@ -301,6 +319,10 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
|
||||
if drag.drag_started() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
}
|
||||
// Elsewhere the system's own title bar above this one moves the window.
|
||||
if mac {
|
||||
note_title_drag(ui.ctx(), full);
|
||||
}
|
||||
if drag.double_clicked() {
|
||||
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
|
||||
|
||||
@@ -455,6 +455,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) {
|
||||
let resp = egui::Modal::new(Id::unique("wordcraft.colorsDialog")).show(ctx, |ui| body(ui, &mut d));
|
||||
// egui's Modal blocks the window behind it, but not the document's keyboard.
|
||||
crate::dialogs::release_keyboard(app, ctx);
|
||||
crate::dialogs::drag_by_title_bar(ctx, &resp.backdrop_response, resp.response.rect);
|
||||
let mut done = resp.inner;
|
||||
if done.is_none() && resp.should_close() {
|
||||
done = Some(false);
|
||||
|
||||
@@ -34,7 +34,9 @@ use bookmarks::bookmark_at_caret;
|
||||
#[cfg(test)]
|
||||
use bookmarks::bookmark_names;
|
||||
pub use chrome::show;
|
||||
pub(crate) use chrome::{block_behind, buttons, cancel_key_pressed, color_menu, forget_nudge, ok_cancel_responses, release_keyboard};
|
||||
pub(crate) use chrome::{
|
||||
block_behind, buttons, cancel_key_pressed, color_menu, drag_by_title_bar, forget_nudge, ok_cancel_responses, release_keyboard,
|
||||
};
|
||||
pub(crate) use dispatch::search_results;
|
||||
pub(crate) use find::swallow_shortcut_text;
|
||||
pub use find::{FindExtra, FindTab, GoToWhat};
|
||||
|
||||
@@ -67,6 +67,16 @@ fn count_dialog_frame(ctx: &egui::Context, id: egui::Id) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Behind a modal window, whose `backdrop` has the pointer, the app still moves by its title bar,
|
||||
/// as in Word for Mac (#5124); not from just inside the window's edge, where it reaches over the
|
||||
/// title bar.
|
||||
pub(crate) fn drag_by_title_bar(ctx: &egui::Context, backdrop: &egui::Response, window: egui::Rect) {
|
||||
let origin = ctx.input(|i| i.pointer.press_origin()).filter(|p| !window.contains(*p));
|
||||
if backdrop.drag_started() && origin.is_some_and(|p| crate::chrome::title_drag_rect(ctx).is_some_and(|r| r.contains(p))) {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes `window` modal (#3859): the document gives up the keyboard now, and from the next frame
|
||||
/// nothing beneath the window takes the pointer or Tab. A press beside it nudges it ([`nudge`]).
|
||||
pub(crate) fn block_behind(app: &WordApp, ctx: &egui::Context, window: &egui::Response) {
|
||||
@@ -81,6 +91,7 @@ pub(crate) fn block_behind(app: &WordApp, ctx: &egui::Context, window: &egui::Re
|
||||
.inner;
|
||||
// The backdrop also has the pointer just inside the window's edge (egui's hit radius).
|
||||
let beside = ctx.input(|i| i.pointer.any_pressed() && i.pointer.press_origin().is_some_and(|p| !window.rect.contains(p)));
|
||||
drag_by_title_bar(ctx, &backdrop, window.rect);
|
||||
ctx.move_to_top(layer);
|
||||
ctx.memory_mut(|m| m.set_modal_layer(layer));
|
||||
ctx.accesskit_node_builder(window.id, |node| node.set_modal());
|
||||
|
||||
@@ -2078,6 +2078,116 @@ fn the_page_waits_for_the_keyboard_until_a_modal_dialog_closes() {
|
||||
assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body), "!", "the page has it after the dialog");
|
||||
}
|
||||
|
||||
/// Drags from `at` to 30 points right in `h`, and counts the window drags the app asked for.
|
||||
fn window_drags(h: &mut egui_kittest::Harness<WordApp>, at: egui::Pos2) -> usize {
|
||||
let mut drags = 0;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
let events = [egui::Event::PointerMoved(at), button(at, true)]
|
||||
.into_iter()
|
||||
.chain([10.0, 20.0, 30.0].map(|dx| egui::Event::PointerMoved(at + vec2(dx, 0.0))))
|
||||
.chain([button(at + vec2(30.0, 0.0), false)]);
|
||||
for e in events {
|
||||
h.event(e);
|
||||
h.step();
|
||||
drags += h.output().viewport_output.values().flat_map(|v| &v.commands).filter(|c| matches!(c, egui::ViewportCommand::StartDrag)).count();
|
||||
}
|
||||
drags
|
||||
}
|
||||
|
||||
/// #5124: behind a modal dialog the window still moves by its title bar, as in Word for Mac, where
|
||||
/// WordCraft draws it, or by the top of File's side menu, which covers it there; elsewhere the
|
||||
/// system's title bar does, and WordCraft's bar under it doesn't.
|
||||
#[test]
|
||||
fn the_window_moves_by_its_title_bar_behind_a_modal_dialog() {
|
||||
for (mac, backstage) in [(true, false), (true, true), (false, false), (false, true)] {
|
||||
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
a.integrated_titlebar = mac;
|
||||
a.ui.backstage = backstage;
|
||||
let mut h = app_harness(a);
|
||||
h.run_steps(6);
|
||||
let font = Dialog::open("font", h.state_mut());
|
||||
h.state_mut().dialog = font;
|
||||
h.run_steps(4);
|
||||
let at = egui::pos2(if backstage { 100.0 } else { 300.0 }, 12.0);
|
||||
assert_eq!(window_drags(&mut h, at), usize::from(mac), "macOS: {mac}, File: {backstage}");
|
||||
assert!(matches!(h.state().dialog, Some(Dialog::Font { .. })), "Font stays");
|
||||
assert_eq!(h.state().ui.backstage, backstage, "nothing behind it opened or closed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a modal state in `a`: the built-in file browser, a dialog by name, or Colors.
|
||||
fn open_modal_state(a: &mut WordApp, state: &str) {
|
||||
match state {
|
||||
"none" => {}
|
||||
"fileBrowser" => {
|
||||
let document = a.session.document_id();
|
||||
a.show_file_browser(
|
||||
crate::file_dialogs::FileDialogRequest::Open { purpose: "document".into() },
|
||||
crate::file_dialogs::AfterPick::OpenDocument { document },
|
||||
false,
|
||||
);
|
||||
}
|
||||
"colors" => a.colors = Some(crate::colors::ColorsDialog::new(None, crate::colors::Target::cmd("format.color"))),
|
||||
dialog => a.dialog = Dialog::open(dialog, a),
|
||||
}
|
||||
}
|
||||
|
||||
/// The file browser, a modal dialog and Colors each block the window; nothing open or a modeless
|
||||
/// dialog doesn't.
|
||||
#[test]
|
||||
fn modal_open_is_true_for_the_file_browser_a_modal_dialog_and_colors() {
|
||||
for (state, modal) in [("none", false), ("find", false), ("font", true), ("fileBrowser", true), ("colors", true)] {
|
||||
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
open_modal_state(&mut a, state);
|
||||
assert_eq!(a.modal_open(), modal, "{state}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #5124: whatever is open, a drag on the title bar macOS draws moves the window; where the system
|
||||
/// draws it, WordCraft's bar under it only does while nothing blocks it.
|
||||
#[test]
|
||||
fn the_title_bar_moves_the_window_in_every_modal_state() {
|
||||
for (state, modal) in [("none", false), ("find", false), ("font", true), ("fileBrowser", true), ("colors", true)] {
|
||||
for mac in [true, false] {
|
||||
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
a.integrated_titlebar = mac;
|
||||
let mut h = app_harness(a);
|
||||
h.run_steps(6);
|
||||
open_modal_state(h.state_mut(), state);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().modal_open(), modal, "{state}");
|
||||
assert_eq!(window_drags(&mut h, egui::pos2(300.0, 12.0)), usize::from(mac || !modal), "{state}, macOS: {mac}");
|
||||
assert_eq!(h.state().modal_open(), modal, "{state} stays open");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #5124: where a dialog reaches over the title bar (a short window), a drag on the dialog is the
|
||||
/// dialog's own and doesn't move the window.
|
||||
#[test]
|
||||
fn a_drag_on_a_dialog_over_the_title_bar_doesnt_move_the_window() {
|
||||
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
a.integrated_titlebar = true;
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 420.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(6);
|
||||
let font = Dialog::open("font", h.state_mut());
|
||||
h.state_mut().dialog = font;
|
||||
h.run_steps(4);
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Font")))).expect("the dialog is open");
|
||||
let at = egui::pos2(rect.center().x, rect.top() + 8.0);
|
||||
assert!(at.y < 30.0, "the dialog reaches over the title bar: {rect:?}");
|
||||
assert_eq!(window_drags(&mut h, at), 0, "a drag on its title");
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Font")))).expect("the dialog is open");
|
||||
assert_eq!(window_drags(&mut h, egui::pos2(rect.center().x, rect.top() + 1.0)), 0, "nor just inside its edge");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_sample_props_map_each_field_to_its_property() {
|
||||
use super::font::font_sample_props;
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
@@ -1,2 +1,9 @@
|
||||
sonar.projectKey=storytold_wordcraft
|
||||
sonar.organization=storytold
|
||||
|
||||
# S4790 (weak hash): SHA-1 is a start-key and "SHA1/1K" checksum algorithm that ODF package
|
||||
# encryption (ODF 1.3 Part 2 §4.16) names, and older office suites wrote it. It is needed to open
|
||||
# those files, not chosen for new security: WordCraft writes a SHA-256 start key and checksum.
|
||||
sonar.issue.ignore.multicriteria=odt_sha1
|
||||
sonar.issue.ignore.multicriteria.odt_sha1.ruleKey=rust:S4790
|
||||
sonar.issue.ignore.multicriteria.odt_sha1.resourceKey=crates/formats/src/odt/crypt.rs
|
||||
|
||||
Reference in New Issue
Block a user