Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -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()
}
}