draggable tabs

This commit is contained in:
WanderingPenwing 2024-07-24 11:26:37 +02:00
parent f9a712fea4
commit c508a18941
5 changed files with 176 additions and 61 deletions

View file

@ -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":[]}]}
{"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":[]}]}

View file

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

View file

@ -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<usize> {
if value.is_nan() || value < 0.0 || value > usize::MAX as f32 {
None
} else {
Some(value.floor() as usize)
}
}

View file

@ -76,6 +76,8 @@ struct Calcifer {
selected_tab: usize,
tabs: Vec<panels::Tab>,
tab_rect: egui::Rect,
mouse_holder: panels::MouseHolder,
command: String,
command_history: Vec<panels::CommandEntry>,
@ -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;

View file

@ -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<CCursorRange>,
pub path: PathBuf,
pub code: String,
pub language: String,
pub saved: bool,
pub scroll_offset: f32,
pub last_cursor: Option<CCursorRange>,
}
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()
}
}