WIP: buildout mouse interactions

This commit is contained in:
Jeremy Soller 2024-02-26 12:05:29 -07:00
parent 61fe3c093d
commit c8f4eb9d34
4 changed files with 224 additions and 136 deletions

View file

@ -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<segmented_button::Entity>),
TabConfig(TabConfig),
TabContextAction(segmented_button::Entity, Action),
TabContextMenu(segmented_button::Entity, Option<Point>),
TabMessage(Option<segmented_button::Entity>, tab::Message),
TabNew,
TabRescan(segmented_button::Entity, Vec<tab::Item>),
@ -862,20 +861,14 @@ impl Application for App {
_ => {}
}
}
Message::TabContextMenu(entity, position_opt) => {
match self.tab_model.data_mut::<Tab>(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::<Tab>(entity) {
Some(tab) => {
@ -981,38 +974,10 @@ impl Application for App {
let entity = self.tab_model.active();
match self.tab_model.data::<Tab>(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

View file

@ -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

View file

@ -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<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
@ -23,9 +29,10 @@ pub struct MouseArea<'a, Message, Theme, Renderer> {
on_back_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_forward_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_forward_release: Option<Box<dyn Fn(Option<Point>) -> 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<Point>) -> 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<Point>,
}
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<Element<'a, Message, Theme, Renderer>>) -> Self {
pub fn new(content: impl Into<Element<'a, Message>>) -> 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<Message, Theme, Renderer>
for MouseArea<'a, Message, Theme, Renderer>
impl<'a, Message> Widget<Message, Theme, Renderer> 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::<State>();
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<MouseArea<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
impl<'a, Message> From<MouseArea<'a, Message>> 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<Message: Clone, Theme, Renderer>(
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
fn update<Message: Clone>(
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<Message: Clone, Theme, Renderer>(
}
}
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;

View file

@ -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<usize>),
Config(TabConfig),
ContextAction(Action),
ContextMenu(Option<Point>),
Drag(Option<Point>),
EditLocation(Option<Location>),
GoNext,
GoPrevious,
Location(Location),
LocationUp,
RightClick(usize),
Scroll(Viewport),
Thumbnail(PathBuf, Result<image::RgbaImage, ()>),
ToggleShowHidden,
View(View),
@ -542,6 +550,8 @@ pub struct Tab {
pub items_opt: Option<Vec<Item>>,
pub view: View,
pub dialog: Option<DialogKind>,
pub drag_opt: Option<Point>,
pub scroll_opt: Option<Viewport>,
pub edit_location: Option<Location>,
pub history_i: usize,
pub history: Vec<Location>,
@ -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<Message> {
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<Message> {
@ -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<Message> {
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()