diff --git a/src/app.rs b/src/app.rs index 59cf293..53d9703 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use cosmic::{ futures::{self, SinkExt}, keyboard::{Event as KeyEvent, Key, Modifiers}, subscription::{self, Subscription}, - window, Event, Length, Point, + window, Event, Length, }, style, widget::{self, segmented_button}, @@ -29,7 +29,7 @@ use crate::{ config::{AppTheme, Config, IconSizes, TabConfig, CONFIG_VERSION}, fl, home_dir, key_bind::{key_binds, KeyBind}, - menu, mouse_area, + menu, operation::Operation, tab::{self, ItemMetadata, Location, Tab}, }; @@ -126,7 +126,6 @@ pub enum Message { TabClose(Option), TabConfig(TabConfig), TabContextAction(segmented_button::Entity, Action), - TabContextMenu(segmented_button::Entity, Option), TabMessage(Option, tab::Message), TabNew, TabRescan(segmented_button::Entity, Vec), @@ -862,20 +861,14 @@ impl Application for App { _ => {} } } - Message::TabContextMenu(entity, position_opt) => { - match self.tab_model.data_mut::(entity) { - Some(tab) => { - // Update context menu position - tab.context_menu = position_opt; - } - _ => {} - } - // Disable side context page - self.core.window.show_context = false; - } Message::TabMessage(entity_opt, tab_message) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let tab::Message::ContextMenu(_point_opt) = tab_message { + // Disable side context page + self.core.window.show_context = false; + } + let mut update_opt = None; match self.tab_model.data_mut::(entity) { Some(tab) => { @@ -981,38 +974,10 @@ impl Application for App { let entity = self.tab_model.active(); match self.tab_model.data::(entity) { Some(tab) => { - let mut mouse_area = mouse_area::MouseArea::new( - tab.view(self.core()) - .map(move |message| Message::TabMessage(Some(entity), message)), - ) - .on_press(move |_point_opt| { - Message::TabMessage(Some(entity), tab::Message::Click(None)) - }) - .on_back_press(move |_point_opt| { - Message::TabMessage(None, tab::Message::GoPrevious) - }) - .on_forward_press(move |_point_opt| { - Message::TabMessage(None, tab::Message::GoNext) - }); - if tab.context_menu.is_some() { - mouse_area = mouse_area - .on_right_press(move |_point_opt| Message::TabContextMenu(entity, None)); - } else { - mouse_area = mouse_area.on_right_press(move |point_opt| { - Message::TabContextMenu(entity, point_opt) - }); - } - let mut popover = widget::popover(mouse_area, menu::context_menu(entity, &tab)); - match tab.context_menu { - Some(point) => { - let rounded = Point::new(point.x.round(), point.y.round()); - popover = popover.position(rounded); - } - None => { - popover = popover.show_popup(false); - } - } - tab_column = tab_column.push(popover); + let tab_view = tab + .view(self.core()) + .map(move |message| Message::TabMessage(Some(entity), message)); + tab_column = tab_column.push(tab_view); } None => { //TODO diff --git a/src/menu.rs b/src/menu.rs index 0e8a57d..f5a8824 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -7,7 +7,6 @@ use cosmic::{ widget::{ self, menu::{ItemHeight, ItemWidth, MenuBar, MenuTree}, - segmented_button, }, Element, }; @@ -17,7 +16,7 @@ use crate::{ app::{Action, Message}, fl, key_bind::KeyBind, - tab::{Location, Tab}, + tab::{self, Location, Tab}, }; macro_rules! menu_button { @@ -35,10 +34,10 @@ macro_rules! menu_button { ); } -pub fn context_menu<'a>(entity: segmented_button::Entity, tab: &Tab) -> Element<'a, Message> { +pub fn context_menu<'a>(tab: &Tab) -> Element<'a, tab::Message> { //TODO: show key bindings in context menu? let menu_action = |label, action| { - menu_button!(widget::text(label)).on_press(Message::TabContextAction(entity, action)) + menu_button!(widget::text(label)).on_press(tab::Message::ContextAction(action)) }; let selected = tab diff --git a/src/mouse_area.rs b/src/mouse_area.rs index 0f343f1..996c504 100644 --- a/src/mouse_area.rs +++ b/src/mouse_area.rs @@ -1,16 +1,22 @@ //! A container for capturing mouse events. -use cosmic::iced_core::{ - event::{self, Event}, - layout, mouse, overlay, renderer, touch, - widget::{tree, Operation, OperationOutputWrapper, Tree}, - Size, {Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget}, +use cosmic::{ + iced_core::{ + border::Border, + event::{self, Event}, + layout, mouse, overlay, + renderer::{self, Quad, Renderer as _}, + touch, + widget::{tree, Operation, OperationOutputWrapper, Tree}, + Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Widget, + }, + Element, Renderer, Theme, }; /// Emit messages on mouse events. #[allow(missing_debug_implementations)] -pub struct MouseArea<'a, Message, Theme, Renderer> { - content: Element<'a, Message, Theme, Renderer>, +pub struct MouseArea<'a, Message> { + content: Element<'a, Message>, on_drag: Option) -> Message + 'a>>, on_press: Option) -> Message + 'a>>, on_release: Option) -> Message + 'a>>, @@ -23,9 +29,10 @@ pub struct MouseArea<'a, Message, Theme, Renderer> { on_back_release: Option) -> Message + 'a>>, on_forward_press: Option) -> Message + 'a>>, on_forward_release: Option) -> Message + 'a>>, + show_drag_box: bool, } -impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { +impl<'a, Message> MouseArea<'a, Message> { /// The message to emit when a drag is initiated. #[must_use] pub fn on_drag(mut self, message: impl Fn(Option) -> Message + 'a) -> Self { @@ -112,6 +119,12 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self.on_forward_release = Some(Box::new(message)); self } + + #[must_use] + pub fn show_drag_box(mut self, show_drag_box: bool) -> Self { + self.show_drag_box = show_drag_box; + self + } } /// Local state of the [`MouseArea`]. @@ -121,9 +134,9 @@ struct State { drag_initiated: Option, } -impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { +impl<'a, Message> MouseArea<'a, Message> { /// Creates a [`MouseArea`] with the given content. - pub fn new(content: impl Into>) -> Self { + pub fn new(content: impl Into>) -> Self { MouseArea { content: content.into(), on_drag: None, @@ -138,14 +151,13 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { on_back_release: None, on_forward_press: None, on_forward_release: None, + show_drag_box: false, } } } -impl<'a, Message, Theme, Renderer> Widget - for MouseArea<'a, Message, Theme, Renderer> +impl<'a, Message> Widget for MouseArea<'a, Message> where - Renderer: renderer::Renderer, Message: Clone, { fn tag(&self) -> tree::Tag { @@ -261,6 +273,38 @@ where cursor, viewport, ); + + if self.show_drag_box { + let state = tree.state.downcast_ref::(); + if let Some(a) = state.drag_initiated { + if let Some(b) = cursor.position() { + let min_x = a.x.min(b.x); + let max_x = a.x.max(b.x); + let min_y = a.y.min(b.y); + let max_y = a.y.max(b.y); + let bounds = Rectangle::new( + Point::new(min_x, min_y), + Size::new(max_x - min_x, max_y - min_y), + ); + let cosmic = theme.cosmic(); + let mut bg_color = cosmic.accent_color(); + //TODO: get correct alpha + bg_color.alpha = 0.2; + renderer.fill_quad( + Quad { + bounds, + border: Border { + color: cosmic.accent_color().into(), + width: 1.0, + radius: cosmic.radius_xs().into(), + }, + ..Default::default() + }, + Color::from(bg_color), + ); + } + } + } } fn overlay<'b>( @@ -275,50 +319,47 @@ where } } -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> +impl<'a, Message> From> for Element<'a, Message> where Message: 'a + Clone, Renderer: 'a + renderer::Renderer, Theme: 'a, { - fn from( - area: MouseArea<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { + fn from(area: MouseArea<'a, Message>) -> Element<'a, Message> { Element::new(area) } } /// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`] /// accordingly. -fn update( - widget: &mut MouseArea<'_, Message, Theme, Renderer>, +fn update( + widget: &mut MouseArea<'_, Message>, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, ) -> event::Status { - if !cursor.is_over(layout.bounds()) { + if state.drag_initiated.is_none() && !cursor.is_over(layout.bounds()) { return event::Status::Ignored; } - if let Some(message) = widget.on_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { - state.drag_initiated = cursor.position(); + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.drag_initiated = cursor.position(); + if let Some(message) = widget.on_press.as_ref() { shell.publish(message(cursor.position_in(layout.bounds()))); return event::Status::Captured; } } - if let Some(message) = widget.on_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - state.drag_initiated = None; + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + state.drag_initiated = None; + if let Some(message) = widget.on_release.as_ref() { shell.publish(message(cursor.position_in(layout.bounds()))); return event::Status::Captured; @@ -397,16 +438,9 @@ fn update( } } - if state.drag_initiated.is_none() && widget.on_drag.is_some() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { - state.drag_initiated = cursor.position(); - } - } else if let Some((message, drag_source)) = widget.on_drag.as_ref().zip(state.drag_initiated) { + if let Some((message, drag_source)) = widget.on_drag.as_ref().zip(state.drag_initiated) { if let Some(position) = cursor.position() { if position.distance(drag_source) > 1.0 { - state.drag_initiated = None; shell.publish(message(cursor.position_in(layout.bounds()))); return event::Status::Captured; diff --git a/src/tab.rs b/src/tab.rs index d8ad459..f0483b0 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -7,11 +7,13 @@ use cosmic::{ keyboard::Modifiers, subscription::{self, Subscription}, //TODO: export in cosmic::widget - widget::horizontal_rule, + widget::{horizontal_rule, scrollable::Viewport}, Alignment, ContentFit, Length, Point, + Rectangle, + Size, }, theme, widget, Element, }; @@ -28,10 +30,12 @@ use std::{ }; use crate::{ + app::Action, config::{IconSizes, TabConfig}, dialog::DialogKind, - fl, + fl, menu, mime_icon::mime_icon, + mouse_area, }; const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); @@ -390,12 +394,16 @@ impl Location { pub enum Message { Click(Option), Config(TabConfig), + ContextAction(Action), + ContextMenu(Option), + Drag(Option), EditLocation(Option), GoNext, GoPrevious, Location(Location), LocationUp, RightClick(usize), + Scroll(Viewport), Thumbnail(PathBuf, Result), ToggleShowHidden, View(View), @@ -542,6 +550,8 @@ pub struct Tab { pub items_opt: Option>, pub view: View, pub dialog: Option, + pub drag_opt: Option, + pub scroll_opt: Option, pub edit_location: Option, pub history_i: usize, pub history: Vec, @@ -555,8 +565,10 @@ impl Tab { location, context_menu: None, items_opt: None, - view: View::List, + view: View::Grid, dialog: None, + drag_opt: None, + scroll_opt: None, edit_location: None, history_i: 0, history, @@ -576,6 +588,23 @@ impl Tab { } } + fn select_by_drag(&mut self, rect: Rectangle) { + let items = match &mut self.items_opt { + Some(some) => some, + None => return, + }; + + println!("{:?}", rect); + let (row, col) = match self.view { + View::Grid => (0, 0), + View::List => (0, 0), + }; + for (i, item) in items.iter_mut().enumerate() { + item.selected = false; + //TODO + } + } + pub fn update(&mut self, message: Message, modifiers: Modifiers) -> bool { let mut cd = None; let mut history_i_opt = None; @@ -629,6 +658,43 @@ impl Tab { Message::Config(config) => { self.config = config; } + Message::ContextAction(action) => { + // Close context menu + self.context_menu = None; + + // TODO: run actions message + println!("TODO {:?}", action); + } + Message::ContextMenu(point_opt) => { + self.context_menu = point_opt; + } + Message::Drag(point_opt) => match point_opt { + Some(point) => { + let drag = match self.drag_opt { + Some(some) => some, + None => { + self.drag_opt = Some(point); + point + } + }; + let min_x = drag.x.min(point.x); + let max_x = drag.x.max(point.x); + let min_y = drag.y.min(point.y); + let max_y = drag.y.max(point.y); + let offset_y = self + .scroll_opt + .map(|x| x.absolute_offset().y) + .unwrap_or_default(); + let rect = Rectangle::new( + Point::new(min_x, min_y + offset_y), + Size::new(max_x - min_x, max_y - min_y), + ); + self.select_by_drag(rect); + } + None => { + self.drag_opt = None; + } + }, Message::EditLocation(edit_location) => { self.edit_location = edit_location; } @@ -679,6 +745,9 @@ impl Tab { } } } + Message::Scroll(viewport) => { + self.scroll_opt = Some(viewport); + } Message::Thumbnail(path, thumbnail_res) => { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { @@ -875,30 +944,27 @@ impl Tab { pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element { let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing; - widget::column::with_children(vec![ - self.location_view(core), - widget::container( - widget::column::with_children(vec![ - widget::icon::from_name("folder-symbolic") - .size(64) - .icon() - .into(), - widget::text(if has_hidden { - fl!("empty-folder-hidden") - } else { - fl!("empty-folder") - }) + widget::column::with_children(vec![widget::container( + widget::column::with_children(vec![ + widget::icon::from_name("folder-symbolic") + .size(64) + .icon() .into(), - ]) - .align_items(Alignment::Center) - .spacing(space_xxs), - ) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .width(Length::Fill) - .height(Length::Fill) - .into(), - ]) + widget::text(if has_hidden { + fl!("empty-folder-hidden") + } else { + fl!("empty-folder") + }) + .into(), + ]) + .align_items(Alignment::Center) + .spacing(space_xxs), + ) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(Length::Fill) + .into()]) .into() } @@ -936,13 +1002,14 @@ impl Tab { .height(item_height) .width(item_width), ) + .padding(0) .style(button_style(item.selected)) .on_press(Message::Click(Some(i))); if self.context_menu.is_some() { children.push(button.into()); } else { children.push( - crate::mouse_area::MouseArea::new(button) + mouse_area::MouseArea::new(button) .on_right_press_no_capture(move |_point_opt| Message::RightClick(i)) .into(), ); @@ -954,13 +1021,10 @@ impl Tab { return self.empty_view(hidden > 0, core); } } - widget::column::with_children(vec![ - self.location_view(core), - widget::scrollable(widget::flex_row(children)) - .width(Length::Fill) - .into(), - ]) - .into() + widget::scrollable(widget::flex_row(children)) + .on_scroll(Message::Scroll) + .width(Length::Fill) + .into() } pub fn list_view(&self, core: &Core) -> Element { @@ -1060,7 +1124,7 @@ impl Tab { children.push(button.into()); } else { children.push( - crate::mouse_area::MouseArea::new(button) + mouse_area::MouseArea::new(button) .on_right_press_no_capture(move |_point_opt| Message::RightClick(i)) .into(), ); @@ -1073,24 +1137,50 @@ impl Tab { } } - widget::column::with_children(vec![ - self.location_view(core).into(), - widget::scrollable( - widget::column::with_children(children) - // Hack to make room for scroll bar - .padding([0, space_xxs, 0, 0]), - ) - .width(Length::Fill) - .into(), - ]) + widget::scrollable( + widget::column::with_children(children) + // Hack to make room for scroll bar + .padding([0, space_xxs, 0, 0]), + ) + .on_scroll(Message::Scroll) + .width(Length::Fill) .into() } pub fn view(&self, core: &Core) -> Element { - widget::container(match self.view { + let location_view = self.location_view(core); + let item_view = match self.view { View::Grid => self.grid_view(core), View::List => self.list_view(core), - }) + }; + let mut mouse_area = + mouse_area::MouseArea::new(widget::container(item_view).height(Length::Fill)) + .on_drag(move |point_opt| Message::Drag(point_opt)) + .on_press(move |_point_opt| Message::Click(None)) + .on_release(move |point_opt| Message::Drag(None)) + .on_back_press(move |_point_opt| Message::GoPrevious) + .on_forward_press(move |_point_opt| Message::GoNext) + .show_drag_box(true); + if self.context_menu.is_some() { + mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None)); + } else { + mouse_area = + mouse_area.on_right_press(move |point_opt| Message::ContextMenu(point_opt)); + } + let mut popover = widget::popover(mouse_area, menu::context_menu(&self)); + match self.context_menu { + Some(point) => { + let rounded = Point::new(point.x.round(), point.y.round()); + popover = popover.position(rounded); + } + None => { + popover = popover.show_popup(false); + } + } + widget::container(widget::column::with_children(vec![ + location_view, + popover.into(), + ])) .height(Length::Fill) .width(Length::Fill) .into()