danceinterpreter_rs/ui/
config_window.rs

1use crate::dataloading::dataprovider::song_data_provider::{
2    SongChange, SongDataEdit, SongDataSource,
3};
4use crate::traktor_api::{TraktorNextMode, TraktorSyncMode, TRAKTOR_SERVER_DEFAULT_ADDR};
5use crate::ui::material_icon;
6use crate::ui::widget::dynamic_text_input::DynamicTextInput;
7use crate::{DanceInterpreter, Message, Window};
8use iced::advanced::Widget;
9use iced::alignment::Vertical;
10use iced::border::Radius;
11use iced::widget::scrollable::{Direction, RelativeOffset, Scrollbar};
12use iced::widget::{
13    button, checkbox, column as col, radio, row, scrollable, text, Button, Column, Row,
14    Scrollable, Space,
15};
16use iced::{font, window, Border, Color, Element, Font, Length, Renderer, Size, Theme};
17use iced_aw::menu::Item;
18use iced_aw::style::{menu_bar::primary, Status};
19use iced_aw::widget::InnerBounds;
20use iced_aw::{iced_aw_font, menu, menu_bar, menu_items, quad, Menu, MenuBar};
21use network_interface::Addr::V4;
22use network_interface::{NetworkInterface, NetworkInterfaceConfig};
23use std::sync::LazyLock;
24
25pub struct ConfigWindow {
26    pub id: window::Id,
27    pub closed: bool,
28    pub size: Size,
29
30    pub enable_autoscroll: bool,
31    pub theme: Theme,
32}
33
34pub static PLAYLIST_SCROLLABLE_ID: LazyLock<iced::widget::Id> =
35    LazyLock::new(iced::widget::Id::unique);
36
37impl Window for ConfigWindow {
38    fn new(id: window::Id) -> Self {
39        Self {
40            id,
41            closed: false,
42            size: Size::default(),
43
44            enable_autoscroll: true,
45            theme: Theme::Dark,
46        }
47    }
48
49    fn on_resize(&mut self, size: Size) {
50        self.size = size;
51    }
52
53    fn on_close(&mut self) {
54        self.closed = true;
55    }
56
57    fn is_closed(&self) -> bool {
58        self.closed
59    }
60}
61
62impl ConfigWindow {
63    pub fn view<'a>(&'a self, dance_interpreter: &'a DanceInterpreter) -> Element<'a, Message> {
64        let menu_bar = self.build_menu_bar(dance_interpreter);
65        let playlist_view = self.build_playlist_view(dance_interpreter);
66        let statics_view = self.build_statics_view(dance_interpreter);
67
68        let content = col![menu_bar, playlist_view, statics_view];
69        content.into()
70    }
71
72    fn get_play_state(
73        &self,
74        dance_interpreter: &DanceInterpreter,
75        playlist_index: usize,
76    ) -> (bool, bool, bool, bool) {
77        let mut is_current = false;
78        let mut is_next = false;
79        let mut is_traktor = false;
80        let is_played = dance_interpreter
81            .data_provider
82            .playlist_played
83            .get(playlist_index)
84            .copied()
85            .unwrap_or(false);
86
87        if let SongDataSource::Playlist(i) = dance_interpreter.data_provider.current {
88            is_current = playlist_index == i;
89            is_next = playlist_index == (i + 1);
90        }
91
92        if let Some(SongDataSource::Playlist(i)) = dance_interpreter.data_provider.next {
93            is_next = playlist_index == i;
94        }
95
96        if matches!(
97            dance_interpreter.data_provider.current,
98            SongDataSource::Traktor
99        ) && let Some(index) = dance_interpreter.data_provider.get_current_traktor_index()
100        {
101            is_traktor = playlist_index == index;
102        }
103
104        (is_current, is_next, is_traktor, is_played)
105    }
106
107    fn build_playlist_view(&'_ self, dance_interpreter: &DanceInterpreter) -> Column<'_, Message> {
108        let trow: Row<_> = row![
109            text!("#").width(Length::Fixed(24.0)),
110            text!("Title").width(Length::Fill),
111            text!("Artist").width(Length::Fill),
112            text!("Dance").width(Length::Fill),
113            Space::new().width(Length::Fill).height(Length::Shrink),
114            Space::new()
115                .width(Length::Fixed(10.0))
116                .height(Length::Shrink),
117        ]
118            .spacing(5);
119
120        let mut playlist_column: Column<'_, _, _, _> = col!().spacing(5);
121
122        for (i, song) in dance_interpreter
123            .data_provider
124            .playlist_songs
125            .iter()
126            .enumerate()
127        {
128            let (is_current, is_next, is_traktor, is_played) =
129                self.get_play_state(dance_interpreter, i);
130            let icon: Element<Message> = if is_traktor {
131                material_icon("agriculture")
132                    .width(Length::Fixed(24.0))
133                    .into()
134            } else if is_current {
135                material_icon("play_arrow")
136                    .width(Length::Fixed(24.0))
137                    .into()
138            } else if is_next {
139                material_icon("skip_next").width(Length::Fixed(24.0)).into()
140            } else if is_played {
141                material_icon("check").width(Length::Fixed(24.0)).into()
142            } else {
143                Space::new()
144                    .width(Length::Fixed(24.0))
145                    .height(Length::Shrink)
146                    .into()
147            };
148
149            let song_row = row![
150                icon,
151                DynamicTextInput::<'_, Message>::new("Title", &song.title)
152                    .width(Length::Fill)
153                    .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Title(v))),
154                DynamicTextInput::<'_, Message>::new("Artist", &song.artist)
155                    .width(Length::Fill)
156                    .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Artist(v))),
157                DynamicTextInput::<'_, Message>::new("Dance", &song.dance)
158                    .width(Length::Fill)
159                    .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Dance(v))),
160                row![
161                    Space::new().width(Length::Fill).height(Length::Shrink),
162                    material_icon_message_button(
163                        "smart_display",
164                        Message::SongChanged(SongChange::PlaylistAbsolute(i))
165                    ),
166                    material_icon_message_button(
167                        "queue_play_next",
168                        Message::SetNextSong(SongDataSource::Playlist(i))
169                    ),
170                    material_icon_message_button(
171                        "delete",
172                        Message::DeleteSong(SongDataSource::Playlist(i))
173                    ),
174                ]
175                .spacing(5)
176                .width(Length::Fill),
177            ]
178                .spacing(5);
179
180            if !playlist_column.children().is_empty() {
181                playlist_column = playlist_column.push(separator());
182            }
183
184            playlist_column = playlist_column.push(song_row);
185        }
186
187        let playlist_scrollable: Scrollable<'_, Message> = scrollable(playlist_column)
188            .width(Length::Fill)
189            .height(Length::Fill)
190            .spacing(5)
191            .id(PLAYLIST_SCROLLABLE_ID.clone());
192
193        col!(trow, playlist_scrollable).spacing(5)
194    }
195
196    fn build_statics_view<'a>(
197        &self,
198        dance_interpreter: &'a DanceInterpreter,
199    ) -> Scrollable<'a, Message> {
200        let bold_font = Font {
201            family: font::Family::SansSerif,
202            weight: font::Weight::Bold,
203            stretch: font::Stretch::Normal,
204            style: font::Style::Normal,
205        };
206
207        let btn_blank: Button<Message> =
208            button(text("Blank").align_y(Vertical::Center).font(bold_font))
209                .style(button::secondary)
210                .on_press(Message::SongChanged(SongChange::Blank));
211        let btn_traktor: Button<Message> =
212            button(text("Traktor").align_y(Vertical::Center).font(bold_font))
213                .style(button::secondary)
214                .on_press(Message::SongChanged(SongChange::Traktor));
215        let mut statics: Vec<Element<_>> = dance_interpreter
216            .data_provider
217            .statics
218            .iter()
219            .enumerate()
220            .map(|(idx, s)| {
221                button(text(&s.dance).font(bold_font))
222                    .style(button::secondary)
223                    .on_press(Message::SongChanged(SongChange::StaticAbsolute(idx)))
224                    .into()
225            })
226            .collect();
227
228        statics.insert(0, btn_blank.into());
229        statics.insert(1, btn_traktor.into());
230
231        scrollable(row(statics).spacing(5))
232            .direction(Direction::Horizontal(Scrollbar::new()))
233            .spacing(5)
234            .width(Length::Fill)
235    }
236
237    fn build_menu_bar<'a>(
238        &self,
239        dance_interpreter: &'a DanceInterpreter,
240    ) -> MenuBar<'a, Message, Theme, Renderer> {
241        let menu_tpl_1 = |items| Menu::new(items).max_width(150.0).offset(15.0).spacing(5.0);
242        let menu_tpl_2 = |items| Menu::new(items).max_width(150.0).offset(0.0).spacing(5.0);
243
244        #[rustfmt::skip]
245        let mb = menu_bar!
246        (
247            (
248                label_message_button_shrink("File", Message::Noop),
249                menu_tpl_1(
250                    menu_items!(
251                        (label_message_button_fill("Open Playlist File", Message::OpenPlaylist)),
252                        (label_message_button_fill("Exit", Message::WindowClosed(self.id))),
253                    )
254                )
255                .spacing(5.0)
256            ),
257            (
258                label_message_button_shrink("Edit", Message::Noop),
259                menu_tpl_1(
260                    menu_items!(
261                        (labeled_message_checkbox("Autoscroll", self.enable_autoscroll, Message::EnableAutoscroll)),
262                        (label_message_button_fill("Reload Statics", Message::ReloadStatics)),
263                        (label_message_button_fill("Add blank song", Message::AddBlankSong(RelativeOffset::END))),
264                    )
265                )
266                .spacing(5.0)
267            ),
268            (
269                label_message_button_shrink("SongWindow", Message::Noop),
270                menu_tpl_1(
271                    menu_items!(
272                        (labeled_message_checkbox("Show Thumbnails", dance_interpreter.song_window.enable_image, Message::EnableImage)),
273                        (labeled_message_checkbox("Show Next Dance", dance_interpreter.song_window.enable_next_dance, Message::EnableNextDance)),
274                    )
275                )
276                .spacing(5.0)
277            ),
278            (
279                label_message_button_shrink("Traktor", Message::Noop),
280                menu_tpl_1(
281                    menu_items!(
282                        (labeled_message_checkbox("Enable Server", dance_interpreter.data_provider.traktor_provider.is_enabled, Message::TraktorEnableServer)),
283                        (
284                            labeled_dynamic_text_input("Server Address", TRAKTOR_SERVER_DEFAULT_ADDR, dance_interpreter.data_provider.traktor_provider.address.as_str(),
285                                Message::TraktorChangeAddress, Some(Message::TraktorSubmitAddress)),
286                            menu_tpl_2(get_network_interface_menu(dance_interpreter))
287                        ),
288                        (separator()),
289                        (labeled_message_checkbox_opt("Enable Debug Logging", dance_interpreter.data_provider.traktor_provider.debug_logging,
290                            dance_interpreter.data_provider.traktor_provider.is_enabled.then_some(Message::TraktorEnableDebugLogging))),
291                        (label_message_button_fill_opt("Reset Connection", dance_interpreter.data_provider.traktor_provider.is_enabled.then_some(Message::TraktorReconnect))),
292                        (separator()),
293                        (
294                            submenu_button("Sync Mode"),
295                            menu_tpl_2(
296                                menu_items!(
297                                    (labeled_message_radio("None", true,
298                                        Some(dance_interpreter.data_provider.traktor_provider.sync_mode.is_none()), |_| Message::TraktorSetSyncMode(None))),
299                                    (labeled_message_radio("X Fader", TraktorSyncMode::Relative,
300                                        dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
301                                    (labeled_message_radio("By Track Number", TraktorSyncMode::AbsoluteByNumber,
302                                        dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
303                                    (labeled_message_radio("By Title / Artist", TraktorSyncMode::AbsoluteByName,
304                                        dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
305                                )
306                            )
307                        ),
308                        (
309                            submenu_button("Next Song Mode"),
310                            menu_tpl_2(
311                                menu_items!(
312                                    (labeled_message_radio("None", true,
313                                        Some(dance_interpreter.data_provider.traktor_provider.next_mode.is_none()), |_| Message::TraktorSetNextMode(None))),
314                                    (labeled_message_radio("From other Deck (by Position)", TraktorNextMode::DeckByPosition,
315                                        dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
316                                    (labeled_message_radio("From other Deck (by Track Number)", TraktorNextMode::DeckByNumber,
317                                        dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
318                                    (labeled_message_radio("From Playlist (by Track Number)", TraktorNextMode::PlaylistByNumber,
319                                        dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
320                                    (labeled_message_radio("From Playlist (by Title / Artist)", TraktorNextMode::PlaylistByName,
321                                        dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
322                                )
323                            )
324                        ),
325                        (
326                            submenu_button("Next Song Mode (Fallback)"),
327                            menu_tpl_2(
328                                menu_items!(
329                                    (labeled_message_radio("None", true,
330                                        Some(dance_interpreter.data_provider.traktor_provider.next_mode_fallback.is_none()), |_| Message::TraktorSetNextModeFallback(None))),
331                                    (labeled_message_radio("From other Deck (by Position)", TraktorNextMode::DeckByPosition,
332                                        dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
333                                    (labeled_message_radio("From other Deck (by Track Number)", TraktorNextMode::DeckByNumber,
334                                        dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
335                                    (labeled_message_radio("From Playlist (by Track Number)", TraktorNextMode::PlaylistByNumber,
336                                        dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
337                                    (labeled_message_radio("From Playlist (by Title / Artist)", TraktorNextMode::PlaylistByName,
338                                        dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
339                                )
340                            )
341                        )
342                    )
343                )
344                .spacing(5.0)
345            )
346        )
347        .spacing(5.0)
348        .draw_path(menu::DrawPath::Backdrop)
349            .style(|theme:&iced::Theme, status: Status | menu::Style{
350                path_border: Border{
351                    radius: Radius::new(6.0),
352                    ..Default::default()
353                },
354                ..primary(theme, status)
355            });
356
357        mb
358    }
359}
360
361fn label_message_button_fill<'a>(
362    label: impl text::IntoFragment<'a>,
363    message: Message,
364) -> button::Button<'a, Message> {
365    label_message_button(label, message).width(Length::Fill)
366}
367
368fn label_message_button_shrink<'a>(
369    label: impl text::IntoFragment<'a>,
370    message: Message,
371) -> button::Button<'a, Message> {
372    label_message_button(label, message).width(Length::Shrink)
373}
374
375fn label_message_button<'a>(
376    label: impl text::IntoFragment<'a>,
377    message: Message,
378) -> button::Button<'a, Message> {
379    button(text(label).align_y(Vertical::Center))
380        .padding([4, 8])
381        .style(button::secondary)
382        .on_press(message)
383}
384
385fn submenu_button(label: &'_ str) -> button::Button<'_, Message, iced::Theme, iced::Renderer> {
386    button(
387        row![
388            text(label).width(Length::Fill).align_y(Vertical::Center),
389            iced_aw_font::right_open()
390                .width(Length::Shrink)
391                .align_y(Vertical::Center),
392        ]
393            .align_y(iced::Alignment::Center),
394    )
395        .padding([4, 8])
396        .style(button::text)
397        .on_press(Message::Noop)
398        .width(Length::Fill)
399}
400
401fn label_message_button_opt(
402    label: &'_ str,
403    message: Option<Message>,
404) -> button::Button<'_, Message> {
405    if let Some(message) = message {
406        label_message_button(label, message)
407    } else {
408        button(text(label).align_y(Vertical::Center))
409            .padding([4, 8])
410            .style(button::secondary)
411    }
412}
413
414fn label_message_button_fill_opt(
415    label: &'_ str,
416    message: Option<Message>,
417) -> button::Button<'_, Message> {
418    label_message_button_opt(label, message).width(Length::Fill)
419}
420
421fn material_icon_message_button(icon_id: &'_ str, message: Message) -> button::Button<'_, Message> {
422    button(material_icon(icon_id))
423        .padding([4, 8])
424        .style(button::secondary)
425        .on_press(message)
426        .width(Length::Shrink)
427}
428
429fn labeled_message_checkbox(
430    label: &'_ str,
431    checked: bool,
432    message: fn(bool) -> Message,
433) -> checkbox::Checkbox<'_, Message> {
434    checkbox(checked)
435        .label(label)
436        .on_toggle(message)
437        .width(Length::Fill)
438    //.style(checkbox::secondary)
439}
440
441fn labeled_message_radio<T: Copy + Eq>(
442    label: &'_ str,
443    value: T,
444    selection: Option<T>,
445    message: fn(T) -> Message,
446) -> radio::Radio<'_, Message> {
447    radio(label, value, selection, message).width(Length::Fill)
448    //.style(checkbox::secondary)
449}
450
451fn labeled_message_checkbox_opt(
452    label: &'_ str,
453    checked: bool,
454    message: Option<fn(bool) -> Message>,
455) -> checkbox::Checkbox<'_, Message> {
456    if let Some(message) = message {
457        labeled_message_checkbox(label, checked, message)
458    } else {
459        checkbox(checked).label(label).width(Length::Fill)
460        //.style(checkbox::secondary)
461    }
462}
463
464fn labeled_dynamic_text_input<'a>(
465    label: &'a str,
466    placeholder: &'a str,
467    value: &'a str,
468    message: fn(String) -> Message,
469    submit_message: Option<Message>,
470) -> Column<'a, Message> {
471    let mut input = DynamicTextInput::<Message>::new(placeholder, value)
472        .width(Length::Fill)
473        .on_change(message);
474
475    if let Some(submit_message) = submit_message {
476        input = input.on_submit(submit_message);
477    }
478
479    col!(text(label).width(Length::Fill), input,).width(Length::Fill)
480}
481
482fn separator() -> quad::Quad {
483    quad::Quad {
484        quad_color: Color::from([0.5; 3]).into(),
485        quad_border: Border {
486            radius: Radius::new(2.0),
487            ..Default::default()
488        },
489        inner_bounds: InnerBounds::Ratio(1.0, 0.2),
490        height: Length::Fixed(5.0),
491        width: Length::Fill,
492        ..Default::default()
493    }
494}
495
496fn get_network_interface_menu(
497    dance_interpreter: &'_ DanceInterpreter,
498) -> Vec<Item<'_, Message, Theme, Renderer>> {
499    let mut interfaces = vec![("any".to_owned(), "0.0.0.0".to_owned())];
500
501    if let Ok(network_interfaces) = NetworkInterface::show() {
502        for i in network_interfaces {
503            for addr in i.addr {
504                let V4(ipv4_addr) = addr else {
505                    continue;
506                };
507
508                interfaces.push((i.name.clone(), ipv4_addr.ip.to_string()));
509            }
510        }
511    }
512
513    let original_addr = dance_interpreter
514        .data_provider
515        .traktor_provider
516        .get_socket_addr()
517        .unwrap_or(TRAKTOR_SERVER_DEFAULT_ADDR.parse().unwrap());
518    let original_port = original_addr.port();
519
520    let interfaces = interfaces
521        .into_iter()
522        .map(|(name, addr)| (name, addr.clone(), format!("{}:{}", addr, original_port)));
523
524    interfaces
525        .into_iter()
526        .map(|(name, addr, addr_with_port)| {
527            Item::new(label_message_button_fill(
528                format!("{}: {}", name, addr),
529                Message::TraktorChangeAndSubmitAddress(addr_with_port),
530            ))
531        })
532        .collect()
533}