Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Columns #635

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/icons/move_column_4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion crates/notedeck_chrome/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn light_color_theme() -> ColorTheme {

// INACTIVE WIDGET
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
inactive_bg_fill: LIGHT_GRAY,
inactive_bg_fill: LIGHTER_GRAY,
inactive_weak_bg_fill: EVEN_DARKER_GRAY,
}
}
Expand Down
21 changes: 20 additions & 1 deletion crates/notedeck_columns/src/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,25 @@ impl Columns {
self.new_column_picker();
}
}

pub fn move_col(&mut self, from_index: usize, to_index: usize) {
if from_index == to_index
|| from_index >= self.columns.len()
|| to_index >= self.columns.len()
{
return;
}

if from_index < to_index {
for i in from_index..to_index {
self.columns.swap_indices(i, i + 1);
}
} else {
for i in (to_index..from_index).rev() {
self.columns.swap_indices(i, i + 1);
}
}
}
}

pub enum IntermediaryRoute {
Expand All @@ -219,6 +238,6 @@ pub enum IntermediaryRoute {
}

pub enum ColumnsAction {
// Switch(usize), TODO: could use for keyboard selection
Switch(usize, usize), // from Switch.0 to Switch.1,
Remove(usize),
}
4 changes: 4 additions & 0 deletions crates/notedeck_columns/src/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ impl SwitchingAction {
ColumnsAction::Remove(index) => {
get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index)
}
ColumnsAction::Switch(from, to) => {
get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to);
}
},
SwitchingAction::Decks(decks_action) => match *decks_action {
DecksAction::Switch(index) => {
Expand Down Expand Up @@ -447,6 +450,7 @@ pub fn render_nav(
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
ctx.accounts.get_selected_account().map(|a| &a.pubkey),
nav.routes(),
col,
)
.show(ui),
NavUiType::Body => render_nav_body(ui, app, ctx, nav.routes().last().expect("top"), col),
Expand Down
269 changes: 258 additions & 11 deletions crates/notedeck_columns/src/ui/column/header.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::colors;
use crate::column::ColumnsAction;
use crate::nav::RenderNavAction;
use crate::nav::SwitchingAction;
use crate::{
column::Columns,
nav::RenderNavAction,
route::Route,
timeline::{ColumnTitle, TimelineId, TimelineKind, TimelineRoute},
ui::{
Expand All @@ -9,6 +12,7 @@ use crate::{
},
};

use egui::Margin;
use egui::{RichText, Stroke, UiBuilder};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
Expand All @@ -20,6 +24,7 @@ pub struct NavTitle<'a> {
columns: &'a Columns,
deck_author: Option<&'a Pubkey>,
routes: &'a [Route],
col_id: usize,
}

impl<'a> NavTitle<'a> {
Expand All @@ -29,13 +34,15 @@ impl<'a> NavTitle<'a> {
columns: &'a Columns,
deck_author: Option<&'a Pubkey>,
routes: &'a [Route],
col_id: usize,
) -> Self {
NavTitle {
ndb,
img_cache,
columns,
deck_author,
routes,
col_id,
}
}

Expand Down Expand Up @@ -77,10 +84,18 @@ impl<'a> NavTitle<'a> {
ui.add_space(chev_x + item_spacing);
}

let remove_column = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());
let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());

if remove_column {
Some(RenderNavAction::RemoveColumn)
if let Some(resp) = title_resp {
match resp {
TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn),
TitleResponse::MoveColumn(to_index) => {
let from = self.col_id;
Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns(
ColumnsAction::Switch(from, to_index),
)))
}
}
} else if back_button_resp.map_or(false, |r| r.clicked()) {
Some(RenderNavAction::Back)
} else {
Expand Down Expand Up @@ -186,6 +201,190 @@ impl<'a> NavTitle<'a> {
}
}

