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
+218
View File
@@ -0,0 +1,218 @@
//! Components for the notebook header.
use warp_core::features::FeatureFlag;
use warpui::{
elements::{
Container, CrossAxisAlignment, Flex, Highlight, MainAxisAlignment, MainAxisSize,
MouseStateHandle, ParentElement, Shrinkable,
},
platform::Cursor,
ui_components::components::{UiComponent, UiComponentStyles},
AppContext, Element, SingletonEntity,
};
use crate::{
appearance::Appearance,
cloud_object::{
breadcrumbs::ContainingObject,
model::view::{Editor, EditorState},
},
drive::sharing::ContentEditability,
notebooks::{active_notebook_data::Mode, styles},
ui_components::{
breadcrumb::{render_breadcrumbs, BreadcrumbState},
buttons::{accent_icon_button, icon_button},
icons::Icon,
},
workspaces::user_profiles::UserProfiles,
};
use super::{super::active_notebook_data::ActiveNotebookData, NotebookAction, EDIT_BUTTON_MARGIN};
/// Component to show details about a notebook:
/// * Interactive breadcrumbs for its location within Warp Drive
/// * The current editor of the notebook
/// * Grab-the-baton UI controls
pub struct DetailsBar {
breadcrumbs: Vec<BreadcrumbState<ContainingObject>>,
edit_mode_button_mouse_state: MouseStateHandle,
}
impl DetailsBar {
pub fn new() -> Self {
Self {
breadcrumbs: Vec::new(),
edit_mode_button_mouse_state: Default::default(),
}
}
/// Update the cached breadcrumbs in the notebook header.
pub fn update_breadcrumbs(&mut self, notebook_data: &ActiveNotebookData, ctx: &AppContext) {
self.breadcrumbs = notebook_data
.breadcrumbs(ctx)
.map(|breadcrumbs| breadcrumbs.into_iter().map(BreadcrumbState::new).collect())
.unwrap_or_default();
}
pub fn render(
&self,
notebook_data: &ActiveNotebookData,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let mut header_row = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::Center);
header_row.add_child(
Shrinkable::new(
2.,
render_breadcrumbs(
self.breadcrumbs.iter().cloned(),
appearance,
|ctx, _, breadcrumb| {
ctx.dispatch_typed_action(NotebookAction::ViewInWarpDrive(
breadcrumb.kind.into_item_id(),
));
},
),
)
.finish(),
);
let mut editing_state_row = Flex::row()
.with_main_axis_alignment(MainAxisAlignment::End)
.with_cross_axis_alignment(CrossAxisAlignment::Center);
if let Some(editor) = notebook_data.current_editor(app) {
editing_state_row.add_child(
Shrinkable::new(1., self.render_editor(&editor, appearance, app)).finish(),
);
}
let editability = if FeatureFlag::SharedWithMe.is_enabled() {
notebook_data.editability(app)
} else {
ContentEditability::Editable
};
if matches!(
editability,
ContentEditability::RequiresLogin | ContentEditability::Editable
) {
editing_state_row.add_child(self.render_mode_toggle(
notebook_data.mode,
editability,
appearance,
));
}
header_row.add_child(Shrinkable::new(1., editing_state_row.finish()).finish());
header_row.finish()
}
/// Renders a toggle button for the editing mode.
fn render_mode_toggle(
&self,
mode: Mode,
editability: ContentEditability,
appearance: &Appearance,
) -> Box<dyn Element> {
let mut edit_button = match mode {
Mode::View => icon_button(
appearance,
Icon::Pencil,
false,
self.edit_mode_button_mouse_state.clone(),
),
Mode::Editing => accent_icon_button(
appearance,
Icon::Pencil,
false,
self.edit_mode_button_mouse_state.clone(),
),
};
if matches!(editability, ContentEditability::RequiresLogin) {
let ui_builder = appearance.ui_builder().clone();
edit_button = edit_button.with_tooltip(move || {
ui_builder
.tool_tip("Sign in to edit".to_string())
.build()
.finish()
});
}
Container::new(
edit_button
.build()
.on_click(move |ctx, _, _| {
if editability.can_edit() {
ctx.dispatch_typed_action(NotebookAction::ToggleMode)
}
})
.with_cursor(Cursor::PointingHand)
.finish(),
)
.with_margin_left(EDIT_BUTTON_MARGIN)
.with_margin_right(EDIT_BUTTON_MARGIN)
.finish()
}
/// Renders a label for the current editor.
fn render_editor(
&self,
editor: &Editor,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let base_text_styles = UiComponentStyles {
font_color: Some(styles::title_text_fill(appearance).into_solid()),
..Default::default()
};
let theme = appearance.theme();
match editor.state {
EditorState::None => appearance
.ui_builder()
.span("Viewing")
.with_style(base_text_styles)
.build()
.finish(),
EditorState::CurrentUser => appearance
.ui_builder()
.span("Editing")
.with_style(base_text_styles)
.build()
.finish(),
EditorState::OtherUserActive | EditorState::OtherUserIdle => {
let editor = editor_display_name(editor.email.as_deref(), app);
appearance
.ui_builder()
.span(format!("{editor} is editing"))
.with_style(base_text_styles)
.with_highlights(
(0..editor.chars().count()).collect(),
Highlight::new().with_foreground_color(
theme.main_text_color(theme.background()).into_solid(),
),
)
.build()
.finish()
}
}
}
}
/// Get the display name for an editor.
fn editor_display_name(email: Option<&str>, app: &AppContext) -> String {
match email {
Some(email) => UserProfiles::as_ref(app)
.displayable_identifier_for_email(email)
.unwrap_or_else(|| email.to_string()),
None => "Other user".to_string(),
}
}
#[cfg(test)]
#[path = "details_bar_tests.rs"]
mod tests;
@@ -0,0 +1,59 @@
use warpui::{App, SingletonEntity};
use crate::{
auth::UserUid,
workspaces::user_profiles::{UserProfileWithUID, UserProfiles},
};
use super::editor_display_name;
fn initialize_app(app: &mut App) {
app.update(crate::settings::init_and_register_user_preferences);
app.add_singleton_model(|_| UserProfiles::new(vec![]));
}
#[test]
fn test_editor_display_name() {
App::test((), |mut app| async move {
initialize_app(&mut app);
UserProfiles::handle(&app).update(&mut app, |profiles, _ctx| {
profiles.insert_profiles(&vec![
UserProfileWithUID {
firebase_uid: UserUid::new("abc123"),
display_name: Some("The Editor".to_string()),
email: "editor@warp.dev".to_string(),
photo_url: "http://example.com/profile.jpg".to_string(),
},
UserProfileWithUID {
firebase_uid: UserUid::new("def456"),
display_name: None,
email: "anon@warp.dev".to_string(),
photo_url: "http://example.com/profile.jpg".to_string(),
},
])
});
app.read(|ctx| {
// If there's no known editor, default to "Other user";
assert_eq!(&editor_display_name(None, ctx), "Other user");
// If the editor doesn't have a profile, default to their email.
assert_eq!(
&editor_display_name(Some("unknown@warp.dev"), ctx),
"unknown@warp.dev"
);
// If the profile is missing a display name, default to the email.
assert_eq!(
&editor_display_name(Some("anon@warp.dev"), ctx),
"anon@warp.dev"
);
// If there's a display name available, use that.
assert_eq!(
&editor_display_name(Some("editor@warp.dev"), ctx),
"The Editor"
);
});
})
}