diff --git a/assets/icons/inspector.svg b/assets/icons/inspector.svg
new file mode 100644
index 00000000..c3517a42
--- /dev/null
+++ b/assets/icons/inspector.svg
@@ -0,0 +1,18 @@
+
diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs
index 603f4a1d..ffa7793d 100644
--- a/crates/story/examples/code-editor.rs
+++ b/crates/story/examples/code-editor.rs
@@ -1,5 +1,3 @@
-use std::sync::LazyLock;
-
use gpui::*;
use gpui_component::{
checkbox::Checkbox,
@@ -11,9 +9,6 @@ use gpui_component::{
};
use story::Assets;
-static LIGHT_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_light());
-static DARK_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_dark());
-
pub struct Example {
input_state: Entity,
language_state: Entity>>,
@@ -31,7 +26,7 @@ impl Example {
let default_language: SharedString = LANGUAGES[0].into();
let input_state = cx.new(|cx| {
InputState::new(window, cx)
- .code_editor(Some(&default_language), &LIGHT_THEME)
+ .code_editor(Some(&default_language), &HighlightTheme::default_light())
.line_number(true)
.tab_size(TabSize {
tab_size: 4,
@@ -89,9 +84,15 @@ impl Example {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
- state.set_highlighter(Highlighter::new(Some(language), &DARK_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(language), &HighlightTheme::default_dark()),
+ cx,
+ );
} else {
- state.set_highlighter(Highlighter::new(Some(language), &LIGHT_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(language), &HighlightTheme::default_dark()),
+ cx,
+ );
}
});
}
diff --git a/crates/story/examples/html.rs b/crates/story/examples/html.rs
index 8468d8d9..b87d7cd5 100644
--- a/crates/story/examples/html.rs
+++ b/crates/story/examples/html.rs
@@ -1,5 +1,3 @@
-use std::sync::LazyLock;
-
use gpui::*;
use gpui_component::{
highlighter::{HighlightTheme, Highlighter},
@@ -10,9 +8,6 @@ use gpui_component::{
};
use story::Assets;
-static LIGHT_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_light());
-static DARK_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_dark());
-
pub struct Example {
input_state: Entity,
resizable_state: Entity,
@@ -27,7 +22,7 @@ impl Example {
pub fn new(window: &mut Window, cx: &mut Context) -> Self {
let input_state = cx.new(|cx| {
InputState::new(window, cx)
- .code_editor(Some(LANG), &LIGHT_THEME)
+ .code_editor(Some(LANG), &HighlightTheme::default_light())
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false,
@@ -65,9 +60,15 @@ impl Render for Example {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
- state.set_highlighter(Highlighter::new(Some(LANG), &DARK_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(LANG), &HighlightTheme::default_dark()),
+ cx,
+ );
} else {
- state.set_highlighter(Highlighter::new(Some(LANG), &LIGHT_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(LANG), &HighlightTheme::default_light()),
+ cx,
+ );
}
});
}
diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs
index 873e8ff8..44652106 100644
--- a/crates/story/examples/markdown.rs
+++ b/crates/story/examples/markdown.rs
@@ -1,4 +1,4 @@
-use std::{rc::Rc, sync::LazyLock};
+use std::rc::Rc;
use gpui::*;
use gpui_component::{
@@ -10,8 +10,6 @@ use gpui_component::{
};
use story::Assets;
-static LIGHT_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_light());
-static DARK_THEME: LazyLock = LazyLock::new(|| HighlightTheme::default_dark());
const LANG: &str = "markdown";
pub struct Example {
@@ -26,7 +24,7 @@ impl Example {
pub fn new(window: &mut Window, cx: &mut Context) -> Self {
let input_state = cx.new(|cx| {
InputState::new(window, cx)
- .code_editor(Some(LANG), &LIGHT_THEME)
+ .code_editor(Some(LANG), &HighlightTheme::default_light())
.line_number(false)
.tab_size(TabSize {
tab_size: 2,
@@ -66,9 +64,15 @@ impl Render for Example {
self.is_dark = is_dark;
self.input_state.update(cx, |state, cx| {
if is_dark {
- state.set_highlighter(Highlighter::new(Some(LANG), &DARK_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(LANG), &HighlightTheme::default_dark()),
+ cx,
+ );
} else {
- state.set_highlighter(Highlighter::new(Some(LANG), &LIGHT_THEME), cx);
+ state.set_highlighter(
+ Highlighter::new(Some(LANG), &HighlightTheme::default_light()),
+ cx,
+ );
}
});
}
@@ -92,7 +96,7 @@ impl Render for Example {
.child(
TextView::markdown("preview", self.input_state.read(cx).value()).style(
TextViewStyle {
- highlight_theme: Rc::new(theme),
+ highlight_theme: Rc::new(theme.clone()),
..Default::default()
},
),
diff --git a/crates/story/src/title_bar.rs b/crates/story/src/title_bar.rs
index 1ea61435..0e6cc5d6 100644
--- a/crates/story/src/title_bar.rs
+++ b/crates/story/src/title_bar.rs
@@ -13,7 +13,7 @@ use gpui_component::{
popup_menu::PopupMenuExt as _,
scroll::ScrollbarShow,
set_locale, ActiveTheme as _, ContextModal as _, IconName, Sizable as _, Theme, ThemeMode,
- TitleBar,
+ TitleBar, ToggleInspector,
};
use crate::{SelectFont, SelectLocale, SelectRadius, SelectScrollbarShow};
@@ -312,6 +312,8 @@ impl Render for FontSizeSelector {
scroll_show == ScrollbarShow::Always,
Box::new(SelectScrollbarShow(ScrollbarShow::Always)),
)
+ .separator()
+ .menu("Inspector", Box::new(ToggleInspector))
})
.anchor(Corner::TopRight),
)
diff --git a/crates/ui/src/description_list.rs b/crates/ui/src/description_list.rs
index ce23bcfd..429e8f30 100644
--- a/crates/ui/src/description_list.rs
+++ b/crates/ui/src/description_list.rs
@@ -57,6 +57,12 @@ impl From for DescriptionText {
}
}
+impl From for DescriptionText {
+ fn from(text: String) -> Self {
+ DescriptionText::String(SharedString::from(text))
+ }
+}
+
impl RenderOnce for DescriptionText {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
match self {
diff --git a/crates/ui/src/highlighter/highlight.rs b/crates/ui/src/highlighter/highlight.rs
index d978fd4e..566a2549 100644
--- a/crates/ui/src/highlighter/highlight.rs
+++ b/crates/ui/src/highlighter/highlight.rs
@@ -18,6 +18,15 @@ static DEFAULT_DARK: LazyLock> = LazyLock::new(|| {
Arc::new(highlighting::ThemeSet::load_from_reader(&mut cursor).unwrap())
});
+pub static LIGHT_THEME: LazyLock = LazyLock::new(|| HighlightTheme {
+ name: "default-light".into(),
+ inner: DEFAULT_LIGHT.clone(),
+});
+pub static DARK_THEME: LazyLock = LazyLock::new(|| HighlightTheme {
+ name: "default-dark".into(),
+ inner: DEFAULT_DARK.clone(),
+});
+
/// Represents a theme for syntax highlighting.
#[derive(Debug, Clone, PartialEq)]
pub struct HighlightTheme {
@@ -27,19 +36,13 @@ pub struct HighlightTheme {
impl HighlightTheme {
/// Default light theme.
- pub fn default_light() -> Self {
- Self {
- name: "default-light".into(),
- inner: DEFAULT_LIGHT.clone(),
- }
+ pub fn default_light() -> &'static Self {
+ &LIGHT_THEME
}
/// Default dark theme.
- pub fn default_dark() -> Self {
- Self {
- name: "default-dark".into(),
- inner: DEFAULT_DARK.clone(),
- }
+ pub fn default_dark() -> &'static Self {
+ &DARK_THEME
}
/// Parse a theme from a string (tmTheme)
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
index 43f79731..06b02674 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -44,6 +44,7 @@ pub enum IconName {
HeartOff,
Inbox,
Info,
+ Inspector,
LayoutDashboard,
Loader,
LoaderCircle,
@@ -122,6 +123,7 @@ impl IconName {
Self::HeartOff => "icons/heart-off.svg",
Self::Inbox => "icons/inbox.svg",
Self::Info => "icons/info.svg",
+ Self::Inspector => "icons/inspector.svg",
Self::LayoutDashboard => "icons/layout-dashboard.svg",
Self::Loader => "icons/loader.svg",
Self::LoaderCircle => "icons/loader-circle.svg",
diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs
index 598ff35d..ecbfdfbf 100644
--- a/crates/ui/src/input/state.rs
+++ b/crates/ui/src/input/state.rs
@@ -614,7 +614,13 @@ impl InputState {
self.replace_text(value, window, cx);
self.history.ignore = false;
// Ensure cursor to start when set text
- self.selected_range = self.text.len()..self.text.len();
+ if self.is_single_line() {
+ self.selected_range = self.text.len()..self.text.len();
+ } else {
+ self.selected_range = 0..0;
+ }
+ // Move scroll to top
+ self.scroll_handle.set_offset(point(px(0.), px(0.)));
cx.notify();
}
diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs
index ad5204e1..91dd9ab6 100644
--- a/crates/ui/src/input/text_input.rs
+++ b/crates/ui/src/input/text_input.rs
@@ -180,6 +180,12 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
.on_action(window.listener_for(&self.state, InputState::enter))
.on_action(window.listener_for(&self.state, InputState::escape))
+ .on_action(window.listener_for(&self.state, InputState::indent))
+ .on_action(window.listener_for(&self.state, InputState::outdent))
+ .on_action(window.listener_for(&self.state, InputState::paste))
+ .on_action(window.listener_for(&self.state, InputState::cut))
+ .on_action(window.listener_for(&self.state, InputState::undo))
+ .on_action(window.listener_for(&self.state, InputState::redo))
})
.on_action(window.listener_for(&self.state, InputState::left))
.on_action(window.listener_for(&self.state, InputState::right))
@@ -190,8 +196,6 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::down))
.on_action(window.listener_for(&self.state, InputState::select_up))
.on_action(window.listener_for(&self.state, InputState::select_down))
- .on_action(window.listener_for(&self.state, InputState::indent))
- .on_action(window.listener_for(&self.state, InputState::outdent))
})
.on_action(window.listener_for(&self.state, InputState::select_all))
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
@@ -208,10 +212,6 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::select_to_end))
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
.on_action(window.listener_for(&self.state, InputState::copy))
- .on_action(window.listener_for(&self.state, InputState::paste))
- .on_action(window.listener_for(&self.state, InputState::cut))
- .on_action(window.listener_for(&self.state, InputState::undo))
- .on_action(window.listener_for(&self.state, InputState::redo))
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
.on_mouse_down(
MouseButton::Left,
diff --git a/crates/ui/src/inspector.rs b/crates/ui/src/inspector.rs
new file mode 100644
index 00000000..f86d043f
--- /dev/null
+++ b/crates/ui/src/inspector.rs
@@ -0,0 +1,222 @@
+use std::cell::OnceCell;
+
+use gpui::{
+ actions, div, prelude::FluentBuilder, px, AnyElement, App, AppContext as _, Context,
+ DivInspectorState, Entity, Inspector, InspectorElementId, InteractiveElement as _, IntoElement,
+ KeyBinding, ParentElement as _, Render, SharedString, Styled, Window,
+};
+
+use crate::{
+ button::{Button, ButtonVariants},
+ clipboard::Clipboard,
+ description_list::DescriptionList,
+ h_flex,
+ highlighter::HighlightTheme,
+ input::{InputState, TextInput},
+ link::Link,
+ v_flex, ActiveTheme, IconName, Selectable, Sizable, TITLE_BAR_HEIGHT,
+};
+
+actions!(inspector, [ToggleInspector]);
+
+/// Initialize the inspector and register the action to toggle it.
+pub fn init(cx: &mut App) {
+ cx.bind_keys(vec![
+ #[cfg(target_os = "macos")]
+ KeyBinding::new("cmd-alt-i", ToggleInspector, None),
+ #[cfg(not(target_os = "macos"))]
+ KeyBinding::new("ctrl-shift-i", ToggleInspector, None),
+ ]);
+
+ cx.on_action(|_: &ToggleInspector, cx| {
+ let Some(active_window) = cx.active_window() else {
+ return;
+ };
+
+ cx.defer(move |cx| {
+ _ = active_window.update(cx, |_, window, cx| {
+ window.toggle_inspector(cx);
+ });
+ });
+ });
+
+ let inspector_el = OnceCell::new();
+ cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
+ let el = inspector_el.get_or_init(|| cx.new(|cx| DivInspector::new(window, cx)));
+ el.update(cx, |this, cx| {
+ this.set_inspector_state(id, state.clone(), cx);
+ this.render(window, cx).into_any_element()
+ })
+ });
+
+ cx.set_inspector_renderer(Box::new(render_inspector));
+}
+
+pub struct DivInspector {
+ inspector_id: Option,
+ inspector_state: Option,
+ input_state: Entity,
+}
+
+impl DivInspector {
+ pub fn new(window: &mut Window, cx: &mut App) -> Self {
+ let theme = if cx.theme().is_dark() {
+ HighlightTheme::default_dark()
+ } else {
+ HighlightTheme::default_light()
+ };
+
+ let input_state = cx.new(|cx| {
+ let mut state = InputState::new(window, cx)
+ .code_editor(Some("json"), theme)
+ .line_number(false);
+ state.set_disabled(true, window, cx);
+ state
+ });
+
+ Self {
+ inspector_id: None,
+ inspector_state: None,
+ input_state,
+ }
+ }
+
+ pub fn set_inspector_state(
+ &mut self,
+ inspector_id: InspectorElementId,
+ state: DivInspectorState,
+ cx: &mut Context,
+ ) {
+ self.inspector_id = Some(inspector_id);
+ self.inspector_state = Some(state);
+ cx.notify();
+ }
+}
+
+impl Render for DivInspector {
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let input_state = self.input_state.clone();
+ let last_styles = input_state.read(cx).value().clone();
+
+ v_flex().size_full().gap_3().text_sm().when_some(
+ self.inspector_state.clone(),
+ |this, state| {
+ let styles = serde_json::to_string_pretty(&state.base_style);
+ let styles: SharedString = match styles {
+ Ok(json) => json,
+ Err(e) => format!("{{ \"error\": \"{}\" }}", e),
+ }
+ .into();
+
+ if styles != last_styles {
+ input_state.update(cx, |s, cx| s.set_value(styles.clone(), window, cx));
+ }
+
+ this.child(
+ DescriptionList::new()
+ .columns(1)
+ .label_width(px(110.))
+ .bordered(false)
+ .child("Origin", format!("{}", state.bounds.origin), 1)
+ .child("Size", format!("{}", state.bounds.size), 1)
+ .child("Content Size", format!("{}", state.content_size), 1),
+ )
+ .child(
+ v_flex()
+ .w_full()
+ .flex_1()
+ .gap_1()
+ .text_sm()
+ .text_color(cx.theme().description_list_label_foreground)
+ .child("Styles")
+ .child(
+ div()
+ .flex_1()
+ .w_full()
+ .font_family("Monaco")
+ .text_size(px(12.))
+ .child(TextInput::new(&input_state).h_full()),
+ ),
+ )
+ },
+ )
+ }
+}
+
+fn render_inspector(
+ inspector: &mut Inspector,
+ window: &mut Window,
+ cx: &mut Context,
+) -> AnyElement {
+ let inspector_element_id = inspector.active_element_id();
+ let source_location =
+ inspector_element_id.map(|id| SharedString::new(format!("{}", id.path.source_location)));
+
+ v_flex()
+ .id("inspector")
+ .size_full()
+ .bg(cx.theme().background)
+ .border_l_1()
+ .border_color(cx.theme().border)
+ .text_color(cx.theme().foreground)
+ .child(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .gap_2()
+ .h(TITLE_BAR_HEIGHT)
+ .line_height(TITLE_BAR_HEIGHT)
+ .overflow_hidden()
+ .px_2()
+ .border_b_1()
+ .border_color(cx.theme().title_bar_border)
+ .bg(cx.theme().title_bar)
+ .child(
+ h_flex()
+ .gap_2()
+ .text_sm()
+ .child(
+ Button::new("inspect")
+ .icon(IconName::Inspector)
+ .selected(inspector.is_picking())
+ .small()
+ .ghost()
+ .on_click(cx.listener(|this, _, window, _| {
+ this.start_picking();
+ window.refresh();
+ })),
+ )
+ .child("Inspector"),
+ )
+ .child(
+ Button::new("close")
+ .icon(IconName::Close)
+ .small()
+ .ghost()
+ .on_click(|_, window, cx| {
+ window.dispatch_action(Box::new(ToggleInspector), cx);
+ }),
+ ),
+ )
+ .child(
+ v_flex()
+ .flex_1()
+ .p_3()
+ .gap_3()
+ .when_some(source_location, |this, source_location| {
+ this.child(
+ h_flex()
+ .gap_1()
+ .text_sm()
+ .child(
+ Link::new("source-location")
+ .href(format!("file://{}", source_location))
+ .child(source_location.clone()),
+ )
+ .child(Clipboard::new("copy-source-location").value(source_location)),
+ )
+ })
+ .children(inspector.render_inspector_states(window, cx)),
+ )
+ .into_any_element()
+}
diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs
index f6415797..6762bad3 100644
--- a/crates/ui/src/lib.rs
+++ b/crates/ui/src/lib.rs
@@ -2,6 +2,7 @@ mod colors;
mod event;
mod focusable;
mod icon;
+mod inspector;
mod kbd;
mod menu;
mod root;
@@ -64,6 +65,7 @@ pub use wry;
pub use crate::Disableable;
pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle;
+pub use inspector::*;
pub use menu::{context_menu, popup_menu};
pub use root::{ContextModal, Root};
pub use styled::*;
@@ -88,6 +90,7 @@ rust_i18n::i18n!("locales", fallback = "en");
/// You can initialize the UI module at your application's entry point.
pub fn init(cx: &mut App) {
theme::init(cx);
+ inspector::init(cx);
date_picker::init(cx);
dock::init(cx);
drawer::init(cx);
diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs
index e9f8b2a7..e7867d97 100644
--- a/crates/ui/src/text/text_view.rs
+++ b/crates/ui/src/text/text_view.rs
@@ -84,7 +84,7 @@ impl Default for TextViewStyle {
Self {
paragraph_gap: rems(1.),
heading_base_font_size: px(14.),
- highlight_theme: Rc::new(HighlightTheme::default_light()),
+ highlight_theme: Rc::new(HighlightTheme::default_light().clone()),
}
}
}