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}