added zxq5_egui crate, a crate for packaging together my UI components so i can reuse them across projects.
- added buttons/tabs - added a simple file tree (work in progress)
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
use eframe::epaint::Color32;
|
|
||||||
use common::prelude::Register;
|
use common::prelude::Register;
|
||||||
use dsa_emulator::EmulatorHandle;
|
use dsa_emulator::EmulatorHandle;
|
||||||
use dsa_emulator::processor::RunningState;
|
use dsa_emulator::processor::RunningState;
|
||||||
use crate::ui::ui_helpers::PaddedWidget;
|
use eframe::epaint::Color32;
|
||||||
|
use zxq5_egui::buttons::PaddedWidget;
|
||||||
|
use crate::ui::ui_helpers::StateWrapper;
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
emulator: EmulatorHandle,
|
emulator: EmulatorHandle,
|
||||||
@@ -23,7 +24,6 @@ impl Controller {
|
|||||||
.inner_margin(10.0)
|
.inner_margin(10.0)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
||||||
use RunningState::*;
|
use RunningState::*;
|
||||||
let status = self.emulator.status();
|
let status = self.emulator.status();
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ impl Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaddedWidget::indicator(status).ui(ui);
|
PaddedWidget::indicator(StateWrapper(status)).ui(ui);
|
||||||
|
|
||||||
if PaddedWidget::button("⟳ Reset All").color(Color32::ORANGE).ui(ui).clicked() {
|
if PaddedWidget::button("⟳ Reset All").color(Color32::ORANGE).ui(ui).clicked() {
|
||||||
self.emulator.when_paused(|emu| emu.reset());
|
self.emulator.when_paused(|emu| emu.reset());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use egui::{Modal, Id, Layout, Align, RichText};
|
use egui::{Align, Id, Layout, Modal, RichText};
|
||||||
use crate::ui::ui_helpers::PaddedWidget;
|
use zxq5_egui::buttons::PaddedWidget;
|
||||||
|
|
||||||
pub struct ConfirmDialog<T> {
|
pub struct ConfirmDialog<T> {
|
||||||
open: bool,
|
open: bool,
|
||||||
|
|||||||
@@ -8,27 +8,29 @@ use crate::ui::tiles::registers::Registers;
|
|||||||
use crate::ui::tiles::{CentralTiles, TilePane};
|
use crate::ui::tiles::{CentralTiles, TilePane};
|
||||||
use dialogs::confirm_dialog::ConfirmDialog;
|
use dialogs::confirm_dialog::ConfirmDialog;
|
||||||
use dialogs::file_dialog::DsaFileDialog;
|
use dialogs::file_dialog::DsaFileDialog;
|
||||||
use dsa_formats::binary_dse::DseExecutable;
|
|
||||||
use egui::{CentralPanel, Panel, Ui};
|
|
||||||
use egui_tiles::TileId;
|
|
||||||
use dsa_emulator::EmulatorController;
|
use dsa_emulator::EmulatorController;
|
||||||
use dsa_emulator::devices::display::Display;
|
use dsa_emulator::devices::display::Display;
|
||||||
use dsa_emulator::devices::serial::Serial;
|
use dsa_emulator::devices::serial::Serial;
|
||||||
|
use dsa_formats::binary_dse::DseExecutable;
|
||||||
|
use egui::{CentralPanel, Panel, Ui};
|
||||||
|
use egui_tiles::TileId;
|
||||||
|
use process_manager::ProcessManager;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
use tiles::PanelFactory;
|
use tiles::PanelFactory;
|
||||||
use tiles::documentation::Documentation;
|
use tiles::documentation::Documentation;
|
||||||
use tiles::editor::Editor;
|
use tiles::editor::Editor;
|
||||||
use tiles::io_devices::framebuffer::FrameBufferUi;
|
use tiles::io_devices::framebuffer::FrameBufferUi;
|
||||||
use process_manager::ProcessManager;
|
use zxq5_egui::file_tree::{FileTree, FileTreeEvent};
|
||||||
use std::fs::File;
|
|
||||||
use std::io::BufReader;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
pub mod appearance;
|
||||||
|
mod controller;
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
|
pub mod process_manager;
|
||||||
mod tiles;
|
mod tiles;
|
||||||
mod ui_helpers;
|
mod ui_helpers;
|
||||||
mod controller;
|
|
||||||
pub mod process_manager;
|
|
||||||
pub mod appearance;
|
|
||||||
|
|
||||||
pub struct DamnSimpleUI {
|
pub struct DamnSimpleUI {
|
||||||
emulator: EmulatorController,
|
emulator: EmulatorController,
|
||||||
@@ -42,10 +44,10 @@ pub struct DamnSimpleUI {
|
|||||||
tools: Vec<PanelFactory>,
|
tools: Vec<PanelFactory>,
|
||||||
|
|
||||||
show_process_manager: bool,
|
show_process_manager: bool,
|
||||||
process_manager: ProcessManager
|
process_manager: ProcessManager,
|
||||||
|
file_tree: FileTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl eframe::App for DamnSimpleUI {
|
impl eframe::App for DamnSimpleUI {
|
||||||
fn ui(&mut self, ui: &mut Ui, frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut Ui, frame: &mut eframe::Frame) {
|
||||||
ui.request_repaint_after(Duration::from_millis(16));
|
ui.request_repaint_after(Duration::from_millis(16));
|
||||||
@@ -72,13 +74,26 @@ impl eframe::App for DamnSimpleUI {
|
|||||||
self.controller.ui(ui);
|
self.controller.ui(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
Panel::left("left_panel").resizable(false).show(ui, |ui| {
|
Panel::left("left_panel").resizable(true).show(ui, |ui| {
|
||||||
// TODO: File Explorer!
|
let inner_res = self.file_tree.ui(ui, &mut |ui: &mut Ui, path: &Path| {
|
||||||
|
ui.button("Load Binary")
|
||||||
|
.clicked()
|
||||||
|
.then_some(Action::LoadBinary(DataSource::File(path.to_path_buf())))
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(event) = inner_res.inner {
|
||||||
|
match event {
|
||||||
|
FileTreeEvent::Clicked(path) => self.perform_action(Action::OpenFile(path)),
|
||||||
|
FileTreeEvent::ContextAction(action) => self.perform_action(action),
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Panel::bottom("bottom_panel").resizable(true).show_collapsible(ui, &mut self.show_process_manager,|ui| {
|
Panel::bottom("bottom_panel")
|
||||||
self.process_manager.ui(ui);
|
.resizable(true)
|
||||||
});
|
.show(ui, |ui| {
|
||||||
|
self.process_manager.ui(ui);
|
||||||
|
});
|
||||||
|
|
||||||
CentralPanel::default().show(ui, |ui| {
|
CentralPanel::default().show(ui, |ui| {
|
||||||
if let Some(action) = self.central_tiles.ui(ui) {
|
if let Some(action) = self.central_tiles.ui(ui) {
|
||||||
@@ -88,11 +103,7 @@ impl eframe::App for DamnSimpleUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl DamnSimpleUI {
|
impl DamnSimpleUI {
|
||||||
|
|
||||||
fn open_or_focus<T: TilePane + 'static>(
|
fn open_or_focus<T: TilePane + 'static>(
|
||||||
central_tiles: &mut CentralTiles,
|
central_tiles: &mut CentralTiles,
|
||||||
slot: &mut Option<TileId>,
|
slot: &mut Option<TileId>,
|
||||||
@@ -104,10 +115,7 @@ impl DamnSimpleUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(cc: &eframe::CreationContext, mut emulator: EmulatorController) -> Self {
|
||||||
cc: &eframe::CreationContext,
|
|
||||||
mut emulator: EmulatorController,
|
|
||||||
) -> Self {
|
|
||||||
appearance::configure_appearance(&cc.egui_ctx);
|
appearance::configure_appearance(&cc.egui_ctx);
|
||||||
let handle = emulator.get_handle().clone();
|
let handle = emulator.get_handle().clone();
|
||||||
Self {
|
Self {
|
||||||
@@ -116,7 +124,7 @@ impl DamnSimpleUI {
|
|||||||
confirm_dialog: ConfirmDialog::new(),
|
confirm_dialog: ConfirmDialog::new(),
|
||||||
file_dialog: DsaFileDialog::new(),
|
file_dialog: DsaFileDialog::new(),
|
||||||
error_dialog: ErrorDialog::default(),
|
error_dialog: ErrorDialog::default(),
|
||||||
|
|
||||||
tools: vec![
|
tools: vec![
|
||||||
PanelFactory::register("Memory Inspector", {
|
PanelFactory::register("Memory Inspector", {
|
||||||
let handle = handle.clone();
|
let handle = handle.clone();
|
||||||
@@ -131,31 +139,32 @@ impl DamnSimpleUI {
|
|||||||
let handle = handle.clone();
|
let handle = handle.clone();
|
||||||
move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
|
move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
|
||||||
}),
|
}),
|
||||||
PanelFactory::register("Serial", {
|
PanelFactory::register("Serial", {
|
||||||
let handle = handle.clone();
|
let handle = handle.clone();
|
||||||
move || Serial::new(0x40000, handle.clone())
|
move || Serial::new(0x40000, handle.clone())
|
||||||
}),
|
}),
|
||||||
PanelFactory::register("Registers", {
|
PanelFactory::register("Registers", {
|
||||||
let handle = handle.clone();
|
let handle = handle.clone();
|
||||||
move || Registers::new(handle.clone())
|
move || Registers::new(handle.clone())
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
emulator,
|
emulator,
|
||||||
show_process_manager: true,
|
show_process_manager: true,
|
||||||
process_manager: ProcessManager::new()
|
process_manager: ProcessManager::new(),
|
||||||
|
file_tree: FileTree::build(&std::env::current_dir().unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perform_action(&mut self, action: Action) {
|
pub fn perform_action(&mut self, action: Action) {
|
||||||
match action {
|
match action {
|
||||||
Action::None => {},
|
Action::None => {}
|
||||||
|
|
||||||
Action::SpawnProcess(spawner) => spawner(&mut self.process_manager),
|
Action::SpawnProcess(spawner) => spawner(&mut self.process_manager),
|
||||||
Action::RequestCloseTab(tile_id) => {
|
Action::RequestCloseTab(tile_id) => {
|
||||||
self.confirm_dialog.open(
|
self.confirm_dialog.open(
|
||||||
"Close Tab",
|
"Close Tab",
|
||||||
"Closing this tab may result in losing unsaved work",
|
"Closing this tab may result in losing unsaved work",
|
||||||
Action::CloseTab(tile_id)
|
Action::CloseTab(tile_id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,15 +175,24 @@ impl DamnSimpleUI {
|
|||||||
Action::Quit => std::process::exit(0),
|
Action::Quit => std::process::exit(0),
|
||||||
|
|
||||||
// Opens a file (usually constructed by the file dialogue)
|
// Opens a file (usually constructed by the file dialogue)
|
||||||
Action::OpenFile(path) => {
|
Action::OpenFile(path) => match path.extension().map(|s| s.to_str()).flatten() {
|
||||||
match path.extension().map(|s| s.to_str()).flatten() {
|
Some("dsa" | "dsc") => {
|
||||||
Some("dsa" | "dsc") => { self.central_tiles.add_tab(Editor::new_open(&path)); },
|
self.central_tiles.add_tab(Editor::new_open(&path));
|
||||||
Some("dsb") => { self.central_tiles.add_tab(BinaryViewer::new_open(&path)); },
|
|
||||||
Some("md") => { self.central_tiles.add_tab(Documentation::open(&path)); },
|
|
||||||
Some(_) => todo!(),
|
|
||||||
None => {},
|
|
||||||
}
|
}
|
||||||
}
|
Some("dsb") => {
|
||||||
|
self.central_tiles.add_tab(BinaryViewer::new_open(&path));
|
||||||
|
}
|
||||||
|
Some("md") => {
|
||||||
|
self.central_tiles.add_tab(Documentation::open(&path));
|
||||||
|
}
|
||||||
|
Some(f) => {
|
||||||
|
self.perform_action(Action::ShowError(format!(
|
||||||
|
"Unsupported file extension: {}",
|
||||||
|
f
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
},
|
||||||
|
|
||||||
Action::ShowError(err) => {
|
Action::ShowError(err) => {
|
||||||
self.error_dialog.open(err);
|
self.error_dialog.open(err);
|
||||||
@@ -187,11 +205,12 @@ impl DamnSimpleUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Action::LoadBinary(DataSource::File(path)) => {
|
Action::LoadBinary(DataSource::File(path)) => {
|
||||||
if let Some(ext) = path.extension().map(|s| s.to_str()).flatten() && ext == "dsb" {
|
if let Some(ext) = path.extension().map(|s| s.to_str()).flatten()
|
||||||
|
&& ext == "dsb"
|
||||||
|
{
|
||||||
let Ok(file) = File::open(path) else {
|
let Ok(file) = File::open(path) else {
|
||||||
self.perform_action(Action::ShowError("Failed to open file".into()));
|
self.perform_action(Action::ShowError("Failed to open file".into()));
|
||||||
return
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
|
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
|
||||||
@@ -215,50 +234,67 @@ impl DamnSimpleUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Action::RequestSaveAs(tile) => {
|
Action::RequestSaveAs(tile) => {
|
||||||
self.file_dialog.open_save(move |path| Action::SaveFile(path, tile));
|
self.file_dialog
|
||||||
|
.open_save(move |path| Action::SaveFile(path, tile));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&mut self, ui: &mut Ui) {
|
fn toolbar(&mut self, ui: &mut Ui) {
|
||||||
egui::MenuBar::new().ui(ui, |ui| {
|
egui::MenuBar::new().ui(ui, |ui| {
|
||||||
|
let save_enabled = self
|
||||||
let save_enabled = self.central_tiles.active_tile()
|
.central_tiles
|
||||||
|
.active_tile()
|
||||||
.and_then(|id| self.central_tiles.pane_mut::<Editor>(id))
|
.and_then(|id| self.central_tiles.pane_mut::<Editor>(id))
|
||||||
.is_some();
|
.is_some();
|
||||||
|
|
||||||
ui.set_height(30.0);
|
ui.set_height(30.0);
|
||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
|
|
||||||
if ui.button("Open").clicked() {
|
if ui.button("Open").clicked() {
|
||||||
self.file_dialog.open_pick(Action::OpenFile);
|
self.file_dialog.open_pick(Action::OpenFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Load Binary").clicked() {
|
if ui.button("Load Binary").clicked() {
|
||||||
self.file_dialog.open_pick(|path| Action::LoadBinary(DataSource::File(path)));
|
self.file_dialog
|
||||||
|
.open_pick(|path| Action::LoadBinary(DataSource::File(path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.add_enabled(save_enabled, egui::Button::new("Save")).clicked() {
|
if ui
|
||||||
|
.add_enabled(save_enabled, egui::Button::new("Save"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
if let Some(active) = self.central_tiles.active_tile() {
|
if let Some(active) = self.central_tiles.active_tile() {
|
||||||
if let Some(editor) = self.central_tiles.pane_mut::<Editor>(active) {
|
if let Some(editor) = self.central_tiles.pane_mut::<Editor>(active) {
|
||||||
match editor.filename().cloned() {
|
match editor.filename().cloned() {
|
||||||
Some(path) => std::fs::write(path, editor.get_buffer().as_bytes()).unwrap(),
|
Some(path) => {
|
||||||
None => self.file_dialog.open_save(move |path| Action::SaveFile(path, active)),
|
std::fs::write(path, editor.get_buffer().as_bytes()).unwrap()
|
||||||
|
}
|
||||||
|
None => self
|
||||||
|
.file_dialog
|
||||||
|
.open_save(move |path| Action::SaveFile(path, active)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.add_enabled(save_enabled, egui::Button::new("Save As")).clicked() {
|
if ui
|
||||||
|
.add_enabled(save_enabled, egui::Button::new("Save As"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
if let Some(active) = self.central_tiles.active_tile() {
|
if let Some(active) = self.central_tiles.active_tile() {
|
||||||
if let Some(editor) = self.central_tiles.pane_mut::<Editor>(active) {
|
if let Some(editor) = self.central_tiles.pane_mut::<Editor>(active) {
|
||||||
self.file_dialog.open_save(move |path| Action::SaveFile(path, active))
|
self.file_dialog
|
||||||
|
.open_save(move |path| Action::SaveFile(path, active))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Exit").clicked() {
|
if ui.button("Exit").clicked() {
|
||||||
self.confirm_dialog.open("Exit","Exit Application\n(you will lose unsaved work)", Action::Quit)
|
self.confirm_dialog.open(
|
||||||
|
"Exit",
|
||||||
|
"Exit Application\n(you will lose unsaved work)",
|
||||||
|
Action::Quit,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,4 +314,3 @@ impl DamnSimpleUI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::state::Action;
|
use crate::state::Action;
|
||||||
use crate::ui::ui_helpers::painted_tab;
|
|
||||||
use crate::ui::ui_helpers::text::strip_ansi;
|
use crate::ui::ui_helpers::text::strip_ansi;
|
||||||
use dsa_tasks::{ProcessAgent, TraceableProcess};
|
use dsa_tasks::{ProcessAgent, TraceableProcess};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
|
use zxq5_egui::tabs::painted_tab;
|
||||||
|
|
||||||
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
|
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::state::Action;
|
|||||||
use crate::state::DataSource::Data;
|
use crate::state::DataSource::Data;
|
||||||
use crate::ui::tiles::TilePane;
|
use crate::ui::tiles::TilePane;
|
||||||
use crate::ui::tiles::editor::syntax::SyntaxFrom;
|
use crate::ui::tiles::editor::syntax::SyntaxFrom;
|
||||||
use crate::ui::ui_helpers::PaddedWidget;
|
|
||||||
use assembler::prelude::Assembler;
|
use assembler::prelude::Assembler;
|
||||||
use dsa_formats::binary_dse::DseExecutable;
|
use dsa_formats::binary_dse::DseExecutable;
|
||||||
use dsa_tasks::TraceableProcess;
|
use dsa_tasks::TraceableProcess;
|
||||||
@@ -13,6 +12,7 @@ use std::any::Any;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use theme::THEME;
|
use theme::THEME;
|
||||||
|
use zxq5_egui::buttons::PaddedWidget;
|
||||||
|
|
||||||
mod syntax;
|
mod syntax;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ pub mod io_devices;
|
|||||||
|
|
||||||
use crate::state::Action;
|
use crate::state::Action;
|
||||||
use crate::state::Action::RequestCloseTab;
|
use crate::state::Action::RequestCloseTab;
|
||||||
use crate::ui::ui_helpers::painted_tab;
|
|
||||||
use eframe::epaint::{Color32, CornerRadius};
|
use eframe::epaint::{Color32, CornerRadius};
|
||||||
use egui::{Id, Response, Stroke, Ui};
|
use egui::{Id, Response, Stroke, Ui};
|
||||||
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
|
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use zxq5_egui::tabs::painted_tab;
|
||||||
|
|
||||||
/// Anything that can live inside a tab in the central tiled area.
|
/// Anything that can live inside a tab in the central tiled area.
|
||||||
/// `as_any_mut` lets callers reach back into a specific pane's concrete
|
/// `as_any_mut` lets callers reach back into a specific pane's concrete
|
||||||
|
|||||||
@@ -1,115 +1,13 @@
|
|||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
use eframe::emath::Vec2;
|
|
||||||
use egui::{Color32};
|
|
||||||
use dsa_emulator::processor::RunningState;
|
use dsa_emulator::processor::RunningState;
|
||||||
|
use zxq5_egui::buttons::StatusIndicator;
|
||||||
|
|
||||||
pub struct PaddedWidget {
|
pub struct StateWrapper(pub RunningState);
|
||||||
text: String,
|
|
||||||
color: Option<Color32>,
|
|
||||||
padding: Option<Vec2>,
|
|
||||||
widget_type: WidgetType
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WidgetType {
|
impl StatusIndicator for StateWrapper {
|
||||||
Button,
|
|
||||||
Indicator,
|
|
||||||
Label
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaddedWidget {
|
|
||||||
pub fn button(text: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.into(),
|
|
||||||
color: None,
|
|
||||||
padding: None,
|
|
||||||
widget_type: WidgetType::Button
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn indicator(i: impl StatusIndicator) -> Self {
|
|
||||||
Self {
|
|
||||||
text: i.label().to_string(),
|
|
||||||
color: Some(i.color()),
|
|
||||||
padding: None,
|
|
||||||
widget_type: WidgetType::Indicator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(text: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.into(),
|
|
||||||
color: None,
|
|
||||||
padding: None,
|
|
||||||
widget_type: WidgetType::Label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color(&mut self, color: Color32) -> &mut Self {
|
|
||||||
self.color = Some(color);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn padding(&mut self, padding: Vec2) -> &mut Self {
|
|
||||||
self.padding = Some(padding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
|
||||||
let text: egui::WidgetText = self.text.clone().into();
|
|
||||||
let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
|
||||||
|
|
||||||
let desired_size = if let WidgetType::Indicator = self.widget_type {
|
|
||||||
galley.size() + egui::vec2(12.0, 0.0) + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
|
|
||||||
} else {
|
|
||||||
galley.size() + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
// picks noninteractive/inactive/hovered/active automatically based on `response`
|
|
||||||
|
|
||||||
let visuals = match self.widget_type {
|
|
||||||
WidgetType::Button => ui.style().interact(&response),
|
|
||||||
_ => &ui.visuals().widgets.inactive,
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.painter().rect(
|
|
||||||
rect,
|
|
||||||
visuals.corner_radius,
|
|
||||||
visuals.bg_fill,
|
|
||||||
visuals.bg_stroke,
|
|
||||||
egui::StrokeKind::Inside,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let WidgetType::Indicator = self.widget_type {
|
|
||||||
let size = egui::vec2(galley.size().clone().x + 6.0, 0.0);
|
|
||||||
let text_pos = rect.center() - galley.size() / 2.0 + egui::vec2(8.0, 0.0);
|
|
||||||
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
|
||||||
ui.painter().circle_filled(rect.center() - size / 2.0, 8.0 / 2.0, self.color.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
let text_pos = rect.center() - galley.size() / 2.0;
|
|
||||||
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub trait StatusIndicator {
|
|
||||||
fn color(&self) -> egui::Color32;
|
|
||||||
|
|
||||||
fn label(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatusIndicator for RunningState {
|
|
||||||
fn color(&self) -> egui::Color32 {
|
fn color(&self) -> egui::Color32 {
|
||||||
match self {
|
match self.0 {
|
||||||
RunningState::Running => egui::Color32::GREEN,
|
RunningState::Running => egui::Color32::GREEN,
|
||||||
RunningState::Paused => egui::Color32::YELLOW,
|
RunningState::Paused => egui::Color32::YELLOW,
|
||||||
RunningState::Halted => egui::Color32::ORANGE,
|
RunningState::Halted => egui::Color32::ORANGE,
|
||||||
@@ -119,7 +17,7 @@ impl StatusIndicator for RunningState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn label(&self) -> String {
|
fn label(&self) -> String {
|
||||||
match self {
|
match self.0 {
|
||||||
RunningState::Running => "Running".into(),
|
RunningState::Running => "Running".into(),
|
||||||
RunningState::Paused => "Paused".into(),
|
RunningState::Paused => "Paused".into(),
|
||||||
RunningState::Halted => "Halted".into(),
|
RunningState::Halted => "Halted".into(),
|
||||||
@@ -129,119 +27,3 @@ impl StatusIndicator for RunningState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared paint logic — draws the crossed lines, using whatever
|
|
||||||
/// interact-state visuals (hover/press color, expansion) egui's style
|
|
||||||
/// would normally pick for this response.
|
|
||||||
fn paint_close_x(ui: &egui::Ui, rect: egui::Rect, response: &egui::Response) {
|
|
||||||
if !ui.is_rect_visible(rect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let visuals = ui.style().interact(response);
|
|
||||||
let inner = rect.shrink(4.0).expand(visuals.expansion);
|
|
||||||
let stroke = visuals.fg_stroke;
|
|
||||||
ui.painter().line_segment([inner.left_top(), inner.right_bottom()], stroke);
|
|
||||||
ui.painter().line_segment([inner.right_top(), inner.left_bottom()], stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Standalone close button — allocates its own space in the current
|
|
||||||
/// layout, like a normal widget. Use this in a `ui.horizontal(...)` row,
|
|
||||||
/// e.g. next to a label in a flat list (process manager tabs, a file
|
|
||||||
/// list with per-row close, etc).
|
|
||||||
pub fn close_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
|
||||||
let (rect, response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click());
|
|
||||||
let response = response.on_hover_cursor(egui::CursorIcon::Default);
|
|
||||||
|
|
||||||
response.widget_info(|| {
|
|
||||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
|
|
||||||
});
|
|
||||||
|
|
||||||
paint_close_x(ui, rect, &response);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close button at an explicit rect you've already computed — for
|
|
||||||
/// cases like tab_ui, where the button needs to sit inside a larger
|
|
||||||
/// region you're laying out manually rather than in normal flow.
|
|
||||||
pub fn close_button_at(ui: &mut egui::Ui, rect: egui::Rect, id: egui::Id) -> egui::Response {
|
|
||||||
let response = ui
|
|
||||||
.interact(rect, id, egui::Sense::click_and_drag())
|
|
||||||
.on_hover_cursor(egui::CursorIcon::Default);
|
|
||||||
|
|
||||||
response.widget_info(|| {
|
|
||||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
|
|
||||||
});
|
|
||||||
|
|
||||||
paint_close_x(ui, rect, &response);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TabResponse {
|
|
||||||
pub response: egui::Response,
|
|
||||||
pub close_clicked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn painted_tab(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
id: egui::Id,
|
|
||||||
title: impl Into<egui::WidgetText>,
|
|
||||||
selected: bool,
|
|
||||||
closable: bool,
|
|
||||||
draggable: bool,
|
|
||||||
) -> TabResponse {
|
|
||||||
let padding = egui::vec2(14.0, 8.0);
|
|
||||||
let close_size = 14.0;
|
|
||||||
let close_gap = 6.0;
|
|
||||||
|
|
||||||
let font_id = egui::TextStyle::Button.resolve(ui.style());
|
|
||||||
let galley = title.into().into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, font_id);
|
|
||||||
|
|
||||||
let extra_width = if closable { close_size + close_gap } else { 0.0 };
|
|
||||||
let desired = egui::vec2(
|
|
||||||
galley.size().x + padding.x * 2.0 + extra_width,
|
|
||||||
ui.available_height().max(galley.size().y + padding.y * 2.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_, rect) = ui.allocate_space(desired);
|
|
||||||
|
|
||||||
let sense = if draggable { egui::Sense::click_and_drag() } else { egui::Sense::click() };
|
|
||||||
let response = ui.interact(rect, id, sense);
|
|
||||||
let response = if draggable {
|
|
||||||
response.on_hover_cursor(egui::CursorIcon::Grab)
|
|
||||||
} else {
|
|
||||||
response
|
|
||||||
};
|
|
||||||
|
|
||||||
response.widget_info(|| {
|
|
||||||
egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected, galley.text())
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut close_clicked = false;
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
|
|
||||||
ui.painter().rect(rect, 4.0, visuals.bg_fill, visuals.bg_stroke, egui::StrokeKind::Inside);
|
|
||||||
|
|
||||||
let text_color = if selected {
|
|
||||||
egui::Color32::WHITE
|
|
||||||
} else {
|
|
||||||
egui::Color32::DARK_GRAY
|
|
||||||
};
|
|
||||||
|
|
||||||
let text_pos = egui::pos2(rect.min.x + padding.x, rect.center().y - galley.size().y / 2.0);
|
|
||||||
ui.painter().galley(text_pos, galley, text_color);
|
|
||||||
|
|
||||||
if closable {
|
|
||||||
let close_rect = egui::Rect::from_min_size(
|
|
||||||
egui::pos2(rect.max.x - padding.x - close_size, rect.center().y - close_size / 2.0),
|
|
||||||
egui::vec2(close_size, close_size),
|
|
||||||
);
|
|
||||||
let close_id = id.with("close");
|
|
||||||
let close_response = close_button_at(ui, close_rect, close_id);
|
|
||||||
|
|
||||||
close_clicked = close_response.clicked() || response.clicked_by(egui::PointerButton::Middle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TabResponse { response, close_clicked }
|
|
||||||
}
|
|
||||||
@@ -5,3 +5,4 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
egui = "0.35.0"
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use egui::{Color32, Vec2};
|
||||||
|
|
||||||
|
pub struct PaddedWidget {
|
||||||
|
text: String,
|
||||||
|
color: Option<Color32>,
|
||||||
|
padding: Option<Vec2>,
|
||||||
|
widget_type: WidgetType
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WidgetType {
|
||||||
|
Button,
|
||||||
|
Indicator,
|
||||||
|
Label
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaddedWidget {
|
||||||
|
pub fn button(text: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
color: None,
|
||||||
|
padding: None,
|
||||||
|
widget_type: WidgetType::Button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indicator(i: impl StatusIndicator) -> Self {
|
||||||
|
Self {
|
||||||
|
text: i.label().to_string(),
|
||||||
|
color: Some(i.color()),
|
||||||
|
padding: None,
|
||||||
|
widget_type: WidgetType::Indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(text: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
color: None,
|
||||||
|
padding: None,
|
||||||
|
widget_type: WidgetType::Label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(&mut self, color: Color32) -> &mut Self {
|
||||||
|
self.color = Some(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn padding(&mut self, padding: Vec2) -> &mut Self {
|
||||||
|
self.padding = Some(padding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
|
let text: egui::WidgetText = self.text.clone().into();
|
||||||
|
let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
||||||
|
|
||||||
|
let desired_size = if let WidgetType::Indicator = self.widget_type {
|
||||||
|
galley.size() + egui::vec2(12.0, 0.0) + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
|
||||||
|
} else {
|
||||||
|
galley.size() + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
// picks noninteractive/inactive/hovered/active automatically based on `response`
|
||||||
|
|
||||||
|
let visuals = match self.widget_type {
|
||||||
|
WidgetType::Button => ui.style().interact(&response),
|
||||||
|
_ => &ui.visuals().widgets.inactive,
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().rect(
|
||||||
|
rect,
|
||||||
|
visuals.corner_radius,
|
||||||
|
visuals.bg_fill,
|
||||||
|
visuals.bg_stroke,
|
||||||
|
egui::StrokeKind::Inside,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let WidgetType::Indicator = self.widget_type {
|
||||||
|
let size = egui::vec2(galley.size().clone().x + 6.0, 0.0);
|
||||||
|
let text_pos = rect.center() - galley.size() / 2.0 + egui::vec2(8.0, 0.0);
|
||||||
|
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
||||||
|
ui.painter().circle_filled(rect.center() - size / 2.0, 8.0 / 2.0, self.color.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
let text_pos = rect.center() - galley.size() / 2.0;
|
||||||
|
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StatusIndicator {
|
||||||
|
fn color(&self) -> egui::Color32;
|
||||||
|
|
||||||
|
fn label(&self) -> String;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use egui::{CollapsingHeader, InnerResponse};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub enum FileTree {
|
||||||
|
Branch {
|
||||||
|
path: PathBuf,
|
||||||
|
children: Vec<FileTree>,
|
||||||
|
},
|
||||||
|
Leaf {
|
||||||
|
filename: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for FileTree {}
|
||||||
|
|
||||||
|
impl PartialEq<Self> for FileTree {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, other),
|
||||||
|
(Self::Branch { .. }, Self::Branch { .. }) | (Self::Leaf { .. }, Self::Leaf { .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd<Self> for FileTree {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for FileTree {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Leaf { filename: a }, Self::Leaf { filename: b }) => a.cmp(b),
|
||||||
|
(Self::Branch { path: a, .. }, Self::Branch { path: b, .. }) => a.cmp(b),
|
||||||
|
(Self::Branch { .. }, _) => Ordering::Less,
|
||||||
|
(_, Self::Branch { .. }) => Ordering::Greater,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FileTreeEvent<A> {
|
||||||
|
Clicked(PathBuf),
|
||||||
|
ContextAction(A),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTree {
|
||||||
|
pub fn ui<A>(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &mut impl FnMut(&mut egui::Ui, &Path) -> Option<A>,
|
||||||
|
) -> InnerResponse<Option<FileTreeEvent<A>>> {
|
||||||
|
let mut action = None;
|
||||||
|
let frame_response = egui::Frame::new().show(ui, |ui| {
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
self.ui_inner(ui, &mut action, ctx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
InnerResponse::new(action, frame_response.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_inner<A>(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
action: &mut Option<FileTreeEvent<A>>,
|
||||||
|
ctx: &mut impl FnMut(&mut egui::Ui, &Path) -> Option<A>,
|
||||||
|
) {
|
||||||
|
match self {
|
||||||
|
Self::Leaf { filename } => {
|
||||||
|
let response = ui.button(filename.file_name().unwrap().to_string_lossy());
|
||||||
|
if response.clicked() {
|
||||||
|
*action = Some(FileTreeEvent::Clicked(PathBuf::from(filename)));
|
||||||
|
}
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
if let Some(a) = ctx(ui, filename) {
|
||||||
|
*action = Some(FileTreeEvent::ContextAction(a));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Self::Branch { path, children } => {
|
||||||
|
CollapsingHeader::new(path.file_name().unwrap().to_string_lossy())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for child in children {
|
||||||
|
child.ui_inner(ui, action, ctx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.header_response
|
||||||
|
.context_menu(|ui| {
|
||||||
|
if let Some(a) = ctx(ui, path) {
|
||||||
|
*action = Some(FileTreeEvent::ContextAction(a));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(path: &Path) -> Self {
|
||||||
|
match path.is_dir() {
|
||||||
|
true => Self::Branch {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
children: {
|
||||||
|
let mut children: Vec<Self> = std::fs::read_dir(path)
|
||||||
|
.map(|ok| {
|
||||||
|
ok.filter_map(|file| file.ok())
|
||||||
|
.map(|file| Self::build(&path.join(file.file_name())))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
children.sort_unstable();
|
||||||
|
children
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false => Self::Leaf {
|
||||||
|
filename: path.to_path_buf(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod buttons;
|
pub mod buttons;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
pub mod file_tree;
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/// Shared paint logic — draws the crossed lines, using whatever
|
||||||
|
/// interact-state visuals (hover/press color, expansion) egui's style
|
||||||
|
/// would normally pick for this response.
|
||||||
|
fn paint_close_x(ui: &egui::Ui, rect: egui::Rect, response: &egui::Response) {
|
||||||
|
if !ui.is_rect_visible(rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let visuals = ui.style().interact(response);
|
||||||
|
let inner = rect.shrink(4.0).expand(visuals.expansion);
|
||||||
|
let stroke = visuals.fg_stroke;
|
||||||
|
ui.painter().line_segment([inner.left_top(), inner.right_bottom()], stroke);
|
||||||
|
ui.painter().line_segment([inner.right_top(), inner.left_bottom()], stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standalone close button — allocates its own space in the current
|
||||||
|
/// layout, like a normal widget. Use this in a `ui.horizontal(...)` row,
|
||||||
|
/// e.g. next to a label in a flat list (process manager tabs, a file
|
||||||
|
/// list with per-row close, etc).
|
||||||
|
pub fn close_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
||||||
|
let (rect, response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click());
|
||||||
|
let response = response.on_hover_cursor(egui::CursorIcon::Default);
|
||||||
|
|
||||||
|
response.widget_info(|| {
|
||||||
|
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
|
||||||
|
});
|
||||||
|
|
||||||
|
paint_close_x(ui, rect, &response);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close button at an explicit rect you've already computed — for
|
||||||
|
/// cases like tab_ui, where the button needs to sit inside a larger
|
||||||
|
/// region you're laying out manually rather than in normal flow.
|
||||||
|
pub fn close_button_at(ui: &mut egui::Ui, rect: egui::Rect, id: egui::Id) -> egui::Response {
|
||||||
|
let response = ui
|
||||||
|
.interact(rect, id, egui::Sense::click_and_drag())
|
||||||
|
.on_hover_cursor(egui::CursorIcon::Default);
|
||||||
|
|
||||||
|
response.widget_info(|| {
|
||||||
|
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
|
||||||
|
});
|
||||||
|
|
||||||
|
paint_close_x(ui, rect, &response);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TabResponse {
|
||||||
|
pub response: egui::Response,
|
||||||
|
pub close_clicked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn painted_tab(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
id: egui::Id,
|
||||||
|
title: impl Into<egui::WidgetText>,
|
||||||
|
selected: bool,
|
||||||
|
closable: bool,
|
||||||
|
draggable: bool,
|
||||||
|
) -> TabResponse {
|
||||||
|
let padding = egui::vec2(14.0, 8.0);
|
||||||
|
let close_size = 14.0;
|
||||||
|
let close_gap = 6.0;
|
||||||
|
|
||||||
|
let font_id = egui::TextStyle::Button.resolve(ui.style());
|
||||||
|
let galley = title.into().into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, font_id);
|
||||||
|
|
||||||
|
let extra_width = if closable { close_size + close_gap } else { 0.0 };
|
||||||
|
let desired = egui::vec2(
|
||||||
|
galley.size().x + padding.x * 2.0 + extra_width,
|
||||||
|
ui.available_height().max(galley.size().y + padding.y * 2.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, rect) = ui.allocate_space(desired);
|
||||||
|
|
||||||
|
let sense = if draggable { egui::Sense::click_and_drag() } else { egui::Sense::click() };
|
||||||
|
let response = ui.interact(rect, id, sense);
|
||||||
|
let response = if draggable {
|
||||||
|
response.on_hover_cursor(egui::CursorIcon::Grab)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
};
|
||||||
|
|
||||||
|
response.widget_info(|| {
|
||||||
|
egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected, galley.text())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut close_clicked = false;
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
|
ui.painter().rect(rect, 4.0, visuals.bg_fill, visuals.bg_stroke, egui::StrokeKind::Inside);
|
||||||
|
|
||||||
|
let text_color = if selected {
|
||||||
|
egui::Color32::WHITE
|
||||||
|
} else {
|
||||||
|
egui::Color32::DARK_GRAY
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_pos = egui::pos2(rect.min.x + padding.x, rect.center().y - galley.size().y / 2.0);
|
||||||
|
ui.painter().galley(text_pos, galley, text_color);
|
||||||
|
|
||||||
|
if closable {
|
||||||
|
let close_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(rect.max.x - padding.x - close_size, rect.center().y - close_size / 2.0),
|
||||||
|
egui::vec2(close_size, close_size),
|
||||||
|
);
|
||||||
|
let close_id = id.with("close");
|
||||||
|
let close_response = close_button_at(ui, close_rect, close_id);
|
||||||
|
|
||||||
|
close_clicked = close_response.clicked() || response.clicked_by(egui::PointerButton::Middle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TabResponse { response, close_clicked }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user