Add editor::ToggleInlayHints command and a bar to toggle editor-related items (#2839)

Closes
https://linear.app/zed-industries/issue/Z-2735/make-inlay-hints-discoverable
Deals with https://github.com/zed-industries/community/issues/1764

Adds `editor::ToggleInlayHints` command and a new panel with two
buttons for 
* toggling hints on/off for every editor separately (overrides settings)

Would benefit from a new icon.

* toggling buffer search on/off 

Does not have a keybinding shown in its tooltip, that is a separate
issue with the way `KeystrokeLabel` gets these for panels not in the
view directly.

Release Notes:

- Adds `editor::ToggleInlayHints` command and a bar to toggle
editor-related items
This commit is contained in:
Kirill Bulatov 2023-08-16 23:50:54 +03:00 committed by GitHub
commit 988ea3c16b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 423 additions and 39 deletions

12
Cargo.lock generated
View File

@ -5714,6 +5714,17 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick_action_bar"
version = "0.1.0"
dependencies = [
"editor",
"gpui",
"search",
"theme",
"workspace",
]
[[package]]
name = "quote"
version = "1.0.32"
@ -9922,6 +9933,7 @@ dependencies = [
"project",
"project_panel",
"project_symbols",
"quick_action_bar",
"rand 0.8.5",
"recent_projects",
"regex",

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3" cy="9" r="1" fill="black"/>
<circle cx="3" cy="5" r="1" fill="black"/>
<path d="M7 3H10M13 3H10M10 3C10 3 10 11 10 11.5" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -7867,7 +7867,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.insert_tree(
"/a",
json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file",
}),
)
@ -8177,7 +8177,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
.insert_tree(
"/a",
json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
"other.rs": "// Test file",
}),
)

View File

@ -302,10 +302,11 @@ actions!(
Hover,
Format,
ToggleSoftWrap,
ToggleInlayHints,
RevealInFinder,
CopyPath,
CopyRelativePath,
CopyHighlightJson
CopyHighlightJson,
]
);
@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::toggle_inlay_hints);
cx.add_action(Editor::reveal_in_finder);
cx.add_action(Editor::copy_path);
cx.add_action(Editor::copy_relative_path);
@ -1237,7 +1239,8 @@ enum GotoDefinitionKind {
}
#[derive(Debug, Clone)]
enum InlayRefreshReason {
enum InlayHintRefreshReason {
Toggle(bool),
SettingsChange(InlayHintSettings),
NewLinesShown,
BufferEdited(HashSet<Arc<Language>>),
@ -1354,8 +1357,8 @@ impl Editor {
}));
}
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
if let project::Event::RefreshInlays = event {
editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
if let project::Event::RefreshInlayHints = event {
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
};
}));
}
@ -2669,13 +2672,41 @@ impl Editor {
}
}
fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
self.refresh_inlay_hints(
InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
cx,
);
}
pub fn inlay_hints_enabled(&self) -> bool {
self.inlay_hint_cache.enabled
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
if self.project.is_none() || self.mode != EditorMode::Full {
return;
}
let (invalidate_cache, required_languages) = match reason {
InlayRefreshReason::SettingsChange(new_settings) => {
InlayHintRefreshReason::Toggle(enabled) => {
self.inlay_hint_cache.enabled = enabled;
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.inlay_hint_cache.clear();
self.splice_inlay_hints(
self.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect(),
Vec::new(),
cx,
);
return;
}
}
InlayHintRefreshReason::SettingsChange(new_settings) => {
match self.inlay_hint_cache.update_settings(
&self.buffer,
new_settings,
@ -2693,11 +2724,13 @@ impl Editor {
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
}
}
InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
InlayRefreshReason::BufferEdited(buffer_languages) => {
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
InlayHintRefreshReason::BufferEdited(buffer_languages) => {
(InvalidationStrategy::BufferEdited, Some(buffer_languages))
}
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
InlayHintRefreshReason::RefreshRequested => {
(InvalidationStrategy::RefreshRequested, None)
}
};
if let Some(InlaySplice {
@ -2774,6 +2807,7 @@ impl Editor {
self.display_map.update(cx, |display_map, cx| {
display_map.splice_inlays(to_remove, to_insert, cx);
});
cx.notify();
}
fn trigger_on_type_formatting(
@ -7696,8 +7730,8 @@ impl Editor {
.cloned()
.collect::<HashSet<_>>();
if !languages_affected.is_empty() {
self.refresh_inlays(
InlayRefreshReason::BufferEdited(languages_affected),
self.refresh_inlay_hints(
InlayHintRefreshReason::BufferEdited(languages_affected),
cx,
);
}
@ -7735,8 +7769,8 @@ impl Editor {
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(true, cx);
self.refresh_inlays(
InlayRefreshReason::SettingsChange(inlay_hint_settings(
self.refresh_inlay_hints(
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(),
&self.buffer.read(cx).snapshot(cx),
cx,

View File

@ -24,7 +24,7 @@ pub struct InlayHintCache {
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
version: usize,
enabled: bool,
pub(super) enabled: bool,
update_tasks: HashMap<ExcerptId, TasksForRanges>,
}
@ -380,7 +380,7 @@ impl InlayHintCache {
}
}
fn clear(&mut self) {
pub fn clear(&mut self) {
self.version += 1;
self.update_tasks.clear();
self.hints.clear();
@ -2001,7 +2001,7 @@ mod tests {
});
}
#[gpui::test]
#[gpui::test(iterations = 10)]
async fn test_multiple_excerpts_large_multibuffer(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
@ -2335,10 +2335,12 @@ mod tests {
all hints should be invalidated and requeried for all of its visible excerpts"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
assert_eq!(
editor.inlay_hint_cache().version,
last_scroll_update_version + expected_layers.len(),
"Due to every excerpt having one hint, cache should update per new excerpt received"
let current_cache_version = editor.inlay_hint_cache().version;
let minimum_expected_version = last_scroll_update_version + expected_layers.len();
assert!(
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
"Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
);
});
}
@ -2683,6 +2685,127 @@ all hints should be invalidated and requeried for all of its visible excerpts"
});
}
#[gpui::test]
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().start_waiting();
let lsp_request_count = Arc::new(AtomicU32::new(0));
let closure_lsp_request_count = Arc::clone(&lsp_request_count);
fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
label: lsp::InlayHintLabel::String(i.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["1".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should display inlays after toggle despite them disabled in settings"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(
editor.inlay_hint_cache().version,
1,
"First toggle should be cache's first update"
);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert!(
cached_hint_labels(editor).is_empty(),
"Should clear hints after 2nd toggle"
);
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 2);
});
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["2".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should query LSP hints for the 2nd time after enabling hints in settings"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, 3);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert!(
cached_hint_labels(editor).is_empty(),
"Should clear hints after enabling in settings and a 3rd toggle"
);
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 4);
});
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["3".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, 5);
});
}
pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking();
@ -2759,6 +2882,12 @@ all hints should be invalidated and requeried for all of its visible excerpts"
.downcast::<Editor>()
.unwrap();
editor.update(cx, |editor, cx| {
assert!(cached_hint_labels(editor).is_empty());
assert!(visible_hint_labels(editor, cx).is_empty());
assert_eq!(editor.inlay_hint_cache().version, 0);
});
("/a/main.rs", editor, fake_server)
}

