mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
117 lines
5.4 KiB
Rust
117 lines
5.4 KiB
Rust
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<Value> {
|
|
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<Value> = 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<Value> {
|
|
const CMD: &str = "format.tabs";
|
|
let fin = |v: Option<f64>| 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<TabStop> = 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);
|
|
}
|
|
})
|
|
}
|