// returns the column index to switch to, if any
fn move_button_section(&mut self, ui: &mut egui::Ui) -> Option<usize> {
let cur_id = ui.id().with("move");
let mut move_resp = ui.add(grab_button());

// showing the hover text while showing the move tooltip causes some weird visuals
if ui.data(|d| d.get_temp::<bool>(cur_id).is_none()) {
move_resp = move_resp.on_hover_text("Moves this column to another positon");
}

if move_resp.clicked() {
ui.data_mut(|d| {
if let Some(val) = d.get_temp::<bool>(cur_id) {
if val {
d.remove_temp::<bool>(cur_id);
} else {
d.insert_temp(cur_id, true);
}
} else {
d.insert_temp(cur_id, true);
}
});
}

ui.data(|d| d.get_temp(cur_id)).and_then(|val| {
if val {
let resp = self.add_move_tooltip(cur_id, &move_resp);
if move_resp.clicked_elsewhere() || resp.is_some() {
ui.data_mut(|d| d.remove_temp::<bool>(cur_id));
}
resp
} else {
None
}
})
}

fn move_tooltip_col_presentation(&mut self, ui: &mut egui::Ui, col: usize) -> egui::Response {
ui.horizontal(|ui| {
self.title_presentation(ui, self.columns.column(col).router().top(), 32.0);
})
.response
}

fn add_move_tooltip(&mut self, id: egui::Id, move_resp: &egui::Response) -> Option<usize> {
let mut inner_resp = None;
move_resp.show_tooltip_ui(|ui| {
let x_range = ui.available_rect_before_wrap().x_range();
let is_dragging = egui::DragAndDrop::payload::<usize>(ui.ctx()).is_some(); // must be outside ui.dnd_drop_zone to capture properly
let (_, _) = ui.dnd_drop_zone::<usize, ()>(
egui::Frame::none()
.inner_margin(Margin::same(8.0))
.rounding(egui::Rounding::same(8.0)),
|ui| {
let distances: Vec<(egui::Response, f32)> =
self.collect_column_distances(ui, id);

if let Some((closest_index, closest_resp, distance)) =
self.find_closest_column(&distances)
{
if is_dragging && closest_index != self.col_id {
if self.should_draw_hint(closest_index, distance) {
ui.painter().hline(
x_range,
self.calculate_hint_y(
&distances,
closest_resp,
closest_index,
distance,
),
egui::Stroke::new(1.0, ui.visuals().text_color()),
);
}

if ui.input(|i| i.pointer.any_released()) {
inner_resp =
Some(self.calculate_new_index(closest_index, distance));
}
}
}
},
);
});
inner_resp
}

fn collect_column_distances(
&mut self,
ui: &mut egui::Ui,
id: egui::Id,
) -> Vec<(egui::Response, f32)> {
let y_margin = 4.0;
let item_frame = egui::Frame::none()
.rounding(egui::Rounding::same(8.0))
.inner_margin(Margin::symmetric(8.0, y_margin));

(0..self.columns.num_columns())
.filter_map(|col| {
let item_id = id.with(col);
let col_resp = if col == self.col_id {
ui.dnd_drag_source(item_id, col, |ui| {
item_frame
.stroke(egui::Stroke::new(2.0, colors::PINK))
.fill(ui.visuals().widgets.noninteractive.bg_stroke.color)
.show(ui, |ui| self.move_tooltip_col_presentation(ui, col));
})
.response
} else {
item_frame
.show(ui, |ui| {
self.move_tooltip_col_presentation(ui, col)
.on_hover_cursor(egui::CursorIcon::NotAllowed)
})
.response
};

ui.input(|i| i.pointer.interact_pos()).map(|pointer| {
let distance = pointer.y - col_resp.rect.center().y;
(col_resp, distance)
})
})
.collect()
}

fn find_closest_column(
&'a self,
distances: &'a [(egui::Response, f32)],
) -> Option<(usize, &'a egui::Response, f32)> {
distances
.iter()
.enumerate()
.min_by(|(_, (_, dist1)), (_, (_, dist2))| {
dist1.abs().partial_cmp(&dist2.abs()).unwrap()
})
.filter(|(index, (_, distance))| {
(index + 1 != self.col_id && *distance > 0.0)
|| (index.saturating_sub(1) != self.col_id && *distance < 0.0)
})
.map(|(index, (resp, dist))| (index, resp, *dist))
}

