danceinterpreter_rs/ui/config_window/
mod.rs1pub mod bottombar;
2pub mod sidebar;
3pub mod top_bar;
4
5use crate::dataloading::dataprovider::song_data_provider::{
6 SongChange, SongDataEdit, SongDataSource,
7};
8use crate::ui::config_window::sidebar::Sidebar;
9use crate::ui::widget::dynamic_text_input::DynamicTextInput;
10use crate::ui::{material_icon, material_icon_sized, with_tooltip};
11use crate::{DanceInterpreter, Message, Window};
12use iced::alignment::Vertical;
13use iced::widget::{
14 Button, Column, Row, Scrollable, Space, button, checkbox, column as col, container, radio, row,
15 scrollable, text, toggler,
16};
17use iced::{Alignment, Element, Length, Pixels, Renderer, Size, Theme, window};
18use iced_aw::iced_aw_font;
19use std::sync::LazyLock;
20use std::time::Instant;
21
22pub struct ConfigWindow {
23 pub id: window::Id,
24 pub closed: bool,
25 pub size: Size,
26 pub enable_autoscroll: bool,
27 pub sidebar: Sidebar,
28 pub is_statics_view: bool,
29 pub theme: Theme,
30 pub follow_system_theme: bool,
31}
32
33pub static PLAYLIST_SCROLLABLE_ID: LazyLock<iced::widget::Id> =
34 LazyLock::new(iced::widget::Id::unique);
35
36impl Window for ConfigWindow {
37 fn new(id: window::Id) -> Self {
38 Self {
39 id,
40 closed: false,
41 size: Size::default(),
42
43 enable_autoscroll: true,
44 sidebar: Sidebar::new(),
45 is_statics_view: false,
46 theme: Theme::Dark,
47 follow_system_theme: true,
48 }
49 }
50
51 fn on_resize(&mut self, size: Size) {
52 self.size = size;
53 }
54
55 fn on_close(&mut self) {
56 self.closed = true;
57 }
58
59 fn is_closed(&self) -> bool {
60 self.closed
61 }
62}
63
64impl ConfigWindow {
65 pub fn view<'a>(&'a self, dance_interpreter: &'a DanceInterpreter) -> Element<'a, Message> {
66 let top_bar = top_bar::build(self, dance_interpreter);
67
68 let content_view = if self.is_statics_view {
69 self.build_statics_view(dance_interpreter)
70 } else {
71 self.build_playlist_view(dance_interpreter)
72 };
73
74 let side_bar = self
75 .sidebar
76 .build(dance_interpreter)
77 .width(self.sidebar.state.interpolate(
78 0.0,
79 (self.size.width / 5.0).min(400.0),
80 Instant::now(),
81 ));
82 let bottom_bar = bottombar::build(dance_interpreter);
83
84 col![row![col![top_bar, content_view], side_bar], bottom_bar]
85 .spacing(5)
86 .into()
87 }
88
89 fn build_playlist_view(&'_ self, dance_interpreter: &DanceInterpreter) -> Column<'_, Message> {
90 let trow: Row<_> = row![
91 text!("#").width(Length::Fixed(24.0)),
92 text!("Title").width(Length::Fill),
93 text!("Artist").width(Length::Fill),
94 text!("Dance").width(Length::Fill),
95 Space::new().width(Length::Fill).height(Length::Shrink),
96 Space::new()
97 .width(Length::Fixed(10.0))
98 .height(Length::Shrink),
99 ]
100 .spacing(5);
101
102 let mut playlist_column: Column<'_, _, _, _> = col!();
103
104 for (i, song) in dance_interpreter
105 .data_provider
106 .playlist_songs
107 .iter()
108 .enumerate()
109 {
110 let (is_current, is_next, is_traktor, is_played) =
111 dance_interpreter.data_provider.get_play_state(i);
112 let icon: Element<Message> = if is_traktor {
113 material_icon("agriculture")
114 .width(Length::Fixed(24.0))
115 .into()
116 } else if is_current {
117 material_icon("play_arrow")
118 .width(Length::Fixed(24.0))
119 .into()
120 } else if is_next {
121 material_icon("skip_next").width(Length::Fixed(24.0)).into()
122 } else if is_played {
123 material_icon("check").width(Length::Fixed(24.0)).into()
124 } else {
125 Space::new()
126 .width(Length::Fixed(24.0))
127 .height(Length::Shrink)
128 .into()
129 };
130
131 let song_row = row![
132 icon,
133 DynamicTextInput::<'_, Message>::new("Title", &song.title)
134 .width(Length::Fill)
135 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Title(v))),
136 DynamicTextInput::<'_, Message>::new("Artist", &song.artist)
137 .width(Length::Fill)
138 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Artist(v))),
139 DynamicTextInput::<'_, Message>::new("Dance", &song.dance)
140 .width(Length::Fill)
141 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Dance(v))),
142 row![
143 Space::new().width(Length::Fill).height(Length::Shrink),
144 with_tooltip(
145 material_icon_message_button(
146 "smart_display",
147 Message::SongChanged(SongChange::PlaylistAbsolute(i))
148 ),
149 "Show now"
150 ),
151 with_tooltip(
152 material_icon_message_button(
153 "queue_play_next",
154 Message::SetNextSong(SongDataSource::Playlist(i))
155 ),
156 "Set as next song"
157 ),
158 with_tooltip(
159 material_icon_message_button(
160 "delete",
161 Message::DeleteSong(SongDataSource::Playlist(i))
162 ),
163 "Delete song"
164 ),
165 ]
166 .spacing(5)
167 .align_y(Alignment::Center)
168 .width(Length::Fill),
169 ]
170 .align_y(Alignment::Center);
171
172 let song_row = container(song_row)
173 .style(move |t| {
174 let color = if i % 2 == 0 {
175 t.extended_palette().background.weakest.color
176 } else {
177 t.extended_palette().background.weaker.color
178 };
179
180 container::Style::default().background(color)
181 })
182 .padding([4, 6])
183 .width(Length::Fill);
184
185 playlist_column = playlist_column.push(song_row);
186 }
187
188 let playlist_scrollable: Scrollable<'_, Message> = scrollable(playlist_column)
189 .width(Length::Fill)
190 .height(Length::Fill)
191 .spacing(5)
192 .id(PLAYLIST_SCROLLABLE_ID.clone());
193
194 col!(trow, playlist_scrollable).spacing(5)
195 }
196
197 fn build_statics_view(&'_ self, _dance_interpreter: &DanceInterpreter) -> Column<'_, Message> {
198 col![].width(Length::Fill).height(Length::Fill)
199 }
200}
201
202fn label_message_button_fill<'a>(
203 label: impl text::IntoFragment<'a>,
204 message: Message,
205) -> Button<'a, Message> {
206 label_message_button(label, message).width(Length::Fill)
207}
208
209fn label_message_button_shrink<'a>(
210 label: impl text::IntoFragment<'a>,
211 message: Message,
212) -> Button<'a, Message> {
213 label_message_button(label, message).width(Length::Shrink)
214}
215
216fn label_message_button<'a>(
217 label: impl text::IntoFragment<'a>,
218 message: Message,
219) -> Button<'a, Message> {
220 button(text(label).align_y(Vertical::Center))
221 .padding([4, 8])
222 .style(button::secondary)
223 .on_press(message)
224}
225
226#[allow(dead_code)]
227fn submenu_button(label: &'_ str) -> Button<'_, Message, Theme, Renderer> {
228 button(
229 row![
230 text(label).width(Length::Fill).align_y(Vertical::Center),
231 iced_aw_font::right_open()
232 .width(Length::Shrink)
233 .align_y(Vertical::Center),
234 ]
235 .align_y(Alignment::Center),
236 )
237 .padding([4, 8])
238 .style(button::text)
239 .on_press(Message::Noop)
240 .width(Length::Fill)
241}
242
243fn label_message_button_opt(label: &'_ str, message: Option<Message>) -> Button<'_, Message> {
244 if let Some(message) = message {
245 label_message_button(label, message)
246 } else {
247 button(text(label).align_y(Vertical::Center))
248 .padding([4, 8])
249 .style(button::primary)
250 }
251}
252
253fn label_message_button_fill_opt(label: &'_ str, message: Option<Message>) -> Button<'_, Message> {
254 label_message_button_opt(label, message).width(Length::Fill)
255}
256
257fn material_icon_message_button(icon_id: &'_ str, message: Message) -> Button<'_, Message> {
258 button(material_icon(icon_id))
259 .style(button::secondary)
261 .on_press(message)
262 .width(Length::Shrink)
263}
264
265fn material_icon_sized_message_button(
266 icon_id: &'_ str,
267 size: impl Into<Pixels>,
268 message: Message,
269) -> Button<'_, Message> {
270 button(material_icon_sized(icon_id, size))
271 .style(button::secondary)
272 .on_press(message)
273 .width(Length::Shrink)
274}
275
276fn labeled_message_checkbox(
277 label: &'_ str,
278 checked: bool,
279 message: fn(bool) -> Message,
280) -> checkbox::Checkbox<'_, Message> {
281 checkbox(checked)
282 .label(label)
283 .on_toggle(message)
284 .width(Length::Fill)
285 }
287
288fn labeled_message_toggler(
289 label: &'_ str,
290 checked: bool,
291 message: fn(bool) -> Message,
292) -> toggler::Toggler<'_, Message> {
293 toggler(checked)
294 .label(label)
295 .on_toggle(message)
296 .width(Length::Fill)
297}
298
299#[allow(dead_code)]
300fn labeled_message_radio<T: Copy + Eq>(
301 label: &'_ str,
302 value: T,
303 selection: T,
304 message: fn(T) -> Message,
305) -> radio::Radio<'_, Message> {
306 radio(label, value, Some(selection), message).width(Length::Fill)
307 }
309
310#[allow(dead_code)]
311fn labeled_message_checkbox_opt(
312 label: &'_ str,
313 checked: bool,
314 message: Option<fn(bool) -> Message>,
315) -> checkbox::Checkbox<'_, Message> {
316 if let Some(message) = message {
317 labeled_message_checkbox(label, checked, message)
318 } else {
319 checkbox(checked).label(label).width(Length::Fill)
320 }
322}
323
324#[allow(dead_code)]
325fn labeled_dynamic_text_input<'a>(
326 label: &'a str,
327 placeholder: &'a str,
328 value: &'a str,
329 message: fn(String) -> Message,
330 submit_message: Option<Message>,
331) -> Column<'a, Message> {
332 let mut input = DynamicTextInput::<Message>::new(placeholder, value)
333 .width(Length::Fill)
334 .on_change(message);
335
336 if let Some(submit_message) = submit_message {
337 input = input.on_submit(submit_message);
338 }
339
340 col!(text(label).width(Length::Fill), input,).width(Length::Fill)
341}