View File

@ -19,7 +19,7 @@ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
ToPoint,
};
@ -301,7 +301,7 @@ impl Editor {
cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
})
.ok()
})
@ -333,7 +333,7 @@ impl Editor {
cx,
);
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

View File

@ -282,7 +282,7 @@ pub enum Event {
new_peer_id: proto::PeerId,
},
CollaboratorLeft(proto::PeerId),
RefreshInlays,
RefreshInlayHints,
}
pub enum LanguageServerState {
@ -2872,7 +2872,7 @@ impl Project {
.upgrade(&cx)
.ok_or_else(|| anyhow!("project dropped"))?;
this.update(&mut cx, |project, cx| {
cx.emit(Event::RefreshInlays);
cx.emit(Event::RefreshInlayHints);
project.remote_id().map(|project_id| {
project.client.send(proto::RefreshInlayHints { project_id })
})
@ -3436,7 +3436,7 @@ impl Project {
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
cx.emit(Event::RefreshInlays);
cx.emit(Event::RefreshInlayHints);
status.pending_work.remove(&token);
cx.notify();
}
@ -6810,7 +6810,7 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
this.update(&mut cx, |_, cx| {
cx.emit(Event::RefreshInlays);
cx.emit(Event::RefreshInlayHints);
});
Ok(proto::Ack {})
}

View File

@ -0,0 +1,22 @@
[package]
name = "quick_action_bar"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/quick_action_bar.rs"
doctest = false
[dependencies]
editor = { path = "../editor" }
gpui = { path = "../gpui" }
search = { path = "../search" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -0,0 +1,163 @@
use editor::Editor;
use gpui::{
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
platform::{CursorStyle, MouseButton},
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
};
use search::{buffer_search, BufferSearchBar};
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub struct QuickActionBar {
buffer_search_bar: ViewHandle<BufferSearchBar>,
active_item: Option<Box<dyn ItemHandle>>,
_inlay_hints_enabled_subscription: Option<Subscription>,
}
impl QuickActionBar {
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
Self {
buffer_search_bar,
active_item: None,
_inlay_hints_enabled_subscription: None,
}
}
fn active_editor(&self) -> Option<ViewHandle<Editor>> {
self.active_item
.as_ref()
.and_then(|item| item.downcast::<Editor>())
}
}
impl Entity for QuickActionBar {
type Event = ();
}
impl View for QuickActionBar {
fn ui_name() -> &'static str {
"QuickActionsBar"
}
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
let Some(editor) = self.active_editor() else { return Empty::new().into_any(); };
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut bar = Flex::row().with_child(render_quick_action_bar_button(
0,
"icons/inlay_hint.svg",
inlay_hints_enabled,
(
"Toggle Inlay Hints".to_string(),
Some(Box::new(editor::ToggleInlayHints)),
),
cx,
|this, cx| {
if let Some(editor) = this.active_editor() {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
});
}
},
));
if editor.read(cx).buffer().read(cx).is_singleton() {
let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
let search_action = buffer_search::Deploy { focus: true };
bar = bar.with_child(render_quick_action_bar_button(
1,
"icons/magnifying_glass.svg",
search_bar_shown,
(
"Buffer Search".to_string(),
Some(Box::new(search_action.clone())),
),
cx,
move |this, cx| {
this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if search_bar_shown {
buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
} else {
buffer_search_bar.deploy(&search_action, cx);
}
});
},
));
}
bar.into_any()
}
}
fn render_quick_action_bar_button<
F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
>(
index: usize,
icon: &'static str,
toggled: bool,
tooltip: (String, Option<Box<dyn Action>>),
cx: &mut ViewContext<QuickActionBar>,
on_click: F,
) -> AnyElement<QuickActionBar> {
enum QuickActionBarButton {}
let theme = theme::current(cx);
let (tooltip_text, action) = tooltip;
MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
let style = theme
.workspace
.toolbar
.toggleable_tool
.in_state(toggled)
.style_for(mouse_state);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
.with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
.into_any_named("quick action bar button")
}
impl ToolbarItemView for QuickActionBar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
match active_pane_item {
Some(active_item) => {
self.active_item = Some(active_item.boxed_clone());
self._inlay_hints_enabled_subscription.take();
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
if inlay_hints_enabled != new_inlay_hints_enabled {
inlay_hints_enabled = new_inlay_hints_enabled;
cx.notify();
}
}));
}
ToolbarItemLocation::PrimaryRight { flex: None }
}
None => {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}
}

