Compare commits
16 Commits
fix-git-ht
...
alert-dial
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5caf6eb041 | ||
|
|
879f850f4f | ||
|
|
e6e72f9f2d | ||
|
|
30d91429e7 | ||
|
|
e255389cbe | ||
|
|
eeba056b84 | ||
|
|
f2338025b7 | ||
|
|
e29f4e6632 | ||
|
|
dd6f0dfc04 | ||
|
|
a84eb44564 | ||
|
|
bd1e57bd4a | ||
|
|
cd811a2014 | ||
|
|
74a5f6a464 | ||
|
|
c916f5d476 | ||
|
|
43ddcdef84 | ||
|
|
be6d13b6d8 |
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -108,6 +108,19 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alert_dialog"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
"menu",
|
||||||
|
"settings",
|
||||||
|
"story",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aliasable"
|
name = "aliasable"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -10864,6 +10877,7 @@ dependencies = [
|
|||||||
name = "storybook"
|
name = "storybook"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"alert_dialog",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"collab_ui",
|
"collab_ui",
|
||||||
@@ -11728,6 +11742,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
name = "title_bar"
|
name = "title_bar"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"alert_dialog",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"call",
|
"call",
|
||||||
"client",
|
"client",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ members = [
|
|||||||
"crates/time_format",
|
"crates/time_format",
|
||||||
"crates/title_bar",
|
"crates/title_bar",
|
||||||
"crates/ui",
|
"crates/ui",
|
||||||
|
"crates/alert_dialog",
|
||||||
"crates/ui_input",
|
"crates/ui_input",
|
||||||
"crates/ui_macros",
|
"crates/ui_macros",
|
||||||
"crates/util",
|
"crates/util",
|
||||||
@@ -298,6 +299,7 @@ theme_selector = { path = "crates/theme_selector" }
|
|||||||
time_format = { path = "crates/time_format" }
|
time_format = { path = "crates/time_format" }
|
||||||
title_bar = { path = "crates/title_bar" }
|
title_bar = { path = "crates/title_bar" }
|
||||||
ui = { path = "crates/ui" }
|
ui = { path = "crates/ui" }
|
||||||
|
alert_dialog = { path = "crates/alert_dialog" }
|
||||||
ui_input = { path = "crates/ui_input" }
|
ui_input = { path = "crates/ui_input" }
|
||||||
ui_macros = { path = "crates/ui_macros" }
|
ui_macros = { path = "crates/ui_macros" }
|
||||||
util = { path = "crates/util" }
|
util = { path = "crates/util" }
|
||||||
|
|||||||
25
crates/alert_dialog/Cargo.toml
Normal file
25
crates/alert_dialog/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "alert_dialog"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/alert_dialog.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
|
menu.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
story = { workspace = true, optional = true }
|
||||||
|
theme.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
stories = ["dep:story"]
|
||||||
420
crates/alert_dialog/src/alert_dialog.rs
Normal file
420
crates/alert_dialog/src/alert_dialog.rs
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
#![deny(missing_docs)]
|
||||||
|
//! Provides the Alert Dialog UI component – A modal dialog that interrupts the user's workflow to convey critical information.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
Action, AppContext, ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, View,
|
||||||
|
WeakView,
|
||||||
|
};
|
||||||
|
use ui::{
|
||||||
|
div, px, v_flex, vh, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, ElementId,
|
||||||
|
ElevationIndex, FluentBuilder, Headline, HeadlineSize, InteractiveElement, IntoElement,
|
||||||
|
ParentElement, Render, RenderOnce, SharedString, Spacing, Styled, StyledTypography,
|
||||||
|
ViewContext, VisualContext, WindowContext,
|
||||||
|
};
|
||||||
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
|
#[derive(Clone, IntoElement)]
|
||||||
|
struct AlertDialogButton {
|
||||||
|
id: ElementId,
|
||||||
|
label: SharedString,
|
||||||
|
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlertDialogButton {
|
||||||
|
fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
label: label.into(),
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||||
|
self.on_click = Some(Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for AlertDialogButton {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
Button::new(self.id, self.label)
|
||||||
|
.size(ButtonSize::Large)
|
||||||
|
.layer(ElevationIndex::ModalSurface)
|
||||||
|
.when_some(self.on_click, |this, on_click| {
|
||||||
|
this.on_click(move |event, cx| {
|
||||||
|
on_click(event, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_DIALOG_WIDTH: f32 = 440.0;
|
||||||
|
const MIN_DIALOG_WIDTH: f32 = 260.0;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum AlertDialogLayout {
|
||||||
|
/// For dialogs short titles and action names.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// Title: "Discard changes?"
|
||||||
|
///
|
||||||
|
/// Actions: "Cancel" | "Discard"
|
||||||
|
#[default]
|
||||||
|
Vertical,
|
||||||
|
/// For dialogs with long titles or action names,
|
||||||
|
/// or large amounts of content.
|
||||||
|
///
|
||||||
|
/// As titles, action names or content get longer, the dialog
|
||||||
|
/// automatically switches to this layout
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An alert dialog that interrupts the user's workflow to convey critical information.
|
||||||
|
///
|
||||||
|
/// Use this component when immediate user attention or action is required.
|
||||||
|
///
|
||||||
|
/// It blocks all other interactions until the user responds, making it suitable
|
||||||
|
/// for important confirmations or critical error messages.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AlertDialog {
|
||||||
|
/// The title of the alert dialog
|
||||||
|
pub title: SharedString,
|
||||||
|
|
||||||
|
/// The main message or content of the alert
|
||||||
|
pub message: Option<SharedString>,
|
||||||
|
|
||||||
|
/// The primary action the user can take
|
||||||
|
primary_action: AlertDialogButton,
|
||||||
|
|
||||||
|
/// The secondary action the user can take
|
||||||
|
secondary_action: AlertDialogButton,
|
||||||
|
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for AlertDialog {}
|
||||||
|
impl FocusableView for AlertDialog {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ModalView for AlertDialog {
|
||||||
|
fn fade_out_background(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AlertDialog {
|
||||||
|
/// Create a new alert dialog
|
||||||
|
pub fn new(
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
|
||||||
|
) -> View<Self> {
|
||||||
|
cx.new_view(|cx| {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
f(
|
||||||
|
Self {
|
||||||
|
title: "Untitled Alert".into(),
|
||||||
|
message: None,
|
||||||
|
primary_action: AlertDialogButton::new("primary-action", "OK"),
|
||||||
|
secondary_action: AlertDialogButton::new("secondary-action", "Cancel"),
|
||||||
|
focus_handle,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the title of the alert dialog
|
||||||
|
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||||
|
self.title = title.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the main message or content of the alert dialog
|
||||||
|
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||||
|
self.message = Some(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the primary action the user can take
|
||||||
|
pub fn primary_action(
|
||||||
|
mut self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.primary_action = AlertDialogButton::new("primary-action", label).on_click(handler);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the secondary action the user can take
|
||||||
|
pub fn secondary_action(
|
||||||
|
mut self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.secondary_action = AlertDialogButton::new("secondary-action", label).on_click(handler);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the secondary action to a dismiss action with a custom label
|
||||||
|
///
|
||||||
|
/// Example: "Close", "Dismiss", "No"
|
||||||
|
pub fn secondary_dismiss_action(mut self, label: impl Into<SharedString>) -> Self {
|
||||||
|
self.secondary_action = AlertDialogButton::new("secondary-action", label)
|
||||||
|
.on_click(|_, cx| cx.dispatch_action(menu::Cancel.boxed_clone()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog_layout(&self) -> AlertDialogLayout {
|
||||||
|
let title_len = self.title.len();
|
||||||
|
let primary_action_len = self.primary_action.label.len();
|
||||||
|
let secondary_action_len = self.secondary_action.label.len();
|
||||||
|
let message_len = self.message.as_ref().map_or(0, |m| m.len());
|
||||||
|
|
||||||
|
if title_len > 35
|
||||||
|
|| primary_action_len > 14
|
||||||
|
|| secondary_action_len > 14
|
||||||
|
|| message_len > 80
|
||||||
|
{
|
||||||
|
AlertDialogLayout::Horizontal
|
||||||
|
} else {
|
||||||
|
AlertDialogLayout::Vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the alert dialog in a new modal
|
||||||
|
pub fn show(&self, workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) {
|
||||||
|
let this = self.clone();
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.toggle_modal(cx, |_cx| this);
|
||||||
|
cx.focus(&focus_handle);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(DismissEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AlertDialog {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let layout = self.dialog_layout();
|
||||||
|
let spacing = if layout == AlertDialogLayout::Horizontal {
|
||||||
|
Spacing::Large4X.rems(cx)
|
||||||
|
} else {
|
||||||
|
Spacing::XLarge.rems(cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.key_context("Alert")
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.occlude()
|
||||||
|
.min_w(px(MIN_DIALOG_WIDTH))
|
||||||
|
.max_w(if layout == AlertDialogLayout::Horizontal {
|
||||||
|
px(MAX_DIALOG_WIDTH)
|
||||||
|
} else {
|
||||||
|
px(MIN_DIALOG_WIDTH)
|
||||||
|
})
|
||||||
|
.max_h(vh(0.75, cx))
|
||||||
|
.flex_none()
|
||||||
|
.overflow_hidden()
|
||||||
|
.p(spacing)
|
||||||
|
.gap(spacing)
|
||||||
|
.rounded_lg()
|
||||||
|
.font_ui(cx)
|
||||||
|
.bg(ElevationIndex::ModalSurface.bg(cx))
|
||||||
|
.shadow(ElevationIndex::ModalSurface.shadow())
|
||||||
|
// Title and message
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
// This is a flex hack. Layout breaks without it ¯\_(ツ)_/¯
|
||||||
|
.min_h(px(1.))
|
||||||
|
.max_w_full()
|
||||||
|
.flex_grow()
|
||||||
|
.when(layout == AlertDialogLayout::Vertical, |this| {
|
||||||
|
// If we had `.text_center()` we would use it here instead of centering the content
|
||||||
|
// since this approach will only work as long as the content is a single line
|
||||||
|
this.justify_center().mx_auto()
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
// Same as above, if `.text_center()` is supported in the future, use here.
|
||||||
|
.when(layout == AlertDialogLayout::Vertical, |this| this.mx_auto())
|
||||||
|
.child(Headline::new(self.title.clone()).size(HeadlineSize::Small)),
|
||||||
|
)
|
||||||
|
.when_some(self.message.clone(), |this, message| {
|
||||||
|
// TODO: When content will be long (example: a document, log or stack trace)
|
||||||
|
// we should render some sort of styled container, as well as allow the content to scroll
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
// Same as above, if `.text_center()` is supported in the future, use here.
|
||||||
|
.when(layout == AlertDialogLayout::Vertical, |this| this.mx_auto())
|
||||||
|
.text_color(cx.theme().colors().text_muted)
|
||||||
|
.text_ui(cx)
|
||||||
|
.child(message.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// Actions & checkbox
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
// Force buttons to stack for Horizontal layout
|
||||||
|
.when(layout == AlertDialogLayout::Vertical, |this| {
|
||||||
|
this.flex_col()
|
||||||
|
})
|
||||||
|
.when(layout == AlertDialogLayout::Horizontal, |this| {
|
||||||
|
this.justify_between()
|
||||||
|
.h(ButtonSize::Large.rems())
|
||||||
|
.gap(Spacing::Medium.rems(cx))
|
||||||
|
})
|
||||||
|
.child(div().flex_shrink_0())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.gap(Spacing::Medium.rems(cx))
|
||||||
|
.when(layout == AlertDialogLayout::Vertical, |this| {
|
||||||
|
this.flex_col_reverse().w_full()
|
||||||
|
})
|
||||||
|
.when(layout == AlertDialogLayout::Horizontal, |this| {
|
||||||
|
this.items_center()
|
||||||
|
})
|
||||||
|
.child(self.secondary_action.clone())
|
||||||
|
.child(self.primary_action.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example stories for [AlertDialog]
|
||||||
|
///
|
||||||
|
/// Run with `script/storybook alert_dialog`
|
||||||
|
#[cfg(feature = "stories")]
|
||||||
|
pub mod alert_dialog_stories {
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
use gpui::{Render, View};
|
||||||
|
use story::{Story, StorySection};
|
||||||
|
use ui::{prelude::*, ElevationIndex};
|
||||||
|
|
||||||
|
use super::AlertDialog;
|
||||||
|
|
||||||
|
pub struct AlertDialogStory {
|
||||||
|
vertical_alert_dialog: View<AlertDialog>,
|
||||||
|
horizontal_alert_dialog: View<AlertDialog>,
|
||||||
|
long_content_alert_dialog: View<AlertDialog>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlertDialogStory {
|
||||||
|
pub fn new(cx: &mut WindowContext) -> Self {
|
||||||
|
let vertical_alert_dialog = AlertDialog::new(cx, |dialog, _cx| {
|
||||||
|
dialog
|
||||||
|
.title("Discard changes?")
|
||||||
|
.message("Something bad could happen...")
|
||||||
|
.primary_action("Discard", |_, _| {
|
||||||
|
println!("Discarded!");
|
||||||
|
})
|
||||||
|
.secondary_action("Cancel", |_, _| {
|
||||||
|
println!("Cancelled!");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let horizontal_alert_dialog = AlertDialog::new(cx, |dialog, _cx| {
|
||||||
|
dialog
|
||||||
|
.title("Do you want to leave the current call?")
|
||||||
|
.message("The current window will be closed, and connections to any shared projects will be terminated.")
|
||||||
|
.primary_action("Leave Call", |_, _| {})
|
||||||
|
.secondary_action("Cancel", |_, _| {})
|
||||||
|
});
|
||||||
|
|
||||||
|
let long_content = r#"{
|
||||||
|
"error": "RuntimeError",
|
||||||
|
"message": "An unexpected error occurred during execution",
|
||||||
|
"stackTrace": [
|
||||||
|
{
|
||||||
|
"fileName": "main.rs",
|
||||||
|
"lineNumber": 42,
|
||||||
|
"functionName": "process_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "utils.rs",
|
||||||
|
"lineNumber": 23,
|
||||||
|
"functionName": "validate_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "core.rs",
|
||||||
|
"lineNumber": 105,
|
||||||
|
"functionName": "execute_operation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let long_content_alert_dialog = AlertDialog::new(cx, |dialog, _cx| {
|
||||||
|
dialog
|
||||||
|
.title("A RuntimeError occurred")
|
||||||
|
.message(long_content)
|
||||||
|
.primary_action("Send Report", |_, _| {})
|
||||||
|
.secondary_action("Close", |_, _| {})
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
vertical_alert_dialog,
|
||||||
|
horizontal_alert_dialog,
|
||||||
|
long_content_alert_dialog,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AlertDialogStory {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Story::container().child(
|
||||||
|
StorySection::new()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.w(px(780.))
|
||||||
|
.h(px(380.))
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(ElevationIndex::Background.bg(cx))
|
||||||
|
.child(self.vertical_alert_dialog.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.w(px(580.))
|
||||||
|
.h(px(420.))
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(ElevationIndex::Background.bg(cx))
|
||||||
|
.child(self.horizontal_alert_dialog.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.w(px(580.))
|
||||||
|
.h(px(780.))
|
||||||
|
.overflow_hidden()
|
||||||
|
.bg(ElevationIndex::Background.bg(cx))
|
||||||
|
.child(self.long_content_alert_dialog.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ strum = { workspace = true, features = ["derive"] }
|
|||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
title_bar = { workspace = true, features = ["stories"] }
|
title_bar = { workspace = true, features = ["stories"] }
|
||||||
ui = { workspace = true, features = ["stories"] }
|
ui = { workspace = true, features = ["stories"] }
|
||||||
|
alert_dialog = { workspace = true, features = ["stories"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use ui::prelude::*;
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum ComponentStory {
|
pub enum ComponentStory {
|
||||||
|
AlertDialog,
|
||||||
ApplicationMenu,
|
ApplicationMenu,
|
||||||
AutoHeightEditor,
|
AutoHeightEditor,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -46,6 +47,9 @@ pub enum ComponentStory {
|
|||||||
impl ComponentStory {
|
impl ComponentStory {
|
||||||
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
|
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
|
||||||
match self {
|
match self {
|
||||||
|
Self::AlertDialog => cx
|
||||||
|
.new_view(|cx| alert_dialog::alert_dialog_stories::AlertDialogStory::new(cx))
|
||||||
|
.into(),
|
||||||
Self::ApplicationMenu => cx
|
Self::ApplicationMenu => cx
|
||||||
.new_view(|cx| title_bar::ApplicationMenuStory::new(cx))
|
.new_view(|cx| title_bar::ApplicationMenuStory::new(cx))
|
||||||
.into(),
|
.into(),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ test-support = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
alert_dialog.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
call.workspace = true
|
call.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod stories;
|
|||||||
|
|
||||||
use crate::application_menu::ApplicationMenu;
|
use crate::application_menu::ApplicationMenu;
|
||||||
use crate::platforms::{platform_linux, platform_mac, platform_windows};
|
use crate::platforms::{platform_linux, platform_mac, platform_windows};
|
||||||
|
use alert_dialog::AlertDialog;
|
||||||
use auto_update::AutoUpdateStatus;
|
use auto_update::AutoUpdateStatus;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
@@ -67,6 +68,7 @@ pub struct TitleBar {
|
|||||||
should_move: bool,
|
should_move: bool,
|
||||||
application_menu: Option<View<ApplicationMenu>>,
|
application_menu: Option<View<ApplicationMenu>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
test_dialog: View<AlertDialog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TitleBar {
|
impl Render for TitleBar {
|
||||||
@@ -84,6 +86,8 @@ impl Render for TitleBar {
|
|||||||
} else {
|
} else {
|
||||||
cx.theme().colors().title_bar_background
|
cx.theme().colors().title_bar_background
|
||||||
};
|
};
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let test_dialog = self.test_dialog.clone();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("titlebar")
|
.id("titlebar")
|
||||||
@@ -139,6 +143,13 @@ impl Render for TitleBar {
|
|||||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
|
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
|
||||||
)
|
)
|
||||||
.child(self.render_collaborator_list(cx))
|
.child(self.render_collaborator_list(cx))
|
||||||
|
.child(
|
||||||
|
Button::new("test-dialog", "Test Dialog").on_click(move |_, cx| {
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
|
||||||
|
test_dialog.update(cx, move |dialog, cx| dialog.show(workspace, cx));
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -220,6 +231,14 @@ impl TitleBar {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let test_dialog = AlertDialog::new(cx, |dialog, _| {
|
||||||
|
dialog
|
||||||
|
.title("Do you want to leave the current call?")
|
||||||
|
.message("The current window will be closed, and connections to any shared projects will be terminated.")
|
||||||
|
.primary_action("Leave Call", |_, _| {})
|
||||||
|
.secondary_dismiss_action("Cancel")
|
||||||
|
});
|
||||||
|
|
||||||
let mut subscriptions = Vec::new();
|
let mut subscriptions = Vec::new();
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
|
cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
|
||||||
@@ -242,6 +261,7 @@ impl TitleBar {
|
|||||||
user_store,
|
user_store,
|
||||||
client,
|
client,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
|
test_dialog,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use gpui::{hsla, point, px, BoxShadow};
|
use gpui::{hsla, point, px, BoxShadow, Hsla, WindowContext};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use theme::{color_alpha, ActiveTheme};
|
||||||
|
|
||||||
/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
|
/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
|
||||||
///
|
///
|
||||||
@@ -62,4 +63,18 @@ impl ElevationIndex {
|
|||||||
_ => smallvec![],
|
_ => smallvec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an appropriate background color for the given elevation index.
|
||||||
|
pub fn bg(self, cx: &WindowContext) -> Hsla {
|
||||||
|
match self {
|
||||||
|
ElevationIndex::Background => cx.theme().colors().background,
|
||||||
|
ElevationIndex::Surface => cx.theme().colors().surface_background,
|
||||||
|
ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background,
|
||||||
|
ElevationIndex::Wash => hsla(0., 0., 0., 0.3),
|
||||||
|
ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background,
|
||||||
|
ElevationIndex::DraggedElement => {
|
||||||
|
color_alpha(cx.theme().colors().elevated_surface_background, 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ pub enum Spacing {
|
|||||||
///
|
///
|
||||||
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
|
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
|
||||||
XXLarge,
|
XXLarge,
|
||||||
|
/// 3X Large spacing - @16px/rem: `16px`|`20px`|`24px`
|
||||||
|
///
|
||||||
|
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
|
||||||
|
Large3X,
|
||||||
|
/// 4X Large spacing - @16px/rem: `20px`|`24px`|`28px`
|
||||||
|
///
|
||||||
|
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
|
||||||
|
Large4X,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spacing {
|
impl Spacing {
|
||||||
@@ -55,6 +63,8 @@ impl Spacing {
|
|||||||
Spacing::Large => 4. / BASE_REM_SIZE_IN_PX,
|
Spacing::Large => 4. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XLarge => 8. / BASE_REM_SIZE_IN_PX,
|
Spacing::XLarge => 8. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XXLarge => 12. / BASE_REM_SIZE_IN_PX,
|
Spacing::XXLarge => 12. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large3X => 16. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large4X => 20. / BASE_REM_SIZE_IN_PX,
|
||||||
},
|
},
|
||||||
UiDensity::Default => match self {
|
UiDensity::Default => match self {
|
||||||
Spacing::None => 0.,
|
Spacing::None => 0.,
|
||||||
@@ -65,6 +75,8 @@ impl Spacing {
|
|||||||
Spacing::Large => 8. / BASE_REM_SIZE_IN_PX,
|
Spacing::Large => 8. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XLarge => 12. / BASE_REM_SIZE_IN_PX,
|
Spacing::XLarge => 12. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XXLarge => 16. / BASE_REM_SIZE_IN_PX,
|
Spacing::XXLarge => 16. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large3X => 20. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large4X => 24. / BASE_REM_SIZE_IN_PX,
|
||||||
},
|
},
|
||||||
UiDensity::Comfortable => match self {
|
UiDensity::Comfortable => match self {
|
||||||
Spacing::None => 0.,
|
Spacing::None => 0.,
|
||||||
@@ -75,6 +87,8 @@ impl Spacing {
|
|||||||
Spacing::Large => 10. / BASE_REM_SIZE_IN_PX,
|
Spacing::Large => 10. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XLarge => 16. / BASE_REM_SIZE_IN_PX,
|
Spacing::XLarge => 16. / BASE_REM_SIZE_IN_PX,
|
||||||
Spacing::XXLarge => 20. / BASE_REM_SIZE_IN_PX,
|
Spacing::XXLarge => 20. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large3X => 24. / BASE_REM_SIZE_IN_PX,
|
||||||
|
Spacing::Large4X => 28. / BASE_REM_SIZE_IN_PX,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ impl HeadlineSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Just added these to get stared with headlines but these
|
||||||
|
// but we should have a per-headline size line height.
|
||||||
/// Returns the line height for the headline size.
|
/// Returns the line height for the headline size.
|
||||||
pub fn line_height(self) -> Rems {
|
pub fn line_height(self) -> Rems {
|
||||||
match self {
|
match self {
|
||||||
@@ -200,6 +202,7 @@ impl RenderOnce for Headline {
|
|||||||
let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
|
let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
.flex_none()
|
||||||
.font(ui_font)
|
.font(ui_font)
|
||||||
.line_height(self.size.line_height())
|
.line_height(self.size.line_height())
|
||||||
.text_size(self.size.rems())
|
.text_size(self.size.rems())
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
/// A trait for elements that can be selected.
|
/// A trait for elements that can be selected.
|
||||||
///
|
///
|
||||||
/// Generally used to enable "toggle" or "active" behavior and styles on an element through the [`Selection`] status.
|
/// Generally used to enable "toggle" or "active" behavior and styles on an element through the [`Selection`] status.
|
||||||
@@ -30,6 +32,16 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Selection {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Unselected => write!(f, "Unselected"),
|
||||||
|
Self::Indeterminate => write!(f, "Indeterminate"),
|
||||||
|
Self::Selected => write!(f, "Selected"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<bool> for Selection {
|
impl From<bool> for Selection {
|
||||||
fn from(selected: bool) -> Self {
|
fn from(selected: bool) -> Self {
|
||||||
if selected {
|
if selected {
|
||||||
@@ -49,3 +61,12 @@ impl From<Option<bool>> for Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<bool> for Selection {
|
||||||
|
fn into(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Selected => true,
|
||||||
|
Self::Unselected | Self::Indeterminate => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user