Skip to main content

danceinterpreter_rs/ui/config_window/
mod.rs

1pub 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        //.padding([4, 8])
260        .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    //.style(checkbox::secondary)
286}
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    //.style(checkbox::secondary)
308}
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        //.style(checkbox::secondary)
321    }
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}