View File

@ -36,7 +36,7 @@ pub enum Event {
}
pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::deploy);
cx.add_action(BufferSearchBar::deploy_bar);
cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::select_next_match);
@ -327,6 +327,19 @@ impl BufferSearchBar {
cx.notify();
}
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
if self.show(cx) {
self.search_suggested(cx);
if deploy.focus {
self.select_query(cx);
cx.focus_self();
}
return true;
}
false
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
return false;
@ -553,21 +566,15 @@ impl BufferSearchBar {
.into_any()
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
let mut propagate_action = true;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
search_bar.search_suggested(cx);
if action.focus {
search_bar.select_query(cx);
cx.focus_self();
}
if search_bar.deploy(action, cx) {
propagate_action = false;
}
});
}
if propagate_action {
cx.propagate_action();
}

View File

@ -399,6 +399,7 @@ pub struct Toolbar {
pub height: f32,
pub item_spacing: f32,
pub nav_button: Interactive<IconButton>,
pub toggleable_tool: Toggleable<Interactive<IconButton>>,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]

View File

@ -54,6 +54,7 @@ plugin_runtime = { path = "../plugin_runtime",optional = true }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
quick_action_bar = { path = "../quick_action_bar" }
recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }

View File

@ -30,6 +30,7 @@ use gpui::{
pub use lsp;
pub use project;
use project_panel::ProjectPanel;
use quick_action_bar::QuickActionBar;
use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize;
use serde_json::to_string_pretty;
@ -262,7 +263,10 @@ pub fn initialize_workspace(
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
toolbar.add_item(buffer_search_bar, cx);
toolbar.add_item(buffer_search_bar.clone(), cx);
let quick_action_bar =
cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
toolbar.add_item(quick_action_bar, cx);
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
toolbar.add_item(project_search_bar, cx);
let submit_feedback_button =

View File

@ -12,6 +12,7 @@ import tabBar from "./tab_bar"
import { interactive } from "../element"
import { titlebar } from "./titlebar"
import { useTheme } from "../theme"
import { toggleable_icon_button } from "../component/icon_button"
export default function workspace(): any {
const theme = useTheme()
@ -149,6 +150,11 @@ export default function workspace(): any {
},
},
}),
toggleable_tool: toggleable_icon_button(theme, {
margin: { left: 8 },
variant: "ghost",
active_color: "accent",
}),
padding: { left: 8, right: 8, top: 4, bottom: 4 },
},
breadcrumb_height: 24,