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 dsa_emulator::EmulatorHandle;
|
||||
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 {
|
||||
emulator: EmulatorHandle,
|
||||
@@ -23,7 +24,6 @@ impl Controller {
|
||||
.inner_margin(10.0)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
use RunningState::*;
|
||||
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() {
|
||||
self.emulator.when_paused(|emu| emu.reset());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use egui::{Modal, Id, Layout, Align, RichText};
|
||||
use crate::ui::ui_helpers::PaddedWidget;
|
||||
use egui::{Align, Id, Layout, Modal, RichText};
|
||||
use zxq5_egui::buttons::PaddedWidget;
|
||||
|
||||
pub struct ConfirmDialog<T> {
|
||||
open: bool,
|
||||
|
||||
@@ -8,27 +8,29 @@ use crate::ui::tiles::registers::Registers;
|
||||
use crate::ui::tiles::{CentralTiles, TilePane};
|
||||
use dialogs::confirm_dialog::ConfirmDialog;
|
||||
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::devices::display::Display;
|
||||
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::documentation::Documentation;
|
||||
use tiles::editor::Editor;
|
||||
use tiles::io_devices::framebuffer::FrameBufferUi;
|
||||
use process_manager::ProcessManager;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::time::Duration;
|
||||
use zxq5_egui::file_tree::{FileTree, FileTreeEvent};
|
||||
|
||||
pub mod appearance;
|
||||
mod controller;
|
||||
mod dialogs;
|
||||
pub mod process_manager;
|
||||
mod tiles;
|
||||
mod ui_helpers;
|
||||
mod controller;
|
||||
pub mod process_manager;
|
||||
pub mod appearance;
|
||||
|
||||
pub struct DamnSimpleUI {
|
||||
emulator: EmulatorController,
|
||||
@@ -42,10 +44,10 @@ pub struct DamnSimpleUI {
|
||||
tools: Vec<PanelFactory>,
|
||||
|
||||
show_process_manager: bool,
|
||||
process_manager: ProcessManager
|
||||
process_manager: ProcessManager,
|
||||
file_tree: FileTree,
|
||||
}
|
||||
|
||||
|
||||
impl eframe::App for DamnSimpleUI {
|
||||
fn ui(&mut self, ui: &mut Ui, frame: &mut eframe::Frame) {
|
||||
ui.request_repaint_after(Duration::from_millis(16));
|
||||
@@ -72,13 +74,26 @@ impl eframe::App for DamnSimpleUI {
|
||||
self.controller.ui(ui);
|
||||
});
|
||||
|
||||
Panel::left("left_panel").resizable(false).show(ui, |ui| {
|
||||
// TODO: File Explorer!
|
||||
Panel::left("left_panel").resizable(true).show(ui, |ui| {
|
||||
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| {
|
||||
self.process_manager.ui(ui);
|
||||
});
|
||||
Panel::bottom("bottom_panel")
|
||||
.resizable(true)
|
||||
.show(ui, |ui| {
|
||||
self.process_manager.ui(ui);
|
||||
});
|
||||
|
||||
CentralPanel::default().show(ui, |ui| {
|
||||
if let Some(action) = self.central_tiles.ui(ui) {
|
||||
@@ -88,11 +103,7 @@ impl eframe::App for DamnSimpleUI {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
impl DamnSimpleUI {
|
||||
|
||||
fn open_or_focus<T: TilePane + 'static>(
|
||||
central_tiles: &mut CentralTiles,
|
||||
slot: &mut Option<TileId>,
|
||||
@@ -104,10 +115,7 @@ impl DamnSimpleUI {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext,
|
||||
mut emulator: EmulatorController,
|
||||
) -> Self {
|
||||
pub fn new(cc: &eframe::CreationContext, mut emulator: EmulatorController) -> Self {
|
||||
appearance::configure_appearance(&cc.egui_ctx);
|
||||
let handle = emulator.get_handle().clone();
|
||||
Self {
|
||||
@@ -116,7 +124,7 @@ impl DamnSimpleUI {
|
||||
confirm_dialog: ConfirmDialog::new(),
|
||||
file_dialog: DsaFileDialog::new(),
|
||||
error_dialog: ErrorDialog::default(),
|
||||
|
||||
|
||||
tools: vec![
|
||||
PanelFactory::register("Memory Inspector", {
|
||||
let handle = handle.clone();
|
||||
@@ -131,31 +139,32 @@ impl DamnSimpleUI {
|
||||
let handle = handle.clone();
|
||||
move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
|
||||
}),
|
||||
PanelFactory::register("Serial", {
|
||||
PanelFactory::register("Serial", {
|
||||
let handle = handle.clone();
|
||||
move || Serial::new(0x40000, handle.clone())
|
||||
}),
|
||||
PanelFactory::register("Registers", {
|
||||
PanelFactory::register("Registers", {
|
||||
let handle = handle.clone();
|
||||
move || Registers::new(handle.clone())
|
||||
})
|
||||
}),
|
||||
],
|
||||
emulator,
|
||||
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) {
|
||||
match action {
|
||||
Action::None => {},
|
||||
Action::None => {}
|
||||
|
||||
Action::SpawnProcess(spawner) => spawner(&mut self.process_manager),
|
||||
Action::RequestCloseTab(tile_id) => {
|
||||
self.confirm_dialog.open(
|
||||
"Close Tab",
|
||||
"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),
|
||||
|
||||
// Opens a file (usually constructed by the file dialogue)
|
||||
Action::OpenFile(path) => {
|
||||
match path.extension().map(|s| s.to_str()).flatten() {
|
||||
Some("dsa" | "dsc") => { 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 => {},
|
||||
Action::OpenFile(path) => match path.extension().map(|s| s.to_str()).flatten() {
|
||||
Some("dsa" | "dsc") => {
|
||||
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(f) => {
|
||||
self.perform_action(Action::ShowError(format!(
|
||||
"Unsupported file extension: {}",
|
||||
f
|
||||
)));
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
|
||||
Action::ShowError(err) => {
|
||||
self.error_dialog.open(err);
|
||||
@@ -187,11 +205,12 @@ impl DamnSimpleUI {
|
||||
}
|
||||
|
||||
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 {
|
||||
self.perform_action(Action::ShowError("Failed to open file".into()));
|
||||
return
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
|
||||
@@ -215,50 +234,67 @@ impl DamnSimpleUI {
|
||||
}
|
||||
|
||||
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) {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
|
||||
let save_enabled = self.central_tiles.active_tile()
|
||||
let save_enabled = self
|
||||
.central_tiles
|
||||
.active_tile()
|
||||
.and_then(|id| self.central_tiles.pane_mut::<Editor>(id))
|
||||
.is_some();
|
||||
|
||||
ui.set_height(30.0);
|
||||
ui.menu_button("File", |ui| {
|
||||
|
||||
if ui.button("Open").clicked() {
|
||||
self.file_dialog.open_pick(Action::OpenFile);
|
||||
}
|
||||
|
||||
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(editor) = self.central_tiles.pane_mut::<Editor>(active) {
|
||||
match editor.filename().cloned() {
|
||||
Some(path) => std::fs::write(path, editor.get_buffer().as_bytes()).unwrap(),
|
||||
None => self.file_dialog.open_save(move |path| Action::SaveFile(path, active)),
|
||||
Some(path) => {
|
||||
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(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() {
|
||||
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::ui::ui_helpers::painted_tab;
|
||||
use crate::ui::ui_helpers::text::strip_ansi;
|
||||
use dsa_tasks::{ProcessAgent, TraceableProcess};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
use zxq5_egui::tabs::painted_tab;
|
||||
|
||||
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::state::Action;
|
||||
use crate::state::DataSource::Data;
|
||||
use crate::ui::tiles::TilePane;
|
||||
use crate::ui::tiles::editor::syntax::SyntaxFrom;
|
||||
use crate::ui::ui_helpers::PaddedWidget;
|
||||
use assembler::prelude::Assembler;
|
||||
use dsa_formats::binary_dse::DseExecutable;
|
||||
use dsa_tasks::TraceableProcess;
|
||||
@@ -13,6 +12,7 @@ use std::any::Any;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use theme::THEME;
|
||||
use zxq5_egui::buttons::PaddedWidget;
|
||||
|
||||
mod syntax;
|
||||
mod theme;
|
||||
|
||||
@@ -8,11 +8,11 @@ pub mod io_devices;
|
||||
|
||||
use crate::state::Action;
|
||||
use crate::state::Action::RequestCloseTab;
|
||||
use crate::ui::ui_helpers::painted_tab;
|
||||
use eframe::epaint::{Color32, CornerRadius};
|
||||
use egui::{Id, Response, Stroke, Ui};
|
||||
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
|
||||
use std::any::Any;
|
||||
use zxq5_egui::tabs::painted_tab;
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -1,115 +1,13 @@
|
||||
pub mod text;
|
||||
|
||||
use eframe::emath::Vec2;
|
||||
use egui::{Color32};
|
||||
use dsa_emulator::processor::RunningState;
|
||||
use zxq5_egui::buttons::StatusIndicator;
|
||||
|
||||
pub struct PaddedWidget {
|
||||
text: String,
|
||||
color: Option<Color32>,
|
||||
padding: Option<Vec2>,
|
||||
widget_type: WidgetType
|
||||
}
|
||||
pub struct StateWrapper(pub RunningState);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
impl StatusIndicator for RunningState {
|
||||
impl StatusIndicator for StateWrapper {
|
||||
fn color(&self) -> egui::Color32 {
|
||||
match self {
|
||||
match self.0 {
|
||||
RunningState::Running => egui::Color32::GREEN,
|
||||
RunningState::Paused => egui::Color32::YELLOW,
|
||||
RunningState::Halted => egui::Color32::ORANGE,
|
||||
@@ -119,7 +17,7 @@ impl StatusIndicator for RunningState {
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
match self {
|
||||
match self.0 {
|
||||
RunningState::Running => "Running".into(),
|
||||
RunningState::Paused => "Paused".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 }
|
||||
}
|
||||
Reference in New Issue
Block a user