diff --git a/apps/gpui/test/gpui/template/components_test.exs b/apps/gpui/test/gpui/template/components_test.exs
index fcedeae7..b9291dc0 100644
--- a/apps/gpui/test/gpui/template/components_test.exs
+++ b/apps/gpui/test/gpui/template/components_test.exs
@@ -139,6 +139,41 @@ defmodule GPUI.Template.ComponentsTest do
end
end
+ test "search-first dialogs hide their heading and retain a controlled top offset" do
+ dialog =
+ ~GPUI"""
+
+ <:content>Search results
+
+ """
+
+ refute dialog.attrs[:show_title]
+ assert dialog.attrs[:title] == "Search"
+ assert dialog.attrs[:margin_top] == 112
+
+ default_dialog =
+ ~GPUI"""
+
+ <:content>Settings content
+
+ """
+
+ assert default_dialog.attrs[:show_title]
+ assert default_dialog.attrs[:margin_top] == nil
+
+ for margin <- [-1, 4097, "80", false] do
+ assigns = %{margin: margin}
+
+ assert_raise ArgumentError, ~r/margin_top/, fn ->
+ ~GPUI"""
+
+ <:content>Search results
+
+ """
+ end
+ end
+ end
+
test "dropdown menus compile repeated textual item slots" do
assert %GPUI.Element{
type: :ui_dropdown_menu,
diff --git a/apps/gpui_components/lib/gpui/components/schema_declarations.ex b/apps/gpui_components/lib/gpui/components/schema_declarations.ex
index 69db4a4c..a125ea57 100644
--- a/apps/gpui_components/lib/gpui/components/schema_declarations.ex
+++ b/apps/gpui_components/lib/gpui/components/schema_declarations.ex
@@ -207,7 +207,9 @@ defmodule GPUI.Components.Schema.Declarations do
id: :string,
open: :boolean,
title: :required_string,
+ show_title: {:default, :boolean, true},
width: {:default, :number, 448.0},
+ margin_top: :number,
overlay: {:default, :boolean, true},
closable: {:default, :boolean, true},
keyboard: {:default, :boolean, true},
diff --git a/apps/gpui_components/lib/gpui/ui/overlay.ex b/apps/gpui_components/lib/gpui/ui/overlay.ex
index c9fcca5e..e60460d0 100644
--- a/apps/gpui_components/lib/gpui/ui/overlay.ex
+++ b/apps/gpui_components/lib/gpui/ui/overlay.ex
@@ -81,6 +81,11 @@ defmodule GPUI.UI.Overlay do
focus while open and restores the previous focus when it closes. Escape and
overlay clicks request closure when enabled.
+ Set `show_title: false` for search-first dialogs without a visible heading.
+ `title` still names the dialog contract and optional trigger. `margin_top`
+ controls the dialog's distance from the top of the window; omitting it retains
+ the native viewport-relative placement.
+
#{ComponentsSchema.component_options_doc(:ui_dialog)}
"""
@spec dialog(dialog_options()) :: Element.t()
@@ -266,16 +271,24 @@ defmodule GPUI.UI.Overlay do
raise ArgumentError, "dialog content must use :trigger and :content named slots"
end
- boolean_attrs = Enum.map([:open, :overlay, :closable, :keyboard, :close_button], &assigns[&1])
+ boolean_attrs =
+ Enum.map([:open, :show_title, :overlay, :closable, :keyboard, :close_button], &assigns[&1])
unless Enum.all?(boolean_attrs, &is_boolean/1) do
raise ArgumentError,
- "dialog open, overlay, closable, keyboard, and close_button must be booleans"
+ "dialog open, show_title, overlay, closable, keyboard, and close_button must be booleans"
end
unless is_number(assigns.width) and assigns.width > 0 and assigns.width <= 4096 do
raise ArgumentError, "dialog width must be greater than 0 and at most 4096"
end
+
+ margin_top = assigns[:margin_top]
+
+ unless is_nil(margin_top) or
+ (is_number(margin_top) and margin_top >= 0 and margin_top <= 4096) do
+ raise ArgumentError, "dialog margin_top must be between 0 and 4096"
+ end
end
defp tooltip_text!(children), do: textual_slot!(children, "tooltip :content")
diff --git a/apps/gpui_components/native/src/generated/schema.rs b/apps/gpui_components/native/src/generated/schema.rs
index bea7448a..70271641 100644
--- a/apps/gpui_components/native/src/generated/schema.rs
+++ b/apps/gpui_components/native/src/generated/schema.rs
@@ -2576,7 +2576,9 @@ pub(crate) struct DialogComponentNode {
pub(crate) id: String,
pub(crate) open: bool,
pub(crate) title: String,
+ pub(crate) show_title: bool,
pub(crate) width: f64,
+ pub(crate) margin_top: Option,
pub(crate) overlay: bool,
pub(crate) closable: bool,
pub(crate) keyboard: bool,
@@ -2595,7 +2597,9 @@ pub(crate) fn decode_generated_dialog_component<'a>(
id: component_id(term)?,
open: component_bool_attr(term, atoms::open())?.unwrap_or(false),
title: component_required_string_attr(term, atoms::title())?,
+ show_title: component_bool_attr(term, atoms::show_title())?.unwrap_or(true),
width: component_number_attr(term, atoms::width())?.unwrap_or(448.0),
+ margin_top: component_number_attr(term, atoms::margin_top())?,
overlay: component_bool_attr(term, atoms::overlay())?.unwrap_or(true),
closable: component_bool_attr(term, atoms::closable())?.unwrap_or(true),
keyboard: component_bool_attr(term, atoms::keyboard())?.unwrap_or(true),
diff --git a/apps/gpui_native/native/src/element/component/overlay/dialog.rs b/apps/gpui_native/native/src/element/component/overlay/dialog.rs
index e7a137a9..c5813d55 100644
--- a/apps/gpui_native/native/src/element/component/overlay/dialog.rs
+++ b/apps/gpui_native/native/src/element/component/overlay/dialog.rs
@@ -18,6 +18,7 @@ pub(crate) fn render_dialog(
ComponentDialog, ComponentOverlayState, DialogConfig,
};
use crate::element::controlled::ControlledBinding;
+ use gpui::prelude::FluentBuilder as _;
use gpui::{
AppContext, InteractiveElement, IntoElement, MouseButton, ParentElement, Role,
StatefulInteractiveElement,
@@ -44,7 +45,11 @@ pub(crate) fn render_dialog(
let content_focus = context.cx.focus_handle();
let config = Arc::new(Mutex::new(DialogConfig {
title: node.title.clone(),
+ show_title: node.show_title,
width: node.width.clamp(1.0, 4096.0) as f32,
+ margin_top: node
+ .margin_top
+ .map(|margin| margin.clamp(0.0, 4096.0) as f32),
overlay: node.overlay,
closable: node.closable,
keyboard: node.keyboard,
@@ -137,7 +142,11 @@ pub(crate) fn render_dialog(
if let Ok(mut config) = dialog.config.lock() {
*config = DialogConfig {
title: node.title.clone(),
+ show_title: node.show_title,
width: node.width.clamp(1.0, 4096.0) as f32,
+ margin_top: node
+ .margin_top
+ .map(|margin| margin.clamp(0.0, 4096.0) as f32),
overlay: node.overlay,
closable: node.closable,
keyboard: node.keyboard,
@@ -201,7 +210,9 @@ pub(crate) fn render_dialog(
.map(|config| config.clone())
.unwrap_or(DialogConfig {
title: "Dialog".to_string(),
+ show_title: true,
width: 448.0,
+ margin_top: None,
overlay: true,
closable: true,
keyboard: true,
@@ -210,6 +221,9 @@ pub(crate) fn render_dialog(
});
let dialog = super::super::apply_component_styles(dialog, config.style)
.width(gpui::px(config.width))
+ .when_some(config.margin_top, |dialog, margin| {
+ dialog.margin_top(gpui::px(margin))
+ })
.overlay(config.overlay)
.overlay_closable(config.closable)
.keyboard(config.keyboard && config.closable)
@@ -242,7 +256,11 @@ pub(crate) fn render_dialog(
}
})
.child(content_view.clone());
- dialog.title(config.title)
+ if config.show_title {
+ dialog.title(config.title)
+ } else {
+ dialog
+ }
});
window.defer(cx, move |window, cx| content_focus.focus(window, cx));
});
diff --git a/apps/gpui_native/native/src/element/component_registry.rs b/apps/gpui_native/native/src/element/component_registry.rs
index 6f1109f8..b5b677a0 100644
--- a/apps/gpui_native/native/src/element/component_registry.rs
+++ b/apps/gpui_native/native/src/element/component_registry.rs
@@ -182,7 +182,9 @@ pub(crate) struct ComponentCombobox {
#[derive(Clone)]
pub(crate) struct DialogConfig {
pub(crate) title: String,
+ pub(crate) show_title: bool,
pub(crate) width: f32,
+ pub(crate) margin_top: Option,
pub(crate) overlay: bool,
pub(crate) closable: bool,
pub(crate) keyboard: bool,
diff --git a/apps/gpui_native/native/src/generated/atoms.rs b/apps/gpui_native/native/src/generated/atoms.rs
index 566aca13..833b00fb 100644
--- a/apps/gpui_native/native/src/generated/atoms.rs
+++ b/apps/gpui_native/native/src/generated/atoms.rs
@@ -87,16 +87,16 @@ mod atoms {
search_placeholder = "search_placeholder", select = "select", selectable =
"selectable", selected = "selected", selected_column = "selected_column",
selected_index = "selected_index", selection = "selection", set_size =
- "set_size", show_line_numbers = "show_line_numbers", show_whitespaces =
- "show_whitespaces", side = "side", size = "size", sizes = "sizes", soft_wrap =
- "soft_wrap", sort = "sort", sort_column = "sort_column", sort_direction =
- "sort_direction", sortable = "sortable", stale_revision = "stale_revision",
- status = "status", step = "step", stride = "stride", style = "style", style_runs
- = "style_runs", submit = "submit", submit_policy = "submit_policy", tab_size =
- "tab_size", tab_width = "tab_width", text = "text", text_align = "text_align",
- text_buffer_lock_failed = "text_buffer_lock_failed", text_overflow =
- "text_overflow", title = "title", toggle = "toggle", top = "top", total_count =
- "total_count", transaction = "transaction", transaction_conflict =
+ "set_size", show_line_numbers = "show_line_numbers", show_title = "show_title",
+ show_whitespaces = "show_whitespaces", side = "side", size = "size", sizes =
+ "sizes", soft_wrap = "soft_wrap", sort = "sort", sort_column = "sort_column",
+ sort_direction = "sort_direction", sortable = "sortable", stale_revision =
+ "stale_revision", status = "status", step = "step", stride = "stride", style =
+ "style", style_runs = "style_runs", submit = "submit", submit_policy =
+ "submit_policy", tab_size = "tab_size", tab_width = "tab_width", text = "text",
+ text_align = "text_align", text_buffer_lock_failed = "text_buffer_lock_failed",
+ text_overflow = "text_overflow", title = "title", toggle = "toggle", top = "top",
+ total_count = "total_count", transaction = "transaction", transaction_conflict =
"transaction_conflict", truncate = "truncate", type_atom = "type", value =
"value", vanilla = "vanilla", variant = "variant", viewport = "viewport",
white_space = "white_space", width = "width", window_blur = "window_blur",
diff --git a/apps/gpui_native/native/src/generated/schema.rs b/apps/gpui_native/native/src/generated/schema.rs
index bea7448a..70271641 100644
--- a/apps/gpui_native/native/src/generated/schema.rs
+++ b/apps/gpui_native/native/src/generated/schema.rs
@@ -2576,7 +2576,9 @@ pub(crate) struct DialogComponentNode {
pub(crate) id: String,
pub(crate) open: bool,
pub(crate) title: String,
+ pub(crate) show_title: bool,
pub(crate) width: f64,
+ pub(crate) margin_top: Option,
pub(crate) overlay: bool,
pub(crate) closable: bool,
pub(crate) keyboard: bool,
@@ -2595,7 +2597,9 @@ pub(crate) fn decode_generated_dialog_component<'a>(
id: component_id(term)?,
open: component_bool_attr(term, atoms::open())?.unwrap_or(false),
title: component_required_string_attr(term, atoms::title())?,
+ show_title: component_bool_attr(term, atoms::show_title())?.unwrap_or(true),
width: component_number_attr(term, atoms::width())?.unwrap_or(448.0),
+ margin_top: component_number_attr(term, atoms::margin_top())?,
overlay: component_bool_attr(term, atoms::overlay())?.unwrap_or(true),
closable: component_bool_attr(term, atoms::closable())?.unwrap_or(true),
keyboard: component_bool_attr(term, atoms::keyboard())?.unwrap_or(true),
diff --git a/apps/gpui_native/test/test/gpui/test/native/navigation_test.exs b/apps/gpui_native/test/test/gpui/test/native/navigation_test.exs
index 4d2260ba..43d9f41c 100644
--- a/apps/gpui_native/test/test/gpui/test/native/navigation_test.exs
+++ b/apps/gpui_native/test/test/gpui/test/native/navigation_test.exs
@@ -31,7 +31,9 @@ defmodule GPUI.Test.Native.NavigationTest do
id="settings-dialog"
open={assigns.open}
title="Settings"
+ show_title={Map.get(assigns, :show_title, true)}
width={280}
+ margin_top={Map.get(assigns, :margin_top)}
phx-change="dialog_changed"
>
<:trigger>
@@ -115,6 +117,23 @@ defmodule GPUI.Test.Native.NavigationTest do
assert_receive {:gpui, ^ui, {:event, %{type: :click, event: "dialog_action"}}}
end
+ test "search-first dialogs omit the heading and retain content activation and Escape", %{ui: ui} do
+ render(ui, DialogView, open: true, margin_top: 24)
+ settle(ui)
+ titled_y = bounds(ui, "dialog-action").y
+
+ render(ui, DialogView, open: false, margin_top: 24)
+ settle(ui)
+ render(ui, DialogView, open: true, show_title: false, margin_top: 24)
+ settle(ui)
+ assert bounds(ui, "dialog-action").y < titled_y
+
+ click(ui, "dialog-action")
+ assert_receive {:gpui, ^ui, {:event, %{type: :click, event: "dialog_action"}}}
+ press(ui, :escape)
+ assert_receive {:gpui, ^ui, {:event, %{type: :change, event: "dialog_changed", value: false}}}
+ end
+
test "dialog routes Escape closure and restores its trigger focus", %{ui: ui} do
render(ui, DialogView, open: false)
focus(ui, "settings-dialog")
diff --git a/codegen/gpui/codegen/native/component/macros.ex b/codegen/gpui/codegen/native/component/macros.ex
index e5f5eea4..598deb4d 100644
--- a/codegen/gpui/codegen/native/component/macros.ex
+++ b/codegen/gpui/codegen/native/component/macros.ex
@@ -93,6 +93,7 @@ defmodule GPUI.Codegen.Native.Component.Macros do
when type in [:number, :positive_number, :unit_number, :edge_fade_size],
do: quote(do: R.f64())
+ defp component_field_type(_name, :number), do: quote(do: R.option(R.f64()))
defp component_field_type(_name, :non_negative_integer), do: quote(do: R.option(R.u64()))
defp component_field_type(_name, {:default, type, _value})
@@ -160,6 +161,9 @@ defmodule GPUI.Codegen.Native.Component.Macros do
defp component_decoder_expr(name, :non_negative_integer),
do: quote(do: unwrap!(component_non_negative_integer_attr(term, unquote(atom_call(name)))))
+ defp component_decoder_expr(name, :number),
+ do: quote(do: unwrap!(component_number_attr(term, unquote(atom_call(name)))))
+
defp component_decoder_expr(name, {:default, type, default})
when type in [:non_negative_integer, :positive_integer] do
helper =