Skip to main content

music21/braille/
text.rs

1//! Braille laid out in lines of so many cells: music21's `braille.text`.
2
3use super::basic::{center, number_to_braille, python_lines, yield_dots};
4use super::lookup::symbol;
5use crate::error::{Error, Result};
6
7/// One line of braille, written into cell by cell: music21's
8/// `BrailleTextLine`.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct BrailleTextLine {
11    /// Whether the line is part of a heading.
12    pub is_heading: bool,
13    /// Whether a group of notes has been written on it.
14    pub contains_note_grouping: bool,
15    /// How many cells the line holds.
16    pub line_length: usize,
17    cells: Vec<String>,
18    /// Where the next cell is written.
19    pub text_location: usize,
20    /// How far the line has been written.
21    pub highest_used_location: usize,
22}
23
24fn refuse(message: &str) -> Error {
25    Error::Notation(message.to_string())
26}
27
28impl BrailleTextLine {
29    /// An empty line of so many cells.
30    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    /// Writes text at the end, after a space where asked: music21's
42    /// `append`.
43    ///
44    /// # Errors
45    ///
46    /// Text that does not fit.
47    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    /// Writes text from a cell on, the line then ending after it: music21's
64    /// `insert`.
65    ///
66    /// # Errors
67    ///
68    /// Text that does not fit there.
69    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    /// Whether text fits at the end, after a space where asked: music21's
82    /// `canAppend`.
83    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    /// Whether text fits from a cell on: music21's `canInsert`.
89    pub fn can_insert(&self, location: usize, text: &str) -> bool {
90        location + text.chars().count() <= self.line_length
91    }
92
93    /// Takes back a music hyphen ending the line: music21's
94    /// `lastHyphenToSpace`.
95    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    /// Every cell of the line, written or not: music21's `allChars`,
106    /// which its own examples compare against a line of spaces.
107    pub fn cells(&self) -> &[String] {
108        &self.cells
109    }
110
111    /// The first `text_location` cells.
112    pub fn text(&self) -> String {
113        self.cells[..self.text_location].concat()
114    }
115}
116
117/// Braille laid out in lines, with headings, measure numbers and the signs
118/// of a hand at the keyboard: music21's `BrailleText`.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct BrailleText {
121    /// How many cells a line holds.
122    pub line_length: usize,
123    /// Every line.
124    pub all_lines: Vec<BrailleTextLine>,
125    /// Whether lines of notes start with the right hand's sign.
126    pub right_hand_symbol: bool,
127    /// Whether lines of notes start with the left hand's sign.
128    pub left_hand_symbol: bool,
129    /// Where each heading's lines start and end.
130    pub all_headings: Vec<(usize, usize)>,
131    show_hand: Option<&'static str>,
132}
133
134impl BrailleText {
135    /// Text of lines this long, with a hand's sign where asked: `right` or
136    /// `left`.
137    ///
138    /// # Errors
139    ///
140    /// A hand music21 does not know.
141    pub fn new(line_length: usize, show_hand: Option<&str>) -> Result<Self> {
142        let mut text = Self {
143            line_length,
144            all_lines: Vec::new(),
145            right_hand_symbol: false,
146            left_hand_symbol: false,
147            all_headings: Vec::new(),
148            show_hand: None,
149        };
150        text.make_new_line();
151        text.set_show_hand(show_hand)?;
152        Ok(text)
153    }
154
155    /// Which hand's sign the notes' lines start with, `right`, `left` or
156    /// none: music21's `showHand` setter. Only that hand's sign is kept.
157    ///
158    /// # Errors
159    ///
160    /// A hand music21 does not know; the signs are then left as they were.
161    pub fn set_show_hand(&mut self, hand: Option<&str>) -> Result<()> {
162        self.show_hand = match hand {
163            Some("right") => Some("right"),
164            Some("left") => Some("left"),
165            None => None,
166            Some(_) => return Err(refuse("Illegal hand sign request.")),
167        };
168        self.right_hand_symbol = hand == Some("right");
169        self.left_hand_symbol = hand == Some("left");
170        Ok(())
171    }
172
173    /// The hand last set with [`Self::set_show_hand`]: music21's `showHand`.
174    pub fn show_hand(&self) -> Option<&'static str> {
175        self.show_hand
176    }
177
178    /// The line being written.
179    pub fn current_line(&mut self) -> &mut BrailleTextLine {
180        self.all_lines.last_mut().expect("a text always has a line")
181    }
182
183    /// Starts a new line: music21's `makeNewLine`.
184    pub fn make_new_line(&mut self) {
185        self.all_lines.push(BrailleTextLine::new(self.line_length));
186    }
187
188    /// A heading, a line each, on lines of its own: music21's `addHeading`.
189    ///
190    /// # Errors
191    ///
192    /// A heading line wider than a line.
193    pub fn add_heading(&mut self, heading: &str) -> Result<()> {
194        if self.current_line().text_location != 0 {
195            self.make_new_line();
196        }
197        let start = self.all_lines.len() - 1;
198        let mut end = start;
199        for line in python_lines(heading) {
200            let current = self.current_line();
201            current.is_heading = true;
202            current.append(&line, false)?;
203            self.make_new_line();
204            end += 1;
205        }
206        self.all_headings.push((start, end));
207        Ok(())
208    }
209
210    /// Words of a long text expression, each where it fits: music21's
211    /// `addLongExpression`.
212    ///
213    /// # Errors
214    ///
215    /// A word wider than a line.
216    pub fn add_long_expression(&mut self, expression: &str) -> Result<()> {
217        for word in expression.split(&symbol("space")) {
218            self.append_or_insert_current(word, true)?;
219        }
220        Ok(())
221    }
222
223    /// A group of notes on a new line, indented two cells or after the
224    /// hand's sign: music21's `addToNewLine`.
225    ///
226    /// # Errors
227    ///
228    /// A group wider than a line.
229    pub fn add_to_new_line(&mut self, grouping: &str) -> Result<()> {
230        self.make_new_line();
231        if self.right_hand_symbol || self.left_hand_symbol {
232            self.optional_add_keyboard_symbols_and_dots(Some(grouping))?;
233            self.current_line().append(grouping, false)
234        } else {
235            self.current_line().insert(2, grouping)
236        }
237    }
238
239    /// Text at the end of the line, or on a new one indented two cells:
240    /// music21's `appendOrInsertCurrent`.
241    ///
242    /// # Errors
243    ///
244    /// Text wider than a line.
245    pub fn append_or_insert_current(&mut self, text: &str, add_space: bool) -> Result<()> {
246        if self.current_line().can_append(text, add_space) {
247            self.current_line().append(text, add_space)
248        } else {
249            self.make_new_line();
250            self.current_line().insert(2, text)
251        }
252    }
253
254    /// Voices written in accord, on the line or a new one: music21's
255    /// `addInaccord`.
256    ///
257    /// # Errors
258    ///
259    /// Voices wider than a line.
260    pub fn add_inaccord(&mut self, inaccord: &str) -> Result<()> {
261        let add_space = self.optional_add_keyboard_symbols_and_dots(Some(inaccord))?;
262        if self.current_line().append(inaccord, add_space).is_err() {
263            self.make_new_line();
264            if self.right_hand_symbol || self.left_hand_symbol {
265                let sign = symbol(if self.right_hand_symbol {
266                    "rh_keyboard"
267                } else {
268                    "lh_keyboard"
269                });
270                self.current_line().insert(2, &sign)?;
271                if let Some(first) = inaccord.chars().next() {
272                    for dot in yield_dots(first) {
273                        self.current_line().append(&dot, false)?;
274                    }
275                }
276                self.current_line().append(inaccord, false)?;
277            } else {
278                self.current_line().insert(2, inaccord)?;
279            }
280        }
281        self.current_line().contains_note_grouping = true;
282        Ok(())
283    }
284
285    /// A measure number at the start of a line: music21's
286    /// `addMeasureNumber`.
287    ///
288    /// # Errors
289    ///
290    /// A number wider than a line.
291    pub fn add_measure_number(&mut self, number: &str) -> Result<()> {
292        if self.current_line().text_location != 0 {
293            self.make_new_line();
294        }
295        self.current_line().append(number, false)
296    }
297
298    /// A measure number given as a number.
299    ///
300    /// # Errors
301    ///
302    /// A number wider than a line.
303    pub fn add_measure_number_value(&mut self, number: i64) -> Result<()> {
304        let braille = number_to_braille(&number.to_string(), true, false)?;
305        self.add_measure_number(&braille)
306    }
307
308    /// The hand's sign and the dots of a group's first cell, before the
309    /// first group of a line, and whether what follows is spaced from it:
310    /// music21's `optionalAddKeyboardSymbolsAndDots`.
311    ///
312    /// # Errors
313    ///
314    /// Signs wider than a line.
315    pub fn optional_add_keyboard_symbols_and_dots(
316        &mut self,
317        grouping: Option<&str>,
318    ) -> Result<bool> {
319        let mut add_space = true;
320        let (right, left) = (self.right_hand_symbol, self.left_hand_symbol);
321        let line = self.current_line();
322        if !line.contains_note_grouping && (right || left) {
323            if line.text_location == 0 {
324                add_space = false;
325            }
326            let sign = symbol(if right { "rh_keyboard" } else { "lh_keyboard" });
327            line.append(&sign, add_space)?;
328            if let Some(first) = grouping.and_then(|grouping| grouping.chars().next()) {
329                for dot in yield_dots(first) {
330                    line.append(&dot, false)?;
331                }
332            }
333            add_space = false;
334        }
335        if line.text_location == 0 {
336            add_space = false;
337        }
338        Ok(add_space)
339    }
340
341    /// Key and time signatures, after a space unless they start the line:
342    /// music21's `addSignatures`.
343    ///
344    /// # Errors
345    ///
346    /// Signatures wider than a line.
347    pub fn add_signatures(&mut self, signatures: &str) -> Result<()> {
348        let add_space = self.current_line().text_location != 0;
349        self.append_or_insert_current(signatures, add_space)
350    }
351
352    /// Each heading line centred over the widest line of music below it,
353    /// up to the next heading: music21's `recenterHeadings`.
354    pub fn recenter_headings(&mut self) {
355        let space = symbol("space");
356        for (start, end) in self.all_headings.clone() {
357            let mut widest = 0;
358            for line in &self.all_lines[end..] {
359                if line.is_heading {
360                    break;
361                }
362                widest = widest.max(line.text_location);
363            }
364            for line in &mut self.all_lines[start..end] {
365                let text = line.text();
366                let trimmed = text.trim_matches(|cell: char| cell.to_string() == space);
367                if widest > trimmed.chars().count() {
368                    let centred = center(trimmed, widest, &space);
369                    // A centred line fits where its widest line does.
370                    let _ = line.insert(0, &centred);
371                    line.text_location = widest;
372                }
373            }
374        }
375    }
376
377    /// The text, headings centred: music21's `__str__`.
378    pub fn render(&mut self) -> String {
379        self.recenter_headings();
380        self.all_lines
381            .iter()
382            .map(BrailleTextLine::text)
383            .collect::<Vec<_>>()
384            .join("\n")
385    }
386}
387
388/// Braille for the two hands at a keyboard, side by side in lines of each:
389/// music21's `BrailleKeyboard`.
390#[derive(Clone, Debug, PartialEq, Eq)]
391pub struct BrailleKeyboard {
392    /// The text written so far.
393    pub text: BrailleText,
394    /// Which of the text's lines the right hand is on.
395    pub right_hand_line: Option<usize>,
396    /// Which of the text's lines the left hand is on.
397    pub left_hand_line: Option<usize>,
398    /// How many digits the highest measure number has, so the numbers line
399    /// up.
400    pub highest_measure_number_length: usize,
401}
402
403impl BrailleKeyboard {
404    /// A keyboard's text of lines this long.
405    pub fn new(line_length: usize) -> Self {
406        Self {
407            text: BrailleText::new(line_length, None).expect("no hand is always allowed"),
408            right_hand_line: None,
409            left_hand_line: None,
410            highest_measure_number_length: 0,
411        }
412    }
413
414    /// A line for each hand, the right one on the current line if it is
415    /// empty: music21's `makeNewLines`.
416    pub fn make_new_lines(&mut self) {
417        let length = self.text.line_length;
418        if self.text.current_line().text_location == 0 {
419            self.right_hand_line = Some(self.text.all_lines.len() - 1);
420        } else {
421            self.text.all_lines.push(BrailleTextLine::new(length));
422            self.right_hand_line = Some(self.text.all_lines.len() - 1);
423        }
424        self.text.all_lines.push(BrailleTextLine::new(length));
425        self.left_hand_line = Some(self.text.all_lines.len() - 1);
426    }
427
428    fn lines(&mut self) -> (&mut BrailleTextLine, &mut BrailleTextLine) {
429        let right = self.right_hand_line.expect("the hands have lines");
430        let left = self.left_hand_line.expect("the hands have lines");
431        assert!(right < left, "the right hand's line is above the left's");
432        let (above, below) = self.text.all_lines.split_at_mut(left);
433        (&mut above[right], &mut below[0])
434    }
435
436    /// A measure's groups for each hand, on their lines or new ones, each
437    /// line starting with the measure number and the hand's sign: music21's
438    /// `addNoteGroupings`.
439    ///
440    /// # Errors
441    ///
442    /// Groups wider than a line.
443    pub fn add_note_groupings(
444        &mut self,
445        measure_number: &str,
446        right: &str,
447        left: &str,
448    ) -> Result<()> {
449        if self.right_hand_line.is_none() && self.left_hand_line.is_none() {
450            self.make_new_lines();
451        }
452        let indent = self
453            .highest_measure_number_length
454            .saturating_sub(measure_number.chars().count());
455        let (right_line, left_line) = self.lines();
456        if right_line.text_location == 0 {
457            right_line.insert(indent, measure_number)?;
458            left_line.text_location = right_line.text_location;
459        }
460        let mut add_space = true;
461        if !right_line.contains_note_grouping {
462            add_space = false;
463            right_line.append(&symbol("rh_keyboard"), true)?;
464            left_line.append(&symbol("lh_keyboard"), true)?;
465            if let Some(first) = right.chars().next() {
466                for dot in yield_dots(first) {
467                    right_line.append(&dot, false)?;
468                }
469            }
470            if let Some(first) = left.chars().next() {
471                for dot in yield_dots(first) {
472                    left_line.append(&dot, false)?;
473                }
474            }
475        }
476        if right_line.can_append(right, add_space) && left_line.can_append(left, add_space) {
477            if !left.is_empty() {
478                left_line.append(left, add_space)?;
479            }
480            if !right.is_empty() {
481                right_line.append(right, add_space)?;
482            }
483        } else {
484            self.make_new_lines();
485            let (right_line, left_line) = self.lines();
486            right_line.insert(indent, measure_number)?;
487            left_line.text_location = right_line.text_location;
488            right_line.append(&symbol("rh_keyboard"), true)?;
489            left_line.append(&symbol("lh_keyboard"), true)?;
490            if let Some(first) = right.chars().next() {
491                for dot in yield_dots(first) {
492                    right_line.append(&dot, false)?;
493                }
494                right_line.append(right, false)?;
495            }
496            if let Some(first) = left.chars().next() {
497                for dot in yield_dots(first) {
498                    left_line.append(&dot, false)?;
499                }
500                left_line.append(left, false)?;
501            }
502        }
503        let (right_line, left_line) = self.lines();
504        if right_line.text_location > left_line.text_location {
505            left_line.text_location = right_line.text_location;
506        } else {
507            right_line.text_location = left_line.text_location;
508        }
509        right_line.contains_note_grouping = true;
510        left_line.contains_note_grouping = true;
511        Ok(())
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    #[test]
520    fn lines_take_what_fits_and_headings_are_centred() -> Result<()> {
521        let mut text = BrailleText::new(10, None)?;
522        text.add_heading("⠁⠃")?;
523        text.add_measure_number("⠼⠁")?;
524        text.append_or_insert_current("⠉⠙⠑⠋", true)?;
525        text.append_or_insert_current("⠛⠓⠊⠚", true)?;
526        assert_eq!(text.render(), "⠀⠀⠀⠁⠃⠀⠀\n⠼⠁⠀⠉⠙⠑⠋\n⠀⠀⠛⠓⠊⠚");
527        Ok(())
528    }
529}