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 fn piano_grid_scrollers<'a, Message, ScrollY, ScrollXY>(
122    keyboard: Element<'a, Message>,
123    notes_content: Element<'a, Message>,
124    notes_h: f32,
125    notes_w: f32,
126    scroll_x: f32,
127    scroll_y: f32,
128    on_scroll_y: ScrollY,
129    on_scroll_xy: ScrollXY,
130) -> PianoGridScrolls<'a, Message>
131where
132    Message: 'a + Clone,
133    ScrollY: Fn(f32) -> Message + Copy + 'static,
134    ScrollXY: Fn(f32, f32) -> Message + Copy + 'static,
135{
136    let keyboard_scroll = scrollable(
137        container(keyboard)
138            .width(Length::Fixed(KEYBOARD_WIDTH))
139            .height(Length::Fixed(notes_h)),
140    )
141    .id(Id::new(KEYS_SCROLL_ID))
142    .direction(scrollable::Direction::Vertical(
143        scrollable::Scrollbar::hidden(),
144    ))
145    .on_scroll(move |viewport| on_scroll_y(viewport.relative_offset().y))
146    .width(Length::Fixed(KEYBOARD_WIDTH))
147    .height(Length::Fill);
148
149    let note_scroll = scrollable(
150        container(notes_content)
151            .width(Length::Shrink)
152            .height(Length::Fixed(notes_h))
153            .style(|_theme| container::Style {
154                background: Some(Background::Color(Color::from_rgba(0.07, 0.07, 0.09, 1.0))),
155                ..container::Style::default()
156            }),
157    )
158    .id(Id::new(NOTES_SCROLL_ID))
159    .direction(scrollable::Direction::Both {
160        vertical: scrollable::Scrollbar::hidden(),
161        horizontal: scrollable::Scrollbar::hidden(),
162    })
163    .on_scroll(move |viewport| {
164        let offset = viewport.relative_offset();
165        on_scroll_xy(offset.x, offset.y)
166    })
167    .width(Length::Fill)
168    .height(Length::Fill);
169
170    let h_scroll = HorizontalScrollbar::new(notes_w, scroll_x, move |x| on_scroll_xy(x, scroll_y))
171        .width(Length::Fill)
172        .height(Length::Fixed(16.0));
173
174    let v_scroll = VerticalScrollbar::new(notes_h, scroll_y, on_scroll_y)
175        .width(Length::Fixed(RIGHT_SCROLL_GUTTER_WIDTH))
176        .height(Length::Fill);
177
178    PianoGridScrolls {
179        keyboard_scroll: keyboard_scroll.into(),
180        note_scroll: note_scroll.into(),
181        h_scroll: h_scroll.into(),
182        v_scroll: v_scroll.into(),
183    }
184}