Detailed changes
@@ -1561,6 +1561,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
+ "story",
"theme",
"theme_selector",
"time",
@@ -7449,6 +7450,7 @@ dependencies = [
"backtrace-on-stack-overflow",
"chrono",
"clap 4.4.4",
+ "collab_ui",
"dialoguer",
"editor",
"fuzzy",
@@ -933,7 +933,7 @@ impl AssistantPanel {
}
fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
- IconButton::new("hamburger_button", Icon::Menu)
+ IconButton::new("hamburger_button", IconName::Menu)
.on_click(cx.listener(|this, _event, cx| {
if this.active_editor().is_some() {
this.set_active_editor_index(None, cx);
@@ -957,7 +957,7 @@ impl AssistantPanel {
}
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
- IconButton::new("split_button", Icon::Snip)
+ IconButton::new("split_button", IconName::Snip)
.on_click(cx.listener(|this, _event, cx| {
if let Some(active_editor) = this.active_editor() {
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
@@ -968,7 +968,7 @@ impl AssistantPanel {
}
fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
- IconButton::new("assist_button", Icon::MagicWand)
+ IconButton::new("assist_button", IconName::MagicWand)
.on_click(cx.listener(|this, _event, cx| {
if let Some(active_editor) = this.active_editor() {
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
@@ -979,7 +979,7 @@ impl AssistantPanel {
}
fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
- IconButton::new("quote_button", Icon::Quote)
+ IconButton::new("quote_button", IconName::Quote)
.on_click(cx.listener(|this, _event, cx| {
if let Some(workspace) = this.workspace.upgrade() {
cx.window_context().defer(move |cx| {
@@ -994,7 +994,7 @@ impl AssistantPanel {
}
fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
- IconButton::new("plus_button", Icon::Plus)
+ IconButton::new("plus_button", IconName::Plus)
.on_click(cx.listener(|this, _event, cx| {
this.new_conversation(cx);
}))
@@ -1004,12 +1004,12 @@ impl AssistantPanel {
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let zoomed = self.zoomed;
- IconButton::new("zoom_button", Icon::Maximize)
+ IconButton::new("zoom_button", IconName::Maximize)
.on_click(cx.listener(|this, _event, cx| {
this.toggle_zoom(&ToggleZoom, cx);
}))
.selected(zoomed)
- .selected_icon(Icon::Minimize)
+ .selected_icon(IconName::Minimize)
.icon_size(IconSize::Small)
.tooltip(move |cx| {
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
@@ -1286,8 +1286,8 @@ impl Panel for AssistantPanel {
}
}
- fn icon(&self, cx: &WindowContext) -> Option<Icon> {
- Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button)
+ fn icon(&self, cx: &WindowContext) -> Option<IconName> {
+ Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -2349,7 +2349,7 @@ impl ConversationEditor {
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error.clone(), cx))
- .child(IconElement::new(Icon::XCircle)),
+ .child(Icon::new(IconName::XCircle)),
)
} else {
None
@@ -2645,7 +2645,7 @@ impl Render for InlineAssistant {
.justify_center()
.w(measurements.gutter_width)
.child(
- IconButton::new("include_conversation", Icon::Ai)
+ IconButton::new("include_conversation", IconName::Ai)
.on_click(cx.listener(|this, _, cx| {
this.toggle_include_conversation(&ToggleIncludeConversation, cx)
}))
@@ -2660,7 +2660,7 @@ impl Render for InlineAssistant {
)
.children(if SemanticIndex::enabled(cx) {
Some(
- IconButton::new("retrieve_context", Icon::MagnifyingGlass)
+ IconButton::new("retrieve_context", IconName::MagnifyingGlass)
.on_click(cx.listener(|this, _, cx| {
this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
}))
@@ -2682,7 +2682,7 @@ impl Render for InlineAssistant {
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
- .child(IconElement::new(Icon::XCircle).color(Color::Error)),
+ .child(Icon::new(IconName::XCircle).color(Color::Error)),
)
} else {
None
@@ -2957,7 +2957,7 @@ impl InlineAssistant {
div()
.id("error")
.tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
- .child(IconElement::new(Icon::XCircle))
+ .child(Icon::new(IconName::XCircle))
.into_any_element()
),
@@ -2965,7 +2965,7 @@ impl InlineAssistant {
div()
.id("error")
.tooltip(|cx| Tooltip::text("Not Indexed", cx))
- .child(IconElement::new(Icon::XCircle))
+ .child(Icon::new(IconName::XCircle))
.into_any_element()
),
@@ -2996,7 +2996,7 @@ impl InlineAssistant {
div()
.id("update")
.tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
- .child(IconElement::new(Icon::Update).color(Color::Info))
+ .child(Icon::new(IconName::Update).color(Color::Info))
.into_any_element()
)
}
@@ -3005,7 +3005,7 @@ impl InlineAssistant {
div()
.id("check")
.tooltip(|cx| Tooltip::text("Index up to date", cx))
- .child(IconElement::new(Icon::Check).color(Color::Success))
+ .child(Icon::new(IconName::Check).color(Color::Success))
.into_any_element()
),
}
@@ -4,7 +4,7 @@ use gpui::{
};
use menu::Cancel;
use util::channel::ReleaseChannel;
-use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
+use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
pub struct UpdateNotification {
version: SemanticVersion,
@@ -30,7 +30,7 @@ impl Render for UpdateNotification {
.child(
div()
.id("cancel")
- .child(IconElement::new(Icon::Close))
+ .child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
),
@@ -9,9 +9,12 @@ pub struct CallSettings {
pub mute_on_join: bool,
}
+/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
+ ///
+ /// Default: false
pub mute_on_join: Option<bool>,
}
@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt};
-use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
+use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
@@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
pub struct User {
pub id: UserId,
pub github_login: String,
- pub avatar_uri: SharedString,
+ pub avatar_uri: SharedUrl,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -9,6 +9,8 @@ path = "src/collab_ui.rs"
doctest = false
[features]
+default = []
+stories = ["dep:story"]
test-support = [
"call/test-support",
"client/test-support",
@@ -44,6 +46,7 @@ project = { path = "../project" }
recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
+story = { path = "../story", optional = true }
feature_flags = { path = "../feature_flags"}
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
@@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
-use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
+use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -281,12 +281,12 @@ impl ChatPanel {
)),
)
.end_child(
- IconButton::new("notes", Icon::File)
+ IconButton::new("notes", IconName::File)
.on_click(cx.listener(Self::open_notes))
.tooltip(|cx| Tooltip::text("Open notes", cx)),
)
.end_child(
- IconButton::new("call", Icon::AudioOn)
+ IconButton::new("call", IconName::AudioOn)
.on_click(cx.listener(Self::join_call))
.tooltip(|cx| Tooltip::text("Join call", cx)),
),
@@ -395,7 +395,7 @@ impl ChatPanel {
.w_8()
.visible_on_hover("")
.children(message_id_to_remove.map(|message_id| {
- IconButton::new(("remove", message_id), Icon::XCircle).on_click(
+ IconButton::new(("remove", message_id), IconName::XCircle).on_click(
cx.listener(move |this, _, cx| {
this.remove_message(message_id, cx);
}),
@@ -429,7 +429,7 @@ impl ChatPanel {
Button::new("sign-in", "Sign in")
.style(ButtonStyle::Filled)
.icon_color(Color::Muted)
- .icon(Icon::Github)
+ .icon(IconName::Github)
.icon_position(IconPosition::Start)
.full_width()
.on_click(cx.listener(move |this, _, cx| {
@@ -622,12 +622,12 @@ impl Panel for ChatPanel {
"ChatPanel"
}
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+ fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
if !is_channels_feature_enabled(cx) {
return None;
}
- Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
+ Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -31,7 +31,7 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
- prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label,
+ prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
ListHeader, ListItem, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
@@ -844,7 +844,7 @@ impl CollabPanel {
.end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element()
} else if is_current_user {
- IconButton::new("leave-call", Icon::Exit)
+ IconButton::new("leave-call", IconName::Exit)
.style(ButtonStyle::Subtle)
.on_click(move |_, cx| Self::leave_call(cx))
.tooltip(|cx| Tooltip::text("Leave Call", cx))
@@ -903,7 +903,7 @@ impl CollabPanel {
h_stack()
.gap_1()
.child(render_tree_branch(is_last, false, cx))
- .child(IconButton::new(0, Icon::Folder)),
+ .child(IconButton::new(0, IconName::Folder)),
)
.child(Label::new(project_name.clone()))
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
@@ -924,7 +924,7 @@ impl CollabPanel {
h_stack()
.gap_1()
.child(render_tree_branch(is_last, false, cx))
- .child(IconButton::new(0, Icon::Screen)),
+ .child(IconButton::new(0, IconName::Screen)),
)
.child(Label::new("Screen"))
.when_some(peer_id, |this, _| {
@@ -965,7 +965,7 @@ impl CollabPanel {
h_stack()
.gap_1()
.child(render_tree_branch(false, true, cx))
- .child(IconButton::new(0, Icon::File)),
+ .child(IconButton::new(0, IconName::File)),
)
.child(div().h_7().w_full().child(Label::new("notes")))
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
@@ -986,7 +986,7 @@ impl CollabPanel {
h_stack()
.gap_1()
.child(render_tree_branch(false, false, cx))
- .child(IconButton::new(0, Icon::MessageBubbles)),
+ .child(IconButton::new(0, IconName::MessageBubbles)),
)
.child(Label::new("chat"))
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
@@ -1757,7 +1757,7 @@ impl CollabPanel {
.child(
Button::new("sign_in", "Sign in")
.icon_color(Color::Muted)
- .icon(Icon::Github)
+ .icon(IconName::Github)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Filled)
.full_width()
@@ -1949,7 +1949,7 @@ impl CollabPanel {
let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link.clone();
- IconButton::new("channel-link", Icon::Copy)
+ IconButton::new("channel-link", IconName::Copy)
.icon_size(IconSize::Small)
.size(ButtonSize::None)
.visible_on_hover("section-header")
@@ -1961,13 +1961,13 @@ impl CollabPanel {
.into_any_element()
}),
Section::Contacts => Some(
- IconButton::new("add-contact", Icon::Plus)
+ IconButton::new("add-contact", IconName::Plus)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
.tooltip(|cx| Tooltip::text("Search for new contact", cx))
.into_any_element(),
),
Section::Channels => Some(
- IconButton::new("add-channel", Icon::Plus)
+ IconButton::new("add-channel", IconName::Plus)
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
.tooltip(|cx| Tooltip::text("Create a channel", cx))
.into_any_element(),
@@ -2038,7 +2038,7 @@ impl CollabPanel {
})
.when(!calling, |el| {
el.child(
- IconButton::new("remove_contact", Icon::Close)
+ IconButton::new("remove_contact", IconName::Close)
.icon_color(Color::Muted)
.visible_on_hover("")
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
@@ -2099,13 +2099,13 @@ impl CollabPanel {
let controls = if is_incoming {
vec![
- IconButton::new("decline-contact", Icon::Close)
+ IconButton::new("decline-contact", IconName::Close)
.on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, false, cx);
}))
.icon_color(color)
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
- IconButton::new("accept-contact", Icon::Check)
+ IconButton::new("accept-contact", IconName::Check)
.on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, true, cx);
}))
@@ -2114,7 +2114,7 @@ impl CollabPanel {
]
} else {
let github_login = github_login.clone();
- vec![IconButton::new("remove_contact", Icon::Close)
+ vec![IconButton::new("remove_contact", IconName::Close)
.on_click(cx.listener(move |this, _, cx| {
this.remove_contact(user_id, &github_login, cx);
}))
@@ -2154,13 +2154,13 @@ impl CollabPanel {
};
let controls = [
- IconButton::new("reject-invite", Icon::Close)
+ IconButton::new("reject-invite", IconName::Close)
.on_click(cx.listener(move |this, _, cx| {
this.respond_to_channel_invite(channel_id, false, cx);
}))
.icon_color(color)
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
- IconButton::new("accept-invite", Icon::Check)
+ IconButton::new("accept-invite", IconName::Check)
.on_click(cx.listener(move |this, _, cx| {
this.respond_to_channel_invite(channel_id, true, cx);
}))
@@ -2178,7 +2178,7 @@ impl CollabPanel {
.child(h_stack().children(controls)),
)
.start_slot(
- IconElement::new(Icon::Hash)
+ Icon::new(IconName::Hash)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -2190,7 +2190,7 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) -> ListItem {
ListItem::new("contact-placeholder")
- .child(IconElement::new(Icon::Plus))
+ .child(Icon::new(IconName::Plus))
.child(Label::new("Add a Contact"))
.selected(is_selected)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
@@ -2274,7 +2274,7 @@ impl CollabPanel {
};
let messages_button = |cx: &mut ViewContext<Self>| {
- IconButton::new("channel_chat", Icon::MessageBubbles)
+ IconButton::new("channel_chat", IconName::MessageBubbles)
.icon_size(IconSize::Small)
.icon_color(if has_messages_notification {
Color::Default
@@ -2286,7 +2286,7 @@ impl CollabPanel {
};
let notes_button = |cx: &mut ViewContext<Self>| {
- IconButton::new("channel_notes", Icon::File)
+ IconButton::new("channel_notes", IconName::File)
.icon_size(IconSize::Small)
.icon_color(if has_notes_notification {
Color::Default
@@ -2343,9 +2343,13 @@ impl CollabPanel {
},
))
.start_slot(
- IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
- .size(IconSize::Small)
- .color(Color::Muted),
+ Icon::new(if is_public {
+ IconName::Public
+ } else {
+ IconName::Hash
+ })
+ .size(IconSize::Small)
+ .color(Color::Muted),
)
.child(
h_stack()
@@ -2414,7 +2418,7 @@ impl CollabPanel {
.indent_level(depth + 1)
.indent_step_size(px(20.))
.start_slot(
- IconElement::new(Icon::Hash)
+ Icon::new(IconName::Hash)
.size(IconSize::Small)
.color(Color::Muted),
);
@@ -2528,10 +2532,10 @@ impl Panel for CollabPanel {
cx.notify();
}
- fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
+ fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
CollaborationPanelSettings::get_global(cx)
.button
- .then(|| ui::Icon::Collab)
+ .then(|| ui::IconName::Collab)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -2669,11 +2673,11 @@ impl Render for DraggedChannelView {
.p_1()
.gap_1()
.child(
- IconElement::new(
+ Icon::new(
if self.channel.visibility == proto::ChannelVisibility::Public {
- Icon::Public
+ IconName::Public
} else {
- Icon::Hash
+ IconName::Hash
},
)
.size(IconSize::Small)
@@ -168,7 +168,7 @@ impl Render for ChannelModal {
.w_px()
.flex_1()
.gap_1()
- .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
+ .child(Icon::new(IconName::Hash).size(IconSize::Medium))
.child(Label::new(channel_name)),
)
.child(
@@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
_ => None,
})
- .child(IconButton::new("ellipsis", Icon::Ellipsis))
+ .child(IconButton::new("ellipsis", IconName::Ellipsis))
.children(
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
Some(
@@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate {
.selected(selected)
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
- .end_slot::<IconElement>(
- icon_path.map(|icon_path| IconElement::from_path(icon_path)),
- ),
+ .end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
)
}
}
@@ -15,7 +15,7 @@ use std::sync::Arc;
use theme::{ActiveTheme, PlayerColors};
use ui::{
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
- IconButton, IconElement, TintColor, Tooltip,
+ IconButton, IconName, TintColor, Tooltip,
};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@@ -213,7 +213,7 @@ impl Render for CollabTitlebarItem {
.child(
div()
.child(
- IconButton::new("leave-call", ui::Icon::Exit)
+ IconButton::new("leave-call", ui::IconName::Exit)
.style(ButtonStyle::Subtle)
.tooltip(|cx| Tooltip::text("Leave call", cx))
.icon_size(IconSize::Small)
@@ -230,9 +230,9 @@ impl Render for CollabTitlebarItem {
IconButton::new(
"mute-microphone",
if is_muted {
- ui::Icon::MicMute
+ ui::IconName::MicMute
} else {
- ui::Icon::Mic
+ ui::IconName::Mic
},
)
.tooltip(move |cx| {
@@ -256,9 +256,9 @@ impl Render for CollabTitlebarItem {
IconButton::new(
"mute-sound",
if is_deafened {
- ui::Icon::AudioOff
+ ui::IconName::AudioOff
} else {
- ui::Icon::AudioOn
+ ui::IconName::AudioOn
},
)
.style(ButtonStyle::Subtle)
@@ -281,7 +281,7 @@ impl Render for CollabTitlebarItem {
)
.when(!read_only, |this| {
this.child(
- IconButton::new("screen-share", ui::Icon::Screen)
+ IconButton::new("screen-share", ui::IconName::Screen)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_screen_sharing)
@@ -573,7 +573,7 @@ impl CollabTitlebarItem {
| client::Status::ReconnectionError { .. } => Some(
div()
.id("disconnected")
- .child(IconElement::new(Icon::Disconnected).size(IconSize::Small))
+ .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
.tooltip(|cx| Tooltip::text("Disconnected", cx))
.into_any_element(),
),
@@ -643,7 +643,7 @@ impl CollabTitlebarItem {
h_stack()
.gap_0p5()
.child(Avatar::new(user.avatar_uri.clone()))
- .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+ .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@@ -665,7 +665,7 @@ impl CollabTitlebarItem {
.child(
h_stack()
.gap_0p5()
- .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+ .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
+use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -553,7 +553,7 @@ impl Render for NotificationPanel {
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Notifications"))
- .child(IconElement::new(Icon::Envelope)),
+ .child(Icon::new(IconName::Envelope)),
)
.map(|this| {
if self.client.user_id().is_none() {
@@ -564,7 +564,7 @@ impl Render for NotificationPanel {
.child(
Button::new("sign_in_prompt_button", "Sign in")
.icon_color(Color::Muted)
- .icon(Icon::Github)
+ .icon(IconName::Github)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Filled)
.full_width()
@@ -655,10 +655,10 @@ impl Panel for NotificationPanel {
}
}
- fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+ fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
(NotificationPanelSettings::get_global(cx).button
&& self.notification_store.read(cx).notification_count() > 0)
- .then(|| Icon::Bell)
+ .then(|| IconName::Bell)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -716,7 +716,7 @@ impl Render for NotificationToast {
.children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
.child(Label::new(self.text.clone()))
.child(
- IconButton::new("close", Icon::Close)
+ IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
)
.on_click(cx.listener(|this, _, cx| {
@@ -1,9 +1,16 @@
+mod collab_notification;
+pub mod incoming_call_notification;
+pub mod project_shared_notification;
+
+#[cfg(feature = "stories")]
+mod stories;
+
use gpui::AppContext;
use std::sync::Arc;
use workspace::AppState;
-pub mod incoming_call_notification;
-pub mod project_shared_notification;
+#[cfg(feature = "stories")]
+pub use stories::*;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
incoming_call_notification::init(app_state, cx);
@@ -0,0 +1,52 @@
+use gpui::{img, prelude::*, AnyElement, SharedUrl};
+use smallvec::SmallVec;
+use ui::prelude::*;
+
+#[derive(IntoElement)]
+pub struct CollabNotification {
+ avatar_uri: SharedUrl,
+ accept_button: Button,
+ dismiss_button: Button,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl CollabNotification {
+ pub fn new(
+ avatar_uri: impl Into<SharedUrl>,
+ accept_button: Button,
+ dismiss_button: Button,
+ ) -> Self {
+ Self {
+ avatar_uri: avatar_uri.into(),
+ accept_button,
+ dismiss_button,
+ children: SmallVec::new(),
+ }
+ }
+}
+
+impl ParentElement for CollabNotification {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+impl RenderOnce for CollabNotification {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ h_stack()
+ .text_ui()
+ .justify_between()
+ .size_full()
+ .overflow_hidden()
+ .elevation_3(cx)
+ .p_2()
+ .gap_2()
+ .child(img(self.avatar_uri).w_12().h_12().rounded_full())
+ .child(v_stack().overflow_hidden().children(self.children))
+ .child(
+ v_stack()
+ .child(self.accept_button)
+ .child(self.dismiss_button),
+ )
+ }
+}
@@ -1,15 +1,12 @@
use crate::notification_window_options;
+use crate::notifications::collab_notification::CollabNotification;
use call::{ActiveCall, IncomingCall};
use futures::StreamExt;
-use gpui::{
- img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
- VisualContext as _, WindowHandle,
-};
+use gpui::{prelude::*, AppContext, WindowHandle};
use settings::Settings;
use std::sync::{Arc, Weak};
use theme::ThemeSettings;
-use ui::prelude::*;
-use ui::{h_stack, v_stack, Button, Label};
+use ui::{prelude::*, Button, Label};
use util::ResultExt;
use workspace::AppState;
@@ -31,8 +28,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(incoming_call) = incoming_call {
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
let window_size = gpui::Size {
- width: px(380.),
- height: px(64.),
+ width: px(400.),
+ height: px(72.),
};
for screen in unique_screens {
@@ -129,35 +126,22 @@ impl Render for IncomingCallNotification {
cx.set_rem_size(ui_font_size);
- h_stack()
- .font(ui_font)
- .text_ui()
- .justify_between()
- .size_full()
- .overflow_hidden()
- .elevation_3(cx)
- .p_2()
- .gap_2()
- .child(
- img(self.state.call.calling_user.avatar_uri.clone())
- .w_12()
- .h_12()
- .rounded_full(),
+ div().size_full().font(ui_font).child(
+ CollabNotification::new(
+ self.state.call.calling_user.avatar_uri.clone(),
+ Button::new("accept", "Accept").on_click({
+ let state = self.state.clone();
+ move |_, cx| state.respond(true, cx)
+ }),
+ Button::new("decline", "Decline").on_click({
+ let state = self.state.clone();
+ move |_, cx| state.respond(false, cx)
+ }),
)
.child(v_stack().overflow_hidden().child(Label::new(format!(
"{} is sharing a project in Zed",
self.state.call.calling_user.github_login
- ))))
- .child(
- v_stack()
- .child(Button::new("accept", "Accept").render(cx).on_click({
- let state = self.state.clone();
- move |_, cx| state.respond(true, cx)
- }))
- .child(Button::new("decline", "Decline").render(cx).on_click({
- let state = self.state.clone();
- move |_, cx| state.respond(false, cx)
- })),
- )
+ )))),
+ )
}
}
@@ -1,12 +1,13 @@
use crate::notification_window_options;
+use crate::notifications::collab_notification::CollabNotification;
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
-use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
+use gpui::{AppContext, Size};
use settings::Settings;
use std::sync::{Arc, Weak};
use theme::ThemeSettings;
-use ui::{h_stack, prelude::*, v_stack, Button, Label};
+use ui::{prelude::*, Button, Label};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -130,51 +131,30 @@ impl Render for ProjectSharedNotification {
cx.set_rem_size(ui_font_size);
- h_stack()
- .font(ui_font)
- .text_ui()
- .justify_between()
- .size_full()
- .overflow_hidden()
- .elevation_3(cx)
- .p_2()
- .gap_2()
- .child(
- img(self.owner.avatar_uri.clone())
- .w_12()
- .h_12()
- .rounded_full(),
- )
- .child(
- v_stack()
- .overflow_hidden()
- .child(Label::new(self.owner.github_login.clone()))
- .child(Label::new(format!(
- "is sharing a project in Zed{}",
- if self.worktree_root_names.is_empty() {
- ""
- } else {
- ":"
- }
- )))
- .children(if self.worktree_root_names.is_empty() {
- None
- } else {
- Some(Label::new(self.worktree_root_names.join(", ")))
- }),
- )
- .child(
- v_stack()
- .child(Button::new("open", "Open").on_click(cx.listener(
- move |this, _event, cx| {
- this.join(cx);
- },
- )))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- move |this, _event, cx| {
- this.dismiss(cx);
- },
- ))),
+ div().size_full().font(ui_font).child(
+ CollabNotification::new(
+ self.owner.avatar_uri.clone(),
+ Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
+ this.join(cx);
+ })),
+ Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
+ this.dismiss(cx);
+ })),
)
+ .child(Label::new(self.owner.github_login.clone()))
+ .child(Label::new(format!(
+ "is sharing a project in Zed{}",
+ if self.worktree_root_names.is_empty() {
+ ""
+ } else {
+ ":"
+ }
+ )))
+ .children(if self.worktree_root_names.is_empty() {
+ None
+ } else {
+ Some(Label::new(self.worktree_root_names.join(", ")))
+ }),
+ )
}
}
@@ -0,0 +1,3 @@
+mod collab_notification;
+
+pub use collab_notification::*;
@@ -0,0 +1,50 @@
+use gpui::prelude::*;
+use story::{StoryContainer, StoryItem, StorySection};
+use ui::prelude::*;
+
+use crate::notifications::collab_notification::CollabNotification;
+
+pub struct CollabNotificationStory;
+
+impl Render for CollabNotificationStory {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let window_container = |width, height| div().w(px(width)).h(px(height));
+
+ StoryContainer::new(
+ "CollabNotification Story",
+ "crates/collab_ui/src/notifications/stories/collab_notification.rs",
+ )
+ .child(
+ StorySection::new().child(StoryItem::new(
+ "Incoming Call Notification",
+ window_container(400., 72.).child(
+ CollabNotification::new(
+ "https://avatars.githubusercontent.com/u/1486634?v=4",
+ Button::new("accept", "Accept"),
+ Button::new("decline", "Decline"),
+ )
+ .child(
+ v_stack()
+ .overflow_hidden()
+ .child(Label::new("maxdeviant is sharing a project in Zed")),
+ ),
+ ),
+ )),
+ )
+ .child(
+ StorySection::new().child(StoryItem::new(
+ "Project Shared Notification",
+ window_container(400., 72.).child(
+ CollabNotification::new(
+ "https://avatars.githubusercontent.com/u/1714999?v=4",
+ Button::new("open", "Open"),
+ Button::new("dismiss", "Dismiss"),
+ )
+ .child(Label::new("iamnbutler"))
+ .child(Label::new("is sharing a project in Zed:"))
+ .child(Label::new("zed")),
+ ),
+ )),
+ )
+ }
+}
@@ -17,7 +17,9 @@ use util::{paths, ResultExt};
use workspace::{
create_and_open_local_file,
item::ItemHandle,
- ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
+ ui::{
+ popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
+ },
StatusItemView, Toast, Workspace,
};
use zed_actions::OpenBrowser;
@@ -51,15 +53,15 @@ impl Render for CopilotButton {
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
let icon = match status {
- Status::Error(_) => Icon::CopilotError,
+ Status::Error(_) => IconName::CopilotError,
Status::Authorized => {
if enabled {
- Icon::Copilot
+ IconName::Copilot
} else {
- Icon::CopilotDisabled
+ IconName::CopilotDisabled
}
}
- _ => Icon::CopilotInit,
+ _ => IconName::CopilotInit,
};
if let Status::Error(e) = status {
@@ -4,7 +4,7 @@ use gpui::{
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
Subscription, ViewContext,
};
-use ui::{prelude::*, Button, Icon, Label};
+use ui::{prelude::*, Button, IconName, Label};
use workspace::ModalView;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
@@ -175,7 +175,7 @@ impl Render for CopilotCodeVerification {
.w_32()
.h_16()
.flex_none()
- .path(Icon::ZedXCopilot.path())
+ .path(IconName::ZedXCopilot.path())
.text_color(cx.theme().colors().icon),
)
.child(prompt)
@@ -36,7 +36,7 @@ use std::{
};
use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls;
-use ui::{h_stack, prelude::*, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, Icon, IconName, Label};
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor {
then.child(
h_stack()
.gap_1()
- .child(IconElement::new(Icon::XCircle).color(Color::Error))
+ .child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(self.summary.error_count.to_string()).color(
if selected {
Color::Default
@@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor {
then.child(
h_stack()
.gap_1()
- .child(
- IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
- )
+ .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
.child(Label::new(self.summary.warning_count.to_string()).color(
if selected {
Color::Default
@@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.flex_none()
.map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
- icon.path(Icon::XCircle.path())
+ icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
- icon.path(Icon::ExclamationTriangle.path())
+ icon.path(IconName::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx))
}
}),
@@ -6,7 +6,7 @@ use gpui::{
};
use language::Diagnostic;
use lsp::LanguageServerId;
-use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
+use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::{Deploy, ProjectDiagnosticsEditor};
@@ -25,7 +25,7 @@ impl Render for DiagnosticIndicator {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().map(|this| {
this.child(
- IconElement::new(Icon::Check)
+ Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Default),
)
@@ -33,7 +33,7 @@ impl Render for DiagnosticIndicator {
(0, warning_count) => h_stack()
.gap_1()
.child(
- IconElement::new(Icon::ExclamationTriangle)
+ Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.color(Color::Warning),
)
@@ -41,7 +41,7 @@ impl Render for DiagnosticIndicator {
(error_count, 0) => h_stack()
.gap_1()
.child(
- IconElement::new(Icon::XCircle)
+ Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
@@ -49,13 +49,13 @@ impl Render for DiagnosticIndicator {
(error_count, warning_count) => h_stack()
.gap_1()
.child(
- IconElement::new(Icon::XCircle)
+ Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
.child(
- IconElement::new(Icon::ExclamationTriangle)
+ Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.color(Color::Warning),
)
@@ -66,7 +66,7 @@ impl Render for DiagnosticIndicator {
Some(
h_stack()
.gap_2()
- .child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
+ .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
.child(
Label::new("Checking…")
.size(LabelSize::Small)
@@ -1,7 +1,7 @@
use crate::ProjectDiagnosticsEditor;
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*;
-use ui::{Icon, IconButton, Tooltip};
+use ui::{IconButton, IconName, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub struct ToolbarControls {
@@ -24,7 +24,7 @@ impl Render for ToolbarControls {
};
div().child(
- IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
+ IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
@@ -1015,7 +1015,6 @@ pub mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let _test_platform = &cx.test_platform;
let mut tab_size = rng.gen_range(1..=4);
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
@@ -99,8 +99,8 @@ use sum_tree::TreeMap;
use text::{OffsetUtf16, Rope};
use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
use ui::{
- h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover,
- Tooltip,
+ h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
+ Popover, Tooltip,
};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
@@ -507,7 +507,7 @@ pub enum SoftWrap {
Column(u32),
}
-#[derive(Clone, Default)]
+#[derive(Clone)]
pub struct EditorStyle {
pub background: Hsla,
pub local_player: PlayerColor,
@@ -519,6 +519,24 @@ pub struct EditorStyle {
pub suggestions_style: HighlightStyle,
}
+impl Default for EditorStyle {
+ fn default() -> Self {
+ Self {
+ background: Hsla::default(),
+ local_player: PlayerColor::default(),
+ text: TextStyle::default(),
+ scrollbar_width: Pixels::default(),
+ syntax: Default::default(),
+ // HACK: Status colors don't have a real default.
+ // We should look into removing the status colors from the editor
+ // style and retrieve them directly from the theme.
+ status: StatusColors::dark(),
+ inlays_style: HighlightStyle::default(),
+ suggestions_style: HighlightStyle::default(),
+ }
+ }
+}
+
type CompletionId = usize;
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
@@ -4223,7 +4241,7 @@ impl Editor {
) -> Option<IconButton> {
if self.available_code_actions.is_some() {
Some(
- IconButton::new("code_actions_indicator", ui::Icon::Bolt)
+ IconButton::new("code_actions_indicator", ui::IconName::Bolt)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.selected(is_active)
@@ -4257,7 +4275,7 @@ impl Editor {
fold_data
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
- IconButton::new(ix as usize, ui::Icon::ChevronDown)
+ IconButton::new(ix as usize, ui::IconName::ChevronDown)
.on_click(cx.listener(move |editor, _e, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
@@ -4269,7 +4287,7 @@ impl Editor {
.icon_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small)
.selected(fold_status == FoldStatus::Folded)
- .selected_icon(ui::Icon::ChevronRight)
+ .selected_icon(ui::IconName::ChevronRight)
.size(ui::ButtonSize::None)
})
})
@@ -9739,7 +9757,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
),
)
.child(
- IconButton::new(("copy-block", cx.block_id), Icon::Copy)
+ IconButton::new(("copy-block", cx.block_id), IconName::Copy)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.style(ButtonStyle::Transparent)
@@ -28,7 +28,7 @@ use gpui::{
AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds,
InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
- MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine,
+ MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
TextStyle, View, ViewContext, WindowContext,
};
@@ -581,41 +581,6 @@ impl EditorElement {
}
}
- fn scroll(
- editor: &mut Editor,
- event: &ScrollWheelEvent,
- position_map: &PositionMap,
- bounds: &InteractiveBounds,
- cx: &mut ViewContext<Editor>,
- ) {
- if !bounds.visibly_contains(&event.position, cx) {
- return;
- }
-
- let line_height = position_map.line_height;
- let max_glyph_width = position_map.em_width;
- let (delta, axis) = match event.delta {
- gpui::ScrollDelta::Pixels(mut pixels) => {
- //Trackpad
- let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
- (pixels, axis)
- }
-
- gpui::ScrollDelta::Lines(lines) => {
- //Not trackpad
- let pixels = point(lines.x * max_glyph_width, lines.y * line_height);
- (pixels, None)
- }
- };
-
- let scroll_position = position_map.snapshot.scroll_position();
- let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width);
- let y = f32::from((scroll_position.y * line_height - delta.y) / line_height);
- let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
- editor.scroll(scroll_position, axis, cx);
- cx.stop_propagation();
- }
-
fn paint_background(
&self,
gutter_bounds: Bounds<Pixels>,
@@ -2450,34 +2415,78 @@ impl EditorElement {
)
}
- fn paint_mouse_listeners(
+ fn paint_scroll_wheel_listener(
&mut self,
- bounds: Bounds<Pixels>,
- gutter_bounds: Bounds<Pixels>,
- text_bounds: Bounds<Pixels>,
+ interactive_bounds: &InteractiveBounds,
layout: &LayoutState,
cx: &mut WindowContext,
) {
- let interactive_bounds = InteractiveBounds {
- bounds: bounds.intersect(&cx.content_mask().bounds),
- stacking_order: cx.stacking_order().clone(),
- };
-
cx.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let interactive_bounds = interactive_bounds.clone();
+ let mut delta = ScrollDelta::default();
move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble
&& interactive_bounds.visibly_contains(&event.position, cx)
{
+ delta = delta.coalesce(event.delta);
editor.update(cx, |editor, cx| {
- Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
+ let position = event.position;
+ let position_map: &PositionMap = &position_map;
+ let bounds = &interactive_bounds;
+ if !bounds.visibly_contains(&position, cx) {
+ return;
+ }
+
+ let line_height = position_map.line_height;
+ let max_glyph_width = position_map.em_width;
+ let (delta, axis) = match delta {
+ gpui::ScrollDelta::Pixels(mut pixels) => {
+ //Trackpad
+ let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
+ (pixels, axis)
+ }
+
+ gpui::ScrollDelta::Lines(lines) => {
+ //Not trackpad
+ let pixels =
+ point(lines.x * max_glyph_width, lines.y * line_height);
+ (pixels, None)
+ }
+ };
+
+ let scroll_position = position_map.snapshot.scroll_position();
+ let x = f32::from(
+ (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width,
+ );
+ let y =
+ f32::from((scroll_position.y * line_height - delta.y) / line_height);
+ let scroll_position =
+ point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
+ editor.scroll(scroll_position, axis, cx);
+ cx.stop_propagation();
});
}
}
});
+ }
+
+ fn paint_mouse_listeners(
+ &mut self,
+ bounds: Bounds<Pixels>,
+ gutter_bounds: Bounds<Pixels>,
+ text_bounds: Bounds<Pixels>,
+ layout: &LayoutState,
+ cx: &mut WindowContext,
+ ) {
+ let interactive_bounds = InteractiveBounds {
+ bounds: bounds.intersect(&cx.content_mask().bounds),
+ stacking_order: cx.stacking_order().clone(),
+ };
+
+ self.paint_scroll_wheel_listener(&interactive_bounds, layout, cx);
cx.on_mouse_event({
let position_map = layout.position_map.clone();
@@ -16,7 +16,7 @@ use lsp::DiagnosticSeverity;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration};
-use ui::{StyledExt, Tooltip};
+use ui::{prelude::*, Tooltip};
use util::TryFutureExt;
use workspace::Workspace;
@@ -514,6 +514,8 @@ impl DiagnosticPopover {
None => self.local_diagnostic.diagnostic.message.clone(),
};
+ let status_colors = cx.theme().status();
+
struct DiagnosticColors {
pub background: Hsla,
pub border: Hsla,
@@ -521,24 +523,24 @@ impl DiagnosticPopover {
let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
DiagnosticSeverity::ERROR => DiagnosticColors {
- background: style.status.error_background,
- border: style.status.error_border,
+ background: status_colors.error_background,
+ border: status_colors.error_border,
},
DiagnosticSeverity::WARNING => DiagnosticColors {
- background: style.status.warning_background,
- border: style.status.warning_border,
+ background: status_colors.warning_background,
+ border: status_colors.warning_border,
},
DiagnosticSeverity::INFORMATION => DiagnosticColors {
- background: style.status.info_background,
- border: style.status.info_border,
+ background: status_colors.info_background,
+ border: status_colors.info_border,
},
DiagnosticSeverity::HINT => DiagnosticColors {
- background: style.status.hint_background,
- border: style.status.hint_border,
+ background: status_colors.hint_background,
+ border: status_colors.hint_border,
},
_ => DiagnosticColors {
- background: style.status.ignored_background,
- border: style.status.ignored_border,
+ background: status_colors.ignored_background,
+ border: status_colors.ignored_border,
},
};
@@ -384,10 +384,12 @@ impl Editor {
) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
- let top_row = scroll_anchor
- .anchor
- .to_point(&self.buffer().read(cx).snapshot(cx))
- .row;
+ let snapshot = &self.buffer().read(cx).snapshot(cx);
+ if !scroll_anchor.anchor.is_valid(snapshot) {
+ log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
+ return;
+ }
+ let top_row = scroll_anchor.anchor.to_point(snapshot).row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
}
@@ -60,8 +60,7 @@ pub fn assert_text_with_selections(
#[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
- // todo!()
- Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
+ Editor::new(EditorMode::Full, buffer, None, cx)
}
pub(crate) fn build_editor_with_project(
@@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project(
buffer: Model<MultiBuffer>,
cx: &mut ViewContext<Editor>,
) -> Editor {
- // todo!()
- Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
+ Editor::new(EditorMode::Full, buffer, Some(project), cx)
}
@@ -1,5 +1,5 @@
use gpui::{Render, ViewContext, WeakView};
-use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
+use ui::{prelude::*, ButtonCommon, IconButton, IconName, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::{feedback_modal::FeedbackModal, GiveFeedback};
@@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton {
})
})
.is_some();
- IconButton::new("give-feedback", Icon::Envelope)
+ IconButton::new("give-feedback", IconName::Envelope)
.style(ui::ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_open)
@@ -488,7 +488,7 @@ impl Render for FeedbackModal {
.child(
Button::new("community_repository", "Community Repository")
.style(ButtonStyle::Transparent)
- .icon(Icon::ExternalLink)
+ .icon(IconName::ExternalLink)
.icon_position(IconPosition::End)
.icon_size(IconSize::Small)
.on_click(open_community_repo),
@@ -50,7 +50,7 @@ impl Render for Menu {
.on_action(|this, move: &MoveDown, cx| {
// ...
})
- .children(todo!())
+ .children(unimplemented!())
}
}
```
@@ -68,7 +68,7 @@ impl Render for Menu {
.on_action(|this, move: &MoveDown, cx| {
// ...
})
- .children(todo!())
+ .children(unimplemented!())
}
}
```
@@ -114,14 +114,26 @@ impl ActionRegistry {
pub(crate) fn load_actions(&mut self) {
for builder in __GPUI_ACTIONS {
let action = builder();
- //todo(remove)
- let name: SharedString = action.name.into();
- self.builders_by_name.insert(name.clone(), action.build);
- self.names_by_type_id.insert(action.type_id, name.clone());
- self.all_names.push(name);
+ self.insert_action(action);
}
}
+ #[cfg(test)]
+ pub(crate) fn load_action<A: Action>(&mut self) {
+ self.insert_action(ActionData {
+ name: A::debug_name(),
+ type_id: TypeId::of::<A>(),
+ build: A::build,
+ });
+ }
+
+ fn insert_action(&mut self, action: ActionData) {
+ let name: SharedString = action.name.into();
+ self.builders_by_name.insert(name.clone(), action.build);
+ self.names_by_type_id.insert(action.type_id, name.clone());
+ self.all_names.push(name);
+ }
+
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
let name = self
@@ -203,7 +215,6 @@ macro_rules! __impl_action {
)
}
- // todo!() why is this needed in addition to name?
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
@@ -45,11 +45,13 @@ use util::{
/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
/// Strongly consider removing after stabilization.
+#[doc(hidden)]
pub struct AppCell {
app: RefCell<AppContext>,
}
impl AppCell {
+ #[doc(hidden)]
#[track_caller]
pub fn borrow(&self) -> AppRef {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
@@ -59,6 +61,7 @@ impl AppCell {
AppRef(self.app.borrow())
}
+ #[doc(hidden)]
#[track_caller]
pub fn borrow_mut(&self) -> AppRefMut {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
@@ -69,6 +72,7 @@ impl AppCell {
}
}
+#[doc(hidden)]
#[derive(Deref, DerefMut)]
pub struct AppRef<'a>(Ref<'a, AppContext>);
@@ -81,6 +85,7 @@ impl<'a> Drop for AppRef<'a> {
}
}
+#[doc(hidden)]
#[derive(Deref, DerefMut)]
pub struct AppRefMut<'a>(RefMut<'a, AppContext>);
@@ -93,6 +98,8 @@ impl<'a> Drop for AppRefMut<'a> {
}
}
+/// A reference to a GPUI application, typically constructed in the `main` function of your app.
+/// You won't interact with this type much outside of initial configuration and startup.
pub struct App(Rc<AppCell>);
/// Represents an application before it is fully launched. Once your app is
@@ -136,6 +143,8 @@ impl App {
self
}
+ /// Invokes a handler when an already-running application is launched.
+ /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock.
pub fn on_reopen<F>(&self, mut callback: F) -> &Self
where
F: 'static + FnMut(&mut AppContext),
@@ -149,18 +158,22 @@ impl App {
self
}
+ /// Returns metadata associated with the application
pub fn metadata(&self) -> AppMetadata {
self.0.borrow().app_metadata.clone()
}
+ /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background.
pub fn background_executor(&self) -> BackgroundExecutor {
self.0.borrow().background_executor.clone()
}
+ /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground.
pub fn foreground_executor(&self) -> ForegroundExecutor {
self.0.borrow().foreground_executor.clone()
}
+ /// Returns a reference to the [`TextSystem`] associated with this app.
pub fn text_system(&self) -> Arc<TextSystem> {
self.0.borrow().text_system.clone()
}
@@ -174,12 +187,6 @@ type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
-// struct FrameConsumer {
-// next_frame_callbacks: Vec<FrameCallback>,
-// task: Task<()>,
-// display_linker
-// }
-
pub struct AppContext {
pub(crate) this: Weak<AppCell>,
pub(crate) platform: Rc<dyn Platform>,
@@ -292,7 +299,7 @@ impl AppContext {
app
}
- /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
+ /// Quit the application gracefully. Handlers registered with [`ModelContext::on_app_quit`]
/// will be given 100ms to complete before exiting.
pub fn shutdown(&mut self) {
let mut futures = Vec::new();
@@ -314,10 +321,12 @@ impl AppContext {
}
}
+ /// Gracefully quit the application via the platform's standard routine.
pub fn quit(&mut self) {
self.platform.quit();
}
+ /// Get metadata about the app and platform.
pub fn app_metadata(&self) -> AppMetadata {
self.app_metadata.clone()
}
@@ -340,6 +349,7 @@ impl AppContext {
result
}
+ /// Arrange a callback to be invoked when the given model or view calls `notify` on its respective context.
pub fn observe<W, E>(
&mut self,
entity: &E,
@@ -355,7 +365,7 @@ impl AppContext {
})
}
- pub fn observe_internal<W, E>(
+ pub(crate) fn observe_internal<W, E>(
&mut self,
entity: &E,
mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static,
@@ -380,15 +390,17 @@ impl AppContext {
subscription
}
- pub fn subscribe<T, E, Evt>(
+ /// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type.
+ /// The callback is provided a handle to the emitting entity and a reference to the emitted event.
+ pub fn subscribe<T, E, Event>(
&mut self,
entity: &E,
- mut on_event: impl FnMut(E, &Evt, &mut AppContext) + 'static,
+ mut on_event: impl FnMut(E, &Event, &mut AppContext) + 'static,
) -> Subscription
where
- T: 'static + EventEmitter<Evt>,
+ T: 'static + EventEmitter<Event>,
E: Entity<T>,
- Evt: 'static,
+ Event: 'static,
{
self.subscribe_internal(entity, move |entity, event, cx| {
on_event(entity, event, cx);
@@ -426,6 +438,9 @@ impl AppContext {
subscription
}
+ /// Returns handles to all open windows in the application.
+ /// Each handle could be downcast to a handle typed for the root view of that window.
+ /// To find all windows of a given type, you could filter on
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.windows
.values()
@@ -565,7 +580,7 @@ impl AppContext {
self.pending_effects.push_back(effect);
}
- /// Called at the end of AppContext::update to complete any side effects
+ /// Called at the end of [`AppContext::update`] to complete any side effects
/// such as notifying observers, emitting events, etc. Effects can themselves
/// cause effects, so we continue looping until all effects are processed.
fn flush_effects(&mut self) {
@@ -82,6 +82,7 @@ impl Context for AsyncAppContext {
}
impl AsyncAppContext {
+ /// Schedules all windows in the application to be redrawn.
pub fn refresh(&mut self) -> Result<()> {
let app = self
.app
@@ -92,14 +93,17 @@ impl AsyncAppContext {
Ok(())
}
+ /// Get an executor which can be used to spawn futures in the background.
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
+ /// Get an executor which can be used to spawn futures in the foreground.
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
+ /// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
let app = self
.app
@@ -109,6 +113,7 @@ impl AsyncAppContext {
Ok(f(&mut lock))
}
+ /// Open a window with the given options based on the root view returned by the given function.
pub fn open_window<V>(
&self,
options: crate::WindowOptions,
@@ -125,6 +130,7 @@ impl AsyncAppContext {
Ok(lock.open_window(options, build_root_view))
}
+ /// Schedule a future to be polled in the background.
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
@@ -19,7 +19,10 @@ use std::{
#[cfg(any(test, feature = "test-support"))]
use collections::HashMap;
-slotmap::new_key_type! { pub struct EntityId; }
+slotmap::new_key_type! {
+ /// A unique identifier for a model or view across the application.
+ pub struct EntityId;
+}
impl EntityId {
pub fn as_u64(self) -> u64 {
@@ -1,3 +1,5 @@
+#![deny(missing_docs)]
+
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
@@ -9,13 +11,19 @@ use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
+/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
+/// an implementation of `Context` with additional methods that are useful in tests.
#[derive(Clone)]
pub struct TestAppContext {
+ #[doc(hidden)]
pub app: Rc<AppCell>,
+ #[doc(hidden)]
pub background_executor: BackgroundExecutor,
+ #[doc(hidden)]
pub foreground_executor: ForegroundExecutor,
+ #[doc(hidden)]
pub dispatcher: TestDispatcher,
- pub test_platform: Rc<TestPlatform>,
+ test_platform: Rc<TestPlatform>,
text_system: Arc<TextSystem>,
}
@@ -76,6 +84,7 @@ impl Context for TestAppContext {
}
impl TestAppContext {
+ /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
pub fn new(dispatcher: TestDispatcher) -> Self {
let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
@@ -95,38 +104,47 @@ impl TestAppContext {
}
}
+ /// returns a new `TestAppContext` re-using the same executors to interleave tasks.
pub fn new_app(&self) -> TestAppContext {
Self::new(self.dispatcher.clone())
}
+ /// Simulates quitting the app.
pub fn quit(&self) {
self.app.borrow_mut().shutdown();
}
+ /// Schedules all windows to be redrawn on the next effect cycle.
pub fn refresh(&mut self) -> Result<()> {
let mut app = self.app.borrow_mut();
app.refresh();
Ok(())
}
+ /// Returns an executor (for running tasks in the background)
pub fn executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
+ /// Returns an executor (for running tasks on the main thread)
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
+ /// Gives you an `&mut AppContext` for the duration of the closure
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
let mut cx = self.app.borrow_mut();
cx.update(f)
}
+ /// Gives you an `&AppContext` for the duration of the closure
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
let cx = self.app.borrow();
f(&*cx)
}
+ /// Adds a new window. The Window will always be backed by a `TestWindow` which
+ /// can be retrieved with `self.test_window(handle)`
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
F: FnOnce(&mut ViewContext<V>) -> V,
@@ -136,12 +154,16 @@ impl TestAppContext {
cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window))
}
+ /// Adds a new window with no content.
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
.any_handle
}
+ /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
+ /// as a `WindowContext` for the rest of the test. Typically you would shadow this context with
+ /// the returned one. `let (view, cx) = cx.add_window_view(...);`
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
@@ -156,18 +178,23 @@ impl TestAppContext {
(view, Box::leak(cx))
}
+ /// returns the TextSystem
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
+ /// Simulates writing to the platform clipboard
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.test_platform.write_to_clipboard(item)
}
+ /// Simulates reading from the platform clipboard.
+ /// This will return the most recent value from `write_to_clipboard`.
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.test_platform.read_from_clipboard()
}
+ /// Simulates choosing a File in the platform's "Open" dialog.
pub fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
@@ -175,22 +202,27 @@ impl TestAppContext {
self.test_platform.simulate_new_path_selection(select_path);
}
+ /// Simulates clicking a button in an platform-level alert dialog.
pub fn simulate_prompt_answer(&self, button_ix: usize) {
self.test_platform.simulate_prompt_answer(button_ix);
}
+ /// Returns true if there's an alert dialog open.
pub fn has_pending_prompt(&self) -> bool {
self.test_platform.has_pending_prompt()
}
+ /// Simulates the user resizing the window to the new size.
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
self.test_window(window_handle).simulate_resize(size);
}
+ /// Returns all windows open in the test.
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.app.borrow().windows().clone()
}
+ /// Run the given task on the main thread.
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
@@ -199,16 +231,20 @@ impl TestAppContext {
self.foreground_executor.spawn(f(self.to_async()))
}
+ /// true if the given global is defined
pub fn has_global<G: 'static>(&self) -> bool {
let app = self.app.borrow();
app.has_global::<G>()
}
+ /// runs the given closure with a reference to the global
+ /// panics if `has_global` would return false.
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
let app = self.app.borrow();
read(app.global(), &app)
}
+ /// runs the given closure with a reference to the global (if set)
pub fn try_read_global<G: 'static, R>(
&self,
read: impl FnOnce(&G, &AppContext) -> R,
@@ -217,11 +253,13 @@ impl TestAppContext {
Some(read(lock.try_global()?, &lock))
}
+ /// sets the global in this context.
pub fn set_global<G: 'static>(&mut self, global: G) {
let mut lock = self.app.borrow_mut();
lock.set_global(global);
}
+ /// updates the global in this context. (panics if `has_global` would return false)
pub fn update_global<G: 'static, R>(
&mut self,
update: impl FnOnce(&mut G, &mut AppContext) -> R,
@@ -230,6 +268,8 @@ impl TestAppContext {
lock.update_global(update)
}
+ /// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background
+ /// thread on the current thread in tests.
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext {
app: Rc::downgrade(&self.app),
@@ -238,10 +278,12 @@ impl TestAppContext {
}
}
+ /// Wait until there are no more pending tasks.
pub fn run_until_parked(&mut self) {
self.background_executor.run_until_parked()
}
+ /// Simulate dispatching an action to the currently focused node in the window.
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
where
A: Action,
@@ -255,7 +297,8 @@ impl TestAppContext {
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
- /// will run backspace on the current editor through the command palette.
+ /// in Zed, this will run backspace on the current editor through the command palette.
+ /// This will also run the background executor until it's parked.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
for keystroke in keystrokes
.split(" ")
@@ -270,7 +313,8 @@ impl TestAppContext {
/// simulate_input takes a string of text to type.
/// cx.simulate_input("abc")
- /// will type abc into your current editor.
+ /// will type abc into your current editor
+ /// This will also run the background executor until it's parked.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke.into(), false);
@@ -279,6 +323,7 @@ impl TestAppContext {
self.background_executor.run_until_parked()
}
+ /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
pub fn dispatch_keystroke(
&mut self,
window: AnyWindowHandle,
@@ -289,6 +334,7 @@ impl TestAppContext {
.simulate_keystroke(keystroke, is_held)
}
+ /// Returns the `TestWindow` backing the given handle.
pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
self.app
.borrow_mut()
@@ -303,6 +349,7 @@ impl TestAppContext {
.clone()
}
+ /// Returns a stream of notifications whenever the View or Model is updated.
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
let (tx, rx) = futures::channel::mpsc::unbounded();
self.update(|cx| {
@@ -319,6 +366,7 @@ impl TestAppContext {
rx
}
+ /// Retuens a stream of events emitted by the given Model.
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
&mut self,
entity: &Model<T>,
@@ -337,6 +385,8 @@ impl TestAppContext {
rx
}
+ /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you
+ /// don't need to jump in at a specific time).
pub async fn condition<T: 'static>(
&mut self,
model: &Model<T>,
@@ -366,6 +416,7 @@ impl TestAppContext {
}
impl<T: Send> Model<T> {
+ /// Block until the next event is emitted by the model, then return it.
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
where
Evt: Send + Clone + 'static,
@@ -395,6 +446,7 @@ impl<T: Send> Model<T> {
}
impl<V: 'static> View<V> {
+ /// Returns a future that resolves when the view is next updated.
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
@@ -421,6 +473,7 @@ impl<V: 'static> View<V> {
}
impl<V> View<V> {
+ /// Returns a future that resolves when the condition becomes true.
pub fn condition<Evt>(
&self,
cx: &TestAppContext,
@@ -471,12 +524,11 @@ impl<V> View<V> {
}
}
- // todo!(start_waiting)
- // cx.borrow().foreground_executor().start_waiting();
+ cx.borrow().background_executor().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
- // cx.borrow().foreground_executor().finish_waiting();
+ cx.borrow().background_executor().finish_waiting();
}
})
.await
@@ -488,6 +540,8 @@ impl<V> View<V> {
use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut, Clone)]
+/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to
+/// run window-specific test code.
pub struct VisualTestContext {
#[deref]
#[deref_mut]
@@ -496,10 +550,14 @@ pub struct VisualTestContext {
}
impl<'a> VisualTestContext {
+ /// Provides the `WindowContext` for the duration of the closure.
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
}
+ /// Create a new VisualTestContext. You would typically shadow the passed in
+ /// TestAppContext with this, as this is typically more useful.
+ /// `let cx = VisualTestContext::from_window(window, cx);`
pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
Self {
cx: cx.clone(),
@@ -507,10 +565,12 @@ impl<'a> VisualTestContext {
}
}
+ /// Wait until there are no more pending tasks.
pub fn run_until_parked(&self) {
self.cx.background_executor.run_until_parked();
}
+ /// Dispatch the action to the currently focused node.
pub fn dispatch_action<A>(&mut self, action: A)
where
A: Action,
@@ -518,24 +578,32 @@ impl<'a> VisualTestContext {
self.cx.dispatch_action(self.window, action)
}
+ /// Read the title off the window (set by `WindowContext#set_window_title`)
pub fn window_title(&mut self) -> Option<String> {
self.cx.test_window(self.window).0.lock().title.clone()
}
+ /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
+ /// Automatically runs until parked.
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
self.cx.simulate_keystrokes(self.window, keystrokes)
}
+ /// Simulate typing text `cx.simulate_input("hello")`
+ /// Automatically runs until parked.
pub fn simulate_input(&mut self, input: &str) {
self.cx.simulate_input(self.window, input)
}
+ /// Simulates the user blurring the window.
pub fn deactivate_window(&mut self) {
if Some(self.window) == self.test_platform.active_window() {
self.test_platform.set_active_window(None)
}
self.background_executor.run_until_parked();
}
+
+ /// Simulates the user closing the window.
/// Returns true if the window was closed.
pub fn simulate_close(&mut self) -> bool {
let handler = self
@@ -672,6 +740,7 @@ impl VisualContext for VisualTestContext {
}
impl AnyWindowHandle {
+ /// Creates the given view in this window.
pub fn build_view<V: Render + 'static>(
&self,
cx: &mut TestAppContext,
@@ -681,6 +750,7 @@ impl AnyWindowHandle {
}
}
+/// An EmptyView for testing.
pub struct EmptyView {}
impl Render for EmptyView {
@@ -31,14 +31,14 @@ pub trait IntoElement: Sized {
/// The specific type of element into which the implementing type is converted.
type Element: Element;
- /// The [ElementId] of self once converted into an [Element].
+ /// The [`ElementId`] of self once converted into an [`Element`].
/// If present, the resulting element's state will be carried across frames.
fn element_id(&self) -> Option<ElementId>;
- /// Convert self into a type that implements [Element].
+ /// Convert self into a type that implements [`Element`].
fn into_element(self) -> Self::Element;
- /// Convert self into a dynamically-typed [AnyElement].
+ /// Convert self into a dynamically-typed [`AnyElement`].
fn into_any_element(self) -> AnyElement {
self.into_element().into_any()
}
@@ -115,7 +115,7 @@ pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
}
-/// You can derive [IntoElement] on any type that implements this trait.
+/// You can derive [`IntoElement`] on any type that implements this trait.
/// It is used to allow views to be expressed in terms of abstract data.
pub trait RenderOnce: 'static {
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
@@ -224,7 +224,7 @@ enum ElementDrawPhase<S> {
},
}
-/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
+/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
impl<E: Element> DrawableElement<E> {
fn new(element: E) -> Self {
DrawableElement {
@@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
- InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
+ InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
StyleRefinement, Styled, WindowContext,
};
use futures::FutureExt;
@@ -12,13 +12,13 @@ use util::ResultExt;
#[derive(Clone, Debug)]
pub enum ImageSource {
/// Image content will be loaded from provided URI at render time.
- Uri(SharedString),
+ Uri(SharedUrl),
Data(Arc<ImageData>),
Surface(CVImageBuffer),
}
-impl From<SharedString> for ImageSource {
- fn from(value: SharedString) -> Self {
+impl From<SharedUrl> for ImageSource {
+ fn from(value: SharedUrl) -> Self {
Self::Uri(value)
}
}
@@ -14,8 +14,8 @@ pub struct Overlay {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
- // todo!();
anchor_position: Option<Point<Pixels>>,
+ // todo!();
// position_mode: OverlayPositionMode,
}
@@ -32,6 +32,12 @@ pub struct ForegroundExecutor {
not_send: PhantomData<Rc<()>>,
}
+/// Task is a primitive that allows work to happen in the background.
+///
+/// It implements [`Future`] so you can `.await` on it.
+///
+/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
+/// the task to continue running in the background, but with no way to return a value.
#[must_use]
#[derive(Debug)]
pub enum Task<T> {
@@ -40,10 +46,12 @@ pub enum Task<T> {
}
impl<T> Task<T> {
+ /// Create a new task that will resolve with the value
pub fn ready(val: T) -> Self {
Task::Ready(Some(val))
}
+ /// Detaching a task runs it to completion in the background
pub fn detach(self) {
match self {
Task::Ready(_) => {}
@@ -57,6 +65,8 @@ where
T: 'static,
E: 'static + Debug,
{
+ /// Run the task to completion in the background and log any
+ /// errors that occur.
#[track_caller]
pub fn detach_and_log_err(self, cx: &mut AppContext) {
let location = core::panic::Location::caller();
@@ -97,6 +107,10 @@ type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
+/// BackgroundExecutor lets you run things on background threads.
+/// In production this is a thread pool with no ordering guarantees.
+/// In tests this is simalated by running tasks one by one in a deterministic
+/// (but arbitrary) order controlled by the `SEED` environment variable.
impl BackgroundExecutor {
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { dispatcher }
@@ -135,6 +149,7 @@ impl BackgroundExecutor {
Task::Spawned(task)
}
+ /// Used by the test harness to run an async test in a syncronous fashion.
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
@@ -145,6 +160,8 @@ impl BackgroundExecutor {
}
}
+ /// Block the current thread until the given future resolves.
+ /// Consider using `block_with_timeout` instead.
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
if let Ok(value) = self.block_internal(true, future, usize::MAX) {
value
@@ -206,6 +223,8 @@ impl BackgroundExecutor {
}
}
+ /// Block the current thread until the given future resolves
+ /// or `duration` has elapsed.
pub fn block_with_timeout<R>(
&self,
duration: Duration,
@@ -238,6 +257,8 @@ impl BackgroundExecutor {
}
}
+ /// Scoped lets you start a number of tasks and waits
+ /// for all of them to complete before returning.
pub async fn scoped<'scope, F>(&self, scheduler: F)
where
F: FnOnce(&mut Scope<'scope>),
@@ -253,6 +274,9 @@ impl BackgroundExecutor {
}
}
+ /// Returns a task that will complete after the given duration.
+ /// Depending on other concurrent tasks the elapsed duration may be longer
+ /// than reqested.
pub fn timer(&self, duration: Duration) -> Task<()> {
let (runnable, task) = async_task::spawn(async move {}, {
let dispatcher = self.dispatcher.clone();
@@ -262,65 +286,81 @@ impl BackgroundExecutor {
Task::Spawned(task)
}
+ /// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
#[cfg(any(test, feature = "test-support"))]
pub fn start_waiting(&self) {
self.dispatcher.as_test().unwrap().start_waiting();
}
+ /// in tests, removes the debugging data added by start_waiting
#[cfg(any(test, feature = "test-support"))]
pub fn finish_waiting(&self) {
self.dispatcher.as_test().unwrap().finish_waiting();
}
+ /// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
#[cfg(any(test, feature = "test-support"))]
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
self.dispatcher.as_test().unwrap().simulate_random_delay()
}
+ /// in tests, indicate that a given task from `spawn_labeled` should run after everything else
#[cfg(any(test, feature = "test-support"))]
pub fn deprioritize(&self, task_label: TaskLabel) {
self.dispatcher.as_test().unwrap().deprioritize(task_label)
}
+ /// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
#[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) {
self.dispatcher.as_test().unwrap().advance_clock(duration)
}
+ /// in tests, run one task.
#[cfg(any(test, feature = "test-support"))]
pub fn tick(&self) -> bool {
self.dispatcher.as_test().unwrap().tick(false)
}
+ /// in tests, run all tasks that are ready to run. If after doing so
+ /// the test still has outstanding tasks, this will panic. (See also `allow_parking`)
#[cfg(any(test, feature = "test-support"))]
pub fn run_until_parked(&self) {
self.dispatcher.as_test().unwrap().run_until_parked()
}
+ /// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
+ /// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
+ /// do take real async time to run.
#[cfg(any(test, feature = "test-support"))]
pub fn allow_parking(&self) {
self.dispatcher.as_test().unwrap().allow_parking();
}
+ /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
#[cfg(any(test, feature = "test-support"))]
pub fn rng(&self) -> StdRng {
self.dispatcher.as_test().unwrap().rng()
}
+ /// How many CPUs are available to the dispatcher
pub fn num_cpus(&self) -> usize {
num_cpus::get()
}
+ /// Whether we're on the main thread.
pub fn is_main_thread(&self) -> bool {
self.dispatcher.is_main_thread()
}
#[cfg(any(test, feature = "test-support"))]
+ /// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
}
}
+/// ForegroundExecutor runs things on the main thread.
impl ForegroundExecutor {
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self {
@@ -329,8 +369,7 @@ impl ForegroundExecutor {
}
}
- /// Enqueues the given closure to be run on any thread. The closure returns
- /// a future which will be run to completion on any available thread.
+ /// Enqueues the given Task to run on the main thread at some point in the future.
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
where
R: 'static,
@@ -350,6 +389,7 @@ impl ForegroundExecutor {
}
}
+/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
pub struct Scope<'a> {
executor: BackgroundExecutor,
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
@@ -18,6 +18,7 @@ mod platform;
pub mod prelude;
mod scene;
mod shared_string;
+mod shared_url;
mod style;
mod styled;
mod subscription;
@@ -67,6 +68,7 @@ pub use refineable::*;
pub use scene::*;
use seal::Sealed;
pub use shared_string::*;
+pub use shared_url::*;
pub use smol::Timer;
pub use style::*;
pub use styled::*;
@@ -1,4 +1,4 @@
-use crate::{ImageData, ImageId, SharedString};
+use crate::{ImageData, ImageId, SharedUrl};
use collections::HashMap;
use futures::{
future::{BoxFuture, Shared},
@@ -44,7 +44,7 @@ impl From<ImageError> for Error {
pub struct ImageCache {
client: Arc<dyn HttpClient>,
- images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
+ images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
}
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
@@ -59,7 +59,7 @@ impl ImageCache {
pub fn get(
&self,
- uri: impl Into<SharedString>,
+ uri: impl Into<SharedUrl>,
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
let uri = uri.into();
let mut images = self.images.lock();
@@ -34,7 +34,7 @@ pub trait InputHandler: 'static + Sized {
) -> Option<Bounds<Pixels>>;
}
-/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input`
+/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`]
/// with an instance during your element's paint.
pub struct ElementInputHandler<V> {
view: View<V>,
@@ -178,6 +178,20 @@ impl ScrollDelta {
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
}
}
+
+ pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
+ match (self, other) {
+ (ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
+ ScrollDelta::Pixels(px_a + px_b)
+ }
+
+ (ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
+ ScrollDelta::Lines(lines_a + lines_b)
+ }
+
+ _ => other,
+ }
+ }
}
#[derive(Clone, Debug, Default)]
@@ -192,8 +192,8 @@ impl DispatchTree {
keymap
.bindings_for_action(action)
.filter(|binding| {
- for i in 1..context_stack.len() {
- let context = &context_stack[0..i];
+ for i in 0..context_stack.len() {
+ let context = &context_stack[0..=i];
if keymap.binding_enabled(binding, context) {
return true;
}
@@ -283,3 +283,76 @@ impl DispatchTree {
*self.node_stack.last().unwrap()
}
}
+
+#[cfg(test)]
+mod tests {
+ use std::{rc::Rc, sync::Arc};
+
+ use parking_lot::Mutex;
+
+ use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap};
+
+ #[derive(PartialEq, Eq)]
+ struct TestAction;
+
+ impl Action for TestAction {
+ fn name(&self) -> &'static str {
+ "test::TestAction"
+ }
+
+ fn debug_name() -> &'static str
+ where
+ Self: ::std::marker::Sized,
+ {
+ "test::TestAction"
+ }
+
+ fn partial_eq(&self, action: &dyn Action) -> bool {
+ action
+ .as_any()
+ .downcast_ref::<Self>()
+ .map_or(false, |a| self == a)
+ }
+
+ fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
+ Box::new(TestAction)
+ }
+
+ fn as_any(&self) -> &dyn ::std::any::Any {
+ self
+ }
+
+ fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
+ where
+ Self: Sized,
+ {
+ Ok(Box::new(TestAction))
+ }
+ }
+
+ #[test]
+ fn test_keybinding_for_action_bounds() {
+ let keymap = Keymap::new(vec![KeyBinding::new(
+ "cmd-n",
+ TestAction,
+ Some("ProjectPanel"),
+ )]);
+
+ let mut registry = ActionRegistry::default();
+
+ registry.load_action::<TestAction>();
+
+ let keymap = Arc::new(Mutex::new(keymap));
+
+ let tree = DispatchTree::new(keymap, Rc::new(registry));
+
+ let contexts = vec![
+ KeyContext::parse("Workspace").unwrap(),
+ KeyContext::parse("ProjectPanel").unwrap(),
+ ];
+
+ let keybinding = tree.bindings_for_action(&TestAction, &contexts);
+
+ assert!(keybinding[0].action.partial_eq(&TestAction))
+ }
+}
@@ -14,12 +14,12 @@ pub struct MacDisplay(pub(crate) CGDirectDisplayID);
unsafe impl Send for MacDisplay {}
impl MacDisplay {
- /// Get the screen with the given [DisplayId].
+ /// Get the screen with the given [`DisplayId`].
pub fn find_by_id(id: DisplayId) -> Option<Self> {
Self::all().find(|screen| screen.id() == id)
}
- /// Get the screen with the given persistent [Uuid].
+ /// Get the screen with the given persistent [`Uuid`].
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
}
@@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
}
fn as_any(&self) -> &dyn std::any::Any {
- todo!()
+ unimplemented!()
}
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
@@ -15,6 +15,7 @@ use std::{
time::Duration,
};
+/// TestPlatform implements the Platform trait for use in tests.
pub struct TestPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
@@ -103,7 +104,6 @@ impl TestPlatform {
}
}
-// todo!("implement out what our tests needed in GPUI 1")
impl Platform for TestPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
@@ -0,0 +1,25 @@
+use derive_more::{Deref, DerefMut};
+
+use crate::SharedString;
+
+/// A [`SharedString`] containing a URL.
+#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)]
+pub struct SharedUrl(SharedString);
+
+impl std::fmt::Debug for SharedUrl {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl std::fmt::Display for SharedUrl {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0.as_ref())
+ }
+}
+
+impl<T: Into<SharedString>> From<T> for SharedUrl {
+ fn from(value: T) -> Self {
+ Self(value.into())
+ }
+}
@@ -37,10 +37,10 @@ where
})))
}
- /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
+ /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions
/// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
- /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
- /// to activate the `[Subscription]`.
+ /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter
+ /// to activate the [`Subscription`].
#[must_use]
pub fn insert(
&self,
@@ -1,3 +1,30 @@
+//! Test support for GPUI.
+//!
+//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context,
+//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run
+//! deterministically even in the face of arbitrary parallelism.
+//!
+//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test`
+//! or `cargo-nextest`, or another runner of your choice.
+//!
+//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts
+//! as you need.
+//!
+//! ## Example
+//!
+//! ```
+//! use gpui;
+//!
+//! #[gpui::test]
+//! async fn test_example(cx: &TestAppContext) {
+//! assert!(true)
+//! }
+//!
+//! #[gpui::test]
+//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) {
+//! assert!(true)
+//! }
+//! ```
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
use futures::StreamExt as _;
use rand::prelude::*;
@@ -68,6 +95,7 @@ impl<T: 'static> futures::Stream for Observation<T> {
}
}
+/// observe returns a stream of the change events from the given `View` or `Model`
pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
let (tx, rx) = smol::channel::unbounded();
let _subscription = cx.update(|cx| {
@@ -1,3 +1,5 @@
+#![deny(missing_docs)]
+
use crate::{
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef,
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
@@ -85,10 +87,12 @@ pub enum DispatchPhase {
}
impl DispatchPhase {
+ /// Returns true if this represents the "bubble" phase.
pub fn bubble(self) -> bool {
self == DispatchPhase::Bubble
}
+ /// Returns true if this represents the "capture" phase.
pub fn capture(self) -> bool {
self == DispatchPhase::Capture
}
@@ -103,7 +107,10 @@ struct FocusEvent {
current_focus_path: SmallVec<[FocusId; 8]>,
}
-slotmap::new_key_type! { pub struct FocusId; }
+slotmap::new_key_type! {
+ /// A globally unique identifier for a focusable element.
+ pub struct FocusId;
+}
thread_local! {
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(4 * 1024 * 1024));
@@ -231,6 +238,7 @@ impl Drop for FocusHandle {
/// FocusableView allows users of your view to easily
/// focus it (using cx.focus_view(view))
pub trait FocusableView: 'static + Render {
+ /// Returns the focus handle associated with this view.
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
@@ -240,9 +248,11 @@ pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
+/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal.
pub struct DismissEvent;
// Holds the state for a specific window.
+#[doc(hidden)]
pub struct Window {
pub(crate) handle: AnyWindowHandle,
pub(crate) removed: bool,
@@ -434,6 +444,7 @@ impl Window {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[repr(C)]
pub struct ContentMask<P: Clone + Default + Debug> {
+ /// The bounds
pub bounds: Bounds<P>,
}
@@ -453,8 +464,8 @@ impl ContentMask<Pixels> {
}
/// Provides access to application state in the context of a single window. Derefs
-/// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes
-/// an `AppContext` and call any `AppContext` methods.
+/// to an [`AppContext`], so you can also pass a [`WindowContext`] to any method that takes
+/// an [`AppContext`] and call any [`AppContext`] methods.
pub struct WindowContext<'a> {
pub(crate) app: &'a mut AppContext,
pub(crate) window: &'a mut Window,
@@ -482,20 +493,20 @@ impl<'a> WindowContext<'a> {
self.window.removed = true;
}
- /// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus
+ /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
/// for elements rendered within this window.
pub fn focus_handle(&mut self) -> FocusHandle {
FocusHandle::new(&self.window.focus_handles)
}
- /// Obtain the currently focused `FocusHandle`. If no elements are focused, returns `None`.
+ /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`.
pub fn focused(&self) -> Option<FocusHandle> {
self.window
.focus
.and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
}
- /// Move focus to the element associated with the given `FocusHandle`.
+ /// Move focus to the element associated with the given [`FocusHandle`].
pub fn focus(&mut self, handle: &FocusHandle) {
if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
return;
@@ -525,11 +536,13 @@ impl<'a> WindowContext<'a> {
self.notify();
}
+ /// Blur the window and don't allow anything in it to be focused again.
pub fn disable_focus(&mut self) {
self.blur();
self.window.focus_enabled = false;
}
+ /// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused();
@@ -591,6 +604,9 @@ impl<'a> WindowContext<'a> {
});
}
+ /// Subscribe to events emitted by a model or view.
+ /// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
+ /// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
pub fn subscribe<Emitter, E, Evt>(
&mut self,
entity: &E,
@@ -754,6 +770,9 @@ impl<'a> WindowContext<'a> {
.request_measured_layout(style, rem_size, measure)
}
+ /// Compute the layout for the given id within the given available space.
+ /// This method is called for its side effect, typically by the framework prior to painting.
+ /// After calling it, you can request the bounds of the given layout node id or any descendant.
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
let mut layout_engine = self.window.layout_engine.take().unwrap();
layout_engine.compute_layout(layout_id, available_space, self);
@@ -788,30 +807,37 @@ impl<'a> WindowContext<'a> {
.retain(&(), |callback| callback(self));
}
+ /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays.
pub fn window_bounds(&self) -> WindowBounds {
self.window.bounds
}
+ /// Returns the size of the drawable area within the window.
pub fn viewport_size(&self) -> Size<Pixels> {
self.window.viewport_size
}
+ /// Returns whether this window is focused by the operating system (receiving key events).
pub fn is_window_active(&self) -> bool {
self.window.active
}
+ /// Toggle zoom on the window.
pub fn zoom_window(&self) {
self.window.platform_window.zoom();
}
+ /// Update the window's title at the platform level.
pub fn set_window_title(&mut self, title: &str) {
self.window.platform_window.set_title(title);
}
+ /// Mark the window as dirty at the platform level.
pub fn set_window_edited(&mut self, edited: bool) {
self.window.platform_window.set_edited(edited);
}
+ /// Determine the display on which the window is visible.
pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.platform
.displays()
@@ -819,6 +845,7 @@ impl<'a> WindowContext<'a> {
.find(|display| display.id() == self.window.display_id)
}
+ /// Show the platform character palette.
pub fn show_character_palette(&self) {
self.window.platform_window.show_character_palette();
}
@@ -936,6 +963,7 @@ impl<'a> WindowContext<'a> {
.on_action(action_type, ArenaRef::from(listener));
}
+ /// Determine whether the given action is available along the dispatch path to the currently focused element.
pub fn is_action_available(&self, action: &dyn Action) -> bool {
let target = self
.focused()
@@ -962,6 +990,7 @@ impl<'a> WindowContext<'a> {
self.window.modifiers
}
+ /// Update the cursor style at the platform level.
pub fn set_cursor_style(&mut self, style: CursorStyle) {
self.window.requested_cursor_style = Some(style)
}
@@ -991,7 +1020,7 @@ impl<'a> WindowContext<'a> {
true
}
- pub fn was_top_layer_under_active_drag(
+ pub(crate) fn was_top_layer_under_active_drag(
&self,
point: &Point<Pixels>,
level: &StackingOrder,
@@ -1649,6 +1678,7 @@ impl<'a> WindowContext<'a> {
self.dispatch_keystroke_observers(event, None);
}
+ /// Determine whether a potential multi-stroke key binding is in progress on this window.
pub fn has_pending_keystrokes(&self) -> bool {
self.window
.rendered_frame
@@ -1715,27 +1745,34 @@ impl<'a> WindowContext<'a> {
subscription
}
+ /// Focus the current window and bring it to the foreground at the platform level.
pub fn activate_window(&self) {
self.window.platform_window.activate();
}
+ /// Minimize the current window at the platform level.
pub fn minimize_window(&self) {
self.window.platform_window.minimize();
}
+ /// Toggle full screen status on the current window at the platform level.
pub fn toggle_full_screen(&self) {
self.window.platform_window.toggle_full_screen();
}
+ /// Present a platform dialog.
+ /// The provided message will be presented, along with buttons for each answer.
+ /// When a button is clicked, the returned Receiver will receive the index of the clicked button.
pub fn prompt(
&self,
level: PromptLevel,
- msg: &str,
+ message: &str,
answers: &[&str],
) -> oneshot::Receiver<usize> {
- self.window.platform_window.prompt(level, msg, answers)
+ self.window.platform_window.prompt(level, message, answers)
}
+ /// Returns all available actions for the focused element.
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
let node_id = self
.window
@@ -1754,6 +1791,7 @@ impl<'a> WindowContext<'a> {
.available_actions(node_id)
}
+ /// Returns key bindings that invoke the given action on the currently focused element.
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.window
.rendered_frame
@@ -1764,6 +1802,7 @@ impl<'a> WindowContext<'a> {
)
}
+ /// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
pub fn bindings_for_action_in(
&self,
action: &dyn Action,
@@ -1782,6 +1821,7 @@ impl<'a> WindowContext<'a> {
dispatch_tree.bindings_for_action(action, &context_stack)
}
+ /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
pub fn listener_for<V: Render, E>(
&self,
view: &View<V>,
@@ -1793,6 +1833,7 @@ impl<'a> WindowContext<'a> {
}
}
+ /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle.
pub fn handler_for<V: Render>(
&self,
view: &View<V>,
@@ -1804,7 +1845,8 @@ impl<'a> WindowContext<'a> {
}
}
- //========== ELEMENT RELATED FUNCTIONS ===========
+ /// Invoke the given function with the given focus handle present on the key dispatch stack.
+ /// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint.
pub fn with_key_dispatch<R>(
&mut self,
context: Option<KeyContext>,
@@ -1843,6 +1885,8 @@ impl<'a> WindowContext<'a> {
}
}
+ /// Register a callback that can interrupt the closing of the current window based the returned boolean.
+ /// If the callback returns false, the window won't be closed.
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
let mut this = self.to_async();
self.window
@@ -2017,19 +2061,24 @@ impl<'a> BorrowMut<AppContext> for WindowContext<'a> {
}
}
+/// This trait contains functionality that is shared across [`ViewContext`] and [`WindowContext`]
pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
+ #[doc(hidden)]
fn app_mut(&mut self) -> &mut AppContext {
self.borrow_mut()
}
+ #[doc(hidden)]
fn app(&self) -> &AppContext {
self.borrow()
}
+ #[doc(hidden)]
fn window(&self) -> &Window {
self.borrow()
}
+ #[doc(hidden)]
fn window_mut(&mut self) -> &mut Window {
self.borrow_mut()
}
@@ -2279,6 +2328,10 @@ impl BorrowMut<Window> for WindowContext<'_> {
impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
+/// Provides access to application state that is specialized for a particular [`View`].
+/// Allows you to interact with focus, emit events, etc.
+/// ViewContext also derefs to [`WindowContext`], giving you access to all of its methods as well.
+/// When you call [`View::update`], you're passed a `&mut V` and an `&mut ViewContext<V>`.
pub struct ViewContext<'a, V> {
window_cx: WindowContext<'a>,
view: &'a View<V>,
@@ -2316,14 +2369,17 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
+ /// Get the entity_id of this view.
pub fn entity_id(&self) -> EntityId {
self.view.entity_id()
}
+ /// Get the view pointer underlying this context.
pub fn view(&self) -> &View<V> {
self.view
}
+ /// Get the model underlying this view.
pub fn model(&self) -> &Model<V> {
&self.view.model
}
@@ -2333,6 +2389,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
&mut self.window_cx
}
+ /// Set a given callback to be run on the next frame.
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
where
V: 'static,
@@ -2350,6 +2407,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ /// Observe another model or view for changes to its state, as tracked by [`ModelContext::notify`].
pub fn observe<V2, E>(
&mut self,
entity: &E,
@@ -2383,6 +2441,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Subscribe to events emitted by another model or view.
+ /// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
+ /// The callback will be invoked with a reference to the current view, a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a view context for the current view.
pub fn subscribe<V2, E, Evt>(
&mut self,
entity: &E,
@@ -2440,6 +2501,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Register a callback to be invoked when the given Model or View is released.
pub fn observe_release<V2, E>(
&mut self,
entity: &E,
@@ -2466,6 +2528,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty.
+ /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn.
pub fn notify(&mut self) {
if !self.window.drawing {
self.window_cx.notify();
@@ -2475,6 +2539,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
+ /// Register a callback to be invoked when the window is resized.
pub fn observe_window_bounds(
&mut self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
@@ -2488,6 +2553,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Register a callback to be invoked when the window is activated or deactivated.
pub fn observe_window_activation(
&mut self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
@@ -2620,6 +2686,10 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Schedule a future to be run asynchronously.
+ /// The given callback is invoked with a [`WeakView<V>`] to avoid leaking the view for a long-running process.
+ /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points.
+ /// The returned future will be polled on the main thread.
pub fn spawn<Fut, R>(
&mut self,
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
@@ -2632,6 +2702,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.window_cx.spawn(|cx| f(view, cx))
}
+ /// Update the global state of the given type.
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: 'static,
@@ -2642,6 +2713,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
result
}
+ /// Register a callback to be invoked when the given global state changes.
pub fn observe_global<G: 'static>(
&mut self,
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
@@ -2660,6 +2732,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
+ /// Add a listener for any mouse event that occurs in the window.
+ /// This is a fairly low level method.
+ /// Typically, you'll want to use methods on UI elements, which perform bounds checking etc.
pub fn on_mouse_event<Event: 'static>(
&mut self,
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
@@ -2672,6 +2747,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ /// Register a callback to be invoked when the given Key Event is dispatched to the window.
pub fn on_key_event<Event: 'static>(
&mut self,
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
@@ -2684,6 +2760,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ /// Register a callback to be invoked when the given Action type is dispatched to the window.
pub fn on_action(
&mut self,
action_type: TypeId,
@@ -2698,6 +2775,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ /// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe].
pub fn emit<Evt>(&mut self, event: Evt)
where
Evt: 'static,
@@ -2711,6 +2789,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ /// Move focus to the current view, assuming it implements [`FocusableView`].
pub fn focus_self(&mut self)
where
V: FocusableView,
@@ -2718,6 +2797,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
}
+ /// Convenience method for accessing view state in an event callback.
+ ///
+ /// Many GPUI callbacks take the form of `Fn(&E, &mut WindowContext)`,
+ /// but it's often useful to be able to access view state in these
+ /// callbacks. This method provides a convenient way to do so.
pub fn listener<E>(
&self,
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
@@ -2827,14 +2911,20 @@ impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> {
}
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
-slotmap::new_key_type! { pub struct WindowId; }
+slotmap::new_key_type! {
+ /// A unique identifier for a window.
+ pub struct WindowId;
+}
impl WindowId {
+ /// Converts this window ID to a `u64`.
pub fn as_u64(&self) -> u64 {
self.0.as_ffi()
}
}
+/// A handle to a window with a specific root view type.
+/// Note that this does not keep the window alive on its own.
#[derive(Deref, DerefMut)]
pub struct WindowHandle<V> {
#[deref]
@@ -2844,6 +2934,8 @@ pub struct WindowHandle<V> {
}
impl<V: 'static + Render> WindowHandle<V> {
+ /// Create a new handle from a window ID.
+ /// This does not check if the root type of the window is `V`.
pub fn new(id: WindowId) -> Self {
WindowHandle {
any_handle: AnyWindowHandle {
@@ -2854,6 +2946,9 @@ impl<V: 'static + Render> WindowHandle<V> {
}
}
+ /// Get the root view out of this window.
+ ///
+ /// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
where
C: Context,
@@ -2865,6 +2960,9 @@ impl<V: 'static + Render> WindowHandle<V> {
}))
}
+ /// Update the root view of this window.
+ ///
+ /// This will fail if the window has been closed or if the root view's type does not match
pub fn update<C, R>(
&self,
cx: &mut C,
@@ -2881,6 +2979,9 @@ impl<V: 'static + Render> WindowHandle<V> {
})?
}
+ /// Read the root view out of this window.
+ ///
+ /// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> {
let x = cx
.windows
@@ -2897,6 +2998,9 @@ impl<V: 'static + Render> WindowHandle<V> {
Ok(x.read(cx))
}
+ /// Read the root view out of this window, with a callback
+ ///
+ /// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
where
C: Context,
@@ -2904,6 +3008,9 @@ impl<V: 'static + Render> WindowHandle<V> {
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
}
+ /// Read the root view pointer off of this window.
+ ///
+ /// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
where
C: Context,
@@ -2911,6 +3018,9 @@ impl<V: 'static + Render> WindowHandle<V> {
cx.read_window(self, |root_view, _cx| root_view.clone())
}
+ /// Check if this window is 'active'.
+ ///
+ /// Will return `None` if the window is closed.
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
cx.windows
.get(self.id)
@@ -2946,6 +3056,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
}
}
+/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct AnyWindowHandle {
pub(crate) id: WindowId,
@@ -2953,10 +3064,13 @@ pub struct AnyWindowHandle {
}
impl AnyWindowHandle {
+ /// Get the ID of this window.
pub fn window_id(&self) -> WindowId {
self.id
}
+ /// Attempt to convert this handle to a window handle with a specific root view type.
+ /// If the types do not match, this will return `None`.
pub fn downcast<T: 'static>(&self) -> Option<WindowHandle<T>> {
if TypeId::of::<T>() == self.state_type {
Some(WindowHandle {
@@ -2968,6 +3082,9 @@ impl AnyWindowHandle {
}
}
+ /// Update the state of the root view of this window.
+ ///
+ /// This will fail if the window has been closed.
pub fn update<C, R>(
self,
cx: &mut C,
@@ -2979,6 +3096,9 @@ impl AnyWindowHandle {
cx.update_window(self, update)
}
+ /// Read the state of the root view of this window.
+ ///
+ /// This will fail if the window has been closed.
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
where
C: Context,
@@ -2999,12 +3119,21 @@ impl AnyWindowHandle {
// }
// }
+/// An identifier for an [`Element`](crate::Element).
+///
+/// Can be constructed with a string, a number, or both, as well
+/// as other internal representations.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ElementId {
+ /// The ID of a View element
View(EntityId),
+ /// An integer ID.
Integer(usize),
+ /// A string based ID.
Name(SharedString),
+ /// An ID that's equated with a focus handle.
FocusHandle(FocusId),
+ /// A combination of a name and an integer.
NamedInteger(SharedString, usize),
}
@@ -3074,7 +3203,8 @@ impl From<(&'static str, u64)> for ElementId {
}
}
-/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
+/// A rectangle to be rendered in the window at the given position and size.
+/// Passed as an argument [`WindowContext::paint_quad`].
#[derive(Clone)]
pub struct PaintQuad {
bounds: Bounds<Pixels>,
@@ -18,33 +18,33 @@ fn test_action_macros() {
impl gpui::Action for RegisterableAction {
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
- todo!()
+ unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
- todo!()
+ unimplemented!()
}
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
- todo!()
+ unimplemented!()
}
fn name(&self) -> &str {
- todo!()
+ unimplemented!()
}
fn debug_name() -> &'static str
where
Self: Sized,
{
- todo!()
+ unimplemented!()
}
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
where
Self: Sized,
{
- todo!()
+ unimplemented!()
}
}
}
@@ -7,26 +7,56 @@ mod test;
use proc_macro::TokenStream;
#[proc_macro]
+/// register_action! can be used to register an action with the GPUI runtime.
+/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead,
+/// but this can be used for fine grained customization.
pub fn register_action(ident: TokenStream) -> TokenStream {
register_action::register_action_macro(ident)
}
#[proc_macro_derive(IntoElement)]
+// #[derive(IntoElement)] is used to create a Component out of anything that implements
+// the `RenderOnce` trait.
pub fn derive_into_element(input: TokenStream) -> TokenStream {
derive_into_element::derive_into_element(input)
}
#[proc_macro_derive(Render)]
+#[doc(hidden)]
pub fn derive_render(input: TokenStream) -> TokenStream {
derive_render::derive_render(input)
}
+// Used by gpui to generate the style helpers.
#[proc_macro]
+#[doc(hidden)]
pub fn style_helpers(input: TokenStream) -> TokenStream {
style_helpers::style_helpers(input)
}
#[proc_macro_attribute]
+/// #[gpui::test] can be used to annotate test functions that run with GPUI support.
+/// it supports both synchronous and asynchronous tests, and can provide you with
+/// as many `TestAppContext` instances as you need.
+/// The output contains a `#[test]` annotation so this can be used with any existing
+/// test harness (`cargo test` or `cargo-nextest`).
+///
+/// ```
+/// #[gpui::test]
+/// async fn test_foo(mut cx: &TestAppContext) { }
+/// ```
+///
+/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance.
+/// this will be seeded with the `SEED` environment variable and is used internally by
+/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests.
+/// Using the same `StdRng` for behaviour in your test will allow you to exercise a wide
+/// variety of scenarios and interleavings just by changing the seed.
+///
+/// #[gpui::test] also takes three different arguments:
+/// - `#[gpui::test(interations=10)]` will run the test ten times with a different initial SEED.
+/// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass.
+/// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the
+/// tests fail so that you can write out more detail about the failure.
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
}
@@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() {
let (buffer, syntax_map) = test_edit_sequence(
"Rust",
&[
- "fn a() { dbg }",
- "fn a() { dbg«!» }",
- "fn a() { dbg!«()» }",
- "fn a() { dbg!(«b») }",
- "fn a() { dbg!(b«.») }",
- "fn a() { dbg!(b.«c») }",
- "fn a() { dbg!(b.c«()») }",
- "fn a() { dbg!(b.c(«vec»)) }",
- "fn a() { dbg!(b.c(vec«!»)) }",
- "fn a() { dbg!(b.c(vec!«[]»)) }",
- "fn a() { dbg!(b.c(vec![«d»])) }",
- "fn a() { dbg!(b.c(vec![d«.»])) }",
- "fn a() { dbg!(b.c(vec![d.«e»])) }",
+ "fn a() { test_macro }",
+ "fn a() { test_macro«!» }",
+ "fn a() { test_macro!«()» }",
+ "fn a() { test_macro!(«b») }",
+ "fn a() { test_macro!(b«.») }",
+ "fn a() { test_macro!(b.«c») }",
+ "fn a() { test_macro!(b.c«()») }",
+ "fn a() { test_macro!(b.c(«vec»)) }",
+ "fn a() { test_macro!(b.c(vec«!»)) }",
+ "fn a() { test_macro!(b.c(vec!«[]»)) }",
+ "fn a() { test_macro!(b.c(vec![«d»])) }",
+ "fn a() { test_macro!(b.c(vec![d«.»])) }",
+ "fn a() { test_macro!(b.c(vec![d.«e»])) }",
],
);
@@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() {
&syntax_map,
&buffer,
&["field"],
- "fn a() { dbg!(b.«c»(vec![d.«e»])) }",
+ "fn a() { test_macro!(b.«c»(vec![d.«e»])) }",
);
}
@@ -30,7 +30,7 @@ use std::{
sync::Arc,
};
use theme::ThemeSettings;
-use ui::{prelude::*, v_stack, ContextMenu, IconElement, KeyBinding, Label, ListItem};
+use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem};
use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -1403,7 +1403,7 @@ impl ProjectPanel {
.indent_step_size(px(settings.indent_size))
.selected(is_selected)
.child(if let Some(icon) = &icon {
- div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
+ div().child(Icon::from_path(icon.to_string()).color(Color::Muted))
} else {
div().size(IconSize::default().rems()).invisible()
})
@@ -1433,6 +1433,9 @@ impl ProjectPanel {
}))
.on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, cx| {
+ // Stop propagation to prevent the catch-all context menu for the project
+ // panel from being deployed.
+ cx.stop_propagation();
this.deploy_context_menu(event.position, entry_id, cx);
},
)),
@@ -1587,7 +1590,7 @@ impl Render for DraggedProjectEntryView {
.indent_level(self.details.depth)
.indent_step_size(px(settings.indent_size))
.child(if let Some(icon) = &self.details.icon {
- div().child(IconElement::from_path(icon.to_string()))
+ div().child(Icon::from_path(icon.to_string()))
} else {
div()
})
@@ -1637,8 +1640,8 @@ impl Panel for ProjectPanel {
cx.notify();
}
- fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
- Some(ui::Icon::FileTree)
+ fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
+ Some(ui::IconName::FileTree)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -6,7 +6,7 @@ use gpui::{
Subscription, View, ViewContext, WeakView,
};
use search::{buffer_search, BufferSearchBar};
-use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
@@ -43,7 +43,7 @@ impl Render for QuickActionBar {
let inlay_hints_button = Some(QuickActionBarButton::new(
"toggle inlay hints",
- Icon::InlayHint,
+ IconName::InlayHint,
editor.read(cx).inlay_hints_enabled(),
Box::new(editor::ToggleInlayHints),
"Toggle Inlay Hints",
@@ -60,7 +60,7 @@ impl Render for QuickActionBar {
let search_button = Some(QuickActionBarButton::new(
"toggle buffer search",
- Icon::MagnifyingGlass,
+ IconName::MagnifyingGlass,
!self.buffer_search_bar.read(cx).is_dismissed(),
Box::new(buffer_search::Deploy { focus: false }),
"Buffer Search",
@@ -77,7 +77,7 @@ impl Render for QuickActionBar {
let assistant_button = QuickActionBarButton::new(
"toggle inline assistant",
- Icon::MagicWand,
+ IconName::MagicWand,
false,
Box::new(InlineAssist),
"Inline Assist",
@@ -107,7 +107,7 @@ impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
#[derive(IntoElement)]
struct QuickActionBarButton {
id: ElementId,
- icon: Icon,
+ icon: IconName,
toggled: bool,
action: Box<dyn Action>,
tooltip: SharedString,
@@ -117,7 +117,7 @@ struct QuickActionBarButton {
impl QuickActionBarButton {
fn new(
id: impl Into<ElementId>,
- icon: Icon,
+ icon: IconName,
toggled: bool,
action: Box<dyn Action>,
tooltip: impl Into<SharedString>,
@@ -25,10 +25,10 @@ const CHUNK_BASE: usize = 6;
#[cfg(not(test))]
const CHUNK_BASE: usize = 16;
-/// Type alias to [HashMatrix], an implementation of a homomorphic hash function. Two [Rope] instances
+/// Type alias to [`HashMatrix`], an implementation of a homomorphic hash function. Two [`Rope`] instances
/// containing the same text will produce the same fingerprint. This hash function is special in that
-/// it allows us to hash individual chunks and aggregate them up the [Rope]'s tree, with the resulting
-/// hash being equivalent to hashing all the text contained in the [Rope] at once.
+/// it allows us to hash individual chunks and aggregate them up the [`Rope`]'s tree, with the resulting
+/// hash being equivalent to hashing all the text contained in the [`Rope`] at once.
pub type RopeFingerprint = HashMatrix;
#[derive(Clone, Default)]
@@ -76,30 +76,35 @@ impl Notification {
}
}
-#[test]
-fn test_notification() {
- // Notifications can be serialized and deserialized.
- for notification in [
- Notification::ContactRequest { sender_id: 1 },
- Notification::ContactRequestAccepted { responder_id: 2 },
- Notification::ChannelInvitation {
- channel_id: 100,
- channel_name: "the-channel".into(),
- inviter_id: 50,
- },
- Notification::ChannelMessageMention {
- sender_id: 200,
- channel_id: 30,
- message_id: 1,
- },
- ] {
- let message = notification.to_proto();
- let deserialized = Notification::from_proto(&message).unwrap();
- assert_eq!(deserialized, notification);
- }
+#[cfg(test)]
+mod tests {
+ use crate::Notification;
+
+ #[test]
+ fn test_notification() {
+ // Notifications can be serialized and deserialized.
+ for notification in [
+ Notification::ContactRequest { sender_id: 1 },
+ Notification::ContactRequestAccepted { responder_id: 2 },
+ Notification::ChannelInvitation {
+ channel_id: 100,
+ channel_name: "the-channel".into(),
+ inviter_id: 50,
+ },
+ Notification::ChannelMessageMention {
+ sender_id: 200,
+ channel_id: 30,
+ message_id: 1,
+ },
+ ] {
+ let message = notification.to_proto();
+ let deserialized = Notification::from_proto(&message).unwrap();
+ assert_eq!(deserialized, notification);
+ }
- // When notifications are serialized, the `kind` and `actor_id` fields are
- // stored separately, and do not appear redundantly in the JSON.
- let notification = Notification::ContactRequest { sender_id: 1 };
- assert_eq!(notification.to_proto().content, "{}");
+ // When notifications are serialized, the `kind` and `actor_id` fields are
+ // stored separately, and do not appear redundantly in the JSON.
+ let notification = Notification::ContactRequest { sender_id: 1 };
+ assert_eq!(notification.to_proto().content, "{}");
+ }
}
@@ -21,7 +21,7 @@ use settings::Settings;
use std::{any::Any, sync::Arc};
use theme::ThemeSettings;
-use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip};
+use ui::{h_stack, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip};
use util::ResultExt;
use workspace::{
item::ItemHandle,
@@ -43,7 +43,7 @@ pub enum Event {
}
pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|editor: &mut Workspace, _| BufferSearchBar::register(editor))
+ cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
.detach();
}
@@ -225,7 +225,7 @@ impl Render for BufferSearchBar {
.border_color(editor_border)
.min_w(rems(384. / 16.))
.rounded_lg()
- .child(IconElement::new(Icon::MagnifyingGlass))
+ .child(Icon::new(IconName::MagnifyingGlass))
.child(self.render_text_input(&self.query_editor, cx))
.children(supported_options.case.then(|| {
self.render_search_option_button(
@@ -287,7 +287,7 @@ impl Render for BufferSearchBar {
this.child(
IconButton::new(
"buffer-search-bar-toggle-replace-button",
- Icon::Replace,
+ IconName::Replace,
)
.style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| {
@@ -323,7 +323,7 @@ impl Render for BufferSearchBar {
)
.when(should_show_replace_input, |this| {
this.child(
- IconButton::new("search-replace-next", ui::Icon::ReplaceNext)
+ IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
.tooltip(move |cx| {
Tooltip::for_action("Replace next", &ReplaceNext, cx)
})
@@ -332,7 +332,7 @@ impl Render for BufferSearchBar {
})),
)
.child(
- IconButton::new("search-replace-all", ui::Icon::ReplaceAll)
+ IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
.tooltip(move |cx| {
Tooltip::for_action("Replace all", &ReplaceAll, cx)
})
@@ -350,7 +350,7 @@ impl Render for BufferSearchBar {
.gap_0p5()
.flex_none()
.child(
- IconButton::new("select-all", ui::Icon::SelectAll)
+ IconButton::new("select-all", ui::IconName::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
.tooltip(|cx| {
Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
@@ -358,13 +358,13 @@ impl Render for BufferSearchBar {
)
.children(match_count)
.child(render_nav_button(
- ui::Icon::ChevronLeft,
+ ui::IconName::ChevronLeft,
self.active_match_index.is_some(),
"Select previous match",
&SelectPrevMatch,
))
.child(render_nav_button(
- ui::Icon::ChevronRight,
+ ui::IconName::ChevronRight,
self.active_match_index.is_some(),
"Select next match",
&SelectNextMatch,
@@ -479,6 +479,11 @@ impl SearchActionsRegistrar for Workspace {
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
+ if workspace.has_active_modal(cx) {
+ cx.propagate();
+ return;
+ }
+
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
@@ -539,11 +544,11 @@ impl BufferSearchBar {
this.select_all_matches(action, cx);
});
registrar.register_handler(|this, _: &editor::Cancel, cx| {
- if !this.dismissed {
+ if this.dismissed {
+ cx.propagate();
+ } else {
this.dismiss(&Dismiss, cx);
- return;
}
- cx.propagate();
});
registrar.register_handler(|this, deploy, cx| {
this.deploy(deploy, cx);
@@ -38,7 +38,7 @@ use std::{
use theme::ThemeSettings;
use ui::{
- h_stack, prelude::*, v_stack, Icon, IconButton, IconElement, Label, LabelCommon, LabelSize,
+ h_stack, prelude::*, v_stack, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
Selectable, ToggleButton, Tooltip,
};
use util::{paths::PathMatcher, ResultExt as _};
@@ -424,7 +424,8 @@ impl Item for ProjectSearchView {
.current()
.as_ref()
.map(|query| {
- let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+ let query = query.replace('\n', "");
+ let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN);
query_text.into()
});
let tab_name = last_query
@@ -432,7 +433,7 @@ impl Item for ProjectSearchView {
.unwrap_or_else(|| "Project search".into());
h_stack()
.gap_2()
- .child(IconElement::new(Icon::MagnifyingGlass).color(if selected {
+ .child(Icon::new(IconName::MagnifyingGlass).color(if selected {
Color::Default
} else {
Color::Muted
@@ -1616,12 +1617,12 @@ impl Render for ProjectSearchBar {
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
.on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
.on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
- .child(IconElement::new(Icon::MagnifyingGlass))
+ .child(Icon::new(IconName::MagnifyingGlass))
.child(self.render_text_input(&search.query_editor, cx))
.child(
h_stack()
.child(
- IconButton::new("project-search-filter-button", Icon::Filter)
+ IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| {
Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
})
@@ -1639,7 +1640,7 @@ impl Render for ProjectSearchBar {
this.child(
IconButton::new(
"project-search-case-sensitive",
- Icon::CaseSensitive,
+ IconName::CaseSensitive,
)
.tooltip(|cx| {
Tooltip::for_action(
@@ -1659,7 +1660,7 @@ impl Render for ProjectSearchBar {
)),
)
.child(
- IconButton::new("project-search-whole-word", Icon::WholeWord)
+ IconButton::new("project-search-whole-word", IconName::WholeWord)
.tooltip(|cx| {
Tooltip::for_action(
"Toggle whole word",
@@ -1738,7 +1739,7 @@ impl Render for ProjectSearchBar {
}),
)
.child(
- IconButton::new("project-search-toggle-replace", Icon::Replace)
+ IconButton::new("project-search-toggle-replace", IconName::Replace)
.on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
@@ -1755,7 +1756,7 @@ impl Render for ProjectSearchBar {
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
- .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small))
+ .child(Icon::new(IconName::Replace).size(ui::IconSize::Small))
.child(self.render_text_input(&search.replacement_editor, cx))
} else {
// Fill out the space if we don't have a replacement editor.
@@ -1764,7 +1765,7 @@ impl Render for ProjectSearchBar {
let actions_column = h_stack()
.when(search.replace_enabled, |this| {
this.child(
- IconButton::new("project-search-replace-next", Icon::ReplaceNext)
+ IconButton::new("project-search-replace-next", IconName::ReplaceNext)
.on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() {
search.update(cx, |this, cx| {
@@ -1775,7 +1776,7 @@ impl Render for ProjectSearchBar {
.tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)),
)
.child(
- IconButton::new("project-search-replace-all", Icon::ReplaceAll)
+ IconButton::new("project-search-replace-all", IconName::ReplaceAll)
.on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() {
search.update(cx, |this, cx| {
@@ -1796,7 +1797,7 @@ impl Render for ProjectSearchBar {
this
})
.child(
- IconButton::new("project-search-prev-match", Icon::ChevronLeft)
+ IconButton::new("project-search-prev-match", IconName::ChevronLeft)
.disabled(search.active_match_index.is_none())
.on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() {
@@ -1810,7 +1811,7 @@ impl Render for ProjectSearchBar {
}),
)
.child(
- IconButton::new("project-search-next-match", Icon::ChevronRight)
+ IconButton::new("project-search-next-match", IconName::ChevronRight)
.disabled(search.active_match_index.is_none())
.on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() {
@@ -60,11 +60,11 @@ impl SearchOptions {
}
}
- pub fn icon(&self) -> ui::Icon {
+ pub fn icon(&self) -> ui::IconName {
match *self {
- SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
- SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
- SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit,
+ SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
+ SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
+ SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit,
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@@ -3,7 +3,7 @@ use ui::IconButton;
use ui::{prelude::*, Tooltip};
pub(super) fn render_nav_button(
- icon: ui::Icon,
+ icon: ui::IconName,
active: bool,
tooltip: &'static str,
action: &'static dyn Action,
@@ -14,6 +14,7 @@ anyhow.workspace = true
backtrace-on-stack-overflow = "0.3.0"
chrono = "0.4"
clap = { version = "4.4", features = ["derive", "string"] }
+collab_ui = { path = "../collab_ui", features = ["stories"] }
strum = { version = "0.25.0", features = ["derive"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor = { path = "../editor" }
@@ -16,6 +16,7 @@ pub enum ComponentStory {
Avatar,
Button,
Checkbox,
+ CollabNotification,
ContextMenu,
Cursor,
Disclosure,
@@ -45,6 +46,9 @@ impl ComponentStory {
Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
+ Self::CollabNotification => cx
+ .new_view(|_| collab_ui::notifications::CollabNotificationStory)
+ .into(),
Self::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(),
Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(),
Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(),
@@ -451,6 +451,18 @@ impl TerminalElement {
}
});
+ let interactive_text_bounds = InteractiveBounds {
+ bounds,
+ stacking_order: cx.stacking_order().clone(),
+ };
+ if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
+ if self.can_navigate_to_selected_word && last_hovered_word.is_some() {
+ cx.set_cursor_style(gpui::CursorStyle::PointingHand)
+ } else {
+ cx.set_cursor_style(gpui::CursorStyle::IBeam)
+ }
+ }
+
let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
div()
.size_full()
@@ -19,7 +19,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane,
- ui::Icon,
+ ui::IconName,
DraggedTab, Pane, Workspace,
};
@@ -71,7 +71,7 @@ impl TerminalPanel {
h_stack()
.gap_2()
.child(
- IconButton::new("plus", Icon::Plus)
+ IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(move |_, cx| {
terminal_panel
@@ -82,10 +82,10 @@ impl TerminalPanel {
)
.child({
let zoomed = pane.is_zoomed();
- IconButton::new("toggle_zoom", Icon::Maximize)
+ IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small)
.selected(zoomed)
- .selected_icon(Icon::Minimize)
+ .selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);
}))
@@ -477,8 +477,8 @@ impl Panel for TerminalPanel {
"TerminalPanel"
}
- fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
- Some(Icon::Terminal)
+ fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
+ Some(IconName::Terminal)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -20,7 +20,7 @@ use terminal::{
Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
};
use terminal_element::TerminalElement;
-use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label};
use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent},
@@ -690,7 +690,7 @@ impl Item for TerminalView {
let title = self.terminal().read(cx).title(true);
h_stack()
.gap_2()
- .child(IconElement::new(Icon::Terminal))
+ .child(Icon::new(IconName::Terminal))
.child(Label::new(title).color(if selected {
Color::Default
} else {
@@ -90,7 +90,7 @@ impl Anchor {
content.summary_for_anchor(self)
}
- /// Returns true when the [Anchor] is located inside a visible fragment.
+ /// Returns true when the [`Anchor`] is located inside a visible fragment.
pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
if *self == Anchor::MIN || *self == Anchor::MAX {
true
@@ -2,7 +2,9 @@ use gpui::{AppContext, Hsla, SharedString};
use crate::{ActiveTheme, Appearance};
-/// A one-based step in a [`ColorScale`].
+/// A collection of colors that are used to style the UI.
+///
+/// Each step has a semantic meaning, and is used to style different parts of the UI.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct ColorScaleStep(usize);
@@ -37,6 +39,10 @@ impl ColorScaleStep {
];
}
+/// A scale of colors for a given [`ColorScaleSet`].
+///
+/// Each [`ColorScale`] contains exactly 12 colors. Refer to
+/// [`ColorScaleStep`] for a reference of what each step is used for.
pub struct ColorScale(Vec<Hsla>);
impl FromIterator<Hsla> for ColorScale {
@@ -229,6 +235,7 @@ impl IntoIterator for ColorScales {
}
}
+/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale.
pub struct ColorScaleSet {
name: SharedString,
light: ColorScale,
@@ -27,7 +27,7 @@ pub struct ThemeSettings {
}
#[derive(Default)]
-pub struct AdjustedBufferFontSize(Pixels);
+pub(crate) struct AdjustedBufferFontSize(Pixels);
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ThemeSettingsContent {
@@ -2,11 +2,12 @@ use gpui::Hsla;
use refineable::Refineable;
use std::sync::Arc;
-use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors};
+use crate::{PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors};
#[derive(Refineable, Clone, Debug)]
#[refineable(Debug, serde::Deserialize)]
pub struct ThemeColors {
+ /// Border color. Used for most borders, is usually a high contrast color.
pub border: Hsla,
/// Border color. Used for deemphasized borders, like a visual divider between two sections
pub border_variant: Hsla,
@@ -219,6 +220,8 @@ pub struct ThemeStyles {
#[refineable]
pub colors: ThemeColors,
+
+ #[refineable]
pub status: StatusColors,
pub player: PlayerColors,
pub syntax: Arc<SyntaxTheme>,
@@ -78,15 +78,6 @@ pub struct StatusColors {
pub warning_border: Hsla,
}
-impl Default for StatusColors {
- /// Don't use this!
- /// We have to have a default to be `[refineable::Refinable]`.
- /// todo!("Find a way to not need this for Refinable")
- fn default() -> Self {
- Self::dark()
- }
-}
-
pub struct DiagnosticColors {
pub error: Hsla,
pub warning: Hsla,
@@ -1,3 +1,11 @@
+//! # Theme
+//!
+//! This crate provides the theme system for Zed.
+//!
+//! ## Overview
+//!
+//! A theme is a collection of colors used to build a consistent appearance for UI components across the application.
+
mod default_colors;
mod default_theme;
mod one_themes;
@@ -0,0 +1,15 @@
+ # Theme
+
+ This crate provides the theme system for Zed.
+
+ ## Overview
+
+ A theme is a collection of colors used to build a consistent appearance for UI components across the application.
+ To produce a theme in Zed,
+
+ A theme is made of of two parts: A [ThemeFamily] and one or more [Theme]s.
+
+//
+ A [ThemeFamily] contains metadata like theme name, author, and theme-specific [ColorScales] as well as a series of themes.
+
+ - [ThemeColors] - A set of colors that are used to style the UI. Refer to the [ThemeColors] documentation for more information.
@@ -1,6 +1,6 @@
use gpui::{ClickEvent, WindowContext};
-/// A trait for elements that can be clicked.
+/// A trait for elements that can be clicked. Enables the use of the `on_click` method.
pub trait Clickable {
/// Sets the click handler that will fire whenever the element is clicked.
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
@@ -1,13 +1,26 @@
use crate::prelude::*;
use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
+/// The shape of an [`Avatar`].
#[derive(Debug, Default, PartialEq, Clone)]
-pub enum Shape {
+pub enum AvatarShape {
+ /// The avatar is shown in a circle.
#[default]
Circle,
+ /// The avatar is shown in a rectangle with rounded corners.
RoundedRectangle,
}
+/// An element that renders a user avatar with customizable appearance options.
+///
+/// # Examples
+///
+/// ```
+/// Avatar::new("path/to/image.png")
+/// .shape(AvatarShape::Circle)
+/// .grayscale(true)
+/// .border_color(cx.theme().colors().border)
+/// ```
#[derive(IntoElement)]
pub struct Avatar {
image: Img,
@@ -18,7 +31,7 @@ pub struct Avatar {
impl RenderOnce for Avatar {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
if self.image.style().corner_radii.top_left.is_none() {
- self = self.shape(Shape::Circle);
+ self = self.shape(AvatarShape::Circle);
}
let size = cx.rem_size();
@@ -66,14 +79,31 @@ impl Avatar {
}
}
- pub fn shape(mut self, shape: Shape) -> Self {
+ /// Sets the shape of the avatar image.
+ ///
+ /// This method allows the shape of the avatar to be specified using a [`Shape`].
+ /// It modifies the corner radius of the image to match the specified shape.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
+ /// ```
+ pub fn shape(mut self, shape: AvatarShape) -> Self {
self.image = match shape {
- Shape::Circle => self.image.rounded_full(),
- Shape::RoundedRectangle => self.image.rounded_md(),
+ AvatarShape::Circle => self.image.rounded_full(),
+ AvatarShape::RoundedRectangle => self.image.rounded_md(),
};
self
}
+ /// Applies a grayscale filter to the avatar image.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
+ /// ```
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.image = self.image.grayscale(grayscale);
self
@@ -2,11 +2,69 @@ use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, IconPosition, KeyBinding};
use crate::{
- ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
+ ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
};
use super::button_icon::ButtonIcon;
+/// An element that creates a button with a label and an optional icon.
+///
+/// Common buttons:
+/// - Label, Icon + Label: [`Button`] (this component)
+/// - Icon only: [`IconButton`]
+/// - Custom: [`ButtonLike`]
+///
+/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
+/// [`ButtonLike`] directly.
+///
+/// # Examples
+///
+/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
+/// indicates what action will be performed when the button is clicked.
+///
+/// ```
+/// Button::new("button_id", "Click me!")
+/// .on_click(|event, cx| {
+/// // Handle click event
+/// });
+/// ```
+///
+/// **A toggleable button**, is typically used in scenarios such as a toolbar,
+/// where the button's state indicates whether a feature is enabled or not, or
+/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
+///
+/// ```
+/// Button::new("button_id", "Click me!")
+/// .icon(IconName::Check)
+/// .selected(some_bool)
+/// .on_click(|event, cx| {
+/// // Handle click event
+/// });
+/// ```
+///
+/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
+///
+/// ```
+/// Button::new("button_id", "Click me!")
+/// .selected(some_bool)
+/// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+/// .on_click(|event, cx| {
+/// // Handle click event
+/// });
+/// ```
+/// This will create a button with a blue tinted background when selected.
+///
+/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container.
+/// The button's content, including text and icons, is centered by default.
+///
+/// ```
+/// let button = Button::new("button_id", "Click me!")
+/// .full_width()
+/// .on_click(|event, cx| {
+/// // Handle click event
+/// });
+/// ```
+///
#[derive(IntoElement)]
pub struct Button {
base: ButtonLike,
@@ -14,15 +72,21 @@ pub struct Button {
label_color: Option<Color>,
label_size: Option<LabelSize>,
selected_label: Option<SharedString>,
- icon: Option<Icon>,
+ icon: Option<IconName>,
icon_position: Option<IconPosition>,
icon_size: Option<IconSize>,
icon_color: Option<Color>,
- selected_icon: Option<Icon>,
+ selected_icon: Option<IconName>,
key_binding: Option<KeyBinding>,
}
impl Button {
+ /// Creates a new [`Button`] with a specified identifier and label.
+ ///
+ /// This is the primary constructor for a [`Button`] component. It initializes
+ /// the button with the provided identifier and label text, setting all other
+ /// properties to their default values, which can be customized using the
+ /// builder pattern methods provided by this struct.
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
base: ButtonLike::new(id),
@@ -39,46 +103,55 @@ impl Button {
}
}
+ /// Sets the color of the button's label.
pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
self.label_color = label_color.into();
self
}
+ /// Defines the size of the button's label.
pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
self.label_size = label_size.into();
self
}
+ /// Sets the label used when the button is in a selected state.
pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
self.selected_label = label.into().map(Into::into);
self
}
- pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ /// Assigns an icon to the button.
+ pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.icon = icon.into();
self
}
+ /// Sets the position of the icon relative to the label.
pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
self.icon_position = icon_position.into();
self
}
+ /// Specifies the size of the button's icon.
pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
self.icon_size = icon_size.into();
self
}
+ /// Sets the color of the button's icon.
pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
self.icon_color = icon_color.into();
self
}
- pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ /// Chooses an icon to display when the button is in a selected state.
+ pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into();
self
}
+ /// Binds a key combination to the button for keyboard shortcuts.
pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
self.key_binding = key_binding.into();
self
@@ -86,6 +159,22 @@ impl Button {
}
impl Selectable for Button {
+ /// Sets the selected state of the button.
+ ///
+ /// This method allows the selection state of the button to be specified.
+ /// It modifies the button's appearance to reflect its selected state.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .selected(true)
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ ///
+ /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
fn selected(mut self, selected: bool) -> Self {
self.base = self.base.selected(selected);
self
@@ -93,6 +182,19 @@ impl Selectable for Button {
}
impl SelectableButton for Button {
+ /// Sets the style for the button when selected.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .selected(true)
+ /// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ /// This results in a button with a blue tinted background when selected.
fn selected_style(mut self, style: ButtonStyle) -> Self {
self.base = self.base.selected_style(style);
self
@@ -100,6 +202,22 @@ impl SelectableButton for Button {
}
impl Disableable for Button {
+ /// Disables the button.
+ ///
+ /// This method allows the button to be disabled. When a button is disabled,
+ /// it doesn't react to user interactions and its appearance is updated to reflect this.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .disabled(true)
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ ///
+ /// This results in a button that is disabled and does not respond to click events.
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self
@@ -107,6 +225,7 @@ impl Disableable for Button {
}
impl Clickable for Button {
+ /// Sets the click event handler for the button.
fn on_click(
mut self,
handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
@@ -117,11 +236,40 @@ impl Clickable for Button {
}
impl FixedWidth for Button {
+ /// Sets a fixed width for the button.
+ ///
+ /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
+ /// Sets a fixed width for the button.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .width(DefiniteLength::Pixels(100))
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ ///
+ /// This sets the button's width to be exactly 100 pixels.
fn width(mut self, width: DefiniteLength) -> Self {
self.base = self.base.width(width);
self
}
+ /// Sets the button to occupy the full width of its container.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .full_width()
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ ///
+ /// This stretches the button to the full width of its container.
fn full_width(mut self) -> Self {
self.base = self.base.full_width();
self
@@ -129,20 +277,42 @@ impl FixedWidth for Button {
}
impl ButtonCommon for Button {
+ /// Sets the button's id.
fn id(&self) -> &ElementId {
self.base.id()
}
+ /// Sets the visual style of the button using a [`ButtonStyle`].
fn style(mut self, style: ButtonStyle) -> Self {
self.base = self.base.style(style);
self
}
+ /// Sets the button's size using a [`ButtonSize`].
fn size(mut self, size: ButtonSize) -> Self {
self.base = self.base.size(size);
self
}
+ /// Sets a tooltip for the button.
+ ///
+ /// This method allows a tooltip to be set for the button. The tooltip is a function that
+ /// takes a mutable reference to a [`WindowContext`] and returns an [`AnyView`]. The tooltip
+ /// is displayed when the user hovers over the button.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// Button::new("button_id", "Click me!")
+ /// .tooltip(|cx| {
+ /// Text::new("This is a tooltip").into()
+ /// })
+ /// .on_click(|event, cx| {
+ /// // Handle click event
+ /// });
+ /// ```
+ ///
+ /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.base = self.base.tooltip(tooltip);
self
@@ -1,4 +1,4 @@
-use crate::{prelude::*, Icon, IconElement, IconSize};
+use crate::{prelude::*, Icon, IconName, IconSize};
/// An icon that appears within a button.
///
@@ -6,17 +6,17 @@ use crate::{prelude::*, Icon, IconElement, IconSize};
/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
#[derive(IntoElement)]
pub(super) struct ButtonIcon {
- icon: Icon,
+ icon: IconName,
size: IconSize,
color: Color,
disabled: bool,
selected: bool,
- selected_icon: Option<Icon>,
+ selected_icon: Option<IconName>,
selected_style: Option<ButtonStyle>,
}
impl ButtonIcon {
- pub fn new(icon: Icon) -> Self {
+ pub fn new(icon: IconName) -> Self {
Self {
icon,
size: IconSize::default(),
@@ -44,7 +44,7 @@ impl ButtonIcon {
self
}
- pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into();
self
}
@@ -88,6 +88,6 @@ impl RenderOnce for ButtonIcon {
self.color
};
- IconElement::new(icon).size(self.size).color(icon_color)
+ Icon::new(icon).size(self.size).color(icon_color)
}
}
@@ -4,10 +4,12 @@ use smallvec::SmallVec;
use crate::prelude::*;
+/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
pub trait SelectableButton: Selectable {
fn selected_style(self, style: ButtonStyle) -> Self;
}
+/// A common set of traits all buttons must implement.
pub trait ButtonCommon: Clickable + Disableable {
/// A unique element ID to identify the button.
fn id(&self) -> &ElementId;
@@ -93,6 +95,7 @@ impl From<ButtonStyle> for Color {
}
}
+/// The visual appearance of a button.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle {
/// A filled button with a solid background color. Provides emphasis versus
@@ -260,8 +263,9 @@ impl ButtonStyle {
}
}
-/// ButtonSize can also be used to help build non-button elements
-/// that are consistently sized with buttons.
+/// The height of a button.
+///
+/// Can also be used to size non-button elements to align with [`Button`]s.
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonSize {
Large,
@@ -1,21 +1,21 @@
use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, SelectableButton};
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize};
use super::button_icon::ButtonIcon;
#[derive(IntoElement)]
pub struct IconButton {
base: ButtonLike,
- icon: Icon,
+ icon: IconName,
icon_size: IconSize,
icon_color: Color,
- selected_icon: Option<Icon>,
+ selected_icon: Option<IconName>,
}
impl IconButton {
- pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+ pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
Self {
base: ButtonLike::new(id),
icon,
@@ -35,7 +35,7 @@ impl IconButton {
self
}
- pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into();
self
}
@@ -1,7 +1,7 @@
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*;
-use crate::{Color, Icon, IconElement, Selection};
+use crate::{Color, Icon, IconName, Selection};
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
@@ -47,7 +47,7 @@ impl RenderOnce for Checkbox {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
- Selection::Selected => Some(IconElement::new(Icon::Check).size(IconSize::Small).color(
+ Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
if self.disabled {
Color::Disabled
} else {
@@ -55,7 +55,7 @@ impl RenderOnce for Checkbox {
},
)),
Selection::Indeterminate => Some(
- IconElement::new(Icon::Dash)
+ Icon::new(IconName::Dash)
.size(IconSize::Small)
.color(if self.disabled {
Color::Disabled
@@ -1,6 +1,6 @@
use crate::{
- h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
- ListSeparator, ListSubHeader,
+ h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
+ ListSubHeader,
};
use gpui::{
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
@@ -14,7 +14,7 @@ enum ContextMenuItem {
Header(SharedString),
Entry {
label: SharedString,
- icon: Option<Icon>,
+ icon: Option<IconName>,
handler: Rc<dyn Fn(&mut WindowContext)>,
action: Option<Box<dyn Action>>,
},
@@ -117,7 +117,7 @@ impl ContextMenu {
label: label.into(),
action: Some(action.boxed_clone()),
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
- icon: Some(Icon::Link),
+ icon: Some(IconName::Link),
});
self
}
@@ -280,7 +280,7 @@ impl Render for ContextMenu {
h_stack()
.gap_1()
.child(Label::new(label.clone()))
- .child(IconElement::new(*icon))
+ .child(Icon::new(*icon))
.into_any_element()
} else {
Label::new(label.clone()).into_any_element()
@@ -1,6 +1,6 @@
use gpui::ClickEvent;
-use crate::{prelude::*, Color, Icon, IconButton, IconSize};
+use crate::{prelude::*, Color, IconButton, IconName, IconSize};
#[derive(IntoElement)]
pub struct Disclosure {
@@ -32,8 +32,8 @@ impl RenderOnce for Disclosure {
IconButton::new(
self.id,
match self.is_open {
- true => Icon::ChevronDown,
- false => Icon::ChevronRight,
+ true => IconName::ChevronDown,
+ false => IconName::ChevronRight,
},
)
.icon_color(Color::Muted)
@@ -7,6 +7,7 @@ enum DividerDirection {
Vertical,
}
+/// The color of a [`Divider`].
#[derive(Default)]
pub enum DividerColor {
Border,
@@ -22,7 +22,7 @@ impl IconSize {
}
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
-pub enum Icon {
+pub enum IconName {
Ai,
ArrowDown,
ArrowLeft,
@@ -111,118 +111,108 @@ pub enum Icon {
ZedXCopilot,
}
-impl Icon {
+impl IconName {
pub fn path(self) -> &'static str {
match self {
- Icon::Ai => "icons/ai.svg",
- Icon::ArrowDown => "icons/arrow_down.svg",
- Icon::ArrowLeft => "icons/arrow_left.svg",
- Icon::ArrowRight => "icons/arrow_right.svg",
- Icon::ArrowUp => "icons/arrow_up.svg",
- Icon::ArrowUpRight => "icons/arrow_up_right.svg",
- Icon::ArrowCircle => "icons/arrow_circle.svg",
- Icon::AtSign => "icons/at_sign.svg",
- Icon::AudioOff => "icons/speaker_off.svg",
- Icon::AudioOn => "icons/speaker_loud.svg",
- Icon::Backspace => "icons/backspace.svg",
- Icon::Bell => "icons/bell.svg",
- Icon::BellOff => "icons/bell_off.svg",
- Icon::BellRing => "icons/bell_ring.svg",
- Icon::Bolt => "icons/bolt.svg",
- Icon::CaseSensitive => "icons/case_insensitive.svg",
- Icon::Check => "icons/check.svg",
- Icon::ChevronDown => "icons/chevron_down.svg",
- Icon::ChevronLeft => "icons/chevron_left.svg",
- Icon::ChevronRight => "icons/chevron_right.svg",
- Icon::ChevronUp => "icons/chevron_up.svg",
- Icon::Close => "icons/x.svg",
- Icon::Collab => "icons/user_group_16.svg",
- Icon::Command => "icons/command.svg",
- Icon::Control => "icons/control.svg",
- Icon::Copilot => "icons/copilot.svg",
- Icon::CopilotDisabled => "icons/copilot_disabled.svg",
- Icon::CopilotError => "icons/copilot_error.svg",
- Icon::CopilotInit => "icons/copilot_init.svg",
- Icon::Copy => "icons/copy.svg",
- Icon::Dash => "icons/dash.svg",
- Icon::Delete => "icons/delete.svg",
- Icon::Disconnected => "icons/disconnected.svg",
- Icon::Ellipsis => "icons/ellipsis.svg",
- Icon::Envelope => "icons/feedback.svg",
- Icon::Escape => "icons/escape.svg",
- Icon::ExclamationTriangle => "icons/warning.svg",
- Icon::Exit => "icons/exit.svg",
- Icon::ExternalLink => "icons/external_link.svg",
- Icon::File => "icons/file.svg",
- Icon::FileDoc => "icons/file_icons/book.svg",
- Icon::FileGeneric => "icons/file_icons/file.svg",
- Icon::FileGit => "icons/file_icons/git.svg",
- Icon::FileLock => "icons/file_icons/lock.svg",
- Icon::FileRust => "icons/file_icons/rust.svg",
- Icon::FileToml => "icons/file_icons/toml.svg",
- Icon::FileTree => "icons/project.svg",
- Icon::Filter => "icons/filter.svg",
- Icon::Folder => "icons/file_icons/folder.svg",
- Icon::FolderOpen => "icons/file_icons/folder_open.svg",
- Icon::FolderX => "icons/stop_sharing.svg",
- Icon::Github => "icons/github.svg",
- Icon::Hash => "icons/hash.svg",
- Icon::InlayHint => "icons/inlay_hint.svg",
- Icon::Link => "icons/link.svg",
- Icon::MagicWand => "icons/magic_wand.svg",
- Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
- Icon::MailOpen => "icons/mail_open.svg",
- Icon::Maximize => "icons/maximize.svg",
- Icon::Menu => "icons/menu.svg",
- Icon::MessageBubbles => "icons/conversations.svg",
- Icon::Mic => "icons/mic.svg",
- Icon::MicMute => "icons/mic_mute.svg",
- Icon::Minimize => "icons/minimize.svg",
- Icon::Option => "icons/option.svg",
- Icon::PageDown => "icons/page_down.svg",
- Icon::PageUp => "icons/page_up.svg",
- Icon::Plus => "icons/plus.svg",
- Icon::Public => "icons/public.svg",
- Icon::Quote => "icons/quote.svg",
- Icon::Replace => "icons/replace.svg",
- Icon::ReplaceAll => "icons/replace_all.svg",
- Icon::ReplaceNext => "icons/replace_next.svg",
- Icon::Return => "icons/return.svg",
- Icon::Screen => "icons/desktop.svg",
- Icon::SelectAll => "icons/select_all.svg",
- Icon::Shift => "icons/shift.svg",
- Icon::Snip => "icons/snip.svg",
- Icon::Space => "icons/space.svg",
- Icon::Split => "icons/split.svg",
- Icon::Tab => "icons/tab.svg",
- Icon::Terminal => "icons/terminal.svg",
- Icon::Update => "icons/update.svg",
- Icon::WholeWord => "icons/word_search.svg",
- Icon::XCircle => "icons/error.svg",
- Icon::ZedXCopilot => "icons/zed_x_copilot.svg",
+ IconName::Ai => "icons/ai.svg",
+ IconName::ArrowDown => "icons/arrow_down.svg",
+ IconName::ArrowLeft => "icons/arrow_left.svg",
+ IconName::ArrowRight => "icons/arrow_right.svg",
+ IconName::ArrowUp => "icons/arrow_up.svg",
+ IconName::ArrowUpRight => "icons/arrow_up_right.svg",
+ IconName::ArrowCircle => "icons/arrow_circle.svg",
+ IconName::AtSign => "icons/at_sign.svg",
+ IconName::AudioOff => "icons/speaker_off.svg",
+ IconName::AudioOn => "icons/speaker_loud.svg",
+ IconName::Backspace => "icons/backspace.svg",
+ IconName::Bell => "icons/bell.svg",
+ IconName::BellOff => "icons/bell_off.svg",
+ IconName::BellRing => "icons/bell_ring.svg",
+ IconName::Bolt => "icons/bolt.svg",
+ IconName::CaseSensitive => "icons/case_insensitive.svg",
+ IconName::Check => "icons/check.svg",
+ IconName::ChevronDown => "icons/chevron_down.svg",
+ IconName::ChevronLeft => "icons/chevron_left.svg",
+ IconName::ChevronRight => "icons/chevron_right.svg",
+ IconName::ChevronUp => "icons/chevron_up.svg",
+ IconName::Close => "icons/x.svg",
+ IconName::Collab => "icons/user_group_16.svg",
+ IconName::Command => "icons/command.svg",
+ IconName::Control => "icons/control.svg",
+ IconName::Copilot => "icons/copilot.svg",
+ IconName::CopilotDisabled => "icons/copilot_disabled.svg",
+ IconName::CopilotError => "icons/copilot_error.svg",
+ IconName::CopilotInit => "icons/copilot_init.svg",
+ IconName::Copy => "icons/copy.svg",
+ IconName::Dash => "icons/dash.svg",
+ IconName::Delete => "icons/delete.svg",
+ IconName::Disconnected => "icons/disconnected.svg",
+ IconName::Ellipsis => "icons/ellipsis.svg",
+ IconName::Envelope => "icons/feedback.svg",
+ IconName::Escape => "icons/escape.svg",
+ IconName::ExclamationTriangle => "icons/warning.svg",
+ IconName::Exit => "icons/exit.svg",
+ IconName::ExternalLink => "icons/external_link.svg",
+ IconName::File => "icons/file.svg",
+ IconName::FileDoc => "icons/file_icons/book.svg",
+ IconName::FileGeneric => "icons/file_icons/file.svg",
+ IconName::FileGit => "icons/file_icons/git.svg",
+ IconName::FileLock => "icons/file_icons/lock.svg",
+ IconName::FileRust => "icons/file_icons/rust.svg",
+ IconName::FileToml => "icons/file_icons/toml.svg",
+ IconName::FileTree => "icons/project.svg",
+ IconName::Filter => "icons/filter.svg",
+ IconName::Folder => "icons/file_icons/folder.svg",
+ IconName::FolderOpen => "icons/file_icons/folder_open.svg",
+ IconName::FolderX => "icons/stop_sharing.svg",
+ IconName::Github => "icons/github.svg",
+ IconName::Hash => "icons/hash.svg",
+ IconName::InlayHint => "icons/inlay_hint.svg",
+ IconName::Link => "icons/link.svg",
+ IconName::MagicWand => "icons/magic_wand.svg",
+ IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
+ IconName::MailOpen => "icons/mail_open.svg",
+ IconName::Maximize => "icons/maximize.svg",
+ IconName::Menu => "icons/menu.svg",
+ IconName::MessageBubbles => "icons/conversations.svg",
+ IconName::Mic => "icons/mic.svg",
+ IconName::MicMute => "icons/mic_mute.svg",
+ IconName::Minimize => "icons/minimize.svg",
+ IconName::Option => "icons/option.svg",
+ IconName::PageDown => "icons/page_down.svg",
+ IconName::PageUp => "icons/page_up.svg",
+ IconName::Plus => "icons/plus.svg",
+ IconName::Public => "icons/public.svg",
+ IconName::Quote => "icons/quote.svg",
+ IconName::Replace => "icons/replace.svg",
+ IconName::ReplaceAll => "icons/replace_all.svg",
+ IconName::ReplaceNext => "icons/replace_next.svg",
+ IconName::Return => "icons/return.svg",
+ IconName::Screen => "icons/desktop.svg",
+ IconName::SelectAll => "icons/select_all.svg",
+ IconName::Shift => "icons/shift.svg",
+ IconName::Snip => "icons/snip.svg",
+ IconName::Space => "icons/space.svg",
+ IconName::Split => "icons/split.svg",
+ IconName::Tab => "icons/tab.svg",
+ IconName::Terminal => "icons/terminal.svg",
+ IconName::Update => "icons/update.svg",
+ IconName::WholeWord => "icons/word_search.svg",
+ IconName::XCircle => "icons/error.svg",
+ IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
}
}
}
#[derive(IntoElement)]
-pub struct IconElement {
+pub struct Icon {
path: SharedString,
color: Color,
size: IconSize,
}
-impl RenderOnce for IconElement {
- fn render(self, cx: &mut WindowContext) -> impl IntoElement {
- svg()
- .size(self.size.rems())
- .flex_none()
- .path(self.path)
- .text_color(self.color.color(cx))
- }
-}
-
-impl IconElement {
- pub fn new(icon: Icon) -> Self {
+impl Icon {
+ pub fn new(icon: IconName) -> Self {
Self {
path: icon.path().into(),
color: Color::default(),
@@ -248,3 +238,13 @@ impl IconElement {
self
}
}
+
+impl RenderOnce for Icon {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ svg()
+ .size(self.size.rems())
+ .flex_none()
+ .path(self.path)
+ .text_color(self.color.color(cx))
+ }
+}
@@ -1,4 +1,4 @@
-use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
+use crate::{h_stack, prelude::*, Icon, IconName, IconSize};
use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
#[derive(IntoElement, Clone)]
@@ -26,16 +26,16 @@ impl RenderOnce for KeyBinding {
.text_color(cx.theme().colors().text_muted)
.when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
.when(keystroke.modifiers.control, |el| {
- el.child(KeyIcon::new(Icon::Control))
+ el.child(KeyIcon::new(IconName::Control))
})
.when(keystroke.modifiers.alt, |el| {
- el.child(KeyIcon::new(Icon::Option))
+ el.child(KeyIcon::new(IconName::Option))
})
.when(keystroke.modifiers.command, |el| {
- el.child(KeyIcon::new(Icon::Command))
+ el.child(KeyIcon::new(IconName::Command))
})
.when(keystroke.modifiers.shift, |el| {
- el.child(KeyIcon::new(Icon::Shift))
+ el.child(KeyIcon::new(IconName::Shift))
})
.when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
.when(key_icon.is_none(), |el| {
@@ -62,21 +62,21 @@ impl KeyBinding {
Some(Self::new(key_binding))
}
- fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
+ fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
match keystroke.key.as_str() {
- "left" => Some(Icon::ArrowLeft),
- "right" => Some(Icon::ArrowRight),
- "up" => Some(Icon::ArrowUp),
- "down" => Some(Icon::ArrowDown),
- "backspace" => Some(Icon::Backspace),
- "delete" => Some(Icon::Delete),
- "return" => Some(Icon::Return),
- "enter" => Some(Icon::Return),
- "tab" => Some(Icon::Tab),
- "space" => Some(Icon::Space),
- "escape" => Some(Icon::Escape),
- "pagedown" => Some(Icon::PageDown),
- "pageup" => Some(Icon::PageUp),
+ "left" => Some(IconName::ArrowLeft),
+ "right" => Some(IconName::ArrowRight),
+ "up" => Some(IconName::ArrowUp),
+ "down" => Some(IconName::ArrowDown),
+ "backspace" => Some(IconName::Backspace),
+ "delete" => Some(IconName::Delete),
+ "return" => Some(IconName::Return),
+ "enter" => Some(IconName::Return),
+ "tab" => Some(IconName::Tab),
+ "space" => Some(IconName::Space),
+ "escape" => Some(IconName::Escape),
+ "pagedown" => Some(IconName::PageDown),
+ "pageup" => Some(IconName::PageUp),
_ => None,
}
}
@@ -120,13 +120,13 @@ impl Key {
#[derive(IntoElement)]
pub struct KeyIcon {
- icon: Icon,
+ icon: IconName,
}
impl RenderOnce for KeyIcon {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
div().w(rems(14. / 16.)).child(
- IconElement::new(self.icon)
+ Icon::new(self.icon)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -134,7 +134,7 @@ impl RenderOnce for KeyIcon {
}
impl KeyIcon {
- pub fn new(icon: Icon) -> Self {
+ pub fn new(icon: IconName) -> Self {
Self { icon }
}
}
@@ -2,6 +2,28 @@ use gpui::WindowContext;
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
+/// A struct representing a label element in the UI.
+///
+/// The `Label` struct stores the label text and common properties for a label element.
+/// It provides methods for modifying these properties.
+///
+/// # Examples
+///
+/// ```
+/// Label::new("Hello, World!")
+/// ```
+///
+/// **A colored label**, for example labeling a dangerous action:
+///
+/// ```
+/// let my_label = Label::new("Delete").color(Color::Error);
+/// ```
+///
+/// **A label with a strikethrough**, for example labeling something that has been deleted:
+///
+/// ```
+/// let my_label = Label::new("Deleted").strikethrough(true);
+/// ```
#[derive(IntoElement)]
pub struct Label {
base: LabelLike,
@@ -9,6 +31,13 @@ pub struct Label {
}
impl Label {
+ /// Create a new `Label` with the given text.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let my_label = Label::new("Hello, World!");
+ /// ```
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: LabelLike::new(),
@@ -18,21 +47,49 @@ impl Label {
}
impl LabelCommon for Label {
+ /// Sets the size of the label using a [`LabelSize`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let my_label = Label::new("Hello, World!").size(LabelSize::Large);
+ /// ```
fn size(mut self, size: LabelSize) -> Self {
self.base = self.base.size(size);
self
}
+ /// Sets the line height style of the label using a [`LineHeightStyle`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let my_label = Label::new("Hello, World!").line_height_style(LineHeightStyle::Normal);
+ /// ```
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
self.base = self.base.line_height_style(line_height_style);
self
}
+ /// Sets the color of the label using a [`Color`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let my_label = Label::new("Hello, World!").color(Color::Primary);
+ /// ```
fn color(mut self, color: Color) -> Self {
self.base = self.base.color(color);
self
}
+ /// Sets the strikethrough property of the label.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let my_label = Label::new("Hello, World!").strikethrough(true);
+ /// ```
fn strikethrough(mut self, strikethrough: bool) -> Self {
self.base = self.base.strikethrough(strikethrough);
self
@@ -19,6 +19,7 @@ pub enum LineHeightStyle {
UiLabel,
}
+/// A common set of traits all labels must implement.
pub trait LabelCommon {
fn size(self, size: LabelSize) -> Self;
fn line_height_style(self, line_height_style: LineHeightStyle) -> Self;
@@ -1,10 +1,10 @@
use crate::prelude::*;
-use crate::{h_stack, Icon, IconElement, IconSize, Label};
+use crate::{h_stack, Icon, IconName, IconSize, Label};
#[derive(IntoElement)]
pub struct ListSubHeader {
label: SharedString,
- start_slot: Option<Icon>,
+ start_slot: Option<IconName>,
inset: bool,
}
@@ -17,7 +17,7 @@ impl ListSubHeader {
}
}
- pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+ pub fn left_icon(mut self, left_icon: Option<IconName>) -> Self {
self.start_slot = left_icon;
self
}
@@ -40,11 +40,10 @@ impl RenderOnce for ListSubHeader {
.flex()
.gap_1()
.items_center()
- .children(self.start_slot.map(|i| {
- IconElement::new(i)
- .color(Color::Muted)
- .size(IconSize::Small)
- }))
+ .children(
+ self.start_slot
+ .map(|i| Icon::new(i).color(Color::Muted).size(IconSize::Small)),
+ )
.child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
@@ -108,6 +108,7 @@ impl<M: ManagedView> PopoverMenu<M> {
}
}
+/// Creates a [`PopoverMenu`]
pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
PopoverMenu {
id: id.into(),
@@ -39,6 +39,7 @@ impl<M: ManagedView> RightClickMenu<M> {
}
}
+/// Creates a [`RightClickMenu`]
pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
RightClickMenu {
id: id.into(),
@@ -2,17 +2,13 @@ use gpui::{div, Div};
use crate::StyledExt;
-/// Horizontally stacks elements.
-///
-/// Sets `flex()`, `flex_row()`, `items_center()`
+/// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()`
#[track_caller]
pub fn h_stack() -> Div {
div().h_flex()
}
-/// Vertically stacks elements.
-///
-/// Sets `flex()`, `flex_col()`
+/// Vertically stacks elements. Sets `flex()`, `flex_col()`
#[track_caller]
pub fn v_stack() -> Div {
div().v_flex()
@@ -1,7 +1,7 @@
use gpui::Render;
use story::Story;
-use crate::{prelude::*, Icon};
+use crate::{prelude::*, IconName};
use crate::{Button, ButtonStyle};
pub struct ButtonStory;
@@ -23,12 +23,12 @@ impl Render for ButtonStory {
.child(Story::label("With `label_color`"))
.child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
.child(Story::label("With `icon`"))
- .child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
+ .child(Button::new("filled_with_icon", "Click me").icon(IconName::FileGit))
.child(Story::label("Selected with `icon`"))
.child(
Button::new("filled_and_selected_with_icon", "Click me")
.selected(true)
- .icon(Icon::FileGit),
+ .icon(IconName::FileGit),
)
.child(Story::label("Default (Subtle)"))
.child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
@@ -3,17 +3,17 @@ use story::Story;
use strum::IntoEnumIterator;
use crate::prelude::*;
-use crate::{Icon, IconElement};
+use crate::{Icon, IconName};
pub struct IconStory;
impl Render for IconStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- let icons = Icon::iter();
+ let icons = IconName::iter();
Story::container()
- .child(Story::title_for::<IconElement>())
+ .child(Story::title_for::<Icon>())
.child(Story::label("All Icons"))
- .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+ .child(div().flex().gap_3().children(icons.map(Icon::new)))
}
}
@@ -2,7 +2,7 @@ use gpui::Render;
use story::{StoryContainer, StoryItem, StorySection};
use crate::{prelude::*, Tooltip};
-use crate::{Icon, IconButton};
+use crate::{IconButton, IconName};
pub struct IconButtonStory;
@@ -10,7 +10,7 @@ impl Render for IconButtonStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let default_button = StoryItem::new(
"Default",
- IconButton::new("default_icon_button", Icon::Hash),
+ IconButton::new("default_icon_button", IconName::Hash),
)
.description("Displays an icon button.")
.usage(
@@ -21,7 +21,7 @@ impl Render for IconButtonStory {
let selected_button = StoryItem::new(
"Selected",
- IconButton::new("selected_icon_button", Icon::Hash).selected(true),
+ IconButton::new("selected_icon_button", IconName::Hash).selected(true),
)
.description("Displays an icon button that is selected.")
.usage(
@@ -32,9 +32,9 @@ impl Render for IconButtonStory {
let selected_with_selected_icon = StoryItem::new(
"Selected with `selected_icon`",
- IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+ IconButton::new("selected_with_selected_icon_button", IconName::AudioOn)
.selected(true)
- .selected_icon(Icon::AudioOff),
+ .selected_icon(IconName::AudioOff),
)
.description(
"Displays an icon button that is selected and shows a different icon when selected.",
@@ -49,7 +49,7 @@ impl Render for IconButtonStory {
let disabled_button = StoryItem::new(
"Disabled",
- IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
+ IconButton::new("disabled_icon_button", IconName::Hash).disabled(true),
)
.description("Displays an icon button that is disabled.")
.usage(
@@ -60,7 +60,7 @@ impl Render for IconButtonStory {
let with_on_click_button = StoryItem::new(
"With `on_click`",
- IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+ IconButton::new("with_on_click_button", IconName::Ai).on_click(|_event, _cx| {
println!("Clicked!");
}),
)
@@ -75,7 +75,7 @@ impl Render for IconButtonStory {
let with_tooltip_button = StoryItem::new(
"With `tooltip`",
- IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+ IconButton::new("with_tooltip_button", IconName::MessageBubbles)
.tooltip(|cx| Tooltip::text("Open messages", cx)),
)
.description("Displays an icon button that has a tooltip when hovered.")
@@ -88,7 +88,7 @@ impl Render for IconButtonStory {
let selected_with_tooltip_button = StoryItem::new(
"Selected with `tooltip`",
- IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+ IconButton::new("selected_with_tooltip_button", IconName::InlayHint)
.selected(true)
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
)
@@ -2,7 +2,7 @@ use gpui::Render;
use story::Story;
use crate::{prelude::*, IconButton};
-use crate::{Icon, ListHeader};
+use crate::{IconName, ListHeader};
pub struct ListHeaderStory;
@@ -13,19 +13,19 @@ impl Render for ListHeaderStory {
.child(Story::label("Default"))
.child(ListHeader::new("Section 1"))
.child(Story::label("With left icon"))
- .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
+ .child(ListHeader::new("Section 2").start_slot(Icon::new(IconName::Bell)))
.child(Story::label("With left icon and meta"))
.child(
ListHeader::new("Section 3")
- .start_slot(IconElement::new(Icon::BellOff))
- .end_slot(IconButton::new("action_1", Icon::Bolt)),
+ .start_slot(Icon::new(IconName::BellOff))
+ .end_slot(IconButton::new("action_1", IconName::Bolt)),
)
.child(Story::label("With multiple meta"))
.child(
ListHeader::new("Section 4")
- .end_slot(IconButton::new("action_1", Icon::Bolt))
- .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
- .end_slot(IconButton::new("action_3", Icon::Plus)),
+ .end_slot(IconButton::new("action_1", IconName::Bolt))
+ .end_slot(IconButton::new("action_2", IconName::ExclamationTriangle))
+ .end_slot(IconButton::new("action_3", IconName::Plus)),
)
}
}
@@ -1,8 +1,8 @@
-use gpui::Render;
+use gpui::{Render, SharedUrl};
use story::Story;
use crate::{prelude::*, Avatar};
-use crate::{Icon, ListItem};
+use crate::{IconName, ListItem};
pub struct ListItemStory;
@@ -18,13 +18,13 @@ impl Render for ListItemStory {
ListItem::new("inset_list_item")
.inset(true)
.start_slot(
- IconElement::new(Icon::Bell)
+ Icon::new(IconName::Bell)
.size(IconSize::Small)
.color(Color::Muted),
)
.child("Hello, world!")
.end_slot(
- IconElement::new(Icon::Bell)
+ Icon::new(IconName::Bell)
.size(IconSize::Small)
.color(Color::Muted),
),
@@ -34,7 +34,7 @@ impl Render for ListItemStory {
ListItem::new("with start slot_icon")
.child("Hello, world!")
.start_slot(
- IconElement::new(Icon::Bell)
+ Icon::new(IconName::Bell)
.size(IconSize::Small)
.color(Color::Muted),
),
@@ -43,7 +43,7 @@ impl Render for ListItemStory {
.child(
ListItem::new("with_start slot avatar")
.child("Hello, world!")
- .start_slot(Avatar::new(SharedString::from(
+ .start_slot(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))),
)
@@ -51,7 +51,7 @@ impl Render for ListItemStory {
.child(
ListItem::new("with_left_avatar")
.child("Hello, world!")
- .end_slot(Avatar::new(SharedString::from(
+ .end_slot(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))),
)
@@ -62,23 +62,23 @@ impl Render for ListItemStory {
.end_slot(
h_stack()
.gap_2()
- .child(Avatar::new(SharedString::from(
+ .child(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
- .child(Avatar::new(SharedString::from(
+ .child(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
- .child(Avatar::new(SharedString::from(
+ .child(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
- .child(Avatar::new(SharedString::from(
+ .child(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
- .child(Avatar::new(SharedString::from(
+ .child(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
))),
)
- .end_hover_slot(Avatar::new(SharedString::from(
+ .end_hover_slot(Avatar::new(SharedUrl::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))),
)
@@ -27,7 +27,7 @@ impl Render for TabStory {
h_stack().child(
Tab::new("tab_1")
.end_slot(
- IconButton::new("close_button", Icon::Close)
+ IconButton::new("close_button", IconName::Close)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall),
@@ -38,16 +38,19 @@ impl Render for TabBarStory {
h_stack().child(
TabBar::new("tab_bar_1")
.start_child(
- IconButton::new("navigate_backward", Icon::ArrowLeft)
+ IconButton::new("navigate_backward", IconName::ArrowLeft)
.icon_size(IconSize::Small),
)
.start_child(
- IconButton::new("navigate_forward", Icon::ArrowRight)
+ IconButton::new("navigate_forward", IconName::ArrowRight)
.icon_size(IconSize::Small),
)
- .end_child(IconButton::new("new", Icon::Plus).icon_size(IconSize::Small))
.end_child(
- IconButton::new("split_pane", Icon::Split).icon_size(IconSize::Small),
+ IconButton::new("new", IconName::Plus).icon_size(IconSize::Small),
+ )
+ .end_child(
+ IconButton::new("split_pane", IconName::Split)
+ .icon_size(IconSize::Small),
)
.children(tabs),
),
@@ -1,4 +1,4 @@
-/// A trait for elements that can be disabled.
+/// A trait for elements that can be disabled. Generally used to implement disabling an element's interactivity and changing it's appearance to reflect that it is disabled.
pub trait Disableable {
/// Sets whether the element is disabled.
fn disabled(self, disabled: bool) -> Self;
@@ -1,6 +1,6 @@
use gpui::DefiniteLength;
-/// A trait for elements that have a fixed with.
+/// A trait for elements that can have a fixed with. Enables the use of the `width` and `full_width` methods.
pub trait FixedWidth {
/// Sets the width of the element.
fn width(self, width: DefiniteLength) -> Self;
@@ -1,3 +1,5 @@
+//! The prelude of this crate. When building UI in Zed you almost always want to import this.
+
pub use gpui::prelude::*;
pub use gpui::{
div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId,
@@ -15,6 +17,6 @@ pub use crate::{h_stack, v_stack};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
pub use crate::{ButtonCommon, Color, StyledExt};
pub use crate::{Headline, HeadlineSize};
-pub use crate::{Icon, IconElement, IconPosition, IconSize};
+pub use crate::{Icon, IconName, IconPosition, IconSize};
pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};
pub use theme::ActiveTheme;
@@ -1,18 +1,27 @@
/// 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.
pub trait Selectable {
/// Sets whether the element is selected.
fn selected(self, selected: bool) -> Self;
}
+/// Represents the selection status of an element.
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selection {
+ /// The element is not selected.
#[default]
Unselected,
+ /// The selection state of the element is indeterminate.
Indeterminate,
+ /// The element is selected.
Selected,
}
impl Selection {
+ /// Returns the inverse of the current selection status.
+ ///
+ /// Indeterminate states become selected if inverted.
pub fn inverse(&self) -> Self {
match self {
Self::Unselected | Self::Indeterminate => Self::Selected,
@@ -14,7 +14,7 @@ fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -
.shadow(index.shadow())
}
-/// Extends [`Styled`](gpui::Styled) with Zed specific styling methods.
+/// Extends [`gpui::Styled`] with Zed-specific styling methods.
pub trait StyledExt: Styled + Sized {
/// Horizontally stacks elements.
///
@@ -30,6 +30,7 @@ pub trait StyledExt: Styled + Sized {
self.flex().flex_col()
}
+ /// Sets the text size using a [`UiTextSize`].
fn text_ui_size(self, size: UiTextSize) -> Self {
self.text_size(size.rems())
}
@@ -40,7 +41,7 @@ pub trait StyledExt: Styled + Sized {
///
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
///
- /// Use [`text_ui_sm`] for regular-sized text.
+ /// Use `text_ui_sm` for smaller text.
fn text_ui(self) -> Self {
self.text_size(UiTextSize::default().rems())
}
@@ -51,7 +52,7 @@ pub trait StyledExt: Styled + Sized {
///
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
///
- /// Use [`text_ui`] for regular-sized text.
+ /// Use `text_ui` for regular-sized text.
fn text_ui_sm(self) -> Self {
self.text_size(UiTextSize::Small.rems())
}
@@ -62,7 +63,7 @@ pub trait StyledExt: Styled + Sized {
///
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
///
- /// Use [`text_ui`] for regular-sized text.
+ /// Use `text_ui` for regular-sized text.
fn text_ui_xs(self) -> Self {
self.text_size(UiTextSize::XSmall.rems())
}
@@ -78,7 +79,7 @@ pub trait StyledExt: Styled + Sized {
self.text_size(settings.buffer_font_size(cx))
}
- /// The [`Surface`](ui::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
+ /// The [`Surface`](ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
///
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
@@ -87,7 +88,7 @@ pub trait StyledExt: Styled + Sized {
elevated(self, cx, ElevationIndex::Surface)
}
- /// Non-Modal Elevated Surfaces appear above the [`Surface`](ui::ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc.
+ /// Non-Modal Elevated Surfaces appear above the [`Surface`](ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc.
///
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
@@ -100,7 +101,7 @@ pub trait StyledExt: Styled + Sized {
///
/// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
///
- /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ui::ElevationIndex::ElevatedSurface) layer.
+ /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ElevationIndex::ElevatedSurface) layer.
///
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
@@ -119,26 +120,32 @@ pub trait StyledExt: Styled + Sized {
self.border_color(cx.theme().colors().border_variant)
}
+ /// Sets the background color to red for debugging when building UI.
fn debug_bg_red(self) -> Self {
self.bg(hsla(0. / 360., 1., 0.5, 1.))
}
+ /// Sets the background color to green for debugging when building UI.
fn debug_bg_green(self) -> Self {
self.bg(hsla(120. / 360., 1., 0.5, 1.))
}
+ /// Sets the background color to blue for debugging when building UI.
fn debug_bg_blue(self) -> Self {
self.bg(hsla(240. / 360., 1., 0.5, 1.))
}
+ /// Sets the background color to yellow for debugging when building UI.
fn debug_bg_yellow(self) -> Self {
self.bg(hsla(60. / 360., 1., 0.5, 1.))
}
+ /// Sets the background color to cyan for debugging when building UI.
fn debug_bg_cyan(self) -> Self {
self.bg(hsla(160. / 360., 1., 0.5, 1.))
}
+ /// Sets the background color to magenta for debugging when building UI.
fn debug_bg_magenta(self) -> Self {
self.bg(hsla(300. / 360., 1., 0.5, 1.))
}
@@ -1,6 +1,7 @@
use gpui::{Hsla, WindowContext};
use theme::ActiveTheme;
+/// Sets a color that has a consistent meaning across all themes.
#[derive(Debug, Default, PartialEq, Copy, Clone)]
pub enum Color {
#[default]
@@ -85,6 +85,7 @@ impl LayerIndex {
}
}
+/// An appropriate z-index for the given layer based on its intended useage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementIndex {
Effect,
@@ -38,6 +38,7 @@ impl UiTextSize {
}
}
+/// The size of a [`Headline`] element
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum HeadlineSize {
XSmall,
@@ -3,8 +3,6 @@
//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI.
//!
-#![doc = include_str!("../docs/building-ui.md")]
-
mod clickable;
mod components;
mod disableable;
@@ -1,3 +1,5 @@
+//! UI-related utilities (e.g. converting dates to a human-readable form).
+
mod format_distance;
pub use format_distance::*;
@@ -7,10 +7,10 @@ pub enum DateTimeType {
}
impl DateTimeType {
- /// Converts the DateTimeType to a NaiveDateTime.
+ /// Converts the [`DateTimeType`] to a [`NaiveDateTime`].
///
- /// If the DateTimeType is already a NaiveDateTime, it will be returned as is.
- /// If the DateTimeType is a DateTime<Local>, it will be converted to a NaiveDateTime.
+ /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is.
+ /// If the [`DateTimeType`] is a [`DateTime<Local>`], it will be converted to a [`NaiveDateTime`].
pub fn to_naive(&self) -> NaiveDateTime {
match self {
DateTimeType::Naive(naive) => *naive,
@@ -68,13 +68,13 @@ impl FormatDistance {
}
}
-/// Calculates the distance in seconds between two NaiveDateTime objects.
+/// Calculates the distance in seconds between two [`NaiveDateTime`] objects.
/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
///
/// ## Arguments
///
-/// * `date` - A NaiveDateTime object representing the date of interest
-/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
+/// * `date` - A [NaiveDateTime`] object representing the date of interest
+/// * `base_date` - A [NaiveDateTime`] object representing the base date against which the comparison is made
fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
let duration = date.signed_duration_since(base_date);
-duration.num_seconds()
@@ -233,12 +233,12 @@ fn distance_string(
///
/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
///
-/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
+/// Use [`format_distance_from_now`] to compare a NaiveDateTime against now.
///
/// # Arguments
///
-/// * `date` - The NaiveDateTime to compare.
-/// * `base_date` - The NaiveDateTime to compare against.
+/// * `date` - The [`NaiveDateTime`] to compare.
+/// * `base_date` - The [`NaiveDateTime`] to compare against.
/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
///
@@ -274,7 +274,7 @@ pub fn format_distance(
///
/// # Arguments
///
-/// * `datetime` - The NaiveDateTime to compare with the current time.
+/// * `datetime` - The [`NaiveDateTime`] to compare with the current time.
/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
///
@@ -28,7 +28,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
fn size(&self, cx: &WindowContext) -> Pixels;
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>);
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+ fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
fn toggle_action(&self) -> Box<dyn Action>;
fn icon_label(&self, _: &WindowContext) -> Option<String> {
@@ -52,7 +52,7 @@ pub trait PanelHandle: Send + Sync {
fn set_active(&self, active: bool, cx: &mut WindowContext);
fn size(&self, cx: &WindowContext) -> Pixels;
fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+ fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
@@ -104,7 +104,7 @@ where
self.update(cx, |this, cx| this.set_size(size, cx))
}
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+ fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
self.read(cx).icon(cx)
}
@@ -395,7 +395,6 @@ impl Dock {
})
.ok();
}
- // todo!() we do not use this event in the production code (even in zed1), remove it
PanelEvent::Activate => {
if let Some(ix) = this
.panel_entries
@@ -775,7 +774,7 @@ pub mod test {
self.size = size.unwrap_or(px(300.));
}
- fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
+ fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
None
}
@@ -101,6 +101,10 @@ impl ModalLayer {
let active_modal = self.active_modal.as_ref()?;
active_modal.modal.view().downcast::<V>().ok()
}
+
+ pub fn has_active_modal(&self) -> bool {
+ self.active_modal.is_some()
+ }
}
impl Render for ModalLayer {
@@ -175,7 +175,7 @@ pub mod simple_message_notification {
};
use std::sync::Arc;
use ui::prelude::*;
- use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
+ use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt};
pub struct MessageNotification {
message: SharedString,
@@ -230,7 +230,7 @@ pub mod simple_message_notification {
.child(
div()
.id("cancel")
- .child(IconElement::new(Icon::Close))
+ .child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
),
@@ -247,105 +247,6 @@ pub mod simple_message_notification {
}))
}
}
- // todo!()
- // impl View for MessageNotification {
- // fn ui_name() -> &'static str {
- // "MessageNotification"
- // }
-
- // fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
- // let theme = theme::current(cx).clone();
- // let theme = &theme.simple_message_notification;
-
- // enum MessageNotificationTag {}
-
- // let click_message = self.click_message.clone();
- // let message = match &self.message {
- // NotificationMessage::Text(text) => {
- // Text::new(text.to_owned(), theme.message.text.clone()).into_any()
- // }
- // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
- // };
- // let on_click = self.on_click.clone();
- // let has_click_action = on_click.is_some();
-
- // Flex::column()
- // .with_child(
- // Flex::row()
- // .with_child(
- // message
- // .contained()
- // .with_style(theme.message.container)
- // .aligned()
- // .top()
- // .left()
- // .flex(1., true),
- // )
- // .with_child(
- // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
- // let style = theme.dismiss_button.style_for(state);
- // Svg::new("icons/x.svg")
- // .with_color(style.color)
- // .constrained()
- // .with_width(style.icon_width)
- // .aligned()
- // .contained()
- // .with_style(style.container)
- // .constrained()
- // .with_width(style.button_width)
- // .with_height(style.button_width)
- // })
- // .with_padding(Padding::uniform(5.))
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.dismiss(&Default::default(), cx);
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .aligned()
- // .constrained()
- // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
- // .aligned()
- // .top()
- // .flex_float(),
- // ),
- // )
- // .with_children({
- // click_message
- // .map(|click_message| {
- // MouseEventHandler::new::<MessageNotificationTag, _>(
- // 0,
- // cx,
- // |state, _| {
- // let style = theme.action_message.style_for(state);
-
- // Flex::row()
- // .with_child(
- // Text::new(click_message, style.text.clone())
- // .contained()
- // .with_style(style.container),
- // )
- // .contained()
- // },
- // )
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // if let Some(on_click) = on_click.as_ref() {
- // on_click(cx);
- // this.dismiss(&Default::default(), cx);
- // }
- // })
- // // Since we're not using a proper overlay, we have to capture these extra events
- // .on_down(MouseButton::Left, |_, _, _| {})
- // .on_up(MouseButton::Left, |_, _, _| {})
- // .with_cursor_style(if has_click_action {
- // CursorStyle::PointingHand
- // } else {
- // CursorStyle::Arrow
- // })
- // })
- // .into_iter()
- // })
- // .into_any()
- // }
- // }
}
pub trait NotifyResultExt {
@@ -31,8 +31,8 @@ use std::{
use theme::ThemeSettings;
use ui::{
- prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
- Tab, TabBar, TabPosition, Tooltip,
+ prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconName, IconSize, Indicator,
+ Label, Tab, TabBar, TabPosition, Tooltip,
};
use ui::{v_stack, ContextMenu};
use util::{maybe, truncate_and_remove_front, ResultExt};
@@ -384,7 +384,7 @@ impl Pane {
h_stack()
.gap_2()
.child(
- IconButton::new("plus", Icon::Plus)
+ IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener(|pane, _, cx| {
@@ -406,7 +406,7 @@ impl Pane {
el.child(Self::render_menu_overlay(new_item_menu))
})
.child(
- IconButton::new("split", Icon::Split)
+ IconButton::new("split", IconName::Split)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener(|pane, _, cx| {
@@ -427,11 +427,11 @@ impl Pane {
)
.child({
let zoomed = pane.is_zoomed();
- IconButton::new("toggle_zoom", Icon::Maximize)
+ IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.selected(zoomed)
- .selected_icon(Icon::Minimize)
+ .selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&crate::ToggleZoom, cx);
}))
@@ -1570,7 +1570,7 @@ impl Pane {
})
.start_slot::<Indicator>(indicator)
.end_slot(
- IconButton::new("close tab", Icon::Close)
+ IconButton::new("close tab", IconName::Close)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall)
@@ -1676,7 +1676,7 @@ impl Pane {
h_stack()
.gap_2()
.child(
- IconButton::new("navigate_backward", Icon::ArrowLeft)
+ IconButton::new("navigate_backward", IconName::ArrowLeft)
.icon_size(IconSize::Small)
.on_click({
let view = cx.view().clone();
@@ -1686,7 +1686,7 @@ impl Pane {
.tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
)
.child(
- IconButton::new("navigate_forward", Icon::ArrowRight)
+ IconButton::new("navigate_forward", IconName::ArrowRight)
.icon_size(IconSize::Small)
.on_click({
let view = cx.view().clone();
@@ -12,7 +12,7 @@ use serde::Deserialize;
use std::sync::Arc;
use ui::{prelude::*, Button};
-const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
+const HANDLE_HITBOX_SIZE: f32 = 4.0;
const HORIZONTAL_MIN_SIZE: f32 = 80.;
const VERTICAL_MIN_SIZE: f32 = 100.;
@@ -579,12 +579,15 @@ mod element {
Size, Style, WeakView, WindowContext,
};
use parking_lot::Mutex;
+ use settings::Settings;
use smallvec::SmallVec;
use ui::prelude::*;
use util::ResultExt;
use crate::Workspace;
+ use crate::WorkspaceSettings;
+
use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
const DIVIDER_SIZE: f32 = 1.0;
@@ -704,7 +707,6 @@ mod element {
proposed_current_pixel_change -= current_pixel_change;
}
- // todo!(schedule serialize)
workspace
.update(cx, |this, cx| this.schedule_serialize(cx))
.log_err();
@@ -834,20 +836,39 @@ mod element {
debug_assert!(flexes.len() == len);
debug_assert!(flex_values_in_bounds(flexes.as_slice()));
+ let magnification_value = WorkspaceSettings::get(None, cx).active_pane_magnification;
+ let active_pane_magnification = if magnification_value == 1. {
+ None
+ } else {
+ Some(magnification_value)
+ };
+
+ let total_flex = if let Some(flex) = active_pane_magnification {
+ self.children.len() as f32 - 1. + flex
+ } else {
+ len as f32
+ };
+
let mut origin = bounds.origin;
- let space_per_flex = bounds.size.along(self.axis) / len as f32;
+ let space_per_flex = bounds.size.along(self.axis) / total_flex;
let mut bounding_boxes = self.bounding_boxes.lock();
bounding_boxes.clear();
for (ix, child) in self.children.iter_mut().enumerate() {
- //todo!(active_pane_magnification)
- // If using active pane magnification, need to switch to using
- // 1 for all non-active panes, and then the magnification for the
- // active pane.
+ let child_flex = active_pane_magnification
+ .map(|magnification| {
+ if self.active_pane_ix == Some(ix) {
+ magnification
+ } else {
+ 1.
+ }
+ })
+ .unwrap_or_else(|| flexes[ix]);
+
let child_size = bounds
.size
- .apply_along(self.axis, |_| space_per_flex * flexes[ix]);
+ .apply_along(self.axis, |_| space_per_flex * child_flex);
let child_bounds = Bounds {
origin,
@@ -857,20 +878,23 @@ mod element {
cx.with_z_index(0, |cx| {
child.draw(origin, child_size.into(), cx);
});
- cx.with_z_index(1, |cx| {
- if ix < len - 1 {
- Self::push_handle(
- self.flexes.clone(),
- state.clone(),
- self.axis,
- ix,
- child_bounds,
- bounds,
- self.workspace.clone(),
- cx,
- );
- }
- });
+
+ if active_pane_magnification.is_none() {
+ cx.with_z_index(1, |cx| {
+ if ix < len - 1 {
+ Self::push_handle(
+ self.flexes.clone(),
+ state.clone(),
+ self.axis,
+ ix,
+ child_bounds,
+ bounds,
+ self.workspace.clone(),
+ cx,
+ );
+ }
+ });
+ }
origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
}
@@ -12,7 +12,7 @@ use gpui::{
WindowContext,
};
use std::sync::{Arc, Weak};
-use ui::{h_stack, prelude::*, Icon, IconElement, Label};
+use ui::{h_stack, prelude::*, Icon, IconName, Label};
pub enum Event {
Close,
@@ -100,7 +100,7 @@ impl Item for SharedScreen {
) -> gpui::AnyElement {
h_stack()
.gap_1()
- .child(IconElement::new(Icon::Screen))
+ .child(Icon::new(IconName::Screen))
.child(
Label::new(format!("{}'s screen", self.user.github_login)).color(if selected {
Color::Default
@@ -133,82 +133,6 @@ impl Render for Toolbar {
}
}
-// todo!()
-// impl View for Toolbar {
-// fn ui_name() -> &'static str {
-// "Toolbar"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let theme = &theme::current(cx).workspace.toolbar;
-
-// let mut primary_left_items = Vec::new();
-// let mut primary_right_items = Vec::new();
-// let mut secondary_item = None;
-// let spacing = theme.item_spacing;
-// let mut primary_items_row_count = 1;
-
-// for (item, position) in &self.items {
-// match *position {
-// ToolbarItemLocation::Hidden => {}
-
-// ToolbarItemLocation::PrimaryLeft { flex } => {
-// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-// let left_item = ChildView::new(item.as_any(), cx).aligned();
-// if let Some((flex, expanded)) = flex {
-// primary_left_items.push(left_item.flex(flex, expanded).into_any());
-// } else {
-// primary_left_items.push(left_item.into_any());
-// }
-// }
-
-// ToolbarItemLocation::PrimaryRight { flex } => {
-// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
-// if let Some((flex, expanded)) = flex {
-// primary_right_items.push(right_item.flex(flex, expanded).into_any());
-// } else {
-// primary_right_items.push(right_item.into_any());
-// }
-// }
-
-// ToolbarItemLocation::Secondary => {
-// secondary_item = Some(
-// ChildView::new(item.as_any(), cx)
-// .constrained()
-// .with_height(theme.height * item.row_count(cx) as f32)
-// .into_any(),
-// );
-// }
-// }
-// }
-
-// let container_style = theme.container;
-// let height = theme.height * primary_items_row_count as f32;
-
-// let mut primary_items = Flex::row().with_spacing(spacing);
-// primary_items.extend(primary_left_items);
-// primary_items.extend(primary_right_items);
-
-// let mut toolbar = Flex::column();
-// if !primary_items.is_empty() {
-// toolbar.add_child(primary_items.constrained().with_height(height));
-// }
-// if let Some(secondary_item) = secondary_item {
-// toolbar.add_child(secondary_item);
-// }
-
-// if toolbar.is_empty() {
-// toolbar.into_any_named("toolbar")
-// } else {
-// toolbar
-// .contained()
-// .with_style(container_style)
-// .into_any_named("toolbar")
-// }
-// }
-// }
-
impl Toolbar {
pub fn new() -> Self {
Self {
@@ -312,10 +236,3 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for View<T> {
self.read(cx).row_count(cx)
}
}
-
-// todo!()
-// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
-// fn from(val: &dyn ToolbarItemViewHandle) -> Self {
-// val.as_any().clone()
-// }
-// }
@@ -943,10 +943,8 @@ impl Workspace {
cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> {
let to_load = if let Some(pane) = pane.upgrade() {
- // todo!("focus")
- // cx.focus(&pane);
-
pane.update(cx, |pane, cx| {
+ pane.focus(cx);
loop {
// Retrieve the weak item handle from the history.
let entry = pane.nav_history_mut().pop(mode, cx)?;
@@ -1631,8 +1629,7 @@ impl Workspace {
});
}
- // todo!("focus")
- // cx.focus_self();
+ cx.focus_self();
cx.notify();
self.serialize_workspace(cx);
}
@@ -1713,6 +1710,7 @@ impl Workspace {
cx.notify();
}
+ // todo!()
// #[cfg(any(test, feature = "test-support"))]
// pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
// self.zoomed.and_then(|view| view.upgrade(cx))
@@ -2992,7 +2990,6 @@ impl Workspace {
cx.notify();
}
- #[allow(unused)]
fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor()
@@ -3386,6 +3383,10 @@ impl Workspace {
div
}
+ pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
+ self.modal_layer.read(cx).has_active_modal()
+ }
+
pub fn active_modal<V: ManagedView + 'static>(
&mut self,
cx: &ViewContext<Self>,
@@ -764,7 +764,6 @@ fn open_bundled_file(
.detach_and_log_err(cx);
}
-// todo!()
#[cfg(test)]
mod tests {
use super::*;