From c508a189410040226b6881c50fe0c672508aaada Mon Sep 17 00:00:00 2001 From: WanderingPenwing Date: Wed, 24 Jul 2024 11:26:37 +0200 Subject: [PATCH] draggable tabs --- calcifer.project | 2 +- src/core/app.rs | 6 ++- src/core/ui.rs | 107 +++++++++++++++++++++++++++++++++++++++-- src/main.rs | 5 ++ src/panels/tabs.rs | 117 +++++++++++++++++++++++---------------------- 5 files changed, 176 insertions(+), 61 deletions(-) diff --git a/calcifer.project b/calcifer.project index a9bd6dd..3dd015b 100644 --- a/calcifer.project +++ b/calcifer.project @@ -1 +1 @@ -{"categories":[{"name":"to do","content":[{"name":"update workflow .yml","description":"make a workflow compiling the calcifer and put the linux in calcifer-{version}\nand the windows in calcifer_windows_{version}\n\nupdate nix\nupdate jiji","id":5},{"name":"repair build.rs","description":"// Hello there","id":1},{"name":"draggable tabs","description":"// Hello there","id":2}]},{"name":"in progress","content":[{"name":"export copy paste fix","description":"// Hello there","id":1}]},{"name":"done","content":[{"name":"move .project file","description":"// Hello there","id":4},{"name":"move config","description":"config from .calcifer/save.json\nto .config/calcifer/state.json","id":1},{"name":"add id to textarea per tab","description":"to improve undo, make each code area of each tab have a unique id (no more undo into another tab)","id":1},{"name":"file tree id ?","description":"// Hello there","id":1},{"name":"open dir in tree ?","description":"// Hello there","id":2},{"name":"fix tab title","description":"// Hello there","id":2},{"name":"when closing last tab","description":"close tab and THEN close calcifer (to save no tab in save.json)","id":1}]},{"name":"+","content":[]}]} \ No newline at end of file +{"categories":[{"name":"to do","content":[{"name":"update workflow .yml","description":"make a workflow compiling the calcifer and put the linux in calcifer-{version}\nand the windows in calcifer_windows_{version}\n\nupdate nix\nupdate jiji","id":5}]},{"name":"in progress","content":[]},{"name":"done","content":[{"name":"move .project file","description":"// Hello there","id":4},{"name":"move config","description":"config from .calcifer/save.json\nto .config/calcifer/state.json","id":1},{"name":"add id to textarea per tab","description":"to improve undo, make each code area of each tab have a unique id (no more undo into another tab)","id":1},{"name":"file tree id ?","description":"// Hello there","id":1},{"name":"open dir in tree ?","description":"// Hello there","id":2},{"name":"fix tab title","description":"// Hello there","id":2},{"name":"when closing last tab","description":"close tab and THEN close calcifer (to save no tab in save.json)","id":1},{"name":"draggable tabs","description":"// Hello there","id":2},{"name":"repair build.rs","description":"// Hello there","id":1},{"name":"export copy paste fix","description":"// Hello there","id":1}]},{"name":"+","content":[]}]} \ No newline at end of file diff --git a/src/core/app.rs b/src/core/app.rs index 60b59c5..2f029f0 100644 --- a/src/core/app.rs +++ b/src/core/app.rs @@ -1,6 +1,6 @@ use eframe::egui; use egui::Color32; -use std::{cmp::min, fs, path::Path, path::PathBuf}; +use std::{cmp::min, cmp::max, fs, path::Path, path::PathBuf}; use crate::core; use crate::editor::themes::DEFAULT_THEMES; @@ -238,6 +238,10 @@ impl Calcifer { false } + + pub fn tab_area_size(&self) -> usize { + max(6, self.tabs.len() + 1) + } } #[allow(clippy::unnecessary_lazy_evaluations)] diff --git a/src/core/ui.rs b/src/core/ui.rs index ac351ad..027d611 100644 --- a/src/core/ui.rs +++ b/src/core/ui.rs @@ -208,8 +208,8 @@ impl Calcifer { 0.0, core::hex_str_to_color(self.theme.bg), ); - StripBuilder::new(ui) - .sizes(Size::remainder(), max(6, self.tabs.len() + 1)) + let response = StripBuilder::new(ui) + .sizes(Size::remainder(), self.tab_area_size()) .sense(egui::Sense::click()) .horizontal(|mut strip| { for (index, tab) in self.tabs.clone().iter().enumerate() { @@ -279,7 +279,8 @@ impl Calcifer { self.open_file(None); } }); - }); + }); + self.tab_rect = response.rect; }); } @@ -444,6 +445,87 @@ impl Calcifer { self.handle_confirm(); } + + pub fn draw_mouse_drag(&mut self, ctx: &egui::Context) { + if ctx.input(|i| i.pointer.is_decidedly_dragging()) { + if let Some(pos) = ctx.input(|i| i.pointer.interact_pos()) { + match self.mouse_holder { + panels::MouseHolder::TabHolder(index) => { + egui::Area::new(egui::Id::new("mouse_holder")) + .fixed_pos(pos) + .show(ctx, |ui| { + let (bg_color, text_color) = if self.selected_tab == index { + (core::hex_str_to_color(self.theme.functions), core::hex_str_to_color(self.theme.bg)) + } else { + (core::hex_str_to_color(self.theme.bg), core::hex_str_to_color(self.theme.comments)) + }; + + + let rect = egui::Rect::from_center_size( + egui::Pos2::new(pos.x, (self.tab_rect.max.y + self.tab_rect.min.y) / 2.0), + egui::Vec2::new((self.tab_rect.max.x - self.tab_rect.min.x) / usize_to_f32(self.tab_area_size()), self.tab_rect.max.y - self.tab_rect.min.y) + ); + + ui.painter().rect_filled( + rect, + 0.0, + bg_color, + ); + let unsaved_indicator = if self.tabs[index].saved { "" } else { "~ " }; + + let _ = ui.put(rect, egui::Label::new(egui::RichText::new(format!(" {}{}", unsaved_indicator, self.tabs[index].get_name())).color(text_color))); + }); + } + panels::MouseHolder::None => { + if self.tab_rect.distance_to_pos(pos) == 0.0 { + let hover_pos : f32 = (pos.x - self.tab_rect.min.x) / ((self.tab_rect.max.x - self.tab_rect.min.x) / usize_to_f32(self.tab_area_size())); + + if let Some(index) = floor_f32(hover_pos) { + if index < self.tabs.len() { + self.mouse_holder = panels::MouseHolder::TabHolder(index); + } + } + } + } + } + } + return + } + + match self.mouse_holder { + panels::MouseHolder::TabHolder(initial_index) => { + if let Some(pos) = ctx.input(|i| i.pointer.interact_pos()) { + if self.tab_rect.distance_to_pos(pos) == 0.0 { + let hover_pos : f32 = (pos.x - self.tab_rect.min.x) / ((self.tab_rect.max.x - self.tab_rect.min.x) / usize_to_f32(self.tab_area_size())); + + if let Some(final_index) = floor_f32(hover_pos) { + if final_index == initial_index { + return + } else if final_index < initial_index { + self.tabs.insert(final_index, self.tabs[initial_index].clone()); + self.tabs.remove(initial_index + 1); + } else { + self.tabs.insert(final_index + 1, self.tabs[initial_index].clone()); + self.tabs.remove(initial_index); + } + + if self.selected_tab == initial_index { + self.selected_tab = final_index; + } else if self.selected_tab < initial_index && self.selected_tab >= final_index { + self.selected_tab += 1; + } else if self.selected_tab > initial_index && self.selected_tab <= final_index { + self.selected_tab -= 1; + } + } + } + } + } + + panels::MouseHolder::None => {} + } + + self.mouse_holder = panels::MouseHolder::None; + } } fn to_syntax(language: &str) -> Syntax { @@ -476,3 +558,22 @@ pub fn format_path(path: &Path) -> String { .join("/") ) } + + +fn usize_to_f32(value: usize) -> f32 { + const MAX_F32: f32 = f32::MAX; + + if value as f64 > MAX_F32 as f64 { + MAX_F32 + } else { + value as f32 + } +} + +fn floor_f32(value: f32) -> Option { + if value.is_nan() || value < 0.0 || value > usize::MAX as f32 { + None + } else { + Some(value.floor() as usize) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a52c241..48cd9b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,8 @@ struct Calcifer { selected_tab: usize, tabs: Vec, + tab_rect: egui::Rect, + mouse_holder: panels::MouseHolder, command: String, command_history: Vec, @@ -117,6 +119,8 @@ impl Default for Calcifer { selected_tab: 0, tabs: vec![panels::Tab::default()], + tab_rect: egui::Rect::EVERYTHING, + mouse_holder: panels::MouseHolder::None, command: String::new(), command_history: Vec::new(), @@ -305,6 +309,7 @@ impl eframe::App for Calcifer { watch = time::Instant::now(); self.draw_windows(ctx); + self.draw_mouse_drag(ctx); self.time_watch[6] = watch.elapsed().as_micros() as f32 / 1000.0; diff --git a/src/panels/tabs.rs b/src/panels/tabs.rs index 6c07731..f5aa347 100644 --- a/src/panels/tabs.rs +++ b/src/panels/tabs.rs @@ -3,76 +3,81 @@ use std::{fs::read_to_string, path::Path, path::PathBuf}; #[derive(Clone, PartialEq)] pub struct Tab { - pub path: PathBuf, - pub code: String, - pub language: String, - pub saved: bool, - pub scroll_offset: f32, - pub last_cursor: Option, + pub path: PathBuf, + pub code: String, + pub language: String, + pub saved: bool, + pub scroll_offset: f32, + pub last_cursor: Option, } impl Default for Tab { - fn default() -> Self { - Self { - path: "untitled".into(), - code: "// Hello there, Master".into(), - language: "rs".into(), - saved: false, - scroll_offset: 0.0, - last_cursor: None, - } - } + fn default() -> Self { + Self { + path: "untitled".into(), + code: "// Hello there, Master".into(), + language: "rs".into(), + saved: false, + scroll_offset: 0.0, + last_cursor: None, + } + } } impl Tab { - pub fn new(path: PathBuf) -> Self { - let text = read_file_contents(&path).replace(&" ".repeat(4), "\t"); - let file_path = format_file_path(&path, &text); - let extension = file_path - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or_default(); + pub fn new(path: PathBuf) -> Self { + let text = read_file_contents(&path).replace(&" ".repeat(4), "\t"); + let file_path = format_file_path(&path, &text); + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or_default(); - Self { - path: file_path.clone(), - code: text, - language: extension.into(), - saved: true, - scroll_offset: 0.0, - last_cursor: None, - } - } + Self { + path: file_path.clone(), + code: text, + language: extension.into(), + saved: true, + scroll_offset: 0.0, + last_cursor: None, + } + } - pub fn get_name(&self) -> String { - self.path - .file_name() - .map_or("untitled".to_string(), |name| { - name.to_string_lossy().to_string() - }) - } + pub fn get_name(&self) -> String { + self.path + .file_name() + .map_or("untitled".to_string(), |name| { + name.to_string_lossy().to_string() + }) + } - pub fn refresh(&mut self) { - let text = read_file_contents(&self.path).replace(&" ".repeat(4), "\t"); - let file_path = format_file_path(&self.path, &text); + pub fn refresh(&mut self) { + let text = read_file_contents(&self.path).replace(&" ".repeat(4), "\t"); + let file_path = format_file_path(&self.path, &text); - self.code = text; - self.path = file_path; - self.saved = true; - } + self.code = text; + self.path = file_path; + self.saved = true; + } +} + +pub enum MouseHolder { + TabHolder(usize), + None, } fn read_file_contents(path: &Path) -> String { - let error_type = "reading file"; - read_to_string(path) - .map_err(|err| format!("// Error {}: {}", error_type, err)) - .unwrap_or_else(|err_msg| err_msg) + let error_type = "reading file"; + read_to_string(path) + .map_err(|err| format!("// Error {}: {}", error_type, err)) + .unwrap_or_else(|err_msg| err_msg) } fn format_file_path(path: &Path, contents: &str) -> PathBuf { - let error_type = "reading file"; - if contents.contains(&format!("Error {}", error_type)) { - "untitled".into() - } else { - path.to_path_buf() - } + let error_type = "reading file"; + if contents.contains(&format!("Error {}", error_type)) { + "untitled".into() + } else { + path.to_path_buf() + } }