537 lines
20 KiB
Rust
537 lines
20 KiB
Rust
use pathfinder_geometry::vector::vec2f;
|
|
use galaxyui::elements::{
|
|
Align, ClippedScrollStateHandle, ClippedScrollable, Container, CornerRadius, Element, Empty,
|
|
Fill, Flex, Hoverable, Icon, MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement,
|
|
Radius, Shrinkable,
|
|
};
|
|
use galaxyui::platform::Cursor;
|
|
use galaxyui::presenter::ChildView;
|
|
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
|
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
|
use galaxyui::{
|
|
AppContext, Entity, EntityId, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
|
ViewHandle, WindowId,
|
|
};
|
|
|
|
use super::section_views::feature_section::FeatureSectionEvent;
|
|
use super::section_views::{
|
|
SectionViewHandle, BUTTON_PADDING, DETAIL_FONT_SIZE, FOOTER_ICON_SIZE, SCROLLBAR_OFFSET,
|
|
SCROLLBAR_WIDTH, SECTION_SPACING, SECTION_SPACING_BOTTOM,
|
|
};
|
|
use super::sections::sections;
|
|
use super::{
|
|
ChangelogSectionView, ContentSectionData, ContentSectionView, FeatureSection,
|
|
FeatureSectionData, FeatureSectionView, Section, TipsCompleted,
|
|
};
|
|
use crate::appearance::Appearance;
|
|
use crate::auth::AuthStateProvider;
|
|
use crate::changelog_model::ChangelogModel;
|
|
use crate::channel::ChannelState;
|
|
use crate::features::FeatureFlag;
|
|
use crate::resource_center::skip_tips_and_write_to_user_defaults;
|
|
use crate::send_telemetry_from_ctx;
|
|
use crate::server::telemetry::TelemetryEvent;
|
|
use crate::settings::Settings;
|
|
use crate::themes::theme::{Blend, Fill as FillTheme};
|
|
use crate::workspace::WorkspaceAction;
|
|
|
|
const SEND_SVG_PATH: &str = "bundled/svg/send.svg";
|
|
|
|
#[derive(Default)]
|
|
struct MouseStateHandles {
|
|
copy_version: MouseStateHandle,
|
|
invite_people: MouseStateHandle,
|
|
skip_tips: MouseStateHandle,
|
|
}
|
|
|
|
pub enum ResourceCenterMainEvent {
|
|
Close,
|
|
}
|
|
|
|
pub struct ResourceCenterMainView {
|
|
button_mouse_states: MouseStateHandles,
|
|
clipped_scroll_state: ClippedScrollStateHandle,
|
|
section_views: Vec<SectionViewHandle>,
|
|
tips_completed: ModelHandle<TipsCompleted>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ResourceCenterMainAction {
|
|
Close,
|
|
SkipTips,
|
|
}
|
|
|
|
impl ResourceCenterMainView {
|
|
pub fn new(
|
|
ctx: &mut ViewContext<Self>,
|
|
tips_completed: ModelHandle<TipsCompleted>,
|
|
changelog_model_handle: ModelHandle<ChangelogModel>,
|
|
) -> Self {
|
|
let action_target = ctx.add_model(|_| ActionTarget::None);
|
|
let section_views = Self::initialize_section_views(
|
|
tips_completed.clone(),
|
|
action_target.clone(),
|
|
ctx,
|
|
changelog_model_handle.clone(),
|
|
);
|
|
Self {
|
|
button_mouse_states: Default::default(),
|
|
clipped_scroll_state: Default::default(),
|
|
section_views,
|
|
tips_completed,
|
|
}
|
|
}
|
|
|
|
fn initialize_section_views(
|
|
tips_completed: ModelHandle<TipsCompleted>,
|
|
action_target: ModelHandle<ActionTarget>,
|
|
ctx: &mut ViewContext<Self>,
|
|
changelog_model_handle: ModelHandle<ChangelogModel>,
|
|
) -> Vec<SectionViewHandle> {
|
|
let sections = sections(ctx);
|
|
|
|
// Set gamified tips count
|
|
let gamified_tips_count = sections
|
|
.iter()
|
|
.map(|section| {
|
|
let mut count = 0;
|
|
if let Section::Feature(data) = section {
|
|
count = data.items.len()
|
|
}
|
|
count
|
|
})
|
|
.sum();
|
|
|
|
tips_completed.update(ctx, |tips_completed, _ctx| {
|
|
tips_completed.set_gamified_tips_count(gamified_tips_count);
|
|
});
|
|
|
|
// Determines if user has completed all tips under Getting Started
|
|
let is_onboarded = sections.iter().any(|section| {
|
|
if let Section::Feature(data) = section {
|
|
let is_section_completed = data.is_section_completed(tips_completed.as_ref(ctx));
|
|
is_section_completed && data.section_name == FeatureSection::GettingStarted
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
|
|
sections
|
|
.iter()
|
|
.map(|section| match section {
|
|
Section::Feature(data) => {
|
|
let is_tips_completed = tips_completed.as_ref(ctx).skipped_or_completed;
|
|
let is_expanded = match data.section_name {
|
|
// Always show What's New section
|
|
FeatureSection::WhatsNew => true,
|
|
FeatureSection::GettingStarted => match ChannelState::app_version() {
|
|
Some(version) => {
|
|
match Settings::has_changelog_been_shown(version, ctx) {
|
|
true => !is_tips_completed && !is_onboarded,
|
|
false => false,
|
|
}
|
|
}
|
|
None => !is_tips_completed && !is_onboarded,
|
|
},
|
|
// Expand Maximize Warp section once user has completed welcome tips,
|
|
// and keep open after users have completed/skipped all tips
|
|
FeatureSection::MaximizeWarp => match ChannelState::app_version() {
|
|
Some(version) => {
|
|
match Settings::has_changelog_been_shown(version, ctx) {
|
|
true => is_tips_completed || is_onboarded,
|
|
false => false,
|
|
}
|
|
}
|
|
None => is_tips_completed || is_onboarded,
|
|
},
|
|
_ => false,
|
|
};
|
|
|
|
// Show tips progress for every section except changelog
|
|
let show_tips_progress = !matches!(data.section_name, FeatureSection::WhatsNew);
|
|
|
|
SectionViewHandle::Feature(Self::build_feature_section_view(
|
|
data,
|
|
action_target.clone(),
|
|
ctx,
|
|
tips_completed.clone(),
|
|
show_tips_progress,
|
|
is_expanded,
|
|
))
|
|
}
|
|
Section::Content(data) => {
|
|
SectionViewHandle::Content(Self::build_content_section_view(data, ctx))
|
|
}
|
|
Section::Changelog() => SectionViewHandle::Changelog(
|
|
Self::build_changelog_section_view(changelog_model_handle.clone(), ctx),
|
|
),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn build_feature_section_view(
|
|
section_data: &FeatureSectionData,
|
|
action_target: ModelHandle<ActionTarget>,
|
|
ctx: &mut ViewContext<ResourceCenterMainView>,
|
|
tips_completed: ModelHandle<TipsCompleted>,
|
|
show_tips_progress: bool,
|
|
is_expanded: bool,
|
|
) -> ViewHandle<FeatureSectionView> {
|
|
let feature_section_view = ctx.add_typed_action_view(|ctx| {
|
|
FeatureSectionView::new(
|
|
section_data.clone(),
|
|
action_target,
|
|
ctx,
|
|
tips_completed.clone(),
|
|
show_tips_progress,
|
|
is_expanded,
|
|
)
|
|
});
|
|
|
|
ctx.subscribe_to_view(&feature_section_view, move |me, _, event, ctx| {
|
|
me.handle_feature_section_event(event, ctx);
|
|
});
|
|
|
|
feature_section_view
|
|
}
|
|
|
|
fn handle_feature_section_event(
|
|
&mut self,
|
|
event: &FeatureSectionEvent,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
match event {
|
|
FeatureSectionEvent::CloseResourceCenter => {
|
|
ctx.emit(ResourceCenterMainEvent::Close);
|
|
ctx.notify();
|
|
}
|
|
FeatureSectionEvent::ExpandSection(section_name) => {
|
|
for section_view in &self.section_views {
|
|
match section_view {
|
|
SectionViewHandle::Feature(feature_view_handle) => {
|
|
if feature_view_handle
|
|
.as_ref(ctx)
|
|
.feature_section_data
|
|
.section_name
|
|
== *section_name
|
|
{
|
|
feature_view_handle.update(ctx, |view, ctx| {
|
|
view.expand_section(ctx);
|
|
})
|
|
}
|
|
}
|
|
SectionViewHandle::Content(_) => {}
|
|
SectionViewHandle::Changelog(_) => {}
|
|
}
|
|
}
|
|
ctx.notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_content_section_view(
|
|
section_data: &ContentSectionData,
|
|
ctx: &mut ViewContext<ResourceCenterMainView>,
|
|
) -> ViewHandle<ContentSectionView> {
|
|
ctx.add_typed_action_view(|ctx| ContentSectionView::new(section_data.clone(), false, ctx))
|
|
}
|
|
|
|
fn build_changelog_section_view(
|
|
changelog_model_handle: ModelHandle<ChangelogModel>,
|
|
ctx: &mut ViewContext<ResourceCenterMainView>,
|
|
) -> ViewHandle<ChangelogSectionView> {
|
|
let showing_new_changelog = match ChannelState::app_version() {
|
|
Some(version) => !Settings::has_changelog_been_shown(version, ctx),
|
|
None => false,
|
|
};
|
|
|
|
ctx.add_typed_action_view(|ctx: &mut ViewContext<_>| {
|
|
ChangelogSectionView::new(changelog_model_handle, showing_new_changelog, ctx)
|
|
})
|
|
}
|
|
|
|
pub fn set_action_target(
|
|
&mut self,
|
|
window_id: WindowId,
|
|
input_id: Option<EntityId>,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
for section_view in &self.section_views {
|
|
match section_view {
|
|
SectionViewHandle::Feature(view_handle) => {
|
|
view_handle.update(ctx, |feature_section_view, ctx| {
|
|
feature_section_view.set_action_target(window_id, input_id, ctx)
|
|
});
|
|
}
|
|
SectionViewHandle::Content(_) => {}
|
|
SectionViewHandle::Changelog(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_body(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let mut body = Flex::column();
|
|
|
|
for section_view in &self.section_views {
|
|
match section_view {
|
|
SectionViewHandle::Feature(feature_view_handle) => {
|
|
body.add_child(ChildView::new(feature_view_handle).finish());
|
|
}
|
|
SectionViewHandle::Content(section_view_handle) => {
|
|
body.add_child(ChildView::new(section_view_handle).finish());
|
|
}
|
|
SectionViewHandle::Changelog(section_view_handle) => {
|
|
body.add_child(ChildView::new(section_view_handle).finish());
|
|
}
|
|
}
|
|
}
|
|
|
|
let theme = appearance.theme();
|
|
|
|
ClippedScrollable::vertical(
|
|
self.clipped_scroll_state.clone(),
|
|
body.finish(),
|
|
SCROLLBAR_WIDTH,
|
|
theme.disabled_text_color(theme.background()).into(),
|
|
theme.main_text_color(theme.background()).into(),
|
|
Fill::None,
|
|
)
|
|
.finish()
|
|
}
|
|
|
|
fn render_current_version(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
// Use a dummy string for git release tag which is not available on local env
|
|
let version = ChannelState::app_version().unwrap_or("v0.local.testing.string_00");
|
|
|
|
let style = UiComponentStyles {
|
|
font_color: Some(appearance.theme().nonactive_ui_text_color().into()),
|
|
..Default::default()
|
|
};
|
|
|
|
let text = appearance
|
|
.ui_builder()
|
|
.wrappable_text(version, true)
|
|
.with_style(style)
|
|
.build()
|
|
.finish();
|
|
|
|
let copy_icon = appearance
|
|
.ui_builder()
|
|
.copy_button(
|
|
FOOTER_ICON_SIZE,
|
|
self.button_mouse_states.copy_version.clone(),
|
|
)
|
|
.build()
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(WorkspaceAction::CopyVersion(version))
|
|
})
|
|
.finish();
|
|
|
|
Container::new(
|
|
Flex::row()
|
|
.with_child(Shrinkable::new(1., Align::new(text).left().finish()).finish())
|
|
.with_child(Shrinkable::new(0.2, Align::new(copy_icon).finish()).finish())
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.finish(),
|
|
)
|
|
.with_margin_left(SECTION_SPACING)
|
|
.with_margin_bottom(BUTTON_PADDING)
|
|
.with_uniform_padding(BUTTON_PADDING)
|
|
.finish()
|
|
}
|
|
|
|
fn render_invite_button(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let default_styles = UiComponentStyles {
|
|
font_size: Some(DETAIL_FONT_SIZE),
|
|
font_family_id: Some(appearance.ui_font_family()),
|
|
font_color: Some(appearance.theme().accent().into()),
|
|
border_radius: Some(CornerRadius::with_all(Radius::Percentage(20.))),
|
|
border_width: Some(1.),
|
|
border_color: Some(appearance.theme().accent().into()),
|
|
padding: Some(Coords {
|
|
top: BUTTON_PADDING,
|
|
bottom: BUTTON_PADDING,
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let hovered_styles = UiComponentStyles {
|
|
background: Some(appearance.theme().accent().into()),
|
|
font_color: Some(
|
|
appearance
|
|
.theme()
|
|
.main_text_color(appearance.theme().accent())
|
|
.into_solid(),
|
|
),
|
|
..default_styles
|
|
};
|
|
|
|
let clicked_color = appearance.theme().accent().blend(
|
|
&FillTheme::black().with_opacity(*appearance.theme().details().button_click_opacity()),
|
|
);
|
|
let clicked_styles = UiComponentStyles {
|
|
background: Some(clicked_color.into()),
|
|
border_color: Some(clicked_color.into()),
|
|
..hovered_styles
|
|
};
|
|
|
|
Container::new(
|
|
appearance
|
|
.ui_builder()
|
|
.button_with_custom_styles(
|
|
ButtonVariant::Outlined,
|
|
self.button_mouse_states.invite_people.clone(),
|
|
default_styles,
|
|
Some(hovered_styles),
|
|
Some(clicked_styles),
|
|
None,
|
|
)
|
|
.with_text_and_icon_label(
|
|
TextAndIcon::new(
|
|
TextAndIconAlignment::IconFirst,
|
|
"Invite a friend to Galaxy",
|
|
Icon::new(SEND_SVG_PATH, appearance.theme().accent()),
|
|
MainAxisSize::Max,
|
|
MainAxisAlignment::Center,
|
|
vec2f(FOOTER_ICON_SIZE, FOOTER_ICON_SIZE),
|
|
)
|
|
.with_inner_padding(BUTTON_PADDING),
|
|
)
|
|
.build()
|
|
.on_click(|ctx, _, _| {
|
|
ctx.dispatch_typed_action(WorkspaceAction::ShowReferralSettingsPage)
|
|
})
|
|
.finish(),
|
|
)
|
|
.with_margin_top(SECTION_SPACING)
|
|
.with_margin_bottom(SECTION_SPACING_BOTTOM)
|
|
.with_margin_left(SECTION_SPACING + SCROLLBAR_OFFSET)
|
|
.with_margin_right(SECTION_SPACING + SCROLLBAR_OFFSET)
|
|
.finish()
|
|
}
|
|
|
|
fn render_skip_tips_button(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
Container::new(
|
|
Align::new(
|
|
Hoverable::new(self.button_mouse_states.skip_tips.clone(), |state| {
|
|
let text_color = if state.is_hovered() {
|
|
appearance.theme().active_ui_text_color().into_solid()
|
|
} else {
|
|
appearance.theme().nonactive_ui_text_color().into_solid()
|
|
};
|
|
|
|
let style = UiComponentStyles {
|
|
font_size: Some(DETAIL_FONT_SIZE),
|
|
font_color: Some(text_color),
|
|
..Default::default()
|
|
};
|
|
|
|
appearance
|
|
.ui_builder()
|
|
.wrappable_text("Mark all as read", false)
|
|
.with_style(style)
|
|
.build()
|
|
.finish()
|
|
})
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(ResourceCenterMainAction::SkipTips)
|
|
})
|
|
.with_cursor(Cursor::PointingHand)
|
|
.finish(),
|
|
)
|
|
.right()
|
|
.finish(),
|
|
)
|
|
.with_margin_bottom(SECTION_SPACING)
|
|
.with_margin_right(SCROLLBAR_OFFSET + SECTION_SPACING)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
/// A model for tracking where the events from the resource center view should be dispatched
|
|
///
|
|
/// Similar to command palette - we need a model to cache the information of where
|
|
/// we should send the actions from the resource center features. When the resource center is opened,
|
|
/// we cache the current active window ID as well as the input ID of the active
|
|
/// tab/pane. By sending all the actions to the input view, we ensure that
|
|
/// they propagate correctly. This propagation assumes that each feature action
|
|
/// must be in the responder chain. If an action is not in the responder chain
|
|
/// (such as a block navigation action) then it won't propagate correctly.
|
|
pub enum ActionTarget {
|
|
None,
|
|
View {
|
|
window_id: WindowId,
|
|
input_id: Option<EntityId>,
|
|
},
|
|
}
|
|
|
|
impl Entity for ActionTarget {
|
|
type Event = ();
|
|
}
|
|
|
|
impl Entity for ResourceCenterMainView {
|
|
type Event = ResourceCenterMainEvent;
|
|
}
|
|
|
|
impl TypedActionView for ResourceCenterMainView {
|
|
type Action = ResourceCenterMainAction;
|
|
|
|
fn handle_action(&mut self, action: &ResourceCenterMainAction, ctx: &mut ViewContext<Self>) {
|
|
use ResourceCenterMainAction::*;
|
|
match action {
|
|
Close => {
|
|
ctx.notify();
|
|
ctx.emit(ResourceCenterMainEvent::Close);
|
|
}
|
|
SkipTips => {
|
|
send_telemetry_from_ctx!(TelemetryEvent::ResourceCenterTipsSkipped, ctx);
|
|
self.tips_completed.update(ctx, |tips_completed, ctx| {
|
|
skip_tips_and_write_to_user_defaults(tips_completed, ctx);
|
|
ctx.notify();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl View for ResourceCenterMainView {
|
|
fn ui_name() -> &'static str {
|
|
"ResourceCenterMain"
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let body = self.render_body(appearance);
|
|
let invite_button = self.render_invite_button(appearance);
|
|
let skip_tips = self.render_skip_tips_button(appearance);
|
|
|
|
let mut main_page = Flex::column();
|
|
|
|
if !AuthStateProvider::as_ref(app)
|
|
.get()
|
|
.is_anonymous_or_logged_out()
|
|
&& !FeatureFlag::AvatarInTabBar.is_enabled()
|
|
{
|
|
main_page = main_page.with_child(invite_button);
|
|
}
|
|
|
|
if !self.tips_completed.as_ref(app).skipped_or_completed
|
|
&& !FeatureFlag::AvatarInTabBar.is_enabled()
|
|
{
|
|
main_page.add_child(skip_tips);
|
|
}
|
|
|
|
main_page = main_page
|
|
.with_child(Shrinkable::new(20., body).finish())
|
|
.with_child(Shrinkable::new(0.1, Empty::new().finish()).finish()); // placeholder to ensure pane extends to bottom of the window
|
|
|
|
if FeatureFlag::Autoupdate.is_enabled() && ChannelState::show_autoupdate_menu_items() {
|
|
let current_version = self.render_current_version(appearance);
|
|
main_page.add_child(current_version);
|
|
}
|
|
|
|
main_page.finish()
|
|
}
|
|
}
|