fn should_draw_hint(&self, closest_index: usize, distance: f32) -> bool {
let is_above = distance < 0.0;
(is_above && closest_index.saturating_sub(1) != self.col_id)
|| (!is_above && closest_index + 1 != self.col_id)
}

fn calculate_new_index(&self, closest_index: usize, distance: f32) -> usize {
let moving_up = self.col_id > closest_index;
match (distance < 0.0, moving_up) {
(true, true) | (false, false) => closest_index,
(true, false) => closest_index.saturating_sub(1),
(false, true) => closest_index + 1,
}
}

fn calculate_hint_y(
&self,
distances: &[(egui::Response, f32)],
closest_resp: &egui::Response,
closest_index: usize,
distance: f32,
) -> f32 {
let y_margin = 4.0;

let offset = if distance < 0.0 {
distances
.get(closest_index.wrapping_sub(1))
.map(|(above_resp, _)| (closest_resp.rect.top() - above_resp.rect.bottom()) / 2.0)
.unwrap_or(y_margin)
} else {
distances
.get(closest_index + 1)
.map(|(below_resp, _)| (below_resp.rect.top() - closest_resp.rect.bottom()) / 2.0)
.unwrap_or(y_margin)
};

if distance < 0.0 {
closest_resp.rect.top() - offset
} else {
closest_resp.rect.bottom() + offset
}
}

fn pubkey_pfp<'txn, 'me>(
&'me mut self,
txn: &'txn Transaction,
Expand Down Expand Up @@ -294,23 +493,39 @@ impl<'a> NavTitle<'a> {
};
}

fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> bool {
fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> {
if !navigating {
self.title_pfp(ui, top, 32.0);
self.title_label(ui, top);
self.title_presentation(ui, top, 32.0);
}

ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if navigating {
self.title_label(ui, top);
self.title_pfp(ui, top, 32.0);
false
self.title_presentation(ui, top, 32.0);
None
} else {
self.delete_button_section(ui)
let move_col = self.move_button_section(ui);
let remove_col = self.delete_button_section(ui);
if let Some(col) = move_col {
Some(TitleResponse::MoveColumn(col))
} else if remove_col {
Some(TitleResponse::RemoveColumn)
} else {
None
}
}
})
.inner
}

fn title_presentation(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) {
self.title_pfp(ui, top, pfp_size);
self.title_label(ui, top);
}
}

enum TitleResponse {
RemoveColumn,
MoveColumn(usize),
}

fn prev<R>(xs: &[R]) -> Option<&R> {
Expand Down Expand Up @@ -338,3 +553,35 @@ fn chevron(

r
}

fn grab_button() -> impl egui::Widget {
|ui: &mut egui::Ui| -> egui::Response {
let max_size = egui::vec2(48.0, 48.0);
let helper = AnimationHelper::new(ui, "grab", max_size);
let painter = ui.painter_at(helper.get_animation_rect());
let min_circle_radius = 2.0;
let cur_circle_radius = helper.scale_1d_pos(min_circle_radius);
let horiz_spacing = 4.0;
let vert_spacing = 10.0;
let horiz_from_center = (horiz_spacing + min_circle_radius) / 2.0;
let vert_from_center = (vert_spacing + min_circle_radius) / 2.0;

let color = ui.style().visuals.noninteractive().fg_stroke.color;

let middle_left = helper.scale_from_center(-horiz_from_center, 0.0);
let middle_right = helper.scale_from_center(horiz_from_center, 0.0);
let top_left = helper.scale_from_center(-horiz_from_center, -vert_from_center);
let top_right = helper.scale_from_center(horiz_from_center, -vert_from_center);
let bottom_left = helper.scale_from_center(-horiz_from_center, vert_from_center);
let bottom_right = helper.scale_from_center(horiz_from_center, vert_from_center);

painter.circle_filled(middle_left, cur_circle_radius, color);
painter.circle_filled(middle_right, cur_circle_radius, color);
painter.circle_filled(top_left, cur_circle_radius, color);
painter.circle_filled(top_right, cur_circle_radius, color);
painter.circle_filled(bottom_left, cur_circle_radius, color);
painter.circle_filled(bottom_right, cur_circle_radius, color);

helper.take_animation_response()
}
}
Loading