use std::rc::Rc;
use druid::{
im,
kurbo::{BezPath, Circle},
lens::Map,
theme,
widget::{
Button, Container, ControllerHost, CrossAxisAlignment, EnvScope, Flex, Label, List, Maybe,
Painter, Scroll, SizedBox, TextBox,
},
Affine, Color, Env, EventCtx, Insets, Key, PaintCtx, RenderContext, TextAlignment, Widget,
WidgetExt,
};
use tf_player::player;
use crate::{
command,
state::{NewSong, SongEdit, SongListItem},
widget::{
common::stack::Stack, controllers::Enter, overlay::Overlay, player_tick::PlayerTick,
tag_edit::TagEdit,
},
State,
};
use self::media_bar::MediaBarState;
const SONG_LIST_ITEM_BACKGROUND: Key<Color> = Key::new("song_list.item.background");
mod media_bar;
pub fn ui() -> impl Widget<State> {
let query_box = query_box();
let main_view = Flex::row()
.with_flex_child(
Scroll::new(songs_ui().lens(State::songs))
.vertical()
.expand_height(),
1.0,
)
.with_child(Maybe::new(|| song_edit(), || SizedBox::empty()).lens(State::song_edit));
let mut root = Flex::column();
root.add_child(Label::new("T𝕌NEF𝕀RE"));
root.add_default_spacer();
root.add_child(query_box);
root.add_default_spacer();
root.add_flex_child(main_view, 1.0);
root.add_default_spacer();
root.add_child(add_song_ui());
root.add_default_spacer();
root.add_child(
Maybe::new(|| media_bar::ui(), || SizedBox::empty()).lens(Map::new(
|s: &State| {
s.player_state.get_playing().map(|p| MediaBarState {
playing: Rc::new(p.clone()),
current_song: s.current_song.clone(),
})
},
|s: &mut State, inner: Option<MediaBarState>| {
s.player_state = Rc::new(
inner
.map(|s| player::State::Playing((*s.playing).clone()))
.unwrap_or(player::State::Idle),
);
},
)),
);
ControllerHost::new(
Stack::new()
.with_child(root.padding(10.0).expand_width())
.with_child(Overlay::new()),
PlayerTick::default(),
)
}
fn songs_ui() -> impl Widget<im::Vector<SongListItem>> {
List::new(|| {
let row = Flex::row()
.with_child(play_song_button())
.with_flex_child(
Label::new(|item: &SongListItem, _: &_| item.song.title.to_owned())
.padding(Insets::uniform(10.0))
.expand_width(),
1.0,
)
.with_child(
Painter::new(|ctx, _, env| draw_icon_button(ctx, env, ICON_EDIT))
.fix_size(36.0, 36.0)
.on_click(|ctx: &mut EventCtx, item: &mut SongListItem, _| {
ctx.submit_command(command::UI_SONG_EDIT_OPEN.with(item.song.id))
}),
)
.with_child(
Painter::new(|ctx, _, env| draw_icon_button(ctx, env, ICON_DELETE))
.fix_size(36.0, 36.0)
.on_click(|ctx: &mut EventCtx, item: &mut SongListItem, _| {
ctx.submit_command(command::SONG_DELETE.with(item.song.id))
}),
)
.expand_width()
.fix_height(48.0);
EnvScope::new(
|env, state| {
env.set(
SONG_LIST_ITEM_BACKGROUND,
if state.selected {
env.get(crate::theme::BACKGROUND_HIGHLIGHT0)
} else {
Color::TRANSPARENT
},
)
},
Container::new(row).background(SONG_LIST_ITEM_BACKGROUND),
)
})
.expand_width()
}
fn query_box() -> impl Widget<State> {
Flex::row()
.with_child(play_query_button())
.with_default_spacer()
.with_flex_child(
ControllerHost::new(
TextBox::new()
.with_placeholder("*")
.with_text_alignment(TextAlignment::Center),
Enter::new(|ctx, _, _| ctx.submit_command(command::QUERY_RUN)),
)
.expand_width()
.lens(State::query),
1.0,
)
}
fn song_edit() -> impl Widget<SongEdit> {
let col =
Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Fill)
.with_child(
Flex::row()
.with_child(Label::new("title"))
.with_child(TextBox::new().lens(SongEdit::title)),
)
.with_default_spacer()
.with_child(
Flex::row()
.with_child(Label::new("source"))
.with_child(TextBox::new().lens(SongEdit::source)),
)
.with_default_spacer()
.with_child(List::new(|| TagEdit::new()).lens(SongEdit::tags))
.with_child(Button::new("+").on_click(|ctx, data: &mut SongEdit, _| {
ctx.submit_command(command::TAG_ADD.with(*data.id))
}))
.with_flex_spacer(1.0)
.with_child(Button::new("CLOSE").on_click(|ctx, _: &mut SongEdit, _| {
ctx.submit_command(command::UI_SONG_EDIT_CLOSE)
}))
.env_scope(|env, _| env.set(theme::BORDER_DARK, Color::TRANSPARENT))
.fix_width(400.0)
.padding(8.0);
Container::new(col).background(crate::theme::BACKGROUND_HIGHLIGHT0)
}
fn add_song_ui() -> impl Widget<State> {
Flex::row()
.with_flex_child(
Flex::column()
.with_child(
TextBox::new()
.with_placeholder("Title")
.lens(NewSong::title)
.lens(State::new_song)
.expand_width(),
)
.with_child(
TextBox::new()
.with_placeholder("Source")
.lens(NewSong::source)
.lens(State::new_song)
.expand_width(),
)
.expand_width(),
1.0,
)
.with_child(Button::new("+").on_click(|ctx, state: &mut State, _| {
ctx.submit_command(command::SONG_ADD.with(state.new_song.clone()))
}))
.cross_axis_alignment(CrossAxisAlignment::Fill)
}
fn play_query_button() -> impl Widget<State> {
Painter::new(|ctx, _: &State, env| draw_icon_button(ctx, env, ICON_PLAY))
.fix_size(36.0, 36.0)
.on_click(|ctx: &mut EventCtx, _, _| {
ctx.submit_command(command::QUERY_PLAY);
})
}
fn play_song_button() -> impl Widget<SongListItem> {
Painter::new(|ctx, _: &SongListItem, env| draw_icon_button(ctx, env, ICON_PLAY))
.fix_size(36.0, 36.0)
.on_click(|ctx: &mut EventCtx, item: &mut SongListItem, _| {
ctx.submit_command(command::SONG_PLAY.with(item.song.id));
})
}
pub const ICON_PLAY: &str = "M0.750,0.567 L0.750,1.433 L1.500,1.000 L0.750,0.567";
pub const ICON_PAUSE: &str = "M0.598,0.521 L0.598,1.479 L0.866,1.479 L0.866,0.521 L0.598,0.521 M1.402,0.521 L1.134,0.521 L1.134,1.479 L1.402,1.479 L1.402,0.521";
pub const ICON_PREV: &str = "M1.250,0.567 L0.700,0.885 L0.700,0.567 L0.500,0.567 L0.500,1.000 L0.500,1.433 L0.700,1.433 L0.700,1.115 L1.250,1.433 L1.250,0.567";
pub const ICON_NEXT: &str = "M0.750,1.433 L1.300,1.115 L1.300,1.433 L1.500,1.433 L1.500,1.000 L1.500,0.567 L1.300,0.567 L1.300,0.885 L0.750,0.567 L0.750,1.433";
pub const ICON_EDIT: &str = "M1.000 0.513 L0.700,0.513 L0.700,1.000 L0.700,1.487 L1.000,1.487 L1.300,1.487 L1.300,1.017 L1.024,1.017 L1.024,0.699 L1.210,0.513 L1.000,0.513 M1.654 0.564 L1.477,0.387 L1.124,0.741 L1.124,0.917 L1.300,0.917 L1.654,0.564";
pub const ICON_DELETE: &str = "M0.814 1.394 L1.000,1.394 L1.186,1.394 A0.044,0.044 0 0,0 1.229,1.357 L1.348,0.695 A0.044,0.044 0 0,0 1.393,0.651 A0.044,0.044 0 0,0 1.348,0.606 L0.652,0.606 A0.044,0.044 0 0,0 0.607,0.651 A0.044,0.044 0 0,0 0.652,0.695 L0.771,1.357 A0.044,0.044 0 0,0 0.814,1.394";
pub fn draw_icon_button(ctx: &mut PaintCtx, env: &Env, icon_svg: &str) {
let size = ctx.size();
let rad = size.min_side() / 2.0;
if ctx.is_hot() {
ctx.fill(
Circle::new((size.to_vec2() / 2.0).to_point(), rad),
&env.get(crate::theme::BACKGROUND_HIGHLIGHT1),
);
}
ctx.fill(
Affine::scale(rad) * BezPath::from_svg(icon_svg).unwrap(),
&env.get(theme::FOREGROUND_LIGHT),
);
}