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