Skip to main content

music21_rs/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    /// The first `text_location` cells.
106    pub fn text(&self) -> String {
107        self.cells[..self.text_location].concat()
108    }
109}
110
111/// Braille laid out in lines, with headings, measure numbers and the signs
112/// of a hand at the keyboard: music21's `BrailleText`.
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct BrailleText {
115    /// How many cells a line holds.
116    pub line_length: usize,
117    /// Every line.
118    pub all_lines: Vec<BrailleTextLine>,
119    /// Whether lines of notes start with the right hand's sign.
120    pub right_hand_symbol: bool,
121    /// Whether lines of notes start with the left hand's sign.
122    pub left_hand_symbol: bool,
123    /// Where each heading's lines start and end.
124    pub all_headings: Vec<(usize, usize)>,
125}
126
127impl BrailleText {
128    /// Text of lines this long, with a hand's sign where asked: `right` or
129    /// `left`.
130    ///
131    /// # Errors
132    ///
133    /// A hand music21 does not know.
134    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    /// Which hand's sign the notes' lines start with: music21's `showHand`
148    /// setter. music21's getter never answers what was set.
149    ///
150    /// # Errors
151    ///
152    /// A hand music21 does not know.
153    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    /// The line being written.
164    pub fn current_line(&mut self) -> &mut BrailleTextLine {
165        self.all_lines.last_mut().expect("a text always has a line")
166    }
167
168    /// Starts a new line: music21's `makeNewLine`.
169    pub fn make_new_line(&mut self) {
170        self.all_lines.push(BrailleTextLine::new(self.line_length));
171    }
172
173    /// A heading, a line each, on lines of its own: music21's `addHeading`.
174    ///
175    /// # Errors
176    ///
177    /// A heading line wider than a line.
178    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    /// Words of a long text expression, each where it fits: music21's
196    /// `addLongExpression`.
197    ///
198    /// # Errors
199    ///
200    /// A word wider than a line.
201    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    /// A group of notes on a new line, indented two cells or after the
209    /// hand's sign: music21's `addToNewLine`.
210    ///
211    /// # Errors
212    ///
213    /// A group wider than a line.
214    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    /// Text at the end of the line, or on a new one indented two cells:
225    /// music21's `appendOrInsertCurrent`.
226    ///
227    /// # Errors
228    ///
229    /// Text wider than a line.
230    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    /// Voices written in accord, on the line or a new one: music21's
240    /// `addInaccord`.
241    ///
242    /// # Errors
243    ///
244    /// Voices wider than a line.
245    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    /// A measure number at the start of a line: music21's
271    /// `addMeasureNumber`.
272    ///
273    /// # Errors
274    ///
275    /// A number wider than a line.
276    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    /// A measure number given as a number.
284    ///
285    /// # Errors
286    ///
287    /// A number wider than a line.
288    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    /// The hand's sign and the dots of a group's first cell, before the
294    /// first group of a line, and whether what follows is spaced from it:
295    /// music21's `optionalAddKeyboardSymbolsAndDots`.
296    ///
297    /// # Errors
298    ///
299    /// Signs wider than a line.
300    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    /// Key and time signatures, after a space unless they start the line:
327    /// music21's `addSignatures`.
328    ///
329    /// # Errors
330    ///
331    /// Signatures wider than a line.
332    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    /// Each heading line centred over the widest line of music below it,
338    /// up to the next heading: music21's `recenterHeadings`.
339    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                    // A centred line fits where its widest line does.
355                    let _ = line.insert(0, &centred);
356                    line.text_location = widest;
357                }
358            }
359        }
360    }
361
362    /// The text, headings centred: music21's `__str__`.
363    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/// Braille for the two hands at a keyboard, side by side in lines of each:
374/// music21's `BrailleKeyboard`.
375#[derive(Clone, Debug, PartialEq, Eq)]
376pub struct BrailleKeyboard {
377    /// The text written so far.
378    pub text: BrailleText,
379    /// Which of the text's lines the right hand is on.
380    pub right_hand_line: Option<usize>,
381    /// Which of the text's lines the left hand is on.
382    pub left_hand_line: Option<usize>,
383    /// How many digits the highest measure number has, so the numbers line
384    /// up.
385    pub highest_measure_number_length: usize,
386}
387
388impl BrailleKeyboard {
389    /// A keyboard's text of lines this long.
390    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    /// A line for each hand, the right one on the current line if it is
400    /// empty: music21's `makeNewLines`.
401    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    /// A measure's groups for each hand, on their lines or new ones, each
422    /// line starting with the measure number and the hand's sign: music21's
423    /// `addNoteGroupings`.
424    ///
425    /// # Errors
426    ///
427    /// Groups wider than a line.
428    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}