Implement drag selection in grid view

This commit is contained in:
Jeremy Soller 2024-02-29 11:25:46 -07:00
parent 9759f3626f
commit 077a29e5ee
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
3 changed files with 315 additions and 224 deletions

View file

@ -17,9 +17,10 @@ use cosmic::{
#[allow(missing_debug_implementations)]
pub struct MouseArea<'a, Message> {
content: Element<'a, Message>,
on_drag: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_drag: Option<Box<dyn Fn(Option<Rectangle>) -> Message + 'a>>,
on_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_right_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_right_press_no_capture: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_right_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
@ -29,13 +30,13 @@ pub struct MouseArea<'a, Message> {
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,
show_drag_rect: bool,
}
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 {
pub fn on_drag(mut self, message: impl Fn(Option<Rectangle>) -> Message + 'a) -> Self {
self.on_drag = Some(Box::new(message));
self
}
@ -54,6 +55,12 @@ impl<'a, Message> MouseArea<'a, Message> {
self
}
#[must_use]
pub fn on_resize(mut self, message: impl Fn(Size) -> Message + 'a) -> Self {
self.on_resize = Some(Box::new(message));
self
}
/// The message to emit on a right button press.
#[must_use]
pub fn on_right_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self {
@ -121,8 +128,8 @@ impl<'a, Message> MouseArea<'a, Message> {
}
#[must_use]
pub fn show_drag_box(mut self, show_drag_box: bool) -> Self {
self.show_drag_box = show_drag_box;
pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
self.show_drag_rect = show_drag_rect;
self
}
}
@ -130,10 +137,31 @@ impl<'a, Message> MouseArea<'a, Message> {
/// Local state of the [`MouseArea`].
#[derive(Default)]
struct State {
last_size: Option<Size>,
// TODO: Support on_mouse_enter and on_mouse_exit
drag_initiated: Option<Point>,
}
impl State {
fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> {
if let Some(drag_source) = self.drag_initiated {
if let Some(position) = cursor.position() {
if position.distance(drag_source) > 1.0 {
let min_x = drag_source.x.min(position.x);
let max_x = drag_source.x.max(position.x);
let min_y = drag_source.y.min(position.y);
let max_y = drag_source.y.max(position.y);
return Some(Rectangle::new(
Point::new(min_x, min_y),
Size::new(max_x - min_x, max_y - min_y),
));
}
}
}
None
}
}
impl<'a, Message> MouseArea<'a, Message> {
/// Creates a [`MouseArea`] with the given content.
pub fn new(content: impl Into<Element<'a, Message>>) -> Self {
@ -142,6 +170,7 @@ impl<'a, Message> MouseArea<'a, Message> {
on_drag: None,
on_press: None,
on_release: None,
on_resize: None,
on_right_press: None,
on_right_press_no_capture: None,
on_right_release: None,
@ -151,7 +180,7 @@ impl<'a, Message> MouseArea<'a, Message> {
on_back_release: None,
on_forward_press: None,
on_forward_release: None,
show_drag_box: false,
show_drag_rect: false,
}
}
}
@ -274,35 +303,25 @@ where
viewport,
);
if self.show_drag_box {
if self.show_drag_rect {
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()
if let Some(bounds) = state.drag_rect(cursor) {
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(),
},
Color::from(bg_color),
);
}
..Default::default()
},
Color::from(bg_color),
);
}
}
}
@ -340,7 +359,17 @@ fn update<Message: Clone>(
shell: &mut Shell<'_, Message>,
state: &mut State,
) -> event::Status {
if state.drag_initiated.is_none() && !cursor.is_over(layout.bounds()) {
let layout_bounds = layout.bounds();
if let Some(message) = widget.on_resize.as_ref() {
let size = layout_bounds.size();
if state.last_size != Some(size) {
shell.publish(message(size));
state.last_size = Some(size);
}
}
if state.drag_initiated.is_none() && !cursor.is_over(layout_bounds) {
return event::Status::Ignored;
}
@ -349,7 +378,7 @@ fn update<Message: Clone>(
{
state.drag_initiated = cursor.position();
if let Some(message) = widget.on_press.as_ref() {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -360,7 +389,7 @@ fn update<Message: Clone>(
{
state.drag_initiated = None;
if let Some(message) = widget.on_release.as_ref() {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -368,7 +397,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_right_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -376,7 +405,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_right_press_no_capture.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Ignored;
}
@ -384,7 +413,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_right_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -392,7 +421,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_middle_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -400,7 +429,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_middle_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -408,7 +437,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_back_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -416,7 +445,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_back_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -424,7 +453,7 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_forward_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
@ -432,20 +461,20 @@ fn update<Message: Clone>(
if let Some(message) = widget.on_forward_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward)) = event {
shell.publish(message(cursor.position_in(layout.bounds())));
shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured;
}
}
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 {
shell.publish(message(cursor.position_in(layout.bounds())));
return event::Status::Captured;
}
}
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
shell.publish(message(drag_rect.intersection(&layout_bounds).map(
|mut rect| {
rect.x -= layout_bounds.x;
rect.y -= layout_bounds.y;
rect
},
)));
}
event::Status::Ignored

View file

@ -9,6 +9,7 @@ use cosmic::{
//TODO: export in cosmic::widget
widget::{horizontal_rule, scrollable::Viewport},
Alignment,
Color,
ContentFit,
Length,
Point,
@ -20,6 +21,7 @@ use cosmic::{
use mime_guess::MimeGuess;
use once_cell::sync::Lazy;
use std::{
cell::Cell,
cmp::Ordering,
collections::HashMap,
fmt,
@ -40,6 +42,8 @@ use crate::{
};
const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
const GRID_ITEM_HEIGHT: usize = 116;
const GRID_ITEM_WIDTH: usize = 96;
//TODO: adjust for locales?
const TIME_FORMAT: &'static str = "%a %-d %b %-Y %r";
static SPECIAL_DIRS: Lazy<HashMap<PathBuf, &'static str>> = Lazy::new(|| {
@ -73,30 +77,39 @@ static SPECIAL_DIRS: Lazy<HashMap<PathBuf, &'static str>> = Lazy::new(|| {
}
special_dirs
});
fn button_style(selected: bool) -> theme::Button {
//TODO: move to libcosmic
fn button_appearance(
theme: &theme::Theme,
selected: bool,
accent: bool,
) -> widget::button::Appearance {
let cosmic = theme.cosmic();
let mut appearance = widget::button::Appearance::new();
if selected {
if accent {
appearance.background = Some(Color::from(cosmic.accent_color()).into());
appearance.icon_color = Some(Color::from(cosmic.on_accent_color()));
appearance.text_color = Some(Color::from(cosmic.on_accent_color()));
} else {
appearance.background = Some(Color::from(cosmic.bg_component_color()).into());
}
}
appearance.border_radius = cosmic.radius_s().into();
appearance
}
fn button_style(selected: bool, accent: bool) -> theme::Button {
//TODO: move to libcosmic?
theme::Button::Custom {
active: Box::new(move |focused, theme| {
let mut appearance =
widget::button::StyleSheet::active(theme, focused, &theme::Button::MenuItem);
if !selected {
appearance.background = None;
}
appearance
}),
disabled: Box::new(move |theme| {
let mut appearance =
widget::button::StyleSheet::disabled(theme, &theme::Button::MenuItem);
if !selected {
appearance.background = None;
}
appearance
button_appearance(theme, selected || focused, accent)
}),
disabled: Box::new(move |theme| button_appearance(theme, selected, accent)),
hovered: Box::new(move |focused, theme| {
widget::button::StyleSheet::hovered(theme, focused, &theme::Button::MenuItem)
button_appearance(theme, selected || focused, accent)
}),
pressed: Box::new(move |focused, theme| {
widget::button::StyleSheet::pressed(theme, focused, &theme::Button::MenuItem)
button_appearance(theme, selected || focused, accent)
}),
}
}
@ -243,6 +256,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
Some(mime) if mime.type_() == "image" => None,
_ => Some(Err(())),
},
rect_opt: Cell::new(None),
selected: false,
click_time: None,
});
@ -323,6 +337,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
icon_handle_grid,
icon_handle_list,
thumbnail_res_opt: Some(Err(())),
rect_opt: Cell::new(None),
selected: false,
click_time: None,
});
@ -369,7 +384,7 @@ pub enum Message {
Config(TabConfig),
ContextAction(Action),
ContextMenu(Option<Point>),
Drag(Option<Point>),
Drag(Option<Rectangle>),
EditLocation(Option<Location>),
GoNext,
GoPrevious,
@ -380,6 +395,7 @@ pub enum Message {
Location(Location),
LocationUp,
Open,
Resize(Size),
RightClick(usize),
Scroll(Viewport),
Thumbnail(PathBuf, Result<image::RgbaImage, ()>),
@ -422,6 +438,7 @@ pub struct Item {
pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle,
pub thumbnail_res_opt: Option<Result<image::RgbaImage, ()>>,
pub rect_opt: Cell<Option<Rectangle>>,
pub selected: bool,
pub click_time: Option<Instant>,
}
@ -510,6 +527,7 @@ impl fmt::Debug for Item {
.field("hidden", &self.hidden)
.field("path", &self.path)
// icon_handles
.field("rect_opt", &self.rect_opt)
.field("selected", &self.selected)
.field("click_time", &self.click_time)
.finish()
@ -535,8 +553,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 size_opt: Option<Size>,
pub edit_location: Option<Location>,
pub edit_location_id: widget::Id,
pub history_i: usize,
@ -555,10 +573,10 @@ impl Tab {
location,
context_menu: None,
items_opt: None,
view: View::List,
view: View::Grid,
dialog: None,
drag_opt: None,
scroll_opt: None,
size_opt: None,
edit_location: None,
edit_location_id: widget::Id::unique(),
history_i: 0,
@ -587,10 +605,12 @@ impl Tab {
None => return,
};
println!("{:?}", rect);
for (_i, item) in items.iter_mut().enumerate() {
item.selected = false;
//TODO
//TODO: modifiers
item.selected = match item.rect_opt.get() {
Some(item_rect) => item_rect.intersects(&rect),
None => false,
};
}
}
@ -661,32 +681,11 @@ impl Tab {
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),
);
Message::Drag(rect_opt) => match rect_opt {
Some(rect) => {
self.select_by_drag(rect);
}
None => {
self.drag_opt = None;
}
None => {}
},
Message::EditLocation(edit_location) => {
if self.edit_location.is_none() && edit_location.is_some() {
@ -793,6 +792,9 @@ impl Tab {
}
}
}
Message::Resize(size) => {
self.size_opt = Some(size);
}
Message::RightClick(click_i) => {
if let Some(ref mut items) = self.items_opt {
if !items.get(click_i).map_or(false, |x| x.selected) {
@ -1106,62 +1108,121 @@ impl Tab {
}
pub fn grid_view(&self, core: &Core) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing;
let cosmic_theme::Spacing {
space_xs,
space_xxs,
space_xxxs,
..
} = core.system_theme().cosmic().spacing;
//TODO: get from config
let item_width = Length::Fixed(96.0);
let item_height = Length::Fixed(116.0);
let TabConfig {
show_hidden,
icon_sizes,
} = self.config;
let mut children: Vec<Element<_>> = Vec::new();
let max_width = match self.size_opt {
Some(size) => {
// Hack to make room for scroll bar
(size.width.floor() as usize)
.checked_sub(space_xs as usize)
.unwrap_or(0)
}
None => 0,
}
.max(GRID_ITEM_WIDTH);
let (cols, column_spacing) = {
let width_m1 = max_width.checked_sub(GRID_ITEM_WIDTH).unwrap_or(0);
let cols_m1 = width_m1 / (GRID_ITEM_WIDTH + space_xxs as usize);
let cols = cols_m1 + 1;
let spacing = width_m1
.checked_div(cols_m1)
.unwrap_or(0)
.checked_sub(GRID_ITEM_WIDTH)
.unwrap_or(0);
(cols, spacing as u16)
};
let mut grid = widget::grid()
.column_spacing(column_spacing)
.row_spacing(space_xxs)
// Hack to make room for scroll bar
.padding([0, space_xxs, 0, 0].into());
if let Some(ref items) = self.items_opt {
let mut count = 0;
let mut col = 0;
let mut row = 0;
let mut hidden = 0;
for (i, item) in items.iter().enumerate() {
if !show_hidden && item.hidden {
item.rect_opt.set(None);
hidden += 1;
continue;
}
item.rect_opt.set(Some(Rectangle::new(
Point::new(
(col * (GRID_ITEM_WIDTH + column_spacing as usize)) as f32,
(row * (GRID_ITEM_HEIGHT + space_xxs as usize)) as f32,
),
Size::new(GRID_ITEM_WIDTH as f32, GRID_ITEM_HEIGHT as f32),
)));
let button = widget::button(
widget::column::with_children(vec![
//TODO: one focus group per grid item (needs custom widget)
let buttons = vec![
widget::button(
widget::icon::icon(item.icon_handle_grid.clone())
.content_fit(ContentFit::Contain)
.size(icon_sizes.grid())
.into(),
widget::text(item.name.clone()).into(),
])
.size(icon_sizes.grid()),
)
.on_press(Message::Click(Some(i)))
.padding(space_xxxs)
.style(button_style(item.selected, false)),
widget::button(widget::text(item.name.clone()))
.on_press(Message::Click(Some(i)))
.padding([0, space_xxs])
.style(button_style(item.selected, true)),
];
let mut column = widget::column::with_capacity(buttons.len())
.align_items(Alignment::Center)
.spacing(space_xxs)
.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(
mouse_area::MouseArea::new(button)
.on_right_press_no_capture(move |_point_opt| Message::RightClick(i))
.into(),
);
.height(Length::Fixed(GRID_ITEM_HEIGHT as f32))
.width(Length::Fixed(GRID_ITEM_WIDTH as f32));
for button in buttons {
if self.context_menu.is_some() {
column = column.push(button)
} else {
column = column.push(
mouse_area::MouseArea::new(button).on_right_press_no_capture(
move |_point_opt| Message::RightClick(i),
),
);
}
}
grid = grid.push(column);
count += 1;
col += 1;
if col >= cols {
col = 0;
row += 1;
grid = grid.insert_row();
}
}
if count == 0 {
return self.empty_view(hidden > 0, core);
}
}
widget::scrollable(widget::flex_row(children))
.on_scroll(Message::Scroll)
.width(Length::Fill)
.into()
//TODO: allow drag in empty area
widget::scrollable(
mouse_area::MouseArea::new(grid)
.on_drag(Message::Drag)
.show_drag_rect(true),
)
.on_scroll(Message::Scroll)
.width(Length::Fill)
.into()
}
pub fn list_view(&self, core: &Core) -> Element<Message> {
@ -1218,9 +1279,12 @@ impl Tab {
} = self.config;
for (i, item) in items.iter().enumerate() {
if !show_hidden && item.hidden {
item.rect_opt.set(None);
hidden += 1;
continue;
}
//TODO: correct rectangle
item.rect_opt.set(None);
if count > 0 {
children.push(horizontal_rule(1).into());
@ -1272,7 +1336,7 @@ impl Tab {
.spacing(space_xxs),
)
.padding(space_xxs)
.style(button_style(item.selected))
.style(button_style(item.selected, false))
.on_press(Message::Click(Some(i)));
if self.context_menu.is_some() {
children.push(button.into());
@ -1309,12 +1373,10 @@ impl Tab {
};
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);
.on_resize(Message::Resize);
if self.context_menu.is_some() {
mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None));
} else {