mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 11:57:36 -06:00
This commit is contained in:
@@ -3377,12 +3377,21 @@ fn link_under(app: &WordApp, layout: &DocLayout, page: usize, x: f32, y: f32, po
|
||||
let at = wordcraft_geom::Point::new(x, y);
|
||||
// The caret falls between characters: the one under the point is on either side of it.
|
||||
[(pos.off, para.next_boundary(pos.off)), (para.prev_boundary(pos.off), pos.off)].into_iter().filter(|(a, b)| a < b).find_map(|(a, b)| {
|
||||
let link = para.props_of_char(a).link.clone().filter(|l| !l.is_empty())?;
|
||||
let link = para.props_of_char(a).link.clone().filter(|l| !l.is_empty()).or_else(|| field_link(para.object_at(a)?))?;
|
||||
let (from, to) = (Pos { off: a, ..pos.clone() }, Pos { off: b, ..pos.clone() });
|
||||
layout.selection_rects(doc, &from, &to, page).into_iter().any(|(pi, r)| pi == page && r.contains(at)).then_some(link)
|
||||
})
|
||||
}
|
||||
|
||||
/// The link a cross-reference field makes: `REF name \h` and `PAGEREF name \h` lead to the
|
||||
/// bookmark `name` (`#name`), as in Word; without `\h` the result is plain text (#2945).
|
||||
fn field_link(obj: &wordcraft_doc::InlineObject) -> Option<String> {
|
||||
let wordcraft_doc::InlineObject::Field { instr, .. } = obj else { return None };
|
||||
let code = wordcraft_engine::cmd::references::FieldCode::parse(instr);
|
||||
let name = code.args.first().filter(|n| !n.is_empty())?;
|
||||
(matches!(code.name.as_str(), "REF" | "PAGEREF") && code.has('h')).then(|| format!("#{name}"))
|
||||
}
|
||||
|
||||
/// A link to a hidden bookmark (`#_Toc…`, `#_Ref…`): a name the program made, which nobody
|
||||
/// chose or sees, so the hover tip says where it goes instead of showing it (#2177).
|
||||
fn is_generated_bookmark_link(link: &str) -> bool {
|
||||
@@ -5061,6 +5070,64 @@ mod tests {
|
||||
assert_eq!(s.sel.focus, start, "at the bookmark's start");
|
||||
}
|
||||
|
||||
/// #2945: only a `REF` or `PAGEREF` with `\h` and a bookmark makes a link.
|
||||
#[test]
|
||||
fn only_hyperlinked_reference_fields_make_links() {
|
||||
let field = |instr: &str| wordcraft_doc::InlineObject::Field {
|
||||
instr: instr.into(),
|
||||
result: "x".into(),
|
||||
locked: false,
|
||||
dirty: false,
|
||||
code: false,
|
||||
form: String::new(),
|
||||
};
|
||||
assert_eq!(field_link(&field(r"REF _Ref000000001 \h")).as_deref(), Some("#_Ref000000001"));
|
||||
assert_eq!(field_link(&field(r"PAGEREF Spot \h")).as_deref(), Some("#Spot"));
|
||||
assert_eq!(field_link(&field(r"ref Spot \* MERGEFORMAT \H")).as_deref(), Some("#Spot"));
|
||||
for plain in [r"REF Spot", r"REF \h", "REF", "", r"PAGE \h", r"TOC \h", r"SEQ Figure \h"] {
|
||||
assert_eq!(field_link(&field(plain)), None, "{plain:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2945: a cross-reference (`REF … \h`) is a link to its target: Ctrl+click on it goes there,
|
||||
/// and its hover tip names no bookmark.
|
||||
#[test]
|
||||
fn ctrl_click_on_a_cross_reference_goes_to_its_target() {
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("Rock\nSee ")), Default::default());
|
||||
a.run("select.text", json!({"text": "Rock"})).unwrap();
|
||||
a.run("references.caption", json!({"label": "Figure", "text": "A rock"})).unwrap();
|
||||
a.run("caret.docEnd", json!({})).unwrap();
|
||||
a.run("insert.crossReference", json!({"to": "figure", "target": "Figure 1", "show": "number"})).unwrap();
|
||||
let caption = a.session.doc.body.iter().position(|b| b.as_para().is_some_and(|p| p.props.style.as_deref() == Some("Caption"))).unwrap();
|
||||
let last = a.session.doc.body.len() - 1;
|
||||
assert_ne!(caption, last);
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
steps(&mut h, 6);
|
||||
// The field sits after "See ": click between its two edges.
|
||||
let obj = wordcraft_doc::para::OBJ.len_utf8();
|
||||
let (before, after) = {
|
||||
let l = h.state_mut().session.layout();
|
||||
let pos = |off| Pos { story: StoryRef::Body, path: wordcraft_doc::Path::top(last), off };
|
||||
(l.caret(&pos(4)).unwrap(), l.caret(&pos(4 + obj)).unwrap())
|
||||
};
|
||||
let at = screen(&mut h, (before.x + after.x) / 2.0, before.top + before.height / 2.0);
|
||||
h.event(egui::Event::PointerMoved(at));
|
||||
h.step();
|
||||
let press = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::COMMAND };
|
||||
h.event_modifiers(press, egui::Modifiers::COMMAND);
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
steps(&mut h, 3);
|
||||
let s = &h.state().session;
|
||||
assert_eq!(s.sel.focus.path.last(), caption, "the caret went to the caption: {:?}", s.sel);
|
||||
}
|
||||
|
||||
/// #688: the drag auto-scroll waits in the middle, ramps in from both edges and never leaves
|
||||
/// the scrollable range.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user