use std::collections::HashMap;
use iced::{widget::Id, Rectangle, Task};
use iced_core::widget::operation::{Operation, Outcome, Scrollable};
use crate::{
app::{
interaction::TrackListKind,
pane::PaneId,
ui::{
search_history_list_id, search_input_id, track_list_id, QUEUE_LIST_ID,
QUEUE_RECENT_LIST_ID,
},
Message,
},
theme,
};
#[derive(Debug, Clone, Default)]
pub struct ListGeometry {
pub bounds: Rectangle,
pub translation_y: f32,
pub rows: Vec<Rectangle>,
}
pub const SIDEBAR_LIST_ID: Id = Id::new("sidebar_playlist_list");
pub const LIBRARY_LIST_ID: Id = Id::new("sidebar_library_list");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainingList {
Queue,
Recent,
Track(PaneId),
Sidebar,
Library,
}
impl ContainingList {
pub fn track_target(self, focused: PaneId) -> Option<(TrackListKind, PaneId)> {
match self {
ContainingList::Queue => Some((TrackListKind::Queue, focused)),
ContainingList::Track(pane) => Some((TrackListKind::Active, pane)),
ContainingList::Recent => Some((TrackListKind::Recent, focused)),
ContainingList::Sidebar | ContainingList::Library => None,
}
}
}
#[derive(Default, Clone, Debug)]
pub struct CaptureBounds {
pub sidebar: Option<ListGeometry>,
pub library: Option<ListGeometry>,
pub queue: Option<ListGeometry>,
pub tracks: HashMap<PaneId, ListGeometry>,
pub recent: Option<ListGeometry>,
pub search_history: HashMap<PaneId, ListGeometry>,
pub search_inputs: HashMap<PaneId, Rectangle>,
pub context_menu: Option<ContextMenuGeometry>,
panes: Vec<PaneId>,
current: Option<Id>,
}
impl From<CaptureBounds> for Task<Message> {
fn from(bounds: CaptureBounds) -> Self {
iced_runtime::task::widget(bounds)
}
}
impl CaptureBounds {
pub fn new() -> Self {
Self::default()
}
pub fn with_panes(panes: Vec<PaneId>) -> Self {
Self {
panes,
..Self::default()
}
}
pub fn track_geo(&self, pane: PaneId) -> Option<&ListGeometry> {
self.tracks.get(&pane)
}
fn is_tracked(&self, id: &Id) -> bool {
*id == SIDEBAR_LIST_ID
|| *id == LIBRARY_LIST_ID
|| *id == QUEUE_LIST_ID
|| *id == QUEUE_RECENT_LIST_ID
|| self.panes.iter().any(|p| *id == track_list_id(*p))
}
fn pane_of(&self, id: &Id) -> Option<PaneId> {
self.panes
.iter()
.copied()
.find(|p| *id == track_list_id(*p))
}
fn geo_mut(&mut self, id: &Id) -> Option<&mut ListGeometry> {
if *id == SIDEBAR_LIST_ID {
return Some(self.sidebar.get_or_insert_with(ListGeometry::default));
}
if *id == LIBRARY_LIST_ID {
return Some(self.library.get_or_insert_with(ListGeometry::default));
}
if *id == QUEUE_LIST_ID {
return Some(self.queue.get_or_insert_with(ListGeometry::default));
}
if *id == QUEUE_RECENT_LIST_ID {
return Some(self.recent.get_or_insert_with(ListGeometry::default));
}
let pane = self.pane_of(id)?;
Some(self.tracks.entry(pane).or_default())
}
pub fn get_containing(&self, point: iced::Point) -> Option<(ContainingList, &ListGeometry)> {
if let Some(g) = &self.queue {
if g.bounds.contains(point) {
return Some((ContainingList::Queue, g));
}
}
if let Some(g) = &self.recent {
if g.bounds.contains(point) {
return Some((ContainingList::Recent, g));
}
}
for (pane, g) in &self.tracks {
if g.bounds.contains(point) {
return Some((ContainingList::Track(*pane), g));
}
}
if let Some(g) = &self.sidebar {
if g.bounds.contains(point) {
return Some((ContainingList::Sidebar, g));
}
}
if let Some(g) = &self.library {
if g.bounds.contains(point) {
return Some((ContainingList::Library, g));
}
}
None
}
}
impl Operation<Message> for CaptureBounds {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Message>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
_content_bounds: Rectangle,
translation: iced::Vector,
_state: &mut dyn Scrollable,
) {
let Some(id) = id else {
self.current = None;
return;
};
if !self.is_tracked(id) {
self.current = None;
return;
}
self.current = Some(id.clone());
if let Some(geo) = self.geo_mut(id) {
*geo = ListGeometry {
bounds,
translation_y: translation.y,
rows: Vec::new(),
};
}
}
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
let Some(target) = self.current.clone() else {
return;
};
if target == QUEUE_LIST_ID
|| target == QUEUE_RECENT_LIST_ID
|| self.pane_of(&target).is_some()
{
return;
}
if id.is_some() {
if let Some(g) = self.geo_mut(&target) {
g.rows.push(bounds);
}
}
}
fn text_input(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
_state: &mut dyn iced_core::widget::operation::TextInput,
) {
let Some(id) = id else {
return;
};
if let Some(pane) = self
.panes
.iter()
.copied()
.find(|p| *id == search_input_id(*p))
{
self.search_inputs.insert(pane, bounds);
}
}
fn finish(&self) -> Outcome<Message> {
Outcome::Some(Message::ListBoundsCaptured(Box::new(self.clone())))
}
}
#[derive(Default, Clone, Debug)]
pub struct CaptureSearchHistoryRows {
pane: PaneId,
geo: Option<ListGeometry>,
current: bool,
}
impl From<CaptureSearchHistoryRows> for Task<Message> {
fn from(capture: CaptureSearchHistoryRows) -> Self {
iced_runtime::task::widget(capture)
}
}
impl CaptureSearchHistoryRows {
pub fn new(pane: PaneId) -> Self {
Self {
pane,
..Self::default()
}
}
}
impl Operation<Message> for CaptureSearchHistoryRows {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Message>)) {
operate(self);
}
fn scrollable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
_content_bounds: Rectangle,
translation: iced::Vector,
_state: &mut dyn Scrollable,
) {
if id == Some(&search_history_list_id(self.pane)) {
self.current = true;
self.geo = Some(ListGeometry {
bounds,
translation_y: translation.y,
rows: Vec::new(),
});
} else {
self.current = false;
}
}
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
if self.current && id.is_some() {
if let Some(g) = &mut self.geo {
g.rows.push(bounds);
}
}
}
fn finish(&self) -> Outcome<Message> {
match self.geo.clone() {
Some(mut geo) => {
if !geo.rows.is_empty() {
let last = theme::SEARCH_DROPDOWN_MAX_ITEMS.min(geo.rows.len()) - 1;
geo.bounds.height = geo.rows[last].y + geo.rows[last].height - geo.bounds.y;
}
Outcome::Some(Message::SearchHistoryBoundsCaptured(self.pane, geo))
}
None => Outcome::None,
}
}
}
#[derive(Debug, Clone)]
pub struct ContextMenuGeometry {
pub panel: Rectangle,
pub row_offsets: Vec<f32>,
pub stable: bool,
}
#[derive(Default, Clone, Debug)]
pub struct CaptureContextMenu {
panel: Option<Rectangle>,
rows: Vec<Rectangle>,
}
impl From<CaptureContextMenu> for Task<Message> {
fn from(capture: CaptureContextMenu) -> Self {
iced_runtime::task::widget(capture)
}
}
impl Operation<Message> for CaptureContextMenu {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<Message>)) {
operate(self);
}
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
use crate::app::ui::{CONTEXT_MENU_PANEL_ID, CONTEXT_MENU_ROW_ID};
match id {
Some(id) if *id == CONTEXT_MENU_ROW_ID => self.rows.push(bounds),
Some(id) if *id == CONTEXT_MENU_PANEL_ID => self.panel = Some(bounds),
_ => {}
}
}
fn finish(&self) -> Outcome<Message> {
let Some(panel) = self.panel else {
return Outcome::None;
};
Outcome::Some(Message::ContextMenuBoundsCaptured {
panel,
row_offsets: self.rows.iter().map(|r| r.y - panel.y).collect(),
})
}
}