use iced::{widget::Id, Rectangle, Task};
use iced_core::widget::operation::{Operation, Outcome, Scrollable};
use crate::{
app::{
ui::{
QUEUE_LIST_ID, QUEUE_RECENT_LIST_ID, SEARCH_HISTORY_LIST_ID, SEARCH_INPUT_ID,
TRACK_LIST_ID,
},
Message,
},
theme,
};
#[derive(Debug, Clone)]
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");
fn is_tracked_list(id: &Id) -> bool {
*id == SIDEBAR_LIST_ID
|| *id == LIBRARY_LIST_ID
|| *id == QUEUE_LIST_ID
|| *id == QUEUE_RECENT_LIST_ID
|| *id == TRACK_LIST_ID
}
#[derive(Default, Clone, Debug)]
pub struct CaptureBounds {
pub sidebar: Option<ListGeometry>,
pub library: Option<ListGeometry>,
pub queue: Option<ListGeometry>,
pub track: Option<ListGeometry>,
pub recent: Option<ListGeometry>,
pub search_history: Option<ListGeometry>,
pub search_input: Option<Rectangle>,
pub context_menu: Option<ContextMenuGeometry>,
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()
}
fn geo_mut(&mut self, id: &Id) -> &mut Option<ListGeometry> {
if *id == SIDEBAR_LIST_ID {
&mut self.sidebar
} else if *id == LIBRARY_LIST_ID {
&mut self.library
} else if *id == QUEUE_LIST_ID {
&mut self.queue
} else if *id == QUEUE_RECENT_LIST_ID {
&mut self.recent
} else {
&mut self.track
}
}
pub fn get_containing(&self, point: iced::Point) -> Option<(Id, &ListGeometry)> {
for (id, geo) in [
(QUEUE_LIST_ID.clone(), &self.queue),
(TRACK_LIST_ID.clone(), &self.track),
(QUEUE_RECENT_LIST_ID.clone(), &self.recent),
(SIDEBAR_LIST_ID.clone(), &self.sidebar),
(LIBRARY_LIST_ID.clone(), &self.library),
] {
if let Some(g) = geo {
if g.bounds.contains(point) {
return Some((id, 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 !is_tracked_list(id) {
self.current = None;
return;
}
self.current = Some(id.clone());
*self.geo_mut(id) = Some(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 == TRACK_LIST_ID || target == QUEUE_RECENT_LIST_ID {
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,
) {
if id == Some(&SEARCH_INPUT_ID) {
self.search_input = Some(bounds);
}
}
fn finish(&self) -> Outcome<Message> {
Outcome::Some(Message::ListBoundsCaptured(Box::new(self.clone())))
}
}
#[derive(Default, Clone, Debug)]
pub struct CaptureSearchHistoryRows {
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() -> Self {
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.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(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(),
})
}
}