Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
use instant::Instant;
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::{
|
||||
elements::{
|
||||
Border, CacheOption, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element,
|
||||
Flex, FormattedTextElement, HighlightedHyperlink, Icon, Image, MouseStateHandle,
|
||||
ParentElement, Radius,
|
||||
},
|
||||
fonts::Weight,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Entity, ModelAsRef, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
changelog_model::{ChangelogHeader, ChangelogModel, ChangelogState, Event as ChangelogEvent},
|
||||
themes::theme::Fill,
|
||||
ui_components::icons,
|
||||
};
|
||||
use crate::{send_telemetry_from_ctx, server::telemetry::TelemetryEvent};
|
||||
|
||||
use super::{feature_section::FeatureSection, SectionAction, SectionView};
|
||||
|
||||
#[derive(Default)]
|
||||
struct ChangelogMouseStateHandles {
|
||||
top_bar_mouse_state: MouseStateHandle,
|
||||
view_changelogs_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
const CHANGELOG_FETCH_ERROR_MSG: &str = "Unable to fetch the latest changelog.";
|
||||
const CHANGELOG_LOADING_MSG: &str = "Loading...";
|
||||
|
||||
pub struct ChangelogSectionView {
|
||||
changelog_model_handle: ModelHandle<ChangelogModel>,
|
||||
changelog_button_mouse_states: ChangelogMouseStateHandles,
|
||||
is_expanded: bool,
|
||||
// If showing changelog after app update, show special "New features" header to draw attention
|
||||
show_special_new_features_header: bool,
|
||||
new_features_highlighted_link: HighlightedHyperlink,
|
||||
improvements_highlighted_link: HighlightedHyperlink,
|
||||
bug_fixes_highlighted_link: HighlightedHyperlink,
|
||||
changelog_fetch_error: FormattedText,
|
||||
changelog_loading: FormattedText,
|
||||
}
|
||||
|
||||
impl Entity for ChangelogSectionView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl TypedActionView for ChangelogSectionView {
|
||||
type Action = SectionAction;
|
||||
|
||||
fn handle_action(&mut self, action: &SectionAction, ctx: &mut ViewContext<Self>) {
|
||||
use SectionAction::*;
|
||||
match action {
|
||||
OpenUrl(url) => {
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::OpenChangelogLink { url: url.clone() },
|
||||
ctx
|
||||
);
|
||||
ctx.open_url(url.as_str());
|
||||
}
|
||||
ToggleExpanded => self.toggle_expanded(ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_formatted_text_from_string(message: String) -> FormattedText {
|
||||
FormattedText {
|
||||
lines: vec![FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(message),
|
||||
])]
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangelogSectionView {
|
||||
pub fn new(
|
||||
changelog_model_handle: ModelHandle<ChangelogModel>,
|
||||
showing_new_changelog: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
ctx.subscribe_to_model(&changelog_model_handle, |me, _, event, ctx| {
|
||||
me.handle_changelog_event(event, ctx);
|
||||
});
|
||||
|
||||
Self {
|
||||
changelog_model_handle,
|
||||
changelog_button_mouse_states: Default::default(),
|
||||
is_expanded: showing_new_changelog,
|
||||
show_special_new_features_header: showing_new_changelog,
|
||||
new_features_highlighted_link: Default::default(),
|
||||
improvements_highlighted_link: Default::default(),
|
||||
bug_fixes_highlighted_link: Default::default(),
|
||||
changelog_fetch_error: create_formatted_text_from_string(
|
||||
CHANGELOG_FETCH_ERROR_MSG.to_string(),
|
||||
),
|
||||
changelog_loading: create_formatted_text_from_string(CHANGELOG_LOADING_MSG.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_changelog_event(&mut self, _: &ChangelogEvent, ctx: &mut ViewContext<Self>) {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Generate the changelog items for the 'New Features' section and add them to the content
|
||||
///
|
||||
/// This is distinct from the additional sections because the 'New Features' section has
|
||||
/// custom logic around displaying the header differently and displaying an image (if
|
||||
/// available)
|
||||
fn generate_new_features_section(
|
||||
&self,
|
||||
content: &mut Flex,
|
||||
model: &ChangelogModel,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let title = ChangelogHeader::NewFeatures.to_string();
|
||||
let icon = icons::Icon::Gift;
|
||||
let Some(markdown) = model.parsed_changelog.get(&title) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Section Title
|
||||
if self.show_special_new_features_header {
|
||||
content.add_child(render_special_changelog_header(
|
||||
&title,
|
||||
render_icon(icon, appearance.theme().terminal_colors().normal.red.into()),
|
||||
appearance,
|
||||
));
|
||||
} else {
|
||||
content.add_child(render_basic_changelog_header(
|
||||
&title,
|
||||
render_icon(
|
||||
icon,
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2()),
|
||||
),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
|
||||
// Image (if available)
|
||||
if let Some(image_source) = &model.image {
|
||||
content.add_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Image::new(image_source.clone(), CacheOption::BySize)
|
||||
.enable_animation_with_start_time(Instant::now())
|
||||
.finish(),
|
||||
)
|
||||
.with_max_height(200.)
|
||||
.with_max_width(350.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(4.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
// Content
|
||||
content.add_child(render_changelog_body(
|
||||
markdown.clone(),
|
||||
self.new_features_highlighted_link.clone(),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
|
||||
/// Generate all of the supported changelog sections and add them to the content
|
||||
///
|
||||
/// The supported sections are, in order:
|
||||
///
|
||||
/// * New features
|
||||
/// * Improvements
|
||||
/// * Bug fixes
|
||||
fn generate_changelog_sections(
|
||||
&self,
|
||||
content: &mut Flex,
|
||||
model: &ChangelogModel,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
self.generate_new_features_section(content, model, appearance);
|
||||
|
||||
let additional_sections = [
|
||||
(
|
||||
ChangelogHeader::Improvements,
|
||||
icons::Icon::Tool,
|
||||
self.improvements_highlighted_link.clone(),
|
||||
),
|
||||
(
|
||||
ChangelogHeader::BugFixes,
|
||||
icons::Icon::Bug,
|
||||
self.bug_fixes_highlighted_link.clone(),
|
||||
),
|
||||
];
|
||||
|
||||
for (section, icon, link) in additional_sections {
|
||||
let title = section.to_string();
|
||||
let Some(markdown) = model.parsed_changelog.get(&title) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Title
|
||||
content.add_child(render_basic_changelog_header(
|
||||
&title,
|
||||
render_icon(
|
||||
icon,
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2()),
|
||||
),
|
||||
appearance,
|
||||
));
|
||||
|
||||
// Content
|
||||
content.add_child(render_changelog_body(markdown.clone(), link, appearance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon(icon: icons::Icon, color: Fill) -> ConstrainedBox {
|
||||
ConstrainedBox::new(Icon::new(icon.into(), color).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
}
|
||||
|
||||
fn render_special_changelog_header(
|
||||
title: &str,
|
||||
icon: ConstrainedBox,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(icon.finish())
|
||||
.with_child(
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(title.to_ascii_uppercase())
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(appearance.theme().failed_block_color().into()),
|
||||
font_weight: Some(Weight::Bold),
|
||||
font_size: Some(16.0),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_padding(12.)
|
||||
.with_vertical_padding(4.)
|
||||
.with_border(
|
||||
Border::all(1.0)
|
||||
.with_border_fill::<Fill>(appearance.theme().terminal_colors().normal.red.into()),
|
||||
)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(12.)
|
||||
.with_margin_right(158.)
|
||||
.with_margin_left(16.)
|
||||
.with_margin_bottom(4.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_basic_changelog_header(
|
||||
title: &str,
|
||||
icon: ConstrainedBox,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(icon.finish())
|
||||
.with_child(
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.span(title.to_string())
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2())
|
||||
.into(),
|
||||
),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_size: Some(16.0),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(12.)
|
||||
.with_margin_right(16.)
|
||||
.with_margin_left(16.)
|
||||
.with_margin_bottom(4.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_changelog_body(
|
||||
parsed_markdown: FormattedText,
|
||||
highlighted_link: HighlightedHyperlink,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
FormattedTextElement::new(
|
||||
parsed_markdown,
|
||||
14.0,
|
||||
appearance.ui_font_family(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().surface_2())
|
||||
.into_solid(),
|
||||
highlighted_link,
|
||||
)
|
||||
.register_default_click_handlers(move |url, ctx, _| {
|
||||
ctx.dispatch_typed_action(SectionAction::OpenUrl(url.url));
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(12.)
|
||||
.with_margin_right(16.)
|
||||
.with_margin_left(16.)
|
||||
.with_margin_bottom(12.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
impl SectionView for ChangelogSectionView {
|
||||
fn is_expanded(&self) -> bool {
|
||||
self.is_expanded
|
||||
}
|
||||
|
||||
fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn section_progress_indicator(
|
||||
&self,
|
||||
_show_gamified: bool,
|
||||
_appearance: &Appearance,
|
||||
_ctx: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn section_link(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
Some(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.link(
|
||||
"Read all changelogs".into(),
|
||||
Some("https://docs.warp.dev/changelog".into()),
|
||||
None,
|
||||
self.changelog_button_mouse_states
|
||||
.view_changelogs_mouse_state
|
||||
.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(UiComponentStyles {
|
||||
border_width: Some(2.),
|
||||
font_size: Some(14.0),
|
||||
font_weight: Some(Weight::Normal),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ChangelogSectionView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ResourceCenterChangelogSectionView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let changelog_model = app.model(&self.changelog_model_handle);
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let header = self.render_section_header(
|
||||
FeatureSection::WhatsNew,
|
||||
false,
|
||||
appearance,
|
||||
self.changelog_button_mouse_states
|
||||
.top_bar_mouse_state
|
||||
.clone(),
|
||||
app,
|
||||
);
|
||||
|
||||
let mut section = Flex::column().with_child(header);
|
||||
|
||||
if self.is_expanded || FeatureFlag::AvatarInTabBar.is_enabled() {
|
||||
let mut content_flex = Flex::column();
|
||||
match &changelog_model.changelog {
|
||||
ChangelogState::Some(_) => {
|
||||
self.generate_changelog_sections(
|
||||
&mut content_flex,
|
||||
changelog_model,
|
||||
appearance,
|
||||
);
|
||||
}
|
||||
ChangelogState::Pending => {
|
||||
content_flex.add_child(render_changelog_body(
|
||||
self.changelog_loading.clone(),
|
||||
self.new_features_highlighted_link.clone(),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
ChangelogState::None => {
|
||||
content_flex.add_child(render_changelog_body(
|
||||
self.changelog_fetch_error.clone(),
|
||||
self.new_features_highlighted_link.clone(),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let content_section = Container::new(content_flex.finish())
|
||||
.with_margin_top(4.)
|
||||
.with_margin_bottom(4.);
|
||||
section.add_child(content_section.finish());
|
||||
}
|
||||
|
||||
section.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use warpui::{
|
||||
elements::{
|
||||
ConstrainedBox, Container, Element, Empty, Flex, MouseStateHandle, ParentElement,
|
||||
Shrinkable,
|
||||
},
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
resource_center::{ContentItem, ContentSectionData},
|
||||
};
|
||||
|
||||
use super::{
|
||||
SectionAction, SectionView, CHEVRON_ICON_SIZE, DESCRIPTION_FONT_SIZE, ICON_PADDING,
|
||||
ITEM_PADDING_BOTTOM, SECTION_SPACING,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct ContentMouseStateHandles {
|
||||
item_handles: Vec<MouseStateHandle>,
|
||||
top_bar_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
pub struct ContentSectionView {
|
||||
content_section_data: ContentSectionData,
|
||||
content_button_mouse_states: ContentMouseStateHandles,
|
||||
is_expanded: bool,
|
||||
}
|
||||
|
||||
impl Entity for ContentSectionView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl TypedActionView for ContentSectionView {
|
||||
type Action = SectionAction;
|
||||
|
||||
fn handle_action(&mut self, action: &SectionAction, ctx: &mut ViewContext<Self>) {
|
||||
use SectionAction::*;
|
||||
match action {
|
||||
OpenUrl(url) => {
|
||||
ctx.open_url(url.as_str());
|
||||
}
|
||||
ToggleExpanded => self.toggle_expanded(ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentSectionView {
|
||||
pub fn new(
|
||||
content_section_data: ContentSectionData,
|
||||
is_expanded: bool,
|
||||
_ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let content_button_mouse_states = ContentMouseStateHandles {
|
||||
item_handles: content_section_data
|
||||
.items
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
content_section_data,
|
||||
content_button_mouse_states,
|
||||
is_expanded,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_link_button(
|
||||
&self,
|
||||
item: &ContentItem,
|
||||
appearance: &Appearance,
|
||||
mouse_state_handle: MouseStateHandle,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let default_link_styles = UiComponentStyles {
|
||||
font_size: Some(13.),
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(ColorU::from(
|
||||
theme
|
||||
.accent()
|
||||
.on_background(theme.surface_2(), MinimumAllowedContrast::Text),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let hovered_and_clicked_styles = UiComponentStyles {
|
||||
font_color: Some(ColorU::from(theme.active_ui_text_color())),
|
||||
..default_link_styles
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.link(
|
||||
item.button_label.to_string(),
|
||||
Some(item.url.into()),
|
||||
None,
|
||||
mouse_state_handle,
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(default_link_styles)
|
||||
.with_hovered_style(hovered_and_clicked_styles)
|
||||
.with_clicked_style(hovered_and_clicked_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(Shrinkable::new(1., Empty::new().finish()).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_content_item(
|
||||
&self,
|
||||
item: &ContentItem,
|
||||
appearance: &Appearance,
|
||||
index: usize,
|
||||
) -> Box<dyn Element> {
|
||||
let mut element = Flex::column();
|
||||
let mouse_state = self.content_button_mouse_states.item_handles[index].clone();
|
||||
let link_button = self.render_link_button(item, appearance, mouse_state);
|
||||
|
||||
// title
|
||||
element.add_child(
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(item.title.to_string(), true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(DESCRIPTION_FONT_SIZE),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_bottom(ITEM_PADDING_BOTTOM)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// description
|
||||
element.add_child(
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(item.description.to_string(), true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(DESCRIPTION_FONT_SIZE),
|
||||
font_color: Some(ColorU::from(
|
||||
appearance.theme().nonactive_ui_text_color(),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_bottom(ITEM_PADDING_BOTTOM)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// link
|
||||
element.add_child(link_button);
|
||||
|
||||
Container::new(element.finish())
|
||||
.with_margin_bottom(SECTION_SPACING)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionView for ContentSectionView {
|
||||
fn is_expanded(&self) -> bool {
|
||||
self.is_expanded
|
||||
}
|
||||
|
||||
fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn section_progress_indicator(
|
||||
&self,
|
||||
_show_gamified: bool,
|
||||
_appearance: &Appearance,
|
||||
_ctx: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn section_link(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ContentSectionView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ResourceCenterContentSectionView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let header = self.render_section_header(
|
||||
self.content_section_data.section_name,
|
||||
false,
|
||||
appearance,
|
||||
self.content_button_mouse_states.top_bar_mouse_state.clone(),
|
||||
app,
|
||||
);
|
||||
|
||||
let mut section = Flex::column().with_child(header);
|
||||
if self.is_expanded {
|
||||
let content_section =
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children(
|
||||
self.content_section_data.items.iter().enumerate().map(
|
||||
|(index, item)| self.render_content_item(item, appearance, index),
|
||||
),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_margin(SECTION_SPACING)
|
||||
.with_margin_left(SECTION_SPACING + CHEVRON_ICON_SIZE + ICON_PADDING);
|
||||
|
||||
section.add_child(content_section.finish());
|
||||
}
|
||||
|
||||
ConstrainedBox::new(Container::new(section.finish()).finish()).finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
send_telemetry_from_ctx,
|
||||
server::telemetry::TelemetryEvent,
|
||||
settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier},
|
||||
themes::theme::Fill,
|
||||
};
|
||||
use warpui::{
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, CrossAxisAlignment, Element, Flex, Hoverable, Icon,
|
||||
MouseState, MouseStateHandle, ParentElement, Shrinkable,
|
||||
},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
Action, AppContext, Entity, EntityId, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext, WindowId,
|
||||
};
|
||||
|
||||
use crate::resource_center::{
|
||||
complete_tips_and_write_to_user_defaults, main_page::ActionTarget,
|
||||
skip_tips_and_write_to_user_defaults, FeatureItem, FeatureSectionData, Tip, TipsCompleted,
|
||||
};
|
||||
|
||||
use super::{
|
||||
SectionAction, SectionView, CHEVRON_ICON_SIZE, DESCRIPTION_FONT_SIZE, ELLIPSE_ICON_SIZE,
|
||||
ELLIPSE_SVG_PATH, ICON_PADDING, ITEM_PADDING_BOTTOM, SCROLLBAR_OFFSET, SECTION_SPACING,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FeatureSection {
|
||||
WhatsNew,
|
||||
GettingStarted,
|
||||
MaximizeWarp,
|
||||
AdvancedSetup,
|
||||
}
|
||||
|
||||
impl FeatureSection {
|
||||
pub fn section_name_string(&self) -> &'static str {
|
||||
match self {
|
||||
FeatureSection::WhatsNew => "What's New?",
|
||||
FeatureSection::GettingStarted => "Getting Started",
|
||||
FeatureSection::MaximizeWarp => "Maximize Warp",
|
||||
FeatureSection::AdvancedSetup => "Advanced Setup",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FeatureMouseStateHandles {
|
||||
item_handles: Vec<MouseStateHandle>,
|
||||
top_bar_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
pub enum FeatureSectionEvent {
|
||||
/// Event fired when the tips dialog should close.
|
||||
CloseResourceCenter,
|
||||
ExpandSection(FeatureSection),
|
||||
}
|
||||
|
||||
pub struct FeatureSectionView {
|
||||
pub feature_section_data: FeatureSectionData,
|
||||
action_target: ModelHandle<ActionTarget>,
|
||||
feature_button_mouse_states: FeatureMouseStateHandles,
|
||||
tips_completed: ModelHandle<TipsCompleted>,
|
||||
show_tips_progress: bool,
|
||||
is_expanded: bool,
|
||||
}
|
||||
|
||||
impl FeatureSectionView {
|
||||
fn on_tips_model_changed(
|
||||
&mut self,
|
||||
_: ModelHandle<TipsCompleted>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
feature_section_data: FeatureSectionData,
|
||||
action_target: ModelHandle<ActionTarget>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
tips_completed: ModelHandle<TipsCompleted>,
|
||||
show_tips_progress: bool,
|
||||
is_expanded: bool,
|
||||
) -> Self {
|
||||
let feature_button_mouse_states = FeatureMouseStateHandles {
|
||||
item_handles: feature_section_data
|
||||
.items
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ctx.observe(&tips_completed, FeatureSectionView::on_tips_model_changed);
|
||||
|
||||
let bindings_notifier = KeybindingChangedNotifier::handle(ctx);
|
||||
ctx.subscribe_to_model(&bindings_notifier, |me, _, event, ctx| {
|
||||
me.handle_keybinding_changed(event, ctx);
|
||||
});
|
||||
|
||||
Self {
|
||||
feature_section_data,
|
||||
action_target,
|
||||
feature_button_mouse_states,
|
||||
tips_completed,
|
||||
show_tips_progress,
|
||||
is_expanded,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keybinding_changed(
|
||||
&mut self,
|
||||
event: &KeybindingChangedEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
KeybindingChangedEvent::BindingChanged {
|
||||
binding_name,
|
||||
new_trigger,
|
||||
} => {
|
||||
if let Some(binding) = self
|
||||
.feature_section_data
|
||||
.items
|
||||
.iter_mut()
|
||||
.find(|data| data.editable_binding_name == Some(binding_name))
|
||||
{
|
||||
binding.shortcut.clone_from(new_trigger);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_section(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_expanded = true;
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
// Turns gamification off without rendering completed modal
|
||||
pub fn skip_gamified_section(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// Turns gamification off and renders completed modal
|
||||
pub fn complete_gamified_section(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
send_telemetry_from_ctx!(TelemetryEvent::ResourceCenterTipsCompleted, ctx);
|
||||
self.tips_completed.update(ctx, |tips_completed, ctx| {
|
||||
complete_tips_and_write_to_user_defaults(tips_completed, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_action_target(
|
||||
&mut self,
|
||||
window_id: WindowId,
|
||||
input_id: Option<EntityId>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.action_target.update(ctx, |action_target, ctx| {
|
||||
*action_target = ActionTarget::View {
|
||||
window_id,
|
||||
input_id,
|
||||
};
|
||||
ctx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn dispatch_feature_action(&self, action: &dyn Action, ctx: &mut ViewContext<Self>) {
|
||||
let (window_id, input_id) = match self.action_target.as_ref(ctx) {
|
||||
ActionTarget::View {
|
||||
window_id,
|
||||
input_id,
|
||||
} => (*window_id, *input_id),
|
||||
ActionTarget::None => return,
|
||||
};
|
||||
|
||||
if let Some(input_id) = input_id {
|
||||
ctx.dispatch_typed_action_for_view(window_id, input_id, action);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_unread_icon(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
ConstrainedBox::new(Icon::new(ELLIPSE_SVG_PATH, appearance.theme().accent()).finish())
|
||||
.with_height(ELLIPSE_ICON_SIZE)
|
||||
.with_width(ELLIPSE_ICON_SIZE)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_top(ELLIPSE_ICON_SIZE)
|
||||
.with_padding_right(ELLIPSE_ICON_SIZE)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_item_title(&self, item: &FeatureItem, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let title_color = appearance.theme().active_ui_text_color();
|
||||
|
||||
Align::new(
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(item.title.to_string(), true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(DESCRIPTION_FONT_SIZE),
|
||||
font_color: (Some(title_color.into())),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_top(3.)
|
||||
.finish(),
|
||||
)
|
||||
.left()
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_description(
|
||||
&self,
|
||||
item: &FeatureItem,
|
||||
appearance: &Appearance,
|
||||
color: Fill,
|
||||
) -> Box<dyn Element> {
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(item.description.to_string(), true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(DESCRIPTION_FONT_SIZE),
|
||||
font_color: Some(color.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn build_feature_item(
|
||||
&self,
|
||||
item: &FeatureItem,
|
||||
appearance: &Appearance,
|
||||
state: Option<&MouseState>,
|
||||
is_completed: bool,
|
||||
show_gamified: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let ui_builder = appearance.ui_builder();
|
||||
let mut element = Flex::column();
|
||||
let mut element_title = Flex::row();
|
||||
|
||||
// title
|
||||
element_title
|
||||
.add_child(Shrinkable::new(1., self.render_item_title(item, appearance)).finish());
|
||||
|
||||
// keyboard shortcut
|
||||
if let Some(keystroke) = &item.shortcut {
|
||||
element_title.add_child(ui_builder.keyboard_shortcut(keystroke).build().finish())
|
||||
}
|
||||
|
||||
element.add_child(
|
||||
Container::new(
|
||||
element_title
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_bottom(ITEM_PADDING_BOTTOM)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let hovered = state.is_some() && state.expect("Expected valid mouse state").is_hovered();
|
||||
let description_color = if hovered && matches!(item.feature, Tip::Action(_)) {
|
||||
theme.active_ui_text_color()
|
||||
} else {
|
||||
theme.nonactive_ui_text_color()
|
||||
};
|
||||
|
||||
// description
|
||||
element.add_child(self.render_description(item, appearance, description_color));
|
||||
|
||||
let mut feature_item = Flex::row();
|
||||
if !is_completed && show_gamified {
|
||||
feature_item.add_child(self.render_unread_icon(appearance));
|
||||
}
|
||||
feature_item.add_child(Shrinkable::new(1., element.finish()).finish());
|
||||
|
||||
let margin_left = if is_completed || !show_gamified {
|
||||
CHEVRON_ICON_SIZE + ICON_PADDING
|
||||
} else {
|
||||
SCROLLBAR_OFFSET
|
||||
};
|
||||
Container::new(feature_item.finish())
|
||||
.with_margin_bottom(SECTION_SPACING)
|
||||
.with_margin_left(margin_left)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn render_feature_item(
|
||||
&self,
|
||||
feature_item: FeatureItem,
|
||||
appearance: &Appearance,
|
||||
index: usize,
|
||||
is_tip_completed: bool,
|
||||
show_gamified: bool,
|
||||
) -> Box<dyn Element> {
|
||||
match feature_item.feature {
|
||||
Tip::Hint(_) => self.build_feature_item(
|
||||
&feature_item,
|
||||
appearance,
|
||||
None,
|
||||
is_tip_completed,
|
||||
show_gamified,
|
||||
),
|
||||
Tip::Action(tip) => {
|
||||
let item_element = Hoverable::new(
|
||||
self.feature_button_mouse_states.item_handles[index].clone(),
|
||||
|state| {
|
||||
self.build_feature_item(
|
||||
&feature_item,
|
||||
appearance,
|
||||
Some(state),
|
||||
is_tip_completed,
|
||||
show_gamified,
|
||||
)
|
||||
},
|
||||
);
|
||||
item_element
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(SectionAction::Click(tip)))
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for FeatureSectionView {
|
||||
type Event = FeatureSectionEvent;
|
||||
}
|
||||
|
||||
impl TypedActionView for FeatureSectionView {
|
||||
type Action = SectionAction;
|
||||
|
||||
fn handle_action(&mut self, action: &SectionAction, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SectionAction::Click(feature) => {
|
||||
let action = ctx
|
||||
.editable_bindings()
|
||||
.find(|action| action.name == feature.editable_binding_name())
|
||||
.map(|action| action.action.clone());
|
||||
|
||||
if let Some(action) = action {
|
||||
self.dispatch_feature_action(action.as_ref(), ctx);
|
||||
}
|
||||
}
|
||||
SectionAction::ToggleExpanded => {
|
||||
self.toggle_expanded(ctx);
|
||||
}
|
||||
SectionAction::CloseResourceCenter => {
|
||||
self.toggle_expanded(ctx);
|
||||
ctx.emit(FeatureSectionEvent::CloseResourceCenter);
|
||||
ctx.notify();
|
||||
}
|
||||
SectionAction::CompleteGamified => {
|
||||
self.complete_gamified_section(ctx);
|
||||
self.toggle_expanded(ctx);
|
||||
}
|
||||
SectionAction::SkipTips => {
|
||||
self.skip_gamified_section(ctx);
|
||||
self.toggle_expanded(ctx);
|
||||
}
|
||||
SectionAction::OpenSection(section_name) => {
|
||||
ctx.emit(FeatureSectionEvent::ExpandSection(*section_name));
|
||||
ctx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionView for FeatureSectionView {
|
||||
fn is_expanded(&self) -> bool {
|
||||
self.is_expanded
|
||||
}
|
||||
|
||||
fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn section_progress_indicator(
|
||||
&self,
|
||||
show_gamified: bool,
|
||||
appearance: &Appearance,
|
||||
ctx: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let tip_count = self.feature_section_data.items.len();
|
||||
let tips_completed_count = self
|
||||
.feature_section_data
|
||||
.tips_completed_count(self.tips_completed.as_ref(ctx));
|
||||
|
||||
// Show progress when section's tips are not yet completed
|
||||
if show_gamified && self.show_tips_progress && tips_completed_count != tip_count {
|
||||
let progress = format!("{tips_completed_count}/{tip_count}");
|
||||
Some(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(progress, false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_size: Some(DESCRIPTION_FONT_SIZE),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn section_link(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl View for FeatureSectionView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ResourceCenterFeatureSectionView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let tips_completed = self.tips_completed.as_ref(app);
|
||||
let show_gamified = !tips_completed.skipped_or_completed;
|
||||
|
||||
let header = self.render_section_header(
|
||||
self.feature_section_data.section_name,
|
||||
show_gamified,
|
||||
appearance,
|
||||
self.feature_button_mouse_states.top_bar_mouse_state.clone(),
|
||||
app,
|
||||
);
|
||||
|
||||
let mut section = Flex::column().with_child(header);
|
||||
if self.is_expanded {
|
||||
let mut feature_section = Container::new(
|
||||
Flex::column()
|
||||
.with_children(self.feature_section_data.items.iter().enumerate().map(
|
||||
|(index, feature_item)| {
|
||||
self.render_feature_item(
|
||||
feature_item.clone(),
|
||||
appearance,
|
||||
index,
|
||||
tips_completed.features_used.contains(&feature_item.feature),
|
||||
show_gamified,
|
||||
)
|
||||
},
|
||||
))
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if !self.feature_section_data.items.is_empty() {
|
||||
feature_section = feature_section.with_uniform_padding(SECTION_SPACING)
|
||||
}
|
||||
|
||||
section.add_child(feature_section.finish());
|
||||
}
|
||||
|
||||
ConstrainedBox::new(Container::new(section.finish()).finish()).finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
pub mod feature_section;
|
||||
pub use feature_section::FeatureSectionView;
|
||||
pub mod content_section;
|
||||
pub use content_section::ContentSectionView;
|
||||
use warp_core::features::FeatureFlag;
|
||||
pub mod changelog_section;
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
resource_center::{section_views::feature_section::FeatureSection, TipAction},
|
||||
};
|
||||
pub use changelog_section::ChangelogSectionView;
|
||||
use warpui::{
|
||||
elements::{
|
||||
Align, Border, ConstrainedBox, Container, CrossAxisAlignment, Element, Flex, Hoverable,
|
||||
Icon, MouseStateHandle, ParentElement, ScrollbarWidth, Shrinkable,
|
||||
},
|
||||
platform::Cursor,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
pub const HEADER_FONT_SIZE: f32 = 16.;
|
||||
pub const SECTION_HEADER_FONT_SIZE: f32 = 16.;
|
||||
pub const DESCRIPTION_FONT_SIZE: f32 = 14.;
|
||||
pub const DETAIL_FONT_SIZE: f32 = 12.;
|
||||
|
||||
pub const KEYBOARD_ICON_SIZE: f32 = 30.;
|
||||
pub const CHEVRON_ICON_SIZE: f32 = 20.;
|
||||
pub const FOOTER_ICON_SIZE: f32 = 15.;
|
||||
pub const ELLIPSE_ICON_SIZE: f32 = 8.;
|
||||
pub const ICON_PADDING: f32 = 3.;
|
||||
|
||||
pub const DROPDOWN_ICON_OPACITY: u8 = 75;
|
||||
|
||||
// TODO: update scrollbar behaviour to not take up space when non-active
|
||||
// Spacing to offset scrollbar width (which makes things off-centered)
|
||||
pub const SCROLLBAR_OFFSET: f32 = 7.;
|
||||
pub const SCROLLBAR_WIDTH: ScrollbarWidth = ScrollbarWidth::Auto;
|
||||
|
||||
pub const SECTION_SPACING_BOTTOM: f32 = 24.;
|
||||
pub const SECTION_SPACING: f32 = 12.;
|
||||
pub const BUTTON_PADDING: f32 = 10.;
|
||||
pub const ITEM_PADDING_BOTTOM: f32 = 6.;
|
||||
|
||||
pub const CHEVRON_DOWN_SKINNY_SVG_PATH: &str = "bundled/svg/chevron-down-skinny.svg";
|
||||
pub const CHEVRON_RIGHT_SKINNY_SVG_PATH: &str = "bundled/svg/chevron-right-skinny.svg";
|
||||
pub const ELLIPSE_SVG_PATH: &str = "bundled/svg/ellipse.svg";
|
||||
|
||||
pub enum SectionViewHandle {
|
||||
Feature(ViewHandle<FeatureSectionView>),
|
||||
Content(ViewHandle<ContentSectionView>),
|
||||
Changelog(ViewHandle<ChangelogSectionView>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SectionAction {
|
||||
OpenUrl(String),
|
||||
ToggleExpanded,
|
||||
Click(TipAction),
|
||||
CloseResourceCenter,
|
||||
CompleteGamified,
|
||||
SkipTips,
|
||||
OpenSection(FeatureSection),
|
||||
}
|
||||
|
||||
pub trait SectionView {
|
||||
fn is_expanded(&self) -> bool;
|
||||
|
||||
fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>);
|
||||
|
||||
fn section_progress_indicator(
|
||||
&self,
|
||||
show_gamified: bool,
|
||||
appearance: &Appearance,
|
||||
ctx: &AppContext,
|
||||
) -> Option<Box<dyn Element>>;
|
||||
|
||||
fn section_link(&self, appearance: &Appearance) -> Option<Box<dyn Element>>;
|
||||
|
||||
fn render_section_header(
|
||||
&self,
|
||||
section_name: FeatureSection,
|
||||
show_gamified: bool,
|
||||
appearance: &Appearance,
|
||||
top_bar_mouse_state: MouseStateHandle,
|
||||
ctx: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
Hoverable::new(top_bar_mouse_state, |state| {
|
||||
let mut section_header = Flex::row();
|
||||
|
||||
let section_title = Shrinkable::new(
|
||||
1.0,
|
||||
Align::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(section_name.section_name_string().to_string(), false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_size: Some(SECTION_HEADER_FONT_SIZE),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.left()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let icon_path = if self.is_expanded() {
|
||||
CHEVRON_DOWN_SKINNY_SVG_PATH
|
||||
} else {
|
||||
CHEVRON_RIGHT_SKINNY_SVG_PATH
|
||||
};
|
||||
|
||||
let icon_color = if state.is_hovered() {
|
||||
appearance.theme().active_ui_detail()
|
||||
} else {
|
||||
appearance
|
||||
.theme()
|
||||
.active_ui_detail()
|
||||
.with_opacity(DROPDOWN_ICON_OPACITY)
|
||||
};
|
||||
|
||||
let dropdown_icon =
|
||||
ConstrainedBox::new(Icon::new(icon_path, icon_color.into_solid()).finish())
|
||||
.with_height(CHEVRON_ICON_SIZE)
|
||||
.with_width(CHEVRON_ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
if !FeatureFlag::AvatarInTabBar.is_enabled() {
|
||||
section_header.add_child(dropdown_icon);
|
||||
}
|
||||
section_header.add_child(section_title);
|
||||
|
||||
if let Some(progress_indicator) =
|
||||
self.section_progress_indicator(show_gamified, appearance, ctx)
|
||||
{
|
||||
section_header.add_child(progress_indicator)
|
||||
}
|
||||
|
||||
if let Some(link) = self.section_link(appearance) {
|
||||
section_header.add_child(link)
|
||||
}
|
||||
|
||||
Container::new(
|
||||
section_header
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_padding(SECTION_SPACING)
|
||||
.with_background(appearance.theme().surface_2())
|
||||
.with_border(
|
||||
Border::top(1.)
|
||||
.with_border_color(appearance.theme().split_pane_border_color().into()),
|
||||
)
|
||||
.finish()
|
||||
})
|
||||
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(SectionAction::ToggleExpanded))
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user