Skip to main content

maolan_widgets/
note_area.rs

1use crate::{
2    midi::{
3        KEYBOARD_WIDTH, KEYS_SCROLL_ID, MIDI_NOTE_COUNT, NOTES_PER_OCTAVE, NOTES_SCROLL_ID,
4        PITCH_MAX, RIGHT_SCROLL_GUTTER_WIDTH, WHITE_KEY_HEIGHT, WHITE_KEYS_PER_OCTAVE,
5    },
6    piano,
7};
8use iced::{
9    Background, Color, Element, Length, Point,
10    widget::{Id, Stack, container, pin, scrollable},
11};
12
13use crate::{horizontal_scrollbar::HorizontalScrollbar, vertical_scrollbar::VerticalScrollbar};
14
15pub struct NoteArea {
16    pub zoom_x: f32,
17    pub zoom_y: f32,
18    pub pixels_per_sample: f32,
19    pub samples_per_bar: Option<f32>,
20    pub playhead_x: Option<f32>,
21    pub playhead_width: f32,
22    pub clip_length_samples: usize,
23}
24
25impl NoteArea {
26    pub fn view<'a, Message: 'a>(self, content: Vec<Element<'a, Message>>) -> Element<'a, Message> {
27        let pitch_count = MIDI_NOTE_COUNT;
28        let row_h = ((WHITE_KEY_HEIGHT * WHITE_KEYS_PER_OCTAVE as f32 / NOTES_PER_OCTAVE as f32)
29            * self.zoom_y)
30            .max(1.0);
31        let notes_h = pitch_count as f32 * row_h;
32        let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
33        let notes_w = (self.clip_length_samples as f32 * pps).max(1.0);
34
35        let mut layers: Vec<Element<'a, Message>> = vec![];
36
37        for i in 0..pitch_count {
38            let pitch = PITCH_MAX.saturating_sub(i as u8);
39            let is_black = piano::is_black_key(pitch);
40            layers.push(
41                pin(container("")
42                    .width(Length::Fixed(notes_w))
43                    .height(Length::Fixed(row_h))
44                    .style(move |_theme| container::Style {
45                        background: Some(Background::Color(if is_black {
46                            Color::from_rgba(0.08, 0.08, 0.10, 0.85)
47                        } else {
48                            Color::from_rgba(0.12, 0.12, 0.14, 0.85)
49                        })),
50                        ..container::Style::default()
51                    }))
52                .position(Point::new(0.0, i as f32 * row_h))
53                .into(),
54            );
55        }
56
57        if let Some(samples_per_bar) = self.samples_per_bar {
58            let beat_samples = (samples_per_bar / 4.0).max(1.0);
59            let mut beat = 0usize;
60            loop {
61                let x = beat as f32 * beat_samples * pps;
62                if x > notes_w {
63                    break;
64                }
65                let bar_line = beat.is_multiple_of(4);
66                layers.push(
67                    pin(container("")
68                        .width(Length::Fixed(if bar_line { 2.0 } else { 1.0 }))
69                        .height(Length::Fixed(notes_h))
70                        .style(move |_theme| container::Style {
71                            background: Some(Background::Color(Color {
72                                r: if bar_line { 0.5 } else { 0.35 },
73                                g: if bar_line { 0.5 } else { 0.35 },
74                                b: if bar_line { 0.55 } else { 0.35 },
75                                a: 0.45,
76                            })),
77                            ..container::Style::default()
78                        }))
79                    .position(Point::new(x, 0.0))
80                    .into(),
81                );
82                beat += 1;
83            }
84        }
85
86        for item in content {
87            layers.push(item);
88        }
89
90        if let Some(x) = self.playhead_x {
91            let x = x.max(0.0);
92            layers.push(
93                pin(container("")
94                    .width(Length::Fixed(self.playhead_width))
95                    .height(Length::Fixed(notes_h))
96                    .style(|_theme| container::Style {
97                        background: Some(Background::Color(Color::from_rgba(
98                            0.95, 0.18, 0.14, 0.95,
99                        ))),
100                        ..container::Style::default()
101                    }))
102                .position(Point::new(x, 0.0))
103                .into(),
104            );
105        }
106
107        Stack::from_vec(layers)
108            .width(Length::Fixed(notes_w))
109            .height(Length::Fixed(notes_h))
110            .into()
111    }
112}
113
114pub struct PianoGridScrolls<'a, Message> {
115    pub keyboard_scroll: Element<'a, Message>,
116    pub note_scroll: Element<'a, Message>,
117    pub h_scroll: Element<'a, Message>,
118    pub v_scroll: Element<'a, Message>,
119}
120
121pub struct PianoGridScrollLayout {
122    pub notes_h: f32,
123    pub notes_w: f32,
124    pub scroll_x: f32,
125    pub scroll_y: f32,
126}
127
128pub struct PianoGridScrollCallbacks<ScrollY, ScrollXY> {
129    pub on_scroll_y: ScrollY,
130    pub on_scroll_xy: ScrollXY,
131}
132
133pub fn piano_grid_scrollers<'a, Message, ScrollY, ScrollXY>(
134    keyboard: Element<'a, Message>,
135    notes_content: Element<'a, Message>,
136    layout: PianoGridScrollLayout,
137    callbacks: PianoGridScrollCallbacks<ScrollY, ScrollXY>,
138) -> PianoGridScrolls<'a, Message>
139where
140    Message: 'a + Clone,
141    ScrollY: Fn(f32) -> Message + Copy + 'static,
142    ScrollXY: Fn(f32, f32) -> Message + Copy + 'static,
143{
144    let PianoGridScrollLayout {
145        notes_h,
146        notes_w,
147        scroll_x,
148        scroll_y,
149    } = layout;
150    let PianoGridScrollCallbacks {
151        on_scroll_y,
152        on_scroll_xy,
153    } = callbacks;
154    let keyboard_scroll = scrollable(
155        container(keyboard)
156            .width(Length::Fixed(KEYBOARD_WIDTH))
157            .height(Length::Fixed(notes_h)),
158    )
159    .id(Id::new(KEYS_SCROLL_ID))
160    .direction(scrollable::Direction::Vertical(
161        scrollable::Scrollbar::hidden(),
162    ))
163    .on_scroll(move |viewport| on_scroll_y(viewport.relative_offset().y))
164    .width(Length::Fixed(KEYBOARD_WIDTH))
165    .height(Length::Fill);
166
167    let note_scroll = scrollable(
168        container(notes_content)
169            .width(Length::Shrink)
170            .height(Length::Fixed(notes_h))
171            .style(|_theme| container::Style {
172                background: Some(Background::Color(Color::from_rgba(0.07, 0.07, 0.09, 1.0))),
173                ..container::Style::default()
174            }),
175    )
176    .id(Id::new(NOTES_SCROLL_ID))
177    .direction(scrollable::Direction::Both {
178        vertical: scrollable::Scrollbar::hidden(),
179        horizontal: scrollable::Scrollbar::hidden(),
180    })
181    .on_scroll(move |viewport| {
182        let offset = viewport.relative_offset();
183        on_scroll_xy(offset.x, offset.y)
184    })
185    .width(Length::Fill)
186    .height(Length::Fill);
187
188    let h_scroll = HorizontalScrollbar::new(notes_w, scroll_x, move |x| on_scroll_xy(x, scroll_y))
189        .width(Length::Fill)
190        .height(Length::Fixed(16.0));
191
192    let v_scroll = VerticalScrollbar::new(notes_h, scroll_y, on_scroll_y)
193        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
194        .height(Length::Fill);
195
196    PianoGridScrolls {
197        keyboard_scroll: keyboard_scroll.into(),
198        note_scroll: note_scroll.into(),
199        h_scroll: h_scroll.into(),
200        v_scroll: v_scroll.into(),
201    }
202}