Pictures in headers/footers can be moved, resized and rotated (#591)
* Pictures in headers/footers can be moved, resized and rotated Hit testing (layout.object_at_in), arrange.bounds/position/nudge/group and the canvas selection, drag, resize and rotation handles used to be body-only, so pictures in headers and footers could not be moved (#82). They now work within their own header/footer story: drag to move, handles to resize, rotation handle to turn, arrows to nudge. Regression test: header_pictures_can_be_resized_and_dragged. * Credit DevCoderMax (Gleyson do Nascimento Carvalho) in About --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
d1064bbaad
commit
0652e7f72f
|
|
@ -41,5 +41,9 @@ sync_github_name = false
|
|||
[people.henno]
|
||||
sync_github_name = true
|
||||
|
||||
[people.DevCoderMax]
|
||||
real_name = "Gleyson do Nascimento Carvalho"
|
||||
display_name = "Gleyson Carvalho"
|
||||
sync_github_name = false
|
||||
[people.alexanderalber]
|
||||
real_name = "Alexander Alber"
|
||||
|
|
|
|||
|
|
@ -470,11 +470,13 @@ fn stroke(s: &mut Session, v: &Value) -> CmdResult {
|
|||
/// under `anchor_y` (positioned from its column and top), or, where no paragraph starts on the
|
||||
/// page, to the nearest line and positioned on the page. Returns where its character went.
|
||||
fn place(s: &mut Session, layout: &DocLayout, obj: InlineObject, page: usize, (x, y): (f32, f32), anchor_y: f32) -> Result<Pos, CmdError> {
|
||||
let (at, on_page) = match super::objects::anchor_paragraph(layout, page, anchor_y) {
|
||||
let (at, on_page) = match super::objects::anchor_paragraph(layout, page, anchor_y, StoryRef::Body) {
|
||||
Some(path) => (Pos { story: StoryRef::Body, path, off: 0 }, false),
|
||||
None => {
|
||||
(super::objects::nearest_line(layout, page, anchor_y).ok_or_else(|| CmdError::Params("no text on that page to anchor to".into()))?, true)
|
||||
}
|
||||
None => (
|
||||
super::objects::nearest_line(layout, page, anchor_y, StoryRef::Body)
|
||||
.ok_or_else(|| CmdError::Params("no text on that page to anchor to".into()))?,
|
||||
true,
|
||||
),
|
||||
};
|
||||
s.doc.insert_object(&at, obj, &CharProps::default())?;
|
||||
let len = OBJ.len_utf8();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ fn has_floating(s: &Session) -> Option<&'static str> {
|
|||
_ => Some("select a floating picture or shape first"),
|
||||
}
|
||||
}
|
||||
/// Stories whose objects can be moved, resized and arranged: the body and headers/footers.
|
||||
/// Objects in table cells (deeper paths), notes and text boxes stay where their text is.
|
||||
fn movable_story(s: &Session, story: StoryRef) -> bool {
|
||||
match story {
|
||||
StoryRef::Body => true,
|
||||
StoryRef::Part(id) => {
|
||||
s.doc.parts.get(&id).is_some_and(|p| matches!(p.kind, wordcraft_doc::PartKind::Header | wordcraft_doc::PartKind::Footer))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Charts and diagrams are not written back on save yet, so their size, position and wrapping stay
|
||||
/// as imported: they can be selected and deleted, not moved, resized or arranged.
|
||||
const FROZEN: &str = "charts and diagrams can't be moved or resized yet";
|
||||
|
|
@ -688,9 +698,10 @@ fn bounds(s: &mut Session, v: &Value) -> CmdResult {
|
|||
/// position afterwards.
|
||||
fn move_object(s: &mut Session, pos: Pos, page: Option<u64>, x: f32, y: f32, (w, h): (f32, f32)) -> Result<Pos, CmdError> {
|
||||
use wordcraft_doc::para::Anchor;
|
||||
if pos.story != StoryRef::Body || pos.path.depth() > 0 {
|
||||
return Err(CmdError::Disabled("only objects in the body text can be moved".into()));
|
||||
if pos.path.depth() > 0 || !movable_story(s, pos.story) {
|
||||
return Err(CmdError::Disabled("only objects in the body text, headers and footers can be moved".into()));
|
||||
}
|
||||
let story = pos.story;
|
||||
let layout = s.layout();
|
||||
let cur = layout.object(&pos, s.page_hint).ok_or_else(|| CmdError::Failed("the object isn't laid out".into()))?;
|
||||
let page = page.map_or(cur.page, |n| usize::try_from(n).unwrap_or(usize::MAX));
|
||||
|
|
@ -698,19 +709,19 @@ fn move_object(s: &mut Session, pos: Pos, page: Option<u64>, x: f32, y: f32, (w,
|
|||
// Keep a corner of it on the page.
|
||||
let x = x.max(12.0 - w).min(pg.w - 12.0);
|
||||
let y = y.max(12.0 - h).min(pg.h.min(MAX_OFFSET) - 12.0);
|
||||
let (anchor, on_page) = match anchor_paragraph(&layout, page, y) {
|
||||
Some(path) => (Pos { story: StoryRef::Body, path, off: 0 }, false),
|
||||
None => (nearest_line(&layout, page, y).ok_or_else(|| CmdError::Params("no text on that page to anchor to".into()))?, true),
|
||||
let (anchor, on_page) = match anchor_paragraph(&layout, page, y, story) {
|
||||
Some(path) => (Pos { story, path, off: 0 }, false),
|
||||
None => (nearest_line(&layout, page, y, story).ok_or_else(|| CmdError::Params("no text on that page to anchor to".into()))?, true),
|
||||
};
|
||||
// Move its character to the anchor (unless it's already in that paragraph).
|
||||
let at = if anchor.path == pos.path && !on_page {
|
||||
let at = if anchor.story == pos.story && anchor.path == pos.path && !on_page {
|
||||
pos
|
||||
} else {
|
||||
let obj = s.doc.para_at(&pos).and_then(|p| p.object_at(pos.off)).cloned().ok_or_else(|| CmdError::Failed("object vanished".into()))?;
|
||||
let len = wordcraft_doc::para::OBJ.len_utf8();
|
||||
s.doc.delete_range(&pos, &Pos { off: pos.off + len, ..pos.clone() })?;
|
||||
let mut at = anchor;
|
||||
if at.path == pos.path && at.off > pos.off {
|
||||
if at.story == pos.story && at.path == pos.path && at.off > pos.off {
|
||||
at.off = at.off.saturating_sub(len);
|
||||
}
|
||||
let at = s.doc.clamp(&at);
|
||||
|
|
@ -798,31 +809,33 @@ fn move_with_text(s: &mut Session, with_text: bool) -> CmdResult {
|
|||
Ok(json!({"object": serde_json::to_value(o).unwrap_or(Value::Null)}))
|
||||
}
|
||||
|
||||
/// The top-level body paragraph under `y` on `page`: the last one starting at or above it (else
|
||||
/// the first starting on the page).
|
||||
pub(crate) fn anchor_paragraph(layout: &wordcraft_layout::DocLayout, page: usize, y: f32) -> Option<wordcraft_doc::Path> {
|
||||
let starts: Vec<(&wordcraft_doc::Path, f32)> = layout
|
||||
.pages
|
||||
.get(page)?
|
||||
.items
|
||||
.iter()
|
||||
/// The top-level paragraph of `story` under `y` on `page`: the last one starting at or above
|
||||
/// it (else the first starting on the page). Body paragraphs come from the page's items;
|
||||
/// header/footer ones from its header and footer.
|
||||
pub(crate) fn anchor_paragraph(layout: &wordcraft_layout::DocLayout, page: usize, y: f32, story: StoryRef) -> Option<wordcraft_doc::Path> {
|
||||
let pg = layout.pages.get(page)?;
|
||||
let items: Box<dyn Iterator<Item = &wordcraft_layout::Placed> + '_> = match story {
|
||||
StoryRef::Body => Box::new(pg.items.iter()),
|
||||
StoryRef::Part(_) => Box::new(pg.header.iter().chain(pg.footer.iter())),
|
||||
};
|
||||
let starts: Vec<(&wordcraft_doc::Path, f32)> = items
|
||||
.filter_map(|it| match it {
|
||||
wordcraft_layout::Placed::Lines { story: StoryRef::Body, path, l0: 0, y: top, .. } if path.depth() == 0 => Some((path, *top)),
|
||||
wordcraft_layout::Placed::Lines { story: s, path, l0: 0, y: top, .. } if *s == story && path.depth() == 0 => Some((path, *top)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
starts.iter().rev().find(|(_, top)| *top <= y + 0.5).or(starts.first()).map(|(p, _)| (*p).clone())
|
||||
}
|
||||
|
||||
/// The start of the top-level body line on `page` nearest to `y`.
|
||||
pub(crate) fn nearest_line(layout: &wordcraft_layout::DocLayout, page: usize, y: f32) -> Option<Pos> {
|
||||
let lines = wordcraft_layout::hit::page_lines(layout.pages.get(page)?, StoryRef::Body);
|
||||
/// The start of the top-level line of `story` on `page` nearest to `y`.
|
||||
pub(crate) fn nearest_line(layout: &wordcraft_layout::DocLayout, page: usize, y: f32, story: StoryRef) -> Option<Pos> {
|
||||
let lines = wordcraft_layout::hit::page_lines(layout.pages.get(page)?, story);
|
||||
let best = lines.iter().filter(|l| l.path.depth() == 0).min_by(|a, b| {
|
||||
let d = |l: &wordcraft_layout::hit::LineHit| if y < l.top { l.top - y } else { (y - l.bottom).max(0.0) };
|
||||
d(a).total_cmp(&d(b))
|
||||
})?;
|
||||
let off = best.para.lines.get(best.li)?.start;
|
||||
Some(Pos { story: StoryRef::Body, path: best.path.clone(), off })
|
||||
Some(Pos { story, path: best.path.clone(), off })
|
||||
}
|
||||
|
||||
fn obj_size(o: &InlineObject) -> (f32, f32) {
|
||||
|
|
@ -1103,7 +1116,7 @@ fn object_ref(s: &Session, v: &Value) -> Result<Pos, CmdError> {
|
|||
Some(n) => all_objects(s).into_iter().nth(usize::try_from(n).unwrap_or(usize::MAX)).map(|(p, _)| p),
|
||||
None => super::parse_pos(v),
|
||||
};
|
||||
pos.filter(|p| p.story == StoryRef::Body && s.doc.para_at(p).and_then(|q| q.object_at(p.off)).is_some_and(InlineObject::is_drawing))
|
||||
pos.filter(|p| movable_story(s, p.story) && s.doc.para_at(p).and_then(|q| q.object_at(p.off)).is_some_and(InlineObject::is_drawing))
|
||||
.ok_or_else(|| CmdError::Params("no picture or shape there".into()))
|
||||
}
|
||||
|
||||
|
|
@ -1155,9 +1168,13 @@ fn group(s: &mut Session, _: &Value) -> CmdResult {
|
|||
picks.sort();
|
||||
let layout = s.layout();
|
||||
let mut found = Vec::new();
|
||||
let story = picks.first().cloned().map(|p| p.story).ok_or_else(|| CmdError::Failed("nothing to group".into()))?;
|
||||
if picks.iter().any(|p| p.story != story) {
|
||||
return Err(CmdError::Disabled("objects in different stories (body, header, footer) can't be grouped".into()));
|
||||
}
|
||||
for pos in &picks {
|
||||
if pos.story != StoryRef::Body || pos.path.depth() > 0 {
|
||||
return Err(CmdError::Disabled("only objects in the body text (not in tables) can be grouped".into()));
|
||||
if pos.path.depth() > 0 || !movable_story(s, pos.story) {
|
||||
return Err(CmdError::Disabled("only objects in the body text, headers and footers (not in tables) can be grouped".into()));
|
||||
}
|
||||
let obj = s.doc.para_at(pos).and_then(|p| p.object_at(pos.off)).cloned().ok_or_else(|| CmdError::Failed("object vanished".into()))?;
|
||||
if matches!(obj, InlineObject::Graphic { .. }) {
|
||||
|
|
@ -1241,8 +1258,8 @@ fn ungroup(s: &mut Session, _: &Value) -> CmdResult {
|
|||
let InlineObject::Group { w, h, float, .. } = &obj else { return Err(CmdError::Disabled("select a group first".into())) };
|
||||
// Its offsets, explicit: the members are placed from the same anchor.
|
||||
let pos = if float.wrap == Wrap::Inline {
|
||||
if pos.story != StoryRef::Body || pos.path.depth() > 0 {
|
||||
return Err(CmdError::Disabled("only groups in the body text can be ungrouped in line with text".into()));
|
||||
if pos.path.depth() > 0 || !movable_story(s, pos.story) {
|
||||
return Err(CmdError::Disabled("only groups in the body text, headers and footers can be ungrouped in line with text".into()));
|
||||
}
|
||||
let hit = s.layout().object(&pos, s.page_hint).ok_or_else(|| CmdError::Failed("the object isn't laid out".into()))?;
|
||||
move_object(s, pos, Some(hit.page as u64), hit.rect.x, hit.rect.y, (*w, *h))?
|
||||
|
|
@ -1698,6 +1715,34 @@ mod tests {
|
|||
assert_eq!(crop, [0.1, 0.2, 0.05, 0.0]);
|
||||
}
|
||||
|
||||
/// A header picture resizes and drags like a body one: the move stays in the header story
|
||||
/// and lands where dropped; the header hit test finds it.
|
||||
#[test]
|
||||
fn header_pictures_can_be_resized_and_dragged() {
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
s.run("insert.editHeader", &json!({})).unwrap();
|
||||
let story = s.sel.focus.story;
|
||||
assert!(matches!(story, StoryRef::Part(_)), "editing the header, got {story:?}");
|
||||
let data = super::super::insert::base64_encode(&png(20, 10));
|
||||
s.run("insert.picture", &json!({"data": data, "width": 100})).unwrap();
|
||||
let (pos, _) = selected(&s).expect("a new header picture is selected");
|
||||
assert_eq!(pos.story, story);
|
||||
s.run("arrange.bounds", &json!({"width": 160, "height": 90})).unwrap();
|
||||
let r = s.run("arrange.bounds", &json!({"page": 0, "x": 200, "y": 60})).unwrap();
|
||||
assert_eq!(r["object"]["float"]["wrap"], "square");
|
||||
let o = s.layout().object(&selected(&s).unwrap().0, 0).expect("laid out");
|
||||
assert_eq!(o.story, story);
|
||||
assert!((o.rect.x - 200.0).abs() < 1.0 && (o.rect.y - 60.0).abs() < 1.0, "{:?}", o.rect);
|
||||
let hit = s.layout().object_at_in(o.page, o.rect.x + 5.0, o.rect.y + 5.0, 4.0, story).expect("header hit");
|
||||
assert_eq!(hit.pos(), o.pos());
|
||||
s.run("arrange.nudge", &json!({"dx": 6, "dy": 4})).unwrap();
|
||||
let n = s.layout().object(&selected(&s).unwrap().0, 0).expect("laid out");
|
||||
assert!((n.rect.x - 206.0).abs() < 1.0 && (n.rect.y - 64.0).abs() < 1.0, "{:?}", n.rect);
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
let u = s.layout().object(&selected(&s).unwrap().0, 0).expect("laid out");
|
||||
assert!((u.rect.x - 200.0).abs() < 1.0 && (u.rect.y - 60.0).abs() < 1.0, "{:?}", u.rect);
|
||||
}
|
||||
|
||||
/// Shape Format › Text (#369): direction and alignment on the box the caret is in, then
|
||||
/// Create Link to an empty box and Break Link, each one undo step.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -137,6 +137,25 @@ fn items_of(page: &Page, story: StoryRef) -> Box<dyn Iterator<Item = &Placed> +
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether header/footer part `id` has text near (x, y) on the page (its header or footer
|
||||
/// lines). Used so an object behind the header text is only grabbed where there's no text,
|
||||
/// mirroring `text_at` for the body.
|
||||
fn hf_text_at(p: &Page, id: u32, x: f32, y: f32) -> bool {
|
||||
let story = StoryRef::Part(id);
|
||||
p.header
|
||||
.iter()
|
||||
.chain(p.footer.iter())
|
||||
.filter_map(|it| match it {
|
||||
Placed::Lines { story: s, para, l0, l1, x: lx, y: ly, .. } if *s == story => Some((para, l0, l1, lx, ly)),
|
||||
_ => None,
|
||||
})
|
||||
.any(|(para, l0, l1, lx, ly)| {
|
||||
let (Some(first), Some(last)) = (para.lines.get(*l0), para.lines.get(l1.checked_sub(1).unwrap_or(0))) else { return false };
|
||||
let bottom = ly + last.top + last.height - first.top;
|
||||
y >= *ly - 2.0 && y <= bottom + 2.0 && x >= *lx - 40.0 && x <= lx + last.right + 40.0
|
||||
})
|
||||
}
|
||||
|
||||
/// Every upright line on a page that belongs to `story` (body, or a header/footer part).
|
||||
/// Turned lines (see [`turned_hit`]) are left out.
|
||||
pub fn page_lines(page: &Page, story: StoryRef) -> Vec<LineHit<'_>> {
|
||||
|
|
@ -237,15 +256,32 @@ impl DocLayout {
|
|||
/// border (within `edge` points either side; inside is its text). Topmost first; objects
|
||||
/// behind the text only where there's no text.
|
||||
pub fn object_at(&self, page: usize, x: f32, y: f32, edge: f32) -> Option<ObjectHit> {
|
||||
self.object_at_in(page, x, y, edge, StoryRef::Body)
|
||||
}
|
||||
|
||||
/// The object of `story` a press at (x, y) grabs. Body looks only at the page's own items
|
||||
/// (as before); a header/footer part looks only at that part's objects in the page's
|
||||
/// header and footer, so a header picture placed over the body never steals body clicks.
|
||||
pub fn object_at_in(&self, page: usize, x: f32, y: f32, edge: f32, story: StoryRef) -> Option<ObjectHit> {
|
||||
let p = self.pages.get(page)?;
|
||||
// A rotated object is grabbed on its rotated shape: the point is taken into its frame.
|
||||
let grabs = |o: &ObjectHit| {
|
||||
let pt = o.unspin(x, y);
|
||||
o.rect.expand(edge).contains(pt) && (o.text_box.is_none() || !o.rect.expand(-edge).contains(pt) || o.rect.w.min(o.rect.h) <= edge * 3.0)
|
||||
};
|
||||
objects(p, page)
|
||||
let cands: Vec<ObjectHit> = match story {
|
||||
StoryRef::Body => objects(p, page).collect(),
|
||||
StoryRef::Part(_) => all_objects(p, page).filter(|o| o.story == story).collect(),
|
||||
};
|
||||
let text_here = match story {
|
||||
StoryRef::Body => text_at(p, page, x, y).is_some(),
|
||||
StoryRef::Part(id) => hf_text_at(p, id, x, y),
|
||||
};
|
||||
cands
|
||||
.iter()
|
||||
.find(|o| !o.behind() && grabs(o))
|
||||
.or_else(|| if text_at(p, page, x, y).is_some() { None } else { objects(p, page).find(|o| o.behind() && grabs(o)) })
|
||||
.or_else(|| if text_here { None } else { cands.iter().find(|o| o.behind() && grabs(o)) })
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// The text box in the header or footer at (x, y) on `page`.
|
||||
|
|
|
|||
|
|
@ -1567,7 +1567,7 @@ fn part_kind(app: &WordApp, s: StoryRef) -> Option<PartKind> {
|
|||
}
|
||||
|
||||
/// The story a text box's story sits in (where the box is), when the caret is in a text box.
|
||||
fn text_box_host(app: &WordApp, layout: &DocLayout) -> Option<StoryRef> {
|
||||
pub(crate) fn text_box_host(app: &WordApp, layout: &DocLayout) -> Option<StoryRef> {
|
||||
match app.session.sel.focus.story {
|
||||
StoryRef::Part(id) if in_text_box(app) => layout.text_box(id, app.session.page_hint).map(|o| o.story),
|
||||
_ => None,
|
||||
|
|
|
|||
|
|
@ -72,9 +72,31 @@ fn connector_end_at(app: &WordApp, layout: &DocLayout, pages: &[Rect], scale: f3
|
|||
}
|
||||
}
|
||||
|
||||
/// The selected object: the selection is exactly one picture, shape or text box in the body.
|
||||
/// The selected object: the selection is exactly one picture, shape or text box, in the body
|
||||
/// or in a header/footer.
|
||||
pub fn selected(app: &WordApp) -> Option<(Pos, &InlineObject)> {
|
||||
wordcraft_engine::cmd::objects::object_selection(&app.session).filter(|(p, _)| p.story == StoryRef::Body)
|
||||
wordcraft_engine::cmd::objects::object_selection(&app.session).filter(|(p, _)| selectable_story(app, p.story))
|
||||
}
|
||||
|
||||
/// Whether objects of `story` can be selected and dragged on the canvas: the body and
|
||||
/// headers/footers.
|
||||
fn selectable_story(app: &WordApp, story: StoryRef) -> bool {
|
||||
match story {
|
||||
StoryRef::Body => true,
|
||||
StoryRef::Part(id) => app.session.doc.parts.get(&id).is_some_and(|p| matches!(p.kind, PartKind::Header | PartKind::Footer)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The story whose objects the canvas handles: the header/footer being edited, else the body.
|
||||
fn active_story(app: &WordApp, layout: &DocLayout) -> StoryRef {
|
||||
let story = crate::canvas::text_box_host(app, layout).unwrap_or(app.session.sel.focus.story);
|
||||
match story {
|
||||
StoryRef::Body => StoryRef::Body,
|
||||
StoryRef::Part(id) if selectable_story(app, StoryRef::Part(id)) => StoryRef::Part(id),
|
||||
// Notes and text-box text: no top-level objects live here, so grabs find nothing and
|
||||
// the press falls through to text handling.
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the object under `hit` keeps its place and size: charts and diagrams can be selected
|
||||
|
|
@ -151,7 +173,7 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, pages: &[Rect]
|
|||
return true;
|
||||
}
|
||||
let multi = resp.double_clicked() || resp.triple_clicked();
|
||||
if !(pressed || multi) || crate::canvas::editing_header_footer(app, layout) {
|
||||
if !(pressed || multi) {
|
||||
return false;
|
||||
}
|
||||
// A selected connector's end: drag it to glue it.
|
||||
|
|
@ -176,7 +198,7 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, pages: &[Rect]
|
|||
None => return true,
|
||||
};
|
||||
// Objects in table cells can be selected and resized, not moved; charts neither.
|
||||
if !fixed(app, &object) && (grab != Grab::Move || movable(&object)) {
|
||||
if !fixed(app, &object) && (grab != Grab::Move || movable(app, &object)) {
|
||||
app.canvas.obj_drag =
|
||||
Some(ObjectDrag { drag: Drag::new(grab, object.page, object.rect, object.spin.deg, at), object, min, picture, end: None });
|
||||
}
|
||||
|
|
@ -221,9 +243,11 @@ pub fn select_at(app: &mut WordApp, layout: &DocLayout, pages: &[Rect], scale: f
|
|||
true
|
||||
}
|
||||
|
||||
/// What a press at `at` would grab: a handle of the shown frame, else an object.
|
||||
/// What a press at `at` would grab: a handle of the shown frame, else an object of the story
|
||||
/// being edited (body, or the header/footer).
|
||||
fn grab_at(app: &WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at: Pos2) -> Option<(ObjectHit, Grab)> {
|
||||
let handle = active(app, layout).filter(|(o, _)| o.story == StoryRef::Body && !fixed(app, o)).and_then(|(o, _)| {
|
||||
let story = active_story(app, layout);
|
||||
let handle = active(app, layout).filter(|(o, _)| o.story == story && !fixed(app, o)).and_then(|(o, _)| {
|
||||
let f = screen(pages, scale, o.page, o.rect)?;
|
||||
if frame::rotate_handle_at(f, o.spin.deg, at) {
|
||||
return Some((o, Grab::Rotate));
|
||||
|
|
@ -232,14 +256,16 @@ fn grab_at(app: &WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at: Po
|
|||
});
|
||||
handle.or_else(|| {
|
||||
let (page, x, y) = crate::canvas::page_at(pages, layout, scale, at)?;
|
||||
// Selectable: body objects (table cells too); notes' objects aren't yet.
|
||||
layout.object_at(page, x, y, EDGE / scale.max(0.01)).filter(|o| o.story == StoryRef::Body).map(|o| (o, Grab::Move))
|
||||
// Selectable: body objects (table cells too) or the header/footer's own objects;
|
||||
// notes' objects aren't yet.
|
||||
layout.object_at_in(page, x, y, EDGE / scale.max(0.01), story).map(|o| (o, Grab::Move))
|
||||
})
|
||||
}
|
||||
|
||||
/// Objects that can be dragged to a new place: those in the body's own paragraphs.
|
||||
fn movable(o: &ObjectHit) -> bool {
|
||||
o.story == StoryRef::Body && o.path.depth() == 0
|
||||
/// Objects that can be dragged to a new place: those in the own paragraphs of the body or of a
|
||||
/// header/footer.
|
||||
fn movable(app: &WordApp, o: &ObjectHit) -> bool {
|
||||
o.path.depth() == 0 && selectable_story(app, o.story)
|
||||
}
|
||||
|
||||
/// Commit a finished drag as one command.
|
||||
|
|
@ -255,7 +281,9 @@ fn drop(app: &mut WordApp, d: &ObjectDrag) {
|
|||
}
|
||||
Grab::Move => json!({"page": d.drag.to_page, "x": r.x, "y": r.y}),
|
||||
// An inline object stays in the text; a floating one keeps its far edges put.
|
||||
Grab::Resize(_) if d.object.floating() && movable(&d.object) => json!({"width": r.w, "height": r.h, "page": d.drag.page, "x": r.x, "y": r.y}),
|
||||
Grab::Resize(_) if d.object.floating() && movable(app, &d.object) => {
|
||||
json!({"width": r.w, "height": r.h, "page": d.drag.page, "x": r.x, "y": r.y})
|
||||
}
|
||||
Grab::Resize(_) => json!({"width": r.w, "height": r.h}),
|
||||
};
|
||||
let _ = app.run("arrange.bounds", params);
|
||||
|
|
@ -317,9 +345,9 @@ pub fn paint(app: &WordApp, painter: &Painter, t: &Tokens, layout: &DocLayout, p
|
|||
if let Some((o, editing)) = active(app, layout)
|
||||
&& let Some(f) = screen(pages, scale, o.page, o.rect)
|
||||
{
|
||||
// Handles where it can be resized and turned (body objects, not charts): else a plain
|
||||
// frame.
|
||||
let handles = o.story == StoryRef::Body && !fixed(app, &o);
|
||||
// Handles where it can be resized and turned (body and header/footer objects,
|
||||
// not charts): else a plain frame.
|
||||
let handles = selectable_story(app, o.story) && !fixed(app, &o);
|
||||
frame::paint(painter, t, f, o.spin.deg, editing, handles, handles);
|
||||
}
|
||||
}
|
||||
|
|
@ -369,9 +397,6 @@ pub fn cursor(app: &WordApp, layout: &DocLayout, pages: &[Rect], scale: f32, at:
|
|||
if let Some(d) = &app.canvas.obj_drag {
|
||||
return Some(if d.end.is_some() { egui::CursorIcon::Crosshair } else { d.drag.grab.cursor() });
|
||||
}
|
||||
if crate::canvas::editing_header_footer(app, layout) {
|
||||
return None;
|
||||
}
|
||||
if connector_end_at(app, layout, pages, scale, at).is_some() {
|
||||
return Some(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue