unified tab style in UI
This commit is contained in:
@@ -51,9 +51,9 @@ impl Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status.clone().ui(ui, padding);
|
if PaddedWidget::indicator(status).ui(ui).clicked() {}
|
||||||
|
|
||||||
if PaddedWidget::button("⟳ Reset All").color(Color32::YELLOW).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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ impl TilePane for Editor {
|
|||||||
|
|
||||||
action = Some(Action::SpawnProcess(Box::new(move |pm| {
|
action = Some(Action::SpawnProcess(Box::new(move |pm| {
|
||||||
pm.spawn(
|
pm.spawn(
|
||||||
|
"Assembler",
|
||||||
Assembler::new(filename),
|
Assembler::new(filename),
|
||||||
move |result| {
|
move |result| {
|
||||||
return if let Ok(bin) = result {
|
return if let Ok(bin) = result {
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ pub mod registers;
|
|||||||
pub mod serial;
|
pub mod serial;
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use eframe::epaint::Color32;
|
||||||
|
use egui::{Id, Response, Sense, TextStyle, Ui, WidgetInfo, WidgetType};
|
||||||
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 crate::ui::Action;
|
use crate::ui::Action;
|
||||||
use crate::ui::Action::RequestCloseTab;
|
use crate::ui::Action::RequestCloseTab;
|
||||||
|
use crate::ui::ui_helpers::{close_button_at, PaddedWidget, 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
|
||||||
@@ -64,6 +67,26 @@ impl Behavior<Pane> for TileBehavior {
|
|||||||
UiResponse::None
|
UiResponse::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tab_ui(
|
||||||
|
&mut self,
|
||||||
|
tiles: &mut Tiles<Pane>,
|
||||||
|
ui: &mut Ui,
|
||||||
|
id: Id,
|
||||||
|
tile_id: TileId,
|
||||||
|
state: &TabState,
|
||||||
|
) -> Response {
|
||||||
|
let title = self.tab_title_for_tile(tiles, tile_id);
|
||||||
|
let draggable = self.is_tile_draggable(tiles, tile_id);
|
||||||
|
|
||||||
|
let tab = painted_tab(ui, id, title, state.active, state.closable, draggable);
|
||||||
|
|
||||||
|
if tab.close_clicked && self.on_tab_close(tiles, tile_id) {
|
||||||
|
tiles.remove(tile_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.on_tab_button(tiles, tile_id, tab.response)
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText {
|
fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText {
|
||||||
pane.title()
|
pane.title()
|
||||||
}
|
}
|
||||||
@@ -86,7 +109,6 @@ impl Behavior<Pane> for TileBehavior {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
|
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
|
||||||
36.0 // taller tab row = more vertical padding around the label
|
36.0 // taller tab row = more vertical padding around the label
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use egui::Ui;
|
|||||||
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 crate::ui::ui_helpers::{close_button, painted_tab};
|
||||||
|
use crate::ui::ui_helpers::text::strip_ansi;
|
||||||
|
|
||||||
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
|
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
|
||||||
|
|
||||||
@@ -62,15 +64,15 @@ impl ProcessManager {
|
|||||||
self.terminated_actions.pop_front()
|
self.terminated_actions.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<P: TraceableProcess>(&mut self, process: P, on_complete: impl FnOnce(Result<P::Output, P::Error>) -> Action + Send + 'static) {
|
pub fn spawn<P: TraceableProcess>(&mut self, name: impl Into<String>, process: P, on_complete: impl FnOnce(Result<P::Output, P::Error>) -> Action + Send + 'static) {
|
||||||
let handle = ProcessHandle::spawn(self.new_id(), "process", process, on_complete);
|
let handle = ProcessHandle::spawn(self.new_id(), name, process, on_complete);
|
||||||
self.handles.push(handle);
|
self.handles.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain_logs(&mut self) {
|
fn drain_logs(&mut self) {
|
||||||
for handle in &mut self.handles {
|
for handle in &mut self.handles {
|
||||||
while let Ok(line) = handle.rx.try_recv() {
|
while let Ok(line) = handle.rx.try_recv() {
|
||||||
handle.logs.push(line);
|
handle.logs.push(strip_ansi(&line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,41 +107,33 @@ impl ProcessManager {
|
|||||||
self.active_tab = self.handles.first().map(|h| h.id);
|
self.active_tab = self.handles.first().map(|h| h.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- tab bar ---
|
|
||||||
|
let mut to_close = None;
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for handle in &self.handles {
|
for handle in &self.handles {
|
||||||
let selected = self.active_tab == Some(handle.id);
|
let selected = self.active_tab == Some(handle.id);
|
||||||
let running = handle.join_handle.is_some();
|
let tab_id = ui.auto_id_with(("process_tab", handle.id));
|
||||||
|
let tab = painted_tab(ui, tab_id, handle.name.clone(), selected, true, false);
|
||||||
|
|
||||||
let label = if running {
|
if tab.response.clicked() {
|
||||||
format!("● {}", handle.name)
|
|
||||||
} else {
|
|
||||||
format!("○ {}", handle.name)
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui.selectable_label(selected, label).clicked() {
|
|
||||||
self.active_tab = Some(handle.id);
|
self.active_tab = Some(handle.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tab.close_clicked {
|
||||||
|
to_close = Some(handle.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
if let Some(id) = to_close {
|
||||||
|
self.handles.retain(|h| h.id != id);
|
||||||
|
if self.active_tab == Some(id) {
|
||||||
|
self.active_tab = self.handles.first().map(|h| h.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- close button for the active tab ---
|
|
||||||
if let Some(active_id) = self.active_tab {
|
if let Some(active_id) = self.active_tab {
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
|
|
||||||
ui.label(egui::RichText::new(&handle.name).strong());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.small_button("✕ Close").clicked() {
|
|
||||||
self.handles.retain(|h| h.id != active_id);
|
|
||||||
self.active_tab = self.handles.first().map(|h| h.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
// --- log view for the active tab ---
|
// --- log view for the active tab ---
|
||||||
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
|
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod text;
|
||||||
|
|
||||||
use eframe::emath::Vec2;
|
use eframe::emath::Vec2;
|
||||||
use egui::{Color32, Frame, Stroke};
|
use egui::{Color32, Frame, Stroke};
|
||||||
use egui::widget_style::{Classes, WidgetState};
|
use egui::widget_style::{Classes, WidgetState};
|
||||||
@@ -17,9 +19,9 @@ enum WidgetType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PaddedWidget {
|
impl PaddedWidget {
|
||||||
pub fn button(text: impl AsRef<str>) -> Self {
|
pub fn button(text: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.as_ref().to_string(),
|
text: text.into(),
|
||||||
color: None,
|
color: None,
|
||||||
padding: None,
|
padding: None,
|
||||||
widget_type: WidgetType::Button
|
widget_type: WidgetType::Button
|
||||||
@@ -35,9 +37,9 @@ impl PaddedWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(text: String) -> Self {
|
pub fn label(text: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text,
|
text: text.into(),
|
||||||
color: None,
|
color: None,
|
||||||
padding: None,
|
padding: None,
|
||||||
widget_type: WidgetType::Label
|
widget_type: WidgetType::Label
|
||||||
@@ -57,7 +59,12 @@ impl PaddedWidget {
|
|||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
let text: egui::WidgetText = self.text.clone().into();
|
let text: egui::WidgetText = self.text.clone().into();
|
||||||
let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
||||||
let desired_size = galley.size() + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0;
|
|
||||||
|
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());
|
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||||
|
|
||||||
@@ -77,116 +84,38 @@ impl PaddedWidget {
|
|||||||
egui::StrokeKind::Inside,
|
egui::StrokeKind::Inside,
|
||||||
);
|
);
|
||||||
|
|
||||||
let text_pos = rect.center() - galley.size() / 2.0;
|
if let WidgetType::Indicator = self.widget_type {
|
||||||
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
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
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn frame_button(ui: &mut egui::Ui, text: impl Into<egui::WidgetText>, color: egui::Color32, padding: Vec2) -> egui::Response {
|
|
||||||
// let text: egui::WidgetText = text.into();
|
|
||||||
// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
|
||||||
// let desired_size = galley.size() + padding * 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 = ui.style().interact(&response);
|
|
||||||
//
|
|
||||||
// ui.painter().rect(
|
|
||||||
// rect,
|
|
||||||
// visuals.corner_radius,
|
|
||||||
// visuals.bg_fill,
|
|
||||||
// visuals.bg_stroke,
|
|
||||||
// egui::StrokeKind::Inside,
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// let text_pos = rect.center() - galley.size() / 2.0;
|
|
||||||
// ui.painter().galley(text_pos, galley, color);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// response
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// pub fn info_field(ui: &mut egui::Ui, text: impl Into<egui::WidgetText>, padding: egui::Vec2) -> egui::Response {
|
|
||||||
// let text: egui::WidgetText = text.into();
|
|
||||||
// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
|
||||||
// let desired_size = galley.size() + padding * 2.0;
|
|
||||||
//
|
|
||||||
// let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
//
|
|
||||||
// if ui.is_rect_visible(rect) {
|
|
||||||
// // Always use `inactive` — no hover highlight, no active/pressed feedback either.
|
|
||||||
// // Response is still fully clickable; this only affects what gets drawn.
|
|
||||||
// let visuals = &ui.visuals().widgets.inactive;
|
|
||||||
//
|
|
||||||
// ui.painter().rect(
|
|
||||||
// rect,
|
|
||||||
// visuals.corner_radius,
|
|
||||||
// visuals.bg_fill,
|
|
||||||
// visuals.bg_stroke,
|
|
||||||
// egui::StrokeKind::Inside,
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// let text_pos = rect.center() - galley.size() / 2.0;
|
|
||||||
// ui.painter().galley(text_pos, galley, visuals.fg_stroke.color);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// response
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
pub fn bordered_frame<R>(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
padding: egui::Vec2,
|
|
||||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
|
||||||
) -> egui::InnerResponse<R> {
|
|
||||||
let visuals = ui.visuals().widgets.inactive; // same visuals a normal button uses
|
|
||||||
|
|
||||||
egui::Frame::new()
|
|
||||||
.inner_margin(egui::Margin::symmetric(padding.x as i8, padding.y as i8))
|
|
||||||
.stroke(visuals.bg_stroke)
|
|
||||||
.corner_radius(visuals.corner_radius)
|
|
||||||
.fill(visuals.bg_fill)
|
|
||||||
.show(ui, add_contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait StatusIndicator {
|
pub trait StatusIndicator {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, padding: egui::Vec2) -> egui::Response {
|
|
||||||
bordered_frame(ui, padding, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let dot_size = 8.0;
|
|
||||||
let (rect, _) = ui.allocate_exact_size(
|
|
||||||
egui::vec2(dot_size, dot_size),
|
|
||||||
egui::Sense::hover(),
|
|
||||||
);
|
|
||||||
ui.painter().circle_filled(rect.center(), dot_size / 2.0, self.color());
|
|
||||||
ui.label( self.label() );
|
|
||||||
})
|
|
||||||
.response
|
|
||||||
})
|
|
||||||
.response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn color(&self) -> egui::Color32;
|
fn color(&self) -> egui::Color32;
|
||||||
|
|
||||||
fn label(&self) -> String;
|
fn label(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl StatusIndicator for RunningState {
|
impl StatusIndicator for RunningState {
|
||||||
fn color(&self) -> egui::Color32 {
|
fn color(&self) -> egui::Color32 {
|
||||||
match self {
|
match self {
|
||||||
RunningState::Running => egui::Color32::from_rgb(0x4c, 0xaf, 0x50),
|
RunningState::Running => egui::Color32::GREEN,
|
||||||
RunningState::Paused => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
RunningState::Paused => egui::Color32::YELLOW,
|
||||||
RunningState::Halted => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
RunningState::Halted => egui::Color32::ORANGE,
|
||||||
RunningState::Breakpoint(_) => egui::Color32::from_rgb(0xe5, 0x39, 0x35),
|
RunningState::Breakpoint(_) => egui::Color32::RED,
|
||||||
RunningState::Shutdown => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
RunningState::Shutdown => egui::Color32::BLUE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,4 +128,121 @@ impl StatusIndicator for RunningState {
|
|||||||
RunningState::Shutdown => "Shutdown".into(),
|
RunningState::Shutdown => "Shutdown".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
pub fn strip_ansi(input: &str) -> String {
|
||||||
|
let mut output = String::with_capacity(input.len());
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\x1B' {
|
||||||
|
if chars.peek() == Some(&'[') {
|
||||||
|
chars.next();
|
||||||
|
while let Some(&next) = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
if next.is_ascii_alphabetic() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user