Skip to main content

music21_rs/
layout.rs

1//! How a score is laid out on the page: music21's `layout` objects.
2//!
3//! A [`ScoreLayout`] gives the scaling and the page, system and staff
4//! layouts a score starts with, and stands in the score itself; a
5//! [`PageLayout`], [`SystemLayout`] or [`StaffLayout`] stands in a measure
6//! and says how the page, system or staff changes there, or that a new page
7//! or system starts. They are read from and written to MusicXML's
8//! `<defaults>`, `<print>` and `<staff-details>`. Margins and distances are
9//! in tenths of a staff space, as MusicXML gives them, kept as the
10//! integer or the decimal the file wrote.
11
12use crate::defaults::FloatType;
13
14/// A length as MusicXML writes it: a whole number, or one with a fraction.
15/// music21 keeps `120` an `int` and `120.5` a `float`, and writes each back
16/// as it was.
17#[derive(Clone, Copy, Debug, PartialEq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum Tenths {
20    /// A whole number.
21    Whole(i64),
22    /// A number with a fraction.
23    Fraction(FloatType),
24}
25
26impl Tenths {
27    /// The length as a number.
28    pub fn value(self) -> FloatType {
29        match self {
30            Self::Whole(whole) => whole as FloatType,
31            Self::Fraction(value) => value,
32        }
33    }
34
35    /// music21's `_floatOrIntStr`: the number the text says, whole where
36    /// it has no fraction -- `120.0` is `120` -- and nothing where the text
37    /// is no number.
38    pub fn parse(text: &str) -> Option<Self> {
39        let value = text.trim().parse::<FloatType>().ok()?;
40        if value.is_finite() && value.fract() == 0.0 && value.abs() < 9.0e15 {
41            Some(Self::Whole(value as i64))
42        } else {
43            Some(Self::Fraction(value))
44        }
45    }
46}
47
48impl std::fmt::Display for Tenths {
49    /// As Python writes the `int` or `float`.
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Self::Whole(whole) => write!(f, "{whole}"),
53            Self::Fraction(value) if value.fract() == 0.0 && value.is_finite() => {
54                write!(f, "{value:.1}")
55            }
56            Self::Fraction(value) => write!(f, "{value}"),
57        }
58    }
59}
60
61/// The scaling and the layouts a score starts with: music21's
62/// `ScoreLayout`, read from MusicXML's `<defaults>`.
63#[derive(Clone, Debug, Default, PartialEq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct ScoreLayout {
66    /// How many millimetres [`ScoreLayout::scaling_tenths`] tenths are:
67    /// `scalingMillimeters`.
68    pub scaling_millimeters: Option<Tenths>,
69    /// How many tenths [`ScoreLayout::scaling_millimeters`] millimetres
70    /// are: `scalingTenths`.
71    pub scaling_tenths: Option<Tenths>,
72    /// The page the score starts on: `pageLayout`.
73    pub page_layout: Option<PageLayout>,
74    /// The systems the score starts with: `systemLayout`.
75    pub system_layout: Option<SystemLayout>,
76    /// The staves the score starts with: `staffLayoutList`.
77    pub staff_layouts: Vec<StaffLayout>,
78}
79
80impl ScoreLayout {
81    /// How many millimetres a number of tenths is, rounded to six places,
82    /// or nought where the scaling is not known: music21's
83    /// `tenthsToMillimeters`.
84    pub fn tenths_to_millimeters(&self, tenths: FloatType) -> FloatType {
85        let (Some(millimeters), Some(per)) = (self.scaling_millimeters, self.scaling_tenths) else {
86            return 0.0;
87        };
88        let value = millimeters.value() / per.value() * tenths;
89        (value * 1e6).round() / 1e6
90    }
91}
92
93/// How a page is laid out, and whether one starts here: music21's
94/// `PageLayout`, MusicXML's `<print new-page>` and `<page-layout>`.
95#[derive(Clone, Debug, Default, PartialEq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97pub struct PageLayout {
98    /// The page's number: `pageNumber`.
99    pub page_number: Option<i64>,
100    /// `leftMargin`.
101    pub left_margin: Option<Tenths>,
102    /// `rightMargin`.
103    pub right_margin: Option<Tenths>,
104    /// `topMargin`.
105    pub top_margin: Option<Tenths>,
106    /// `bottomMargin`.
107    pub bottom_margin: Option<Tenths>,
108    /// `pageHeight`.
109    pub page_height: Option<Tenths>,
110    /// `pageWidth`.
111    pub page_width: Option<Tenths>,
112    /// Whether a new page starts here: `isNew`.
113    pub is_new: Option<bool>,
114}
115
116/// How a system is laid out, and whether one starts here: music21's
117/// `SystemLayout`, MusicXML's `<print new-system>` and `<system-layout>`.
118#[derive(Clone, Debug, Default, PartialEq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub struct SystemLayout {
121    /// `leftMargin`.
122    pub left_margin: Option<Tenths>,
123    /// `rightMargin`.
124    pub right_margin: Option<Tenths>,
125    /// `topMargin`.
126    pub top_margin: Option<Tenths>,
127    /// `bottomMargin`.
128    pub bottom_margin: Option<Tenths>,
129    /// How far the system stands from the one before: `distance`.
130    pub distance: Option<Tenths>,
131    /// How far the first system of a page stands from its top:
132    /// `topDistance`.
133    pub top_distance: Option<Tenths>,
134    /// Whether a new system starts here: `isNew`.
135    pub is_new: Option<bool>,
136}
137
138/// What a staff is for: music21's `StaffType`.
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub enum StaffType {
142    /// An ordinary staff.
143    #[default]
144    Regular,
145    /// One the player need not play.
146    Ossia,
147    /// Cue notes.
148    Cue,
149    /// An editorial addition.
150    Editorial,
151    /// Notes for another instrument the player reads.
152    Alternate,
153    /// Anything else.
154    Other,
155}
156
157impl StaffType {
158    /// The value MusicXML writes in `<staff-type>`.
159    pub fn as_str(self) -> &'static str {
160        match self {
161            Self::Regular => "regular",
162            Self::Ossia => "ossia",
163            Self::Cue => "cue",
164            Self::Editorial => "editorial",
165            Self::Alternate => "alternate",
166            Self::Other => "other",
167        }
168    }
169
170    /// The staff type a `<staff-type>` names.
171    pub fn from_name(name: &str) -> Option<Self> {
172        Some(match name {
173            "regular" => Self::Regular,
174            "ossia" => Self::Ossia,
175            "cue" => Self::Cue,
176            "editorial" => Self::Editorial,
177            "alternate" => Self::Alternate,
178            "other" => Self::Other,
179            _ => return None,
180        })
181    }
182}
183
184/// How a staff stands from the one above it and what it is: music21's
185/// `StaffLayout`, which MusicXML splits into `<staff-layout>` and
186/// `<staff-details>`.
187#[derive(Clone, Debug, Default, PartialEq)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub struct StaffLayout {
190    /// How far the staff stands from the one above: `distance`.
191    pub distance: Option<Tenths>,
192    /// Which staff of its part it is, where it says: `staffNumber`.
193    pub staff_number: Option<i64>,
194    /// Its size, a percentage of the ordinary staff's: `staffSize`.
195    pub staff_size: Option<FloatType>,
196    /// How many lines it has, where not five: `staffLines`.
197    pub staff_lines: Option<i64>,
198    /// Whether it is hidden: `hidden`, nothing to inherit it.
199    pub hidden: Option<bool>,
200    /// What it is for: `staffType`.
201    pub staff_type: StaffType,
202}
203
204/// One of music21's layout objects, as it stands in a stream.
205#[derive(Clone, Debug, PartialEq)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
207pub enum Layout {
208    /// A [`ScoreLayout`].
209    Score(Box<ScoreLayout>),
210    /// A [`PageLayout`].
211    Page(PageLayout),
212    /// A [`SystemLayout`].
213    System(SystemLayout),
214    /// A [`StaffLayout`].
215    Staff(StaffLayout),
216}
217
218impl Layout {
219    /// The music21 class it is.
220    pub fn class_name(&self) -> &'static str {
221        match self {
222            Self::Score(_) => "ScoreLayout",
223            Self::Page(_) => "PageLayout",
224            Self::System(_) => "SystemLayout",
225            Self::Staff(_) => "StaffLayout",
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn a_length_is_read_and_written_as_music21_keeps_it() {
236        assert_eq!(Tenths::parse("120"), Some(Tenths::Whole(120)));
237        assert_eq!(Tenths::parse("30.25"), Some(Tenths::Fraction(30.25)));
238        assert_eq!(Tenths::parse("120.0"), Some(Tenths::Whole(120)));
239        assert_eq!(Tenths::parse("x"), None);
240        assert_eq!(Tenths::Whole(120).to_string(), "120");
241        assert_eq!(Tenths::Fraction(30.25).to_string(), "30.25");
242        assert_eq!(Tenths::Fraction(7.0).to_string(), "7.0");
243    }
244
245    /// music21's `tenthsToMillimeters` doctest.
246    #[test]
247    fn tenths_are_turned_into_millimeters() {
248        let layout = ScoreLayout {
249            scaling_millimeters: Some(Tenths::Whole(7)),
250            scaling_tenths: Some(Tenths::Whole(40)),
251            ..ScoreLayout::default()
252        };
253        assert_eq!(layout.tenths_to_millimeters(40.0), 7.0);
254        assert_eq!(ScoreLayout::default().tenths_to_millimeters(40.0), 0.0);
255    }
256}