ravenrs_sheet/engine/cmd/format/
sizes.rs1use super::{
2 CellRef, EngineError, Json, RangeRef, Result, Session, StyleId, bad, edit, f64_param, parse_lines, split_sheet, str_param, target_sheet,
3};
4
5fn line_targets(s: &Session, p: &Json, rows: bool) -> Result<(usize, Vec<(u32, u32)>)> {
12 let (sheet, lines) = resolve_lines(s, p, rows)?;
13 if rows {
15 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Formatting rows", |p| p.format_rows)?;
16 } else {
17 crate::engine::cmd::refuse_unless_allowed(s, sheet, "Formatting columns", |p| p.format_columns)?;
18 }
19 Ok((sheet, lines))
20}
21
22fn resolve_lines(s: &Session, p: &Json, rows: bool) -> Result<(usize, Vec<(u32, u32)>)> {
23 let key = if rows { "rows" } else { "cols" };
24 let mut sheet = target_sheet(s, p)?;
25 if let Some(t) = str_param(p, key) {
26 let (prefix, body) = split_sheet(t);
27 if let Some(name) = prefix {
28 let named = s.doc()?.wb.sheet_index(&name).ok_or_else(|| bad(key, format!("no sheet named `{name}`")))?;
29 if p.get("sheet").is_some_and(|v| !v.is_null()) && named != sheet {
30 return Err(bad(key, format!("`{t}` names a sheet other than `sheet`")));
31 }
32 sheet = named;
33 }
34 let lines = body
36 .split(',')
37 .map(|part| {
38 let r = parse_lines(part).ok_or_else(|| bad(key, format!("not a range: {part}")))?;
39 Ok(if rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) })
40 })
41 .collect::<Result<Vec<_>>>()?;
42 return Ok((sheet, lines));
43 }
44 let d = s.doc()?;
45 if sheet != d.wb.active_sheet() {
46 return Err(bad(key, format!("missing `{key}`: the selection is on the active sheet")));
47 }
48 Ok((sheet, d.selection.ranges.iter().map(|r| if rows { (r.start.row, r.end.row) } else { (r.start.col, r.end.col) }).collect()))
49}
50
51pub(super) fn row_height(s: &mut Session, p: &Json) -> Result<Json> {
55 let h = match (f64_param(p, "height"), f64_param(p, "points")) {
56 (Some(h), _) => {
57 if !(0.0..=546.0).contains(&h) {
58 return Err(EngineError::Other("Row height must be between 0 and 546 pixels (409.5 points).".into()));
59 }
60 h
61 }
62 (None, Some(pt)) => {
63 if !(0.0..=409.5).contains(&pt) {
64 return Err(EngineError::Other("Row height must be between 0 and 409.5 points.".into()));
65 }
66 pt * 4.0 / 3.0
67 }
68 _ => return Err(bad("home.rowHeight", "missing `height` (pixels) or `points`")),
69 };
70 let (si, lines) = line_targets(s, p, true)?;
71 edit(s, |cx| {
72 let sh = cx.sheet_mut(si)?;
73 for (a, b) in lines {
74 for r in a..=b.min(a.saturating_add(1_048_575)) {
75 let e = sh.rows.entry(r).or_default();
76 e.size = Some(h as f32);
77 e.hidden = h == 0.0;
78 e.custom = true;
79 }
80 }
81 cx.structural = true; Ok(Json::Null)
83 })
84}
85
86pub(super) fn col_width(s: &mut Session, p: &Json) -> Result<Json> {
87 let w = match (f64_param(p, "width"), f64_param(p, "chars")) {
88 (Some(w), _) => {
89 if !(0.0..=1800.0).contains(&w) {
90 return Err(EngineError::Other("Column width must be between 0 and 1800 pixels.".into()));
91 }
92 w
93 }
94 (None, Some(c)) => {
95 if !(0.0..=255.0).contains(&c) {
96 return Err(EngineError::Other("Column width must be between 0 and 255 characters.".into()));
97 }
98 chars_to_points(c)
99 }
100 _ => return Err(bad("home.columnWidth", "missing `width` (pixels) or `chars`")),
101 };
102 let (si, lines) = line_targets(s, p, false)?;
103 edit(s, |cx| {
104 let sh = cx.sheet_mut(si)?;
105 for (a, b) in lines {
106 for c in a..=b {
107 let e = sh.cols.entry(c).or_default();
108 e.size = Some(w as f32);
109 e.hidden = w == 0.0;
110 }
111 }
112 Ok(Json::Null)
113 })
114}
115
116pub fn chars_to_points(c: f64) -> f64 {
118 if c <= 0.0 {
119 0.0
120 } else if c < 1.0 {
121 (c * 12.0).round()
122 } else {
123 (c * 7.0 + 5.0).round()
124 }
125}
126
127pub fn approx_text_width(text: &str, size: f32, bold: bool) -> f32 {
130 let mut w = 0.0;
131 for c in text.chars() {
132 w += match c {
133 'i' | 'l' | 'j' | '\'' | '.' | ',' | ':' | ';' | '!' | '|' => 0.28,
134 'f' | 't' | 'r' | 'I' | ' ' | '(' | ')' | '-' => 0.38,
135 'm' | 'w' | 'M' | 'W' => 0.85,
136 c if c.is_ascii_uppercase() => 0.65,
137 c if c.is_ascii_digit() => 0.56,
138 c if c.is_ascii() => 0.52,
139 _ => 1.0,
140 };
141 }
142 w * size * if bold { 1.07 } else { 1.0 } * 96.0 / 72.0
143}
144
145const EXACT_TEXT_WIDTHS: usize = 2_000;
148
149pub(super) fn autofit_cols(s: &mut Session, p: &Json) -> Result<Json> {
150 let (si, lines) = line_targets(s, p, false)?;
151 let measure = s.text_width.clone();
152 edit(s, |cx| {
153 let Some(sh) = cx.wb.sheet(si) else { return Ok(Json::Null) };
154 let mut widths = Vec::new();
155 let mut measured: std::collections::HashMap<StyleId, std::collections::HashMap<String, f32>> = Default::default();
156 let mut distinct = 0usize;
157 let used = sh.used_range();
158 for (a, b) in lines {
159 for c in a..=b.min(a + 16383) {
160 let Some(u) = used else { continue };
161 let col = RangeRef::new(CellRef::new(u.start.row, c), CellRef::new(u.end.row, c));
162 let mut max: f32 = 0.0;
163 for (cell, _) in sh.cells.iter_range(col) {
164 if sh.merge_at(cell).is_some_and(|m| !m.is_single()) {
165 continue;
166 }
167 let id = sh.style_id(cell);
168 let st = cx.wb.styles.get(id);
169 let text = crate::engine::display::cell_text(&cx.wb, sh, cell);
170 for line in text.lines() {
171 let w = match &measure {
172 Some(m) => {
173 let known = measured.entry(id).or_default();
174 match known.get(line) {
175 Some(w) => *w,
176 None => {
177 let w = m(line, &st.font, distinct < EXACT_TEXT_WIDTHS);
178 known.insert(line.to_owned(), w);
179 distinct += 1;
180 w
181 }
182 }
183 }
184 None => approx_text_width(line, st.font.size, st.font.bold),
185 };
186 max = max.max(w + 6.0 + st.align.indent as f32 * 9.0 + sh.filter_button_space(cell));
187 }
188 }
189 if max > 0.0 {
190 widths.push((c, max.clamp(8.0, 1785.0)));
191 }
192 }
193 }
194 let sh = cx.sheet_mut(si)?;
195 for (c, w) in widths {
196 let e = sh.cols.entry(c).or_default();
197 e.size = Some(w.ceil());
198 e.hidden = false;
199 }
200 Ok(Json::Null)
201 })
202}
203
204pub(super) fn autofit_rows(s: &mut Session, p: &Json) -> Result<Json> {
205 let (si, lines) = line_targets(s, p, true)?;
206 edit(s, |cx| {
207 for (a, b) in lines {
208 let b = b.min(a + 100_000);
209 if let Some(sh) = cx.wb.sheet_mut(si) {
210 for r in a..=b {
211 if let Some(e) = sh.rows.get_mut(&r) {
212 e.custom = false;
213 e.hidden = false;
214 }
215 }
216 }
217 cx.fit_rows.extend((a..=b).map(|r| (si, r)));
218 }
219 cx.structural = true; Ok(Json::Null)
221 })
222}
223
224pub(super) fn default_width(s: &mut Session, p: &Json) -> Result<Json> {
225 let w = f64_param(p, "width").ok_or_else(|| bad("home.defaultWidth", "missing `width`"))?;
226 let si = target_sheet(s, p)?;
227 edit(s, |cx| {
228 cx.sheet_mut(si)?.default_col_width = w.clamp(0.0, 1800.0) as f32;
229 Ok(Json::Null)
230 })
231}
232
233pub(super) fn hide(s: &mut Session, p: &Json, rows: bool, on: bool) -> Result<Json> {
234 let (si, lines) = line_targets(s, p, rows)?;
235 edit(s, |cx| {
236 let sh = cx.sheet_mut(si)?;
237 for (a, b) in lines {
238 let (a, b) = if !on && a == b { (a.saturating_sub(1), b + 1) } else { (a, b) };
241 for i in a..=b.min(a.saturating_add(1_048_575)) {
242 let map = if rows { &mut sh.rows } else { &mut sh.cols };
243 if on {
244 map.entry(i).or_default().hidden = true;
245 } else if let Some(e) = map.get_mut(&i) {
246 e.hidden = false;
247 if e.size == Some(0.0) {
248 e.size = None;
249 }
250 }
251 }
252 }
253 cx.structural = true; Ok(Json::Null)
255 })
256}