use super::paragraph::{clamp_pt, finite_key, tab_align, tabs_params}; use super::{Result, Session, TabStop, TextDir, Value, bad, bool_or, current_para, format_para, json, ok, resolve}; /// What the horizontal ruler shows while editing text: the paragraph's indents and tab stops, /// and where its text area sits on the slide. pub(super) fn ruler(s: &mut Session, _p: &Value) -> Result { let props = current_para(s); let st = s.doc()?; let t = st.selection.text.clone().ok_or_else(|| bad("format.ruler", "not editing text"))?; let (x, y, w, rotated) = match (t.notes, st.shape(t.shape)) { (false, Some(sh)) => { let ctx = crate::cmd::text::ctx_for(&st.doc, &st.selection); let x = match &ctx { Some(c) => resolve::xfrm(c, sh), None => sh.xfrm.unwrap_or_default(), }; let body = match &ctx { Some(c) => resolve::body(c, sh), None => sh.text.as_ref().map(|b| b.body.clone()).unwrap_or_default(), }; let (l, r, tp) = (body.inset_l.unwrap_or(7.2), body.inset_r.unwrap_or(7.2), body.inset_t.unwrap_or(3.6)); (x.x + l, x.y + tp, (x.w - l - r).max(0.0), x.rot != 0.0 || body.vert.is_some_and(|v| v != TextDir::Horizontal)) } _ => (0.0, 0.0, 0.0, true), }; let tabs: Vec = props.tabs.clone().unwrap_or_default().iter().map(|t| json!({"pos": t.pos, "align": t.align})).collect(); Ok(json!({ "x": x, "y": y, "width": w, "rotated": rotated, "marginLeft": props.margin_left.unwrap_or(0.0), "indent": props.indent.unwrap_or(0.0), "tabs": tabs, "defaultTab": props.default_tab.unwrap_or(72.0), })) } const MAX_TABS: usize = 32; pub(super) fn tabs(s: &mut Session, p: &Value) -> Result { const CMD: &str = "format.tabs"; let fin = |v: Option| v.filter(|x| x.is_finite()).map(|x| x.clamp(0.0, 4000.0)); let align_of = |v: &Value| match v.get("align").and_then(Value::as_str).unwrap_or("l") { "ctr" | "center" => "ctr", "r" | "right" => "r", "dec" | "decimal" => "dec", _ => "l", }; let add = p.get("add").and_then(|a| fin(a.get("pos").and_then(Value::as_f64)).map(|pos| (pos, align_of(a).to_string()))); let mv = p.get("move").and_then(|m| Some((fin(m.get("from").and_then(Value::as_f64))?, fin(m.get("to").and_then(Value::as_f64))?))); let remove = fin(p.get("remove").and_then(Value::as_f64)); // `set` replaces every stop: entries without a usable position are dropped, an unknown // alignment or a non-object entry is an error; stops within 0.05 pt collapse (the last wins). let set = match p.get("set") { None => None, Some(Value::Array(items)) => { if items.len() > 64 { return Err(bad(CMD, "`set` has more than 64 tab stops")); } let mut out: Vec = Vec::new(); for (i, item) in items.iter().enumerate() { let Some(obj) = item.as_object() else { return Err(bad(CMD, format!("tab stop {i} must be an object"))); }; let Some(pos) = obj.get("pos").and_then(Value::as_f64).filter(|n| n.is_finite()).map(clamp_pt) else { continue; }; let align = match obj.get("align").and_then(Value::as_str) { None => "l", Some(a) => tab_align(a).ok_or_else(|| bad(CMD, format!("tab stop {i} align must be l, ctr, r or dec")))?, }; if let Some(prev) = out.iter_mut().find(|t| (t.pos - pos).abs() < 0.05) { *prev = TabStop { pos, align: align.to_string() }; } else { out.push(TabStop { pos, align: align.to_string() }); } } Some(out) } Some(_) => return Err(bad(CMD, "`set` must be an array")), }; let clear = bool_or(p, "clear", false); let default_tab = finite_key(p, CMD, "defaultTab")?.map(|v| v.clamp(1.0, 1584.0)); let edits_stops = set.is_some() || add.is_some() || mv.is_some() || remove.is_some() || clear; if !edits_stops && default_tab.is_none() { // No fields: open the Tabs dialog on the caret's paragraph. s.ui_requests.push(crate::UiRequest::Dialog { id: "tabs".into(), params: tabs_params(s) }); return ok(); } // The stops as the caret's paragraph shows them (inherited ones included), so editing a // placeholder's tab that came from its layout works like editing one set on the paragraph. let base = current_para(s).tabs.unwrap_or_default(); let near = |a: f64, b: f64| (a - b).abs() < 0.5; let mut stops = if clear { vec![] } else { set.unwrap_or(base) }; if let Some(at) = remove { stops.retain(|t| !near(t.pos, at)); } if let Some((from, to)) = mv && let Some(t) = stops.iter_mut().find(|t| near(t.pos, from)) { t.pos = to; } if let Some((pos, align)) = add { stops.retain(|t| !near(t.pos, pos)); stops.push(TabStop { pos, align }); } stops.sort_by(|a, b| a.pos.total_cmp(&b.pos)); stops.dedup_by(|a, b| near(a.pos, b.pos)); stops.truncate(MAX_TABS); format_para(s, &move |para| { if edits_stops { para.props.tabs = Some(stops.clone()); } if let Some(d) = default_tab { para.props.default_tab = Some(d); } }) }