onboarding: Create basic onboarding page (#34723)
Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
parent
bf8aba566c
commit
bc5c5cf5d6
8 changed files with 409 additions and 0 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -10983,6 +10983,23 @@ dependencies = [
|
|||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
@ -20223,6 +20240,7 @@ dependencies = [
|
|||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
"outline",
|
||||
"outline_panel",
|
||||
"parking_lot",
|
||||
|
|
|
@ -108,6 +108,7 @@ members = [
|
|||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
"crates/onboarding",
|
||||
"crates/open_ai",
|
||||
"crates/open_router",
|
||||
"crates/outline",
|
||||
|
@ -325,6 +326,7 @@ net = { path = "crates/net" }
|
|||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
onboarding = { path = "crates/onboarding" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
open_router = { path = "crates/open_router", features = ["schemars"] }
|
||||
outline = { path = "crates/outline" }
|
||||
|
|
28
crates/onboarding/Cargo.toml
Normal file
28
crates/onboarding/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/onboarding.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
1
crates/onboarding/LICENSE-GPL
Symbolic link
1
crates/onboarding/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../LICENSE-GPL
|
352
crates/onboarding/src/onboarding.rs
Normal file
352
crates/onboarding/src/onboarding.rs
Normal file
|
@ -0,0 +1,352 @@
|
|||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use theme::{ThemeMode, ThemeSettings};
|
||||
use ui::{
|
||||
ButtonCommon as _, ButtonSize, ButtonStyle, Clickable as _, Color, Divider, FluentBuilder,
|
||||
Headline, InteractiveElement, KeyBinding, Label, LabelCommon, ParentElement as _,
|
||||
StatefulInteractiveElement, Styled, ToggleButton, Toggleable as _, Vector, VectorName, div,
|
||||
h_flex, rems, v_container, v_flex,
|
||||
};
|
||||
use workspace::{
|
||||
AppState, Workspace, WorkspaceId,
|
||||
dock::DockPosition,
|
||||
item::{Item, ItemEvent},
|
||||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
pub struct OnBoardingFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for OnBoardingFeatureFlag {
|
||||
const NAME: &'static str = "onboarding";
|
||||
}
|
||||
|
||||
pub const FIRST_OPEN: &str = "first_open";
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
/// Opens the onboarding view.
|
||||
OpenOnboarding
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.on_action(|_: &OpenOnboarding, cx| {
|
||||
with_active_or_new_workspace(cx, |workspace, window, cx| {
|
||||
workspace
|
||||
.with_local_workspace(window, cx, |workspace, window, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<Onboarding>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let settings_page = Onboarding::new(workspace.weak_handle(), cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(settings_page),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
cx.observe_new::<Workspace>(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&onboarding_actions);
|
||||
});
|
||||
|
||||
cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_action_types(onboarding_actions.iter());
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&onboarding_actions);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
|
||||
open_new(
|
||||
Default::default(),
|
||||
app_state,
|
||||
cx,
|
||||
|workspace, window, cx| {
|
||||
{
|
||||
workspace.toggle_dock(DockPosition::Left, window, cx);
|
||||
let onboarding_page = Onboarding::new(workspace.weak_handle(), cx);
|
||||
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
|
||||
|
||||
window.focus(&onboarding_page.focus_handle(cx));
|
||||
|
||||
cx.notify();
|
||||
};
|
||||
db::write_and_log(cx, || {
|
||||
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn read_theme_selection(cx: &App) -> ThemeMode {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings
|
||||
.theme_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| selection.mode())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
|
||||
settings.set_mode(theme_mode);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SelectedPage {
|
||||
Basics,
|
||||
Editing,
|
||||
AiSetup,
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_page: SelectedPage,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_page_nav(
|
||||
&mut self,
|
||||
page: SelectedPage,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let text = match page {
|
||||
SelectedPage::Basics => "Basics",
|
||||
SelectedPage::Editing => "Editing",
|
||||
SelectedPage::AiSetup => "AI Setup",
|
||||
};
|
||||
let binding = match page {
|
||||
SelectedPage::Basics => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
|
||||
}
|
||||
SelectedPage::Editing => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
|
||||
}
|
||||
SelectedPage::AiSetup => {
|
||||
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
|
||||
}
|
||||
};
|
||||
let selected = self.selected_page == page;
|
||||
h_flex()
|
||||
.id(text)
|
||||
.rounded_sm()
|
||||
.child(text)
|
||||
.child(binding)
|
||||
.h_8()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.map(|this| {
|
||||
if selected {
|
||||
this.bg(Color::Selected.color(cx))
|
||||
.border_l_1()
|
||||
.border_color(Color::Accent.color(cx))
|
||||
} else {
|
||||
this.text_color(Color::Muted.color(cx))
|
||||
}
|
||||
})
|
||||
.hover(|style| {
|
||||
if selected {
|
||||
style.bg(Color::Selected.color(cx).opacity(0.6))
|
||||
} else {
|
||||
style.bg(Color::Selected.color(cx).opacity(0.3))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.selected_page = page;
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
match self.selected_page {
|
||||
SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(),
|
||||
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme_mode = read_theme_selection(cx);
|
||||
|
||||
v_container().child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(Label::new("Theme"))
|
||||
.child(
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.child(
|
||||
ToggleButton::new("light", "Light")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::Light)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Light, cx))
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new("dark", "Dark")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::Dark)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::Dark, cx))
|
||||
.last(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new("system", "System")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.toggle_state(theme_mode == ThemeMode::System)
|
||||
.on_click(|_, _, cx| write_theme_selection(ThemeMode::System, cx))
|
||||
.middle(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
// div().child("editing page")
|
||||
"Right"
|
||||
}
|
||||
|
||||
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child("ai setup page")
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.image_cache(gpui::retain_all("onboarding-page"))
|
||||
.key_context("onboarding-page")
|
||||
.px_24()
|
||||
.py_12()
|
||||
.items_start()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_1_3()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.pt_0p5()
|
||||
.child(Vector::square(VectorName::ZedLogo, rems(2.)))
|
||||
.child(
|
||||
v_flex()
|
||||
.left_1()
|
||||
.items_center()
|
||||
.child(Headline::new("Welcome to Zed"))
|
||||
.child(
|
||||
Label::new("The editor for what's next")
|
||||
.color(Color::Muted)
|
||||
.italic(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.p_1()
|
||||
.child(Divider::horizontal_dashed())
|
||||
.child(
|
||||
v_flex().gap_1().children([
|
||||
self.render_page_nav(SelectedPage::Basics, window, cx)
|
||||
.into_element(),
|
||||
self.render_page_nav(SelectedPage::Editing, window, cx)
|
||||
.into_element(),
|
||||
self.render_page_nav(SelectedPage::AiSetup, window, cx)
|
||||
.into_element(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
// .child(Divider::vertical_dashed())
|
||||
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for Onboarding {}
|
||||
|
||||
impl Focusable for Onboarding {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for Onboarding {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Onboarding".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Onboarding Page Opened")
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(Onboarding::new(self.workspace.clone(), cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
|
@ -99,6 +99,7 @@ nc.workspace = true
|
|||
nix = { workspace = true, features = ["pthread", "signal"] }
|
||||
node_runtime.workspace = true
|
||||
notifications.workspace = true
|
||||
onboarding.workspace = true
|
||||
outline.workspace = true
|
||||
outline_panel.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
|
|
@ -24,6 +24,7 @@ use reqwest_client::ReqwestClient;
|
|||
|
||||
use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use onboarding::show_onboarding_view;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use recent_projects::{SshSettings, open_ssh_project};
|
||||
|
@ -619,6 +620,7 @@ pub fn main() {
|
|||
markdown_preview::init(cx);
|
||||
svg_preview::init(cx);
|
||||
welcome::init(cx);
|
||||
onboarding::init(cx);
|
||||
settings_ui::init(cx);
|
||||
extensions_ui::init(cx);
|
||||
zeta::init(cx);
|
||||
|
@ -1039,6 +1041,10 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
|
|||
);
|
||||
}
|
||||
}
|
||||
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
let state = app_state.clone();
|
||||
cx.update(|cx| show_onboarding_view(state, cx))?.await?;
|
||||
// cx.update(|cx| show_welcome_view(app_state, cx))?.await?;
|
||||
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
cx.update(|cx| show_welcome_view(app_state, cx))?.await?;
|
||||
} else {
|
||||
|
|
|
@ -3957,6 +3957,7 @@ mod tests {
|
|||
language::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
welcome::init(cx);
|
||||
onboarding::init(cx);
|
||||
Project::init_settings(cx);
|
||||
app_state
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue