1use super::basic::{center, number_to_braille, python_lines, yield_dots};
4use super::lookup::symbol;
5use crate::error::{Error, Result};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct BrailleTextLine {
11 pub is_heading: bool,
13 pub contains_note_grouping: bool,
15 pub line_length: usize,
17 cells: Vec<String>,
18 pub text_location: usize,
20 pub highest_used_location: usize,
22}
23
24fn refuse(message: &str) -> Error {
25 Error::Notation(message.to_string())
26}
27
28impl BrailleTextLine {
29 pub fn new(line_length: usize) -> Self {
31 Self {
32 is_heading: false,
33 contains_note_grouping: false,
34 line_length,
35 cells: vec![symbol("space"); line_length],
36 text_location: 0,
37 highest_used_location: 0,
38 }
39 }
40
41 pub fn append(&mut self, text: &str, add_space: bool) -> Result<()> {
48 if !self.can_append(text, add_space) {
49 return Err(refuse("Text does not fit at end of braille text line."));
50 }
51 if add_space {
52 self.cells[self.text_location] = symbol("space");
53 self.text_location += 1;
54 }
55 for cell in text.chars() {
56 self.cells[self.text_location] = cell.to_string();
57 self.text_location += 1;
58 }
59 self.highest_used_location = self.text_location;
60 Ok(())
61 }
62
63 pub fn insert(&mut self, location: usize, text: &str) -> Result<()> {
70 if !self.can_insert(location, text) {
71 return Err(refuse("Text cannot be inserted at specified location."));
72 }
73 for (at, cell) in (location..).zip(text.chars()) {
74 self.cells[at] = cell.to_string();
75 }
76 self.text_location = location + text.chars().count();
77 self.highest_used_location = self.highest_used_location.max(self.text_location);
78 Ok(())
79 }
80
81 pub fn can_append(&self, text: &str, add_space: bool) -> bool {
84 let search = self.highest_used_location.max(self.text_location);
85 search + text.chars().count() + usize::from(add_space) <= self.line_length
86 }
87
88 pub fn can_insert(&self, location: usize, text: &str) -> bool {
90 location + text.chars().count() <= self.line_length
91 }
92
93 pub fn last_hyphen_to_space(&mut self) {
96 let Some(previous) = self.text_location.checked_sub(1) else {
97 return;
98 };
99 if self.cells[previous] == symbol("music_hyphen") {
100 self.cells[previous] = symbol("space");
101 self.text_location -= 1;
102 }
103 }
104
105 pub fn text(&self) -> String {
107 self.cells[..self.text_location].concat()
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct BrailleText {
115 pub line_length: usize,
117 pub all_lines: Vec<BrailleTextLine>,
119 pub right_hand_symbol: bool,
121 pub left_hand_symbol: bool,
123 pub all_headings: Vec<(usize, usize)>,
125}
126
127impl BrailleText {
128 pub fn new(line_length: usize, show_hand: Option<&str>) -> Result<Self> {
135 let mut text = Self {
136 line_length,
137 all_lines: Vec::new(),
138 right_hand_symbol: false,
139 left_hand_symbol: false,
140 all_headings: Vec::new(),
141 };
142 text.make_new_line();
143 text.set_show_hand(show_hand)?;
144 Ok(text)
145 }
146
147 pub fn set_show_hand(&mut self, hand: Option<&str>) -> Result<()> {
154 match hand {
155 Some("right") => self.right_hand_symbol = true,
156 Some("left") => self.left_hand_symbol = true,
157 None => {}
158 Some(_) => return Err(refuse("Illegal hand sign request.")),
159 }
160 Ok(())
161 }
162
163 pub fn current_line(&mut self) -> &mut BrailleTextLine {
165 self.all_lines.last_mut().expect("a text always has a line")
166 }
167
168 pub fn make_new_line(&mut self) {
170 self.all_lines.push(BrailleTextLine::new(self.line_length));
171 }
172
173 pub fn add_heading(&mut self, heading: &str) -> Result<()> {
179 if self.current_line().text_location != 0 {
180 self.make_new_line();
181 }
182 let start = self.all_lines.len() - 1;
183 let mut end = start;
184 for line in python_lines(heading) {
185 let current = self.current_line();
186 current.is_heading = true;
187 current.append(&line, false)?;
188 self.make_new_line();
189 end += 1;
190 }
191 self.all_headings.push((start, end));
192 Ok(())
193 }
194
195 pub fn add_long_expression(&mut self, expression: &str) -> Result<()> {
202 for word in expression.split(&symbol("space")) {
203 self.append_or_insert_current(word, true)?;
204 }
205 Ok(())
206 }
207
208 pub fn add_to_new_line(&mut self, grouping: &str) -> Result<()> {
215 self.make_new_line();
216 if self.right_hand_symbol || self.left_hand_symbol {
217 self.optional_add_keyboard_symbols_and_dots(Some(grouping))?;
218 self.current_line().append(grouping, false)
219 } else {
220 self.current_line().insert(2, grouping)
221 }
222 }
223
224 pub fn append_or_insert_current(&mut self, text: &str, add_space: bool) -> Result<()> {
231 if self.current_line().can_append(text, add_space) {
232 self.current_line().append(text, add_space)
233 } else {
234 self.make_new_line();
235 self.current_line().insert(2, text)
236 }
237 }
238
239 pub fn add_inaccord(&mut self, inaccord: &str) -> Result<()> {
246 let add_space = self.optional_add_keyboard_symbols_and_dots(Some(inaccord))?;
247 if self.current_line().append(inaccord, add_space).is_err() {
248 self.make_new_line();
249 if self.right_hand_symbol || self.left_hand_symbol {
250 let sign = symbol(if self.right_hand_symbol {
251 "rh_keyboard"
252 } else {
253 "lh_keyboard"
254 });
255 self.current_line().insert(2, &sign)?;
256 if let Some(first) = inaccord.chars().next() {
257 for dot in yield_dots(first) {
258 self.current_line().append(&dot, false)?;
259 }
260 }
261 self.current_line().append(inaccord, false)?;
262 } else {
263 self.current_line().insert(2, inaccord)?;
264 }
265 }
266 self.current_line().contains_note_grouping = true;
267 Ok(())
268 }
269
270 pub fn add_measure_number(&mut self, number: &str) -> Result<()> {
277 if self.current_line().text_location != 0 {
278 self.make_new_line();
279 }
280 self.current_line().append(number, false)
281 }
282
283 pub fn add_measure_number_value(&mut self, number: i64) -> Result<()> {
289 let braille = number_to_braille(&number.to_string(), true, false)?;
290 self.add_measure_number(&braille)
291 }
292
293 pub fn optional_add_keyboard_symbols_and_dots(
301 &mut self,
302 grouping: Option<&str>,
303 ) -> Result<bool> {
304 let mut add_space = true;
305 let (right, left) = (self.right_hand_symbol, self.left_hand_symbol);
306 let line = self.current_line();
307 if !line.contains_note_grouping && (right || left) {
308 if line.text_location == 0 {
309 add_space = false;
310 }
311 let sign = symbol(if right { "rh_keyboard" } else { "lh_keyboard" });
312 line.append(&sign, add_space)?;
313 if let Some(first) = grouping.and_then(|grouping| grouping.chars().next()) {
314 for dot in yield_dots(first) {
315 line.append(&dot, false)?;
316 }
317 }
318 add_space = false;
319 }
320 if line.text_location == 0 {
321 add_space = false;
322 }
323 Ok(add_space)
324 }
325
326 pub fn add_signatures(&mut self, signatures: &str) -> Result<()> {
333 let add_space = self.current_line().text_location != 0;
334 self.append_or_insert_current(signatures, add_space)
335 }
336
337 pub fn recenter_headings(&mut self) {
340 let space = symbol("space");
341 for (start, end) in self.all_headings.clone() {
342 let mut widest = 0;
343 for line in &self.all_lines[end..] {
344 if line.is_heading {
345 break;
346 }
347 widest = widest.max(line.text_location);
348 }
349 for line in &mut self.all_lines[start..end] {
350 let text = line.text();
351 let trimmed = text.trim_matches(|cell: char| cell.to_string() == space);
352 if widest > trimmed.chars().count() {
353 let centred = center(trimmed, widest, &space);
354 let _ = line.insert(0, ¢red);
356 line.text_location = widest;
357 }
358 }
359 }
360 }
361
362 pub fn render(&mut self) -> String {
364 self.recenter_headings();
365 self.all_lines
366 .iter()
367 .map(BrailleTextLine::text)
368 .collect::<Vec<_>>()
369 .join("\n")
370 }
371}
372
373#[derive(Clone, Debug, PartialEq, Eq)]
376pub struct BrailleKeyboard {
377 pub text: BrailleText,
379 pub right_hand_line: Option<usize>,
381 pub left_hand_line: Option<usize>,
383 pub highest_measure_number_length: usize,
386}
387
388impl BrailleKeyboard {
389 pub fn new(line_length: usize) -> Self {
391 Self {
392 text: BrailleText::new(line_length, None).expect("no hand is always allowed"),
393 right_hand_line: None,
394 left_hand_line: None,
395 highest_measure_number_length: 0,
396 }
397 }
398
399 pub fn make_new_lines(&mut self) {
402 let length = self.text.line_length;
403 if self.text.current_line().text_location == 0 {
404 self.right_hand_line = Some(self.text.all_lines.len() - 1);
405 } else {
406 self.text.all_lines.push(BrailleTextLine::new(length));
407 self.right_hand_line = Some(self.text.all_lines.len() - 1);
408 }
409 self.text.all_lines.push(BrailleTextLine::new(length));
410 self.left_hand_line = Some(self.text.all_lines.len() - 1);
411 }
412
413 fn lines(&mut self) -> (&mut BrailleTextLine, &mut BrailleTextLine) {
414 let right = self.right_hand_line.expect("the hands have lines");
415 let left = self.left_hand_line.expect("the hands have lines");
416 assert!(right < left, "the right hand's line is above the left's");
417 let (above, below) = self.text.all_lines.split_at_mut(left);
418 (&mut above[right], &mut below[0])
419 }
420
421 pub fn add_note_groupings(
429 &mut self,
430 measure_number: &str,
431 right: &str,
432 left: &str,
433 ) -> Result<()> {
434 if self.right_hand_line.is_none() && self.left_hand_line.is_none() {
435 self.make_new_lines();
436 }
437 let indent = self
438 .highest_measure_number_length
439 .saturating_sub(measure_number.chars().count());
440 let (right_line, left_line) = self.lines();
441 if right_line.text_location == 0 {
442 right_line.insert(indent, measure_number)?;
443 left_line.text_location = right_line.text_location;
444 }
445 let mut add_space = true;
446 if !right_line.contains_note_grouping {
447 add_space = false;
448 right_line.append(&symbol("rh_keyboard"), true)?;
449 left_line.append(&symbol("lh_keyboard"), true)?;
450 if let Some(first) = right.chars().next() {
451 for dot in yield_dots(first) {
452 right_line.append(&dot, false)?;
453 }
454 }
455 if let Some(first) = left.chars().next() {
456 for dot in yield_dots(first) {
457 left_line.append(&dot, false)?;
458 }
459 }
460 }
461 if right_line.can_append(right, add_space) && left_line.can_append(left, add_space) {
462 if !left.is_empty() {
463 left_line.append(left, add_space)?;
464 }
465 if !right.is_empty() {
466 right_line.append(right, add_space)?;
467 }
468 } else {
469 self.make_new_lines();
470 let (right_line, left_line) = self.lines();
471 right_line.insert(indent, measure_number)?;
472 left_line.text_location = right_line.text_location;
473 right_line.append(&symbol("rh_keyboard"), true)?;
474 left_line.append(&symbol("lh_keyboard"), true)?;
475 if let Some(first) = right.chars().next() {
476 for dot in yield_dots(first) {
477 right_line.append(&dot, false)?;
478 }
479 right_line.append(right, false)?;
480 }
481 if let Some(first) = left.chars().next() {
482 for dot in yield_dots(first) {
483 left_line.append(&dot, false)?;
484 }
485 left_line.append(left, false)?;
486 }
487 }
488 let (right_line, left_line) = self.lines();
489 if right_line.text_location > left_line.text_location {
490 left_line.text_location = right_line.text_location;
491 } else {
492 right_line.text_location = left_line.text_location;
493 }
494 right_line.contains_note_grouping = true;
495 left_line.contains_note_grouping = true;
496 Ok(())
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn lines_take_what_fits_and_headings_are_centred() -> Result<()> {
506 let mut text = BrailleText::new(10, None)?;
507 text.add_heading("⠁⠃")?;
508 text.add_measure_number("⠼⠁")?;
509 text.append_or_insert_current("⠉⠙⠑⠋", true)?;
510 text.append_or_insert_current("⠛⠓⠊⠚", true)?;
511 assert_eq!(text.render(), "⠀⠀⠀⠁⠃⠀⠀\n⠼⠁⠀⠉⠙⠑⠋\n⠀⠀⠛⠓⠊⠚");
512 Ok(())
513 }
514}