Skip to main content

acorde_core/model/
change_hint.rs

1use serde::{Deserialize, Serialize};
2
3/// Granularity of the change produced by a command.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub enum ChangeScope {
6    /// Score-level change: metadata, global tempo, add/delete part, new score.
7    Global,
8    /// Only a specific part changed (by part index).
9    Part(usize),
10    /// One or more measures in a specific part+staff changed.
11    Measures {
12        part: usize,
13        staff: usize,
14        start: usize,
15        end: usize,
16    },
17}
18
19/// Lightweight hint returned by [`crate::ScoreEngine::apply`].
20///
21/// Consumers use this to decide which subsystems need updating (layout, audio)
22/// without diffing the full score.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChangeHint {
25    pub scope: ChangeScope,
26    /// True when the layout engine must be re-invoked (measure added/deleted,
27    /// multi-rest count changed, clef or key/time sig change).
28    pub layout_dirty: bool,
29    /// True when playback events must be recomputed (pitch, duration, or tempo changed).
30    pub playback_dirty: bool,
31}
32
33impl ChangeHint {
34    /// Merge two hints, taking the broader scope and OR-ing dirty flags.
35    pub fn merge(self, other: ChangeHint) -> ChangeHint {
36        ChangeHint {
37            scope: merge_scope(self.scope, other.scope),
38            layout_dirty: self.layout_dirty || other.layout_dirty,
39            playback_dirty: self.playback_dirty || other.playback_dirty,
40        }
41    }
42}
43
44fn merge_scope(a: ChangeScope, b: ChangeScope) -> ChangeScope {
45    match (a, b) {
46        (ChangeScope::Global, _) | (_, ChangeScope::Global) => ChangeScope::Global,
47        (ChangeScope::Part(pa), ChangeScope::Part(pb)) if pa == pb => ChangeScope::Part(pa),
48        (ChangeScope::Part(_), ChangeScope::Part(_)) => ChangeScope::Global,
49        (
50            ChangeScope::Measures {
51                part: pa,
52                staff: sa,
53                start: s1,
54                end: e1,
55            },
56            ChangeScope::Measures {
57                part: pb,
58                staff: sb,
59                start: s2,
60                end: e2,
61            },
62        ) if pa == pb && sa == sb => ChangeScope::Measures {
63            part: pa,
64            staff: sa,
65            start: s1.min(s2),
66            end: e1.max(e2),
67        },
68        _ => ChangeScope::Global,
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn hint(scope: ChangeScope, layout: bool, playback: bool) -> ChangeHint {
77        ChangeHint {
78            scope,
79            layout_dirty: layout,
80            playback_dirty: playback,
81        }
82    }
83
84    #[test]
85    fn merge_global_wins() {
86        let a = hint(ChangeScope::Global, false, true);
87        let b = hint(ChangeScope::Part(0), true, false);
88        let m = a.merge(b);
89        assert_eq!(m.scope, ChangeScope::Global);
90        assert!(m.layout_dirty);
91        assert!(m.playback_dirty);
92    }
93
94    #[test]
95    fn merge_same_part_stays_part() {
96        let a = hint(ChangeScope::Part(1), false, true);
97        let b = hint(ChangeScope::Part(1), true, false);
98        let m = a.merge(b);
99        assert_eq!(m.scope, ChangeScope::Part(1));
100    }
101
102    #[test]
103    fn merge_different_parts_becomes_global() {
104        let a = hint(ChangeScope::Part(0), false, false);
105        let b = hint(ChangeScope::Part(1), false, false);
106        let m = a.merge(b);
107        assert_eq!(m.scope, ChangeScope::Global);
108    }
109
110    #[test]
111    fn merge_measures_same_staff_extends_range() {
112        let a = hint(
113            ChangeScope::Measures {
114                part: 0,
115                staff: 0,
116                start: 2,
117                end: 4,
118            },
119            false,
120            true,
121        );
122        let b = hint(
123            ChangeScope::Measures {
124                part: 0,
125                staff: 0,
126                start: 1,
127                end: 3,
128            },
129            false,
130            true,
131        );
132        let m = a.merge(b);
133        assert_eq!(
134            m.scope,
135            ChangeScope::Measures {
136                part: 0,
137                staff: 0,
138                start: 1,
139                end: 4
140            }
141        );
142    }
143
144    #[test]
145    fn merge_measures_different_staff_becomes_global() {
146        let a = hint(
147            ChangeScope::Measures {
148                part: 0,
149                staff: 0,
150                start: 0,
151                end: 1,
152            },
153            false,
154            true,
155        );
156        let b = hint(
157            ChangeScope::Measures {
158                part: 0,
159                staff: 1,
160                start: 0,
161                end: 1,
162            },
163            false,
164            true,
165        );
166        let m = a.merge(b);
167        assert_eq!(m.scope, ChangeScope::Global);
168    }
169}