Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
use warpui::{
|
||||
elements::{Container, Flex, MouseStateHandle, ParentElement},
|
||||
fonts::Weight,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ai::facts::{AIFact, AIMemory, CloudAIFact},
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{index::DriveIndexAction, CloudObjectTypeAndId, DriveObjectType},
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveAIFact {
|
||||
id: CloudObjectTypeAndId,
|
||||
ai_fact: CloudAIFact,
|
||||
}
|
||||
|
||||
impl WarpDriveAIFact {
|
||||
pub fn new(id: CloudObjectTypeAndId, ai_fact: CloudAIFact) -> Self {
|
||||
Self { id, ai_fact }
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveAIFact {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
match &self.ai_fact.model().string_model {
|
||||
AIFact::Memory(AIMemory { content, name, .. }) => {
|
||||
if let Some(name) = name {
|
||||
if !name.is_empty() {
|
||||
Some(name.clone())
|
||||
} else {
|
||||
Some(content.clone())
|
||||
}
|
||||
} else {
|
||||
Some(content.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.ai_fact.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::AIFact)
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::OpenAIFactCollection)
|
||||
}
|
||||
|
||||
fn preview(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
let title_to_render = match &self.ai_fact.model().string_model {
|
||||
AIFact::Memory(AIMemory { content, .. }) => content.clone(),
|
||||
};
|
||||
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(title_to_render, true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
Some(
|
||||
Flex::column()
|
||||
.with_child(Container::new(title).finish())
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.ai_fact.metadata.pending_changes_statuses.render_icon(
|
||||
sync_queue_is_dequeueing,
|
||||
hover_state,
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use warpui::{elements::MouseStateHandle, AppContext, Element};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{index::DriveIndexAction, DriveObjectType},
|
||||
server::ids::ClientId,
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveAIFactCollection {
|
||||
id: ClientId,
|
||||
}
|
||||
|
||||
impl WarpDriveAIFactCollection {
|
||||
pub fn new(id: ClientId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ClientId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveAIFactCollection {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
Some("Rules".to_string())
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::AIFactCollection)
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::OpenAIFactCollection)
|
||||
}
|
||||
|
||||
fn preview(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::AIFactCollection
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
_sync_queue_is_dequeueing: bool,
|
||||
_hover_state: MouseStateHandle,
|
||||
_appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use itertools::Itertools;
|
||||
use warp_core::context_flag::ContextFlag;
|
||||
use warpui::{
|
||||
elements::{Clipped, Container, Flex, MouseStateHandle, ParentElement},
|
||||
fonts::Weight,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element, SingletonEntity,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::{
|
||||
model::actions::{ObjectActionType, ObjectActions},
|
||||
CloudObjectMetadata,
|
||||
},
|
||||
drive::{index::DriveIndexAction, CloudObjectTypeAndId, DriveObjectType},
|
||||
env_vars::{CloudEnvVarCollection, EnvVarValue},
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveEnvVarCollection {
|
||||
id: CloudObjectTypeAndId,
|
||||
env_var_collection: CloudEnvVarCollection,
|
||||
}
|
||||
|
||||
impl WarpDriveEnvVarCollection {
|
||||
pub fn new(id: CloudObjectTypeAndId, env_var_collection: CloudEnvVarCollection) -> Self {
|
||||
Self {
|
||||
id,
|
||||
env_var_collection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveEnvVarCollection {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
self.env_var_collection.model().string_model.title.clone()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.env_var_collection.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::EnvVarCollection)
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
// If running the workflow is disabled (true for some web views),
|
||||
// we should just open the workflow instead.
|
||||
if !ContextFlag::RunWorkflow.is_enabled() {
|
||||
Some(DriveIndexAction::OpenObject(self.id))
|
||||
} else {
|
||||
Some(DriveIndexAction::RunObject(self.id))
|
||||
}
|
||||
}
|
||||
|
||||
fn preview(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
let title_text = self.env_var_collection.model().string_model.title.clone();
|
||||
let title_to_render = if let Some(title) = title_text {
|
||||
title
|
||||
} else {
|
||||
"Untitled".to_string()
|
||||
};
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(title_to_render, true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let mut text = Flex::column().with_child(Container::new(title).finish());
|
||||
|
||||
if let Some(description) = self
|
||||
.env_var_collection
|
||||
.model()
|
||||
.string_model
|
||||
.description
|
||||
.clone()
|
||||
{
|
||||
let description_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph(description.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(12.),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
text.add_child(
|
||||
Container::new(description_text.build().finish())
|
||||
.with_margin_top(4.)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
let rows = self
|
||||
.env_var_collection
|
||||
.model()
|
||||
.string_model
|
||||
.vars
|
||||
.iter()
|
||||
.map(|var| {
|
||||
Clipped::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.label(match &var.value {
|
||||
EnvVarValue::Constant(val) => format!("{}: {}", var.name, val),
|
||||
EnvVarValue::Command(cmd) => format!("{}: {}", var.name, cmd.name),
|
||||
EnvVarValue::Secret(sec) => {
|
||||
format!("{}: {}", var.name, sec.get_display_name())
|
||||
}
|
||||
})
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_size: Some(12.),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
text.add_child(
|
||||
Container::new(Flex::column().with_children(rows).finish())
|
||||
.with_margin_top(8.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
Some(text.finish())
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.env_var_collection
|
||||
.metadata
|
||||
.pending_changes_statuses
|
||||
.render_icon(sync_queue_is_dequeueing, hover_state, appearance)
|
||||
}
|
||||
|
||||
fn action_summary(&self, app: &AppContext) -> Option<String> {
|
||||
ObjectActions::as_ref(app)
|
||||
.get_action_history_summary_for_action_type(&self.id.uid(), ObjectActionType::Execute)
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::{elements::MouseStateHandle, AppContext, Element};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{
|
||||
cloud_object_styling::warp_drive_icon_color, folders::CloudFolder, index::DriveIndexAction,
|
||||
CloudObjectTypeAndId, DriveObjectType,
|
||||
},
|
||||
themes::theme::Fill,
|
||||
ui_components::icons::Icon,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveFolder {
|
||||
id: CloudObjectTypeAndId,
|
||||
folder: CloudFolder,
|
||||
}
|
||||
|
||||
impl WarpDriveFolder {
|
||||
pub fn new(id: CloudObjectTypeAndId, folder: CloudFolder) -> Self {
|
||||
Self { id, folder }
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveFolder {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
if self.folder.model().name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.folder.model().name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.folder.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::Folder)
|
||||
}
|
||||
|
||||
fn icon(&self, appearance: &Appearance, color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
let icon_fill =
|
||||
color.unwrap_or(warp_drive_icon_color(appearance, DriveObjectType::Folder).into());
|
||||
let icon = if FeatureFlag::WarpPacks.is_enabled() && self.folder.model().is_warp_pack {
|
||||
Icon::PackageCheck
|
||||
} else {
|
||||
Icon::from(DriveObjectType::Folder)
|
||||
};
|
||||
|
||||
Some(icon.to_warpui_icon(icon_fill).finish())
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_folder_open(&self) -> Option<bool> {
|
||||
Some(self.folder.model().is_open)
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::ToggleFolderOpen(self.folder.id))
|
||||
}
|
||||
|
||||
fn preview(&self, _: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.folder.metadata.pending_changes_statuses.render_icon(
|
||||
sync_queue_is_dequeueing,
|
||||
hover_state,
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,944 @@
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
use warpui::{
|
||||
elements::{
|
||||
AcceptedByDropTarget, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Draggable, DraggableState, DropShadow, Empty, Flex, Hoverable,
|
||||
MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Radius, SavePosition, Shrinkable, SizeConstraintCondition,
|
||||
SizeConstraintSwitch, Stack,
|
||||
},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
presenter::PositionCache,
|
||||
ui_components::{
|
||||
components::{UiComponent, UiComponentStyles},
|
||||
text::Span,
|
||||
},
|
||||
AppContext, Element, SingletonEntity, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cloud_object::{
|
||||
model::{persistence::CloudModel, view::CloudViewModel},
|
||||
CloudObject, CloudObjectMetadataExt, Owner,
|
||||
},
|
||||
drive::CloudObjectTypeAndId,
|
||||
workspaces::{user_profiles::UserProfiles, user_workspaces::UserWorkspaces},
|
||||
};
|
||||
|
||||
use crate::workspace::header_toolbar_item::HeaderToolbarItemKind;
|
||||
use crate::workspace::tab_settings::TabSettings;
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::Space,
|
||||
drive::{
|
||||
index::{
|
||||
DriveIndexAction, AUTOSCROLL_DETECTION_DISTANCE, AUTOSCROLL_SPEED_MULTIPLIER,
|
||||
DRIVE_INDEX_VIEW_POSITION_ID, FOLDER_DEPTH_INDENT, INDEX_CONTENT_MARGIN_LEFT,
|
||||
ITEM_FONT_SIZE, ITEM_MARGIN_BOTTOM, ITEM_PADDING_HORIZONTAL, ITEM_PADDING_VERTICAL,
|
||||
},
|
||||
panel::WARP_DRIVE_POSITION_ID,
|
||||
},
|
||||
menu::Menu,
|
||||
ui_components::{
|
||||
blended_colors,
|
||||
icons::{Icon, ICON_DIMENSIONS},
|
||||
menu_button::{
|
||||
highlight_icon_button_with_context_menu_drive, icon_button_with_context_menu_drive,
|
||||
MenuDirection,
|
||||
},
|
||||
},
|
||||
};
|
||||
use crate::{cloud_object::CloudObjectLocation, drive::items::WarpDriveItem};
|
||||
|
||||
use super::WarpDriveItemId;
|
||||
|
||||
pub(crate) fn tools_panel_menu_direction(app: &AppContext) -> MenuDirection {
|
||||
let config = TabSettings::as_ref(app)
|
||||
.header_toolbar_chip_selection
|
||||
.clone();
|
||||
if config
|
||||
.left_items()
|
||||
.contains(&HeaderToolbarItemKind::ToolsPanel)
|
||||
{
|
||||
MenuDirection::Right
|
||||
} else {
|
||||
MenuDirection::Left
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ItemStates {
|
||||
pub item_mouse_state: MouseStateHandle,
|
||||
pub item_hover_state: MouseStateHandle,
|
||||
pub menu_button_state: MouseStateHandle,
|
||||
pub draggable_state: DraggableState,
|
||||
pub item_sync_icon_hover_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
struct WarpDriveItemStyles {
|
||||
// Height of each item
|
||||
item_height: f32,
|
||||
/// Default styles of the WarpDriveItem
|
||||
default: UiComponentStyles,
|
||||
/// On top of the default styles, active contains extra styling for when the item is being dragged
|
||||
dragged: UiComponentStyles,
|
||||
/// Similarly to active styles, hovered contains extra styling for a hovered item
|
||||
hovered: UiComponentStyles,
|
||||
}
|
||||
|
||||
impl WarpDriveItemStyles {
|
||||
fn merge(self, style: UiComponentStyles) -> Self {
|
||||
Self {
|
||||
default: self.default.merge(style),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
fn default(appearance: &Appearance) -> WarpDriveItemStyles {
|
||||
let theme = appearance.theme();
|
||||
let item_height = ITEM_FONT_SIZE * 2.0 - ITEM_MARGIN_BOTTOM;
|
||||
let background = theme.background();
|
||||
WarpDriveItemStyles {
|
||||
item_height,
|
||||
default: UiComponentStyles::default()
|
||||
.set_font_color(blended_colors::text_sub(theme, background))
|
||||
.set_font_family_id(appearance.ui_builder().ui_font_family())
|
||||
.set_font_size(ITEM_FONT_SIZE),
|
||||
dragged: UiComponentStyles::default()
|
||||
.set_font_family_id(appearance.ui_builder().ui_font_family())
|
||||
.set_font_size(ITEM_FONT_SIZE)
|
||||
.set_font_color(theme.foreground().into())
|
||||
.set_background(
|
||||
warp_core::ui::theme::color::internal_colors::fg_overlay_4(theme).into(),
|
||||
)
|
||||
.set_border_color(theme.accent().into()),
|
||||
hovered: UiComponentStyles::default()
|
||||
.set_font_family_id(appearance.ui_builder().ui_font_family())
|
||||
.set_font_size(ITEM_FONT_SIZE)
|
||||
.set_font_color(blended_colors::text_main(theme, background))
|
||||
.set_background(
|
||||
warp_core::ui::theme::color::internal_colors::fg_overlay_2(theme).into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A UI wrapper around a row in warp drive that holds important UI state for the row and implements
|
||||
/// a unified look for all rows in warp drive, like padding and hover states.
|
||||
///
|
||||
/// The item-specific information like icon, name, click_action, and preview modal are abstracted as much as
|
||||
/// possible into the WarpDriveType enum.
|
||||
pub struct WarpDriveRow<'a> {
|
||||
item: Box<dyn WarpDriveItem>,
|
||||
space: Space,
|
||||
item_states: ItemStates,
|
||||
overflow_button: Box<dyn Element>,
|
||||
/// how many levels into a folder hierachy the row is.
|
||||
/// 0 means the object is in the root directory.
|
||||
folder_depth: usize,
|
||||
sync_icon: Option<Box<dyn Element>>,
|
||||
can_move: bool,
|
||||
styles: WarpDriveItemStyles,
|
||||
menu_open: bool,
|
||||
share_dialog_open: bool,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
overflow_on_left: bool,
|
||||
appearance: &'a Appearance,
|
||||
}
|
||||
|
||||
impl<'a> WarpDriveRow<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
item: Box<dyn WarpDriveItem>,
|
||||
item_states: ItemStates,
|
||||
space: Space,
|
||||
folder_depth: usize,
|
||||
menu: ViewHandle<Menu<DriveIndexAction>>,
|
||||
can_move: bool,
|
||||
has_menu_items: bool,
|
||||
menu_open: bool,
|
||||
share_dialog_open: bool,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
menu_direction: MenuDirection,
|
||||
appearance: &'a Appearance,
|
||||
) -> Option<Self> {
|
||||
let warp_drive_item_id = item.warp_drive_id();
|
||||
let overflow_button = match has_menu_items {
|
||||
true => {
|
||||
if is_focused || item_states.draggable_state.is_dragging() {
|
||||
ConstrainedBox::new(
|
||||
highlight_icon_button_with_context_menu_drive(
|
||||
Icon::DotsVertical,
|
||||
move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
DriveIndexAction::ToggleItemOverflowMenu {
|
||||
space,
|
||||
warp_drive_item_id,
|
||||
},
|
||||
);
|
||||
},
|
||||
item_states.menu_button_state.clone(),
|
||||
&menu,
|
||||
menu_open,
|
||||
menu_direction,
|
||||
appearance,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(20.)
|
||||
.with_height(ICON_DIMENSIONS)
|
||||
.finish()
|
||||
} else {
|
||||
ConstrainedBox::new(
|
||||
icon_button_with_context_menu_drive(
|
||||
Icon::DotsVertical,
|
||||
move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
DriveIndexAction::ToggleItemOverflowMenu {
|
||||
space,
|
||||
warp_drive_item_id,
|
||||
},
|
||||
);
|
||||
},
|
||||
item_states.menu_button_state.clone(),
|
||||
&menu,
|
||||
menu_open,
|
||||
menu_direction,
|
||||
None, /* cursor */
|
||||
appearance,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(20.)
|
||||
.with_height(ICON_DIMENSIONS)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
false => Empty::new().finish(),
|
||||
};
|
||||
|
||||
let sync_icon = item.sync_status_icon(
|
||||
sync_queue_is_dequeueing,
|
||||
item_states.item_sync_icon_hover_state.clone(),
|
||||
appearance,
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
item,
|
||||
space,
|
||||
item_states,
|
||||
overflow_button,
|
||||
folder_depth,
|
||||
sync_icon,
|
||||
can_move,
|
||||
styles: WarpDriveItemStyles::default(appearance),
|
||||
menu_open,
|
||||
share_dialog_open,
|
||||
is_selected,
|
||||
is_focused,
|
||||
overflow_on_left: matches!(menu_direction, MenuDirection::Left),
|
||||
appearance,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_from_cloud_object(
|
||||
object: &dyn CloudObject,
|
||||
item_states: ItemStates,
|
||||
space: Space,
|
||||
folder_depth: usize,
|
||||
menu: ViewHandle<Menu<DriveIndexAction>>,
|
||||
can_move: bool,
|
||||
has_menu_items: bool,
|
||||
menu_open: bool,
|
||||
share_dialog_open: bool,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
menu_direction: MenuDirection,
|
||||
appearance: &'a Appearance,
|
||||
) -> Option<Self> {
|
||||
let item = object.to_warp_drive_item(appearance)?;
|
||||
Self::new(
|
||||
item,
|
||||
item_states,
|
||||
space,
|
||||
folder_depth,
|
||||
menu,
|
||||
can_move,
|
||||
has_menu_items,
|
||||
menu_open,
|
||||
share_dialog_open,
|
||||
is_selected,
|
||||
is_focused,
|
||||
sync_queue_is_dequeueing,
|
||||
menu_direction,
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn should_show_preview(&self) -> bool {
|
||||
self.item_states
|
||||
.item_hover_state
|
||||
.lock()
|
||||
.expect("Should be able to lock")
|
||||
.is_hovered()
|
||||
&& !self
|
||||
.item_states
|
||||
.item_sync_icon_hover_state
|
||||
.lock()
|
||||
.expect("Should be able to lock")
|
||||
.is_hovered()
|
||||
}
|
||||
|
||||
/// Wraps an object preview in some uniformly-styled modal. Also conditionally returns an empty
|
||||
/// view if there's limited horizontal space.
|
||||
pub fn render_preview(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.item.preview(appearance).map(|content_preview| {
|
||||
let mut stacked_preview_panels: Vec<Box<dyn Element>> =
|
||||
vec![Container::new(content_preview)
|
||||
.with_uniform_padding(16.)
|
||||
.finish()];
|
||||
|
||||
stacked_preview_panels.extend(self.render_shared_object_owner(appearance, app));
|
||||
|
||||
// Tracks whether the object history rectangle is the bottommost preview panel, which determines if we
|
||||
// need to render with rounded corners or not.
|
||||
let countdown = self.render_object_deletion_countdown(appearance, app);
|
||||
|
||||
// If there's an object history preview, add this panel to our vector.
|
||||
if let Some(object_history) =
|
||||
self.render_object_history(appearance, countdown.is_none(), app)
|
||||
{
|
||||
// Insert above the permadeletion stat if it exists.
|
||||
stacked_preview_panels.push(object_history);
|
||||
}
|
||||
|
||||
// If there's a deletion stat, add this panel to our vector.
|
||||
if let Some(countdown) = countdown {
|
||||
stacked_preview_panels.push(
|
||||
Container::new(Empty::new().finish())
|
||||
.with_border(
|
||||
Border::bottom(1.).with_border_fill(appearance.theme().outline()),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
stacked_preview_panels.push(countdown);
|
||||
}
|
||||
|
||||
// The full hover preview is the column containing all these sub-panels.
|
||||
let full_hover_preview = Flex::column().with_children(stacked_preview_panels);
|
||||
|
||||
SizeConstraintSwitch::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(full_hover_preview.finish())
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.with_background(appearance.theme().surface_2())
|
||||
.with_border(
|
||||
Border::all(1.).with_border_fill(appearance.theme().surface_3()),
|
||||
)
|
||||
.with_drop_shadow(DropShadow::default())
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(400.)
|
||||
.finish(),
|
||||
vec![(
|
||||
SizeConstraintCondition::WidthLessThan(180.),
|
||||
Empty::new().finish(),
|
||||
)],
|
||||
)
|
||||
.finish()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a Box<dyn Element> representing a displayable view of this cloud object's history, including
|
||||
/// for example the last metadata on edits.
|
||||
fn render_object_history(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
with_rounded_bottom: bool,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
if let Some(metadata) = self.item.metadata() {
|
||||
let editing_history = metadata.semantic_editing_history(app);
|
||||
|
||||
let action_history = self.item.action_summary(app);
|
||||
|
||||
let full_object_history_text = match (editing_history, action_history) {
|
||||
(Some(edits), Some(actions)) => format!("{edits} | {actions}"),
|
||||
(Some(edits), None) => edits,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let history_text = appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(full_object_history_text, false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
// Render the element to span its parent horizontally
|
||||
let text_spanning_parent = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(Shrinkable::new(1., history_text).finish())
|
||||
.finish();
|
||||
|
||||
let container = Container::new(text_spanning_parent)
|
||||
.with_background(appearance.theme().surface_1())
|
||||
.with_padding_top(8.)
|
||||
.with_padding_bottom(8.)
|
||||
.with_padding_left(16.)
|
||||
.with_padding_right(16.);
|
||||
|
||||
// Conditionally add the rounded bottom
|
||||
Some(if with_rounded_bottom {
|
||||
container
|
||||
.with_corner_radius(CornerRadius::with_bottom(Radius::Pixels(8.)))
|
||||
.finish()
|
||||
} else {
|
||||
container.finish()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Render owner information, only for shared objects.
|
||||
fn render_shared_object_owner(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let WarpDriveItemId::Object(object_id) = self.item.warp_drive_id() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if CloudViewModel::as_ref(app).object_space(&object_id.uid(), app) != Some(Space::Shared) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let owner = CloudModel::as_ref(app)
|
||||
.get_by_uid(&object_id.uid())?
|
||||
.permissions()
|
||||
.owner;
|
||||
|
||||
let mut owner_label = "From ".to_string();
|
||||
match owner {
|
||||
Owner::User { user_uid } => {
|
||||
match UserProfiles::as_ref(app).displayable_identifier_for_uid(user_uid) {
|
||||
Some(user) => owner_label.push_str(&user),
|
||||
None => owner_label.push_str("unknown user"),
|
||||
}
|
||||
}
|
||||
Owner::Team { team_uid, .. } => owner_label.push_str(
|
||||
UserWorkspaces::as_ref(app)
|
||||
.team_from_uid(team_uid)
|
||||
.map_or("unknown team", |team| &team.name),
|
||||
),
|
||||
}
|
||||
|
||||
let background = appearance.theme().surface_1();
|
||||
let text_color = appearance.theme().sub_text_color(background);
|
||||
|
||||
let icon = Container::new(
|
||||
ConstrainedBox::new(Icon::Users.to_warpui_icon(text_color).finish())
|
||||
.with_height(15.)
|
||||
.with_width(15.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(6.)
|
||||
.finish();
|
||||
|
||||
let owner_text = appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(owner_label, false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(text_color.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
Some(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon)
|
||||
.with_child(Shrinkable::new(1., owner_text).finish())
|
||||
.finish(),
|
||||
)
|
||||
.with_background(background)
|
||||
.with_vertical_padding(8.)
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_object_deletion_countdown(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
if let Some(metadata) = self.item.metadata() {
|
||||
if let Some(countdown) = metadata.semantic_permadeletion_countdown(app) {
|
||||
let icon_and_text = Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Icon::Clock
|
||||
.to_warpui_icon(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2()),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(15.)
|
||||
.with_width(15.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(6.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(countdown, false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
Some(
|
||||
Container::new(icon_and_text)
|
||||
.with_background(appearance.theme().surface_1())
|
||||
.with_padding_top(8.)
|
||||
.with_padding_bottom(8.)
|
||||
.with_padding_left(16.)
|
||||
.with_padding_right(16.)
|
||||
.with_corner_radius(CornerRadius::with_bottom(Radius::Pixels(8.)))
|
||||
.finish(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_chevron(&self, style: UiComponentStyles) -> Box<dyn Element> {
|
||||
// Only render chevron for folders
|
||||
if let Some(is_open) = self.item.is_folder_open() {
|
||||
let chevron_icon = if is_open {
|
||||
Icon::ChevronDown
|
||||
} else {
|
||||
Icon::ChevronRight
|
||||
};
|
||||
|
||||
let icon_color = style.font_color.unwrap_or_else(|| {
|
||||
blended_colors::text_sub(
|
||||
self.appearance.theme(),
|
||||
self.appearance.theme().background(),
|
||||
)
|
||||
});
|
||||
|
||||
Container::new(
|
||||
ConstrainedBox::new(chevron_icon.to_warpui_icon(icon_color.into()).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(4.)
|
||||
.finish()
|
||||
} else {
|
||||
// Not a folder, render empty spacer to maintain alignment
|
||||
Container::new(
|
||||
ConstrainedBox::new(Empty::new().finish())
|
||||
.with_width(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(4.)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon(&self, style: UiComponentStyles) -> Box<dyn Element> {
|
||||
let icon_to_render = match self.item.warp_drive_id() {
|
||||
// This sets the icon color of folders correctly in color contrast cases, e.g. being dragged or focused
|
||||
WarpDriveItemId::Object(CloudObjectTypeAndId::Folder(_))
|
||||
if style == self.styles.dragged =>
|
||||
{
|
||||
self.item
|
||||
.icon(self.appearance, Some(style.font_color.unwrap().into()))
|
||||
}
|
||||
_ => self.item.icon(self.appearance, None),
|
||||
};
|
||||
|
||||
if let Some(icon) = icon_to_render {
|
||||
Container::new(
|
||||
ConstrainedBox::new(icon)
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(8.)
|
||||
.finish()
|
||||
} else {
|
||||
Empty::new().finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_secondary_icon(&self, style: UiComponentStyles) -> Box<dyn Element> {
|
||||
let icon_to_render = match self.item.warp_drive_id() {
|
||||
WarpDriveItemId::Object(CloudObjectTypeAndId::Folder(_)) => self
|
||||
.item
|
||||
.secondary_icon(Some(style.font_color.unwrap().into())),
|
||||
_ => self.item.secondary_icon(None),
|
||||
};
|
||||
|
||||
if let Some(icon) = icon_to_render {
|
||||
Container::new(
|
||||
ConstrainedBox::new(icon)
|
||||
.with_width(self.styles.default.font_size.unwrap_or_default())
|
||||
.with_height(self.styles.default.font_size.unwrap_or_default())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding_left(2.)
|
||||
.finish()
|
||||
} else {
|
||||
Empty::new().finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_item_name(&self, style: UiComponentStyles) -> Box<dyn Element> {
|
||||
Span::new(
|
||||
self.item
|
||||
.display_name()
|
||||
.unwrap_or_else(|| "Untitled".to_string()),
|
||||
style,
|
||||
)
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn render_item(&self, style: UiComponentStyles) -> Box<dyn Element> {
|
||||
let chevron = self.render_chevron(style);
|
||||
let icon = self.render_icon(style);
|
||||
let name = self.render_item_name(style);
|
||||
let secondary_icon = self.render_secondary_icon(style);
|
||||
|
||||
let item = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(Shrinkable::new(1., name).finish())
|
||||
.with_child(secondary_icon)
|
||||
.finish();
|
||||
|
||||
let action = self.item.click_action();
|
||||
let space = self.space;
|
||||
let warp_drive_item_id = self.item.warp_drive_id();
|
||||
match warp_drive_item_id {
|
||||
WarpDriveItemId::Object(_)
|
||||
| WarpDriveItemId::AIFactCollection
|
||||
| WarpDriveItemId::MCPServerCollection => {
|
||||
Hoverable::new(self.item_states.item_mouse_state.clone(), move |_| {
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(chevron)
|
||||
.with_child(icon)
|
||||
.with_child(Shrinkable::new(1., item).finish())
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(FOLDER_DEPTH_INDENT * self.folder_depth as f32)
|
||||
.finish()
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
if let Some(action) = action.clone() {
|
||||
ctx.dispatch_typed_action(action);
|
||||
}
|
||||
})
|
||||
.on_right_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(DriveIndexAction::ToggleItemOverflowMenu {
|
||||
space,
|
||||
warp_drive_item_id,
|
||||
});
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a callback for calculating the Drag bounds within Warp Drive
|
||||
fn drag_bounds_callback() -> impl Fn(&PositionCache, Vector2F) -> Option<RectF> {
|
||||
move |position_cache, window: Vector2F| {
|
||||
let drive_index = position_cache.get_position(WARP_DRIVE_POSITION_ID)?;
|
||||
|
||||
let top_left = drive_index.origin();
|
||||
|
||||
Some(RectF::from_points(top_left, window))
|
||||
}
|
||||
}
|
||||
|
||||
impl UiComponent for WarpDriveRow<'_> {
|
||||
type ElementType = SavePosition;
|
||||
|
||||
fn build(self) -> Self::ElementType {
|
||||
let is_dragging = self.item_states.draggable_state.is_dragging();
|
||||
let overflow_on_left = self.overflow_on_left;
|
||||
|
||||
// This is ONLY for rendering the font color correctly, which is set at the render_item level
|
||||
let style = if is_dragging || self.is_focused {
|
||||
self.styles.dragged
|
||||
} else {
|
||||
self.styles.default
|
||||
};
|
||||
let inner_item = self.render_item(style);
|
||||
|
||||
// Hoverable here doesn't have any action, it's mostly used for setting background styling based on
|
||||
// the mouse state
|
||||
let hoverable_item = Hoverable::new(
|
||||
self.item_states.item_hover_state.clone(),
|
||||
move |mouse_state| {
|
||||
// If dragging or object has been focused, then theme accent background.
|
||||
// If hovering / menu is open / object has been selected, then thick overlay background.
|
||||
// If an object is both selected and focused, show focused background
|
||||
let container_background_fill = if is_dragging || self.is_focused {
|
||||
self.styles.dragged.background
|
||||
} else if mouse_state.is_hovered()
|
||||
|| self.menu_open
|
||||
|| self.is_selected
|
||||
|| self.share_dialog_open
|
||||
{
|
||||
self.styles.hovered.background
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let show_overflow = mouse_state.is_hovered() || self.menu_open;
|
||||
|
||||
let mut items_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
items_row.add_child(Shrinkable::new(1., inner_item).finish());
|
||||
if let Some(sync_icon) = self.sync_icon {
|
||||
items_row.add_child(Container::new(sync_icon).with_margin_right(4.).finish());
|
||||
}
|
||||
|
||||
let row_element: Box<dyn Element> = if overflow_on_left {
|
||||
let row_container = Container::new(items_row.finish())
|
||||
.with_margin_left(INDEX_CONTENT_MARGIN_LEFT)
|
||||
.with_padding_right(ITEM_PADDING_HORIZONTAL)
|
||||
.with_padding_left(ITEM_PADDING_HORIZONTAL)
|
||||
.with_padding_top(ITEM_PADDING_VERTICAL)
|
||||
.with_padding_bottom(ITEM_PADDING_VERTICAL)
|
||||
.finish();
|
||||
if show_overflow {
|
||||
let mut stack = Stack::new().with_child(row_container);
|
||||
stack.add_positioned_child(
|
||||
self.overflow_button,
|
||||
OffsetPositioning::offset_from_parent(
|
||||
pathfinder_geometry::vector::vec2f(0., 0.),
|
||||
ParentOffsetBounds::ParentByPosition,
|
||||
ParentAnchor::MiddleLeft,
|
||||
ChildAnchor::MiddleLeft,
|
||||
),
|
||||
);
|
||||
stack.finish()
|
||||
} else {
|
||||
row_container
|
||||
}
|
||||
} else {
|
||||
if show_overflow {
|
||||
items_row.add_child(self.overflow_button);
|
||||
}
|
||||
Container::new(items_row.finish())
|
||||
.with_margin_left(INDEX_CONTENT_MARGIN_LEFT)
|
||||
.with_padding_right(ITEM_PADDING_HORIZONTAL)
|
||||
.with_padding_left(ITEM_PADDING_HORIZONTAL)
|
||||
.with_padding_top(ITEM_PADDING_VERTICAL)
|
||||
.with_padding_bottom(ITEM_PADDING_VERTICAL)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let result_container = Container::new(
|
||||
ConstrainedBox::new(row_element)
|
||||
.with_height(self.styles.item_height)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_bottom(ITEM_MARGIN_BOTTOM)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)));
|
||||
|
||||
match container_background_fill {
|
||||
Some(background_fill) => {
|
||||
result_container.with_background(background_fill).finish()
|
||||
}
|
||||
None => result_container.finish(),
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.finish();
|
||||
|
||||
match self.item.warp_drive_id() {
|
||||
WarpDriveItemId::Object(item) => {
|
||||
let save_position_child = match self.can_move {
|
||||
true => {
|
||||
Draggable::new(self.item_states.draggable_state, hoverable_item)
|
||||
.with_drag_bounds_callback(drag_bounds_callback())
|
||||
.with_accepted_by_drop_target_fn(move |drop_data, app| {
|
||||
let Some(location) =
|
||||
drop_data.as_any().downcast_ref::<CloudObjectLocation>()
|
||||
else {
|
||||
return AcceptedByDropTarget::No;
|
||||
};
|
||||
let cloud_model = CloudModel::handle(app);
|
||||
if cloud_model.as_ref(app).can_move_object_to_location(
|
||||
&item.uid(),
|
||||
*location,
|
||||
app,
|
||||
) {
|
||||
AcceptedByDropTarget::Yes
|
||||
} else {
|
||||
AcceptedByDropTarget::No
|
||||
}
|
||||
})
|
||||
.on_drop(move |ctx, _, _, data| {
|
||||
if let Some(location) = data.and_then(|data| {
|
||||
data.as_any().downcast_ref::<CloudObjectLocation>()
|
||||
}) {
|
||||
ctx.dispatch_typed_action(DriveIndexAction::DropIndexItem {
|
||||
cloud_object_type_and_id: item,
|
||||
drop_target_location: *location,
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_drag(move |ctx, _, dragged_item, data| {
|
||||
// First, check if we are over a drop target for styling
|
||||
if let Some(location) = data.and_then(|data| {
|
||||
data.as_any().downcast_ref::<CloudObjectLocation>()
|
||||
}) {
|
||||
ctx.dispatch_typed_action(
|
||||
DriveIndexAction::UpdateCurrentDropTarget {
|
||||
drop_target_location: *location,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ctx.dispatch_typed_action(DriveIndexAction::ClearDropTarget)
|
||||
}
|
||||
|
||||
// On a drag event, check to see if the index needs to be scrolled up or down
|
||||
// to reveal new content.
|
||||
if let Some(drive_index_view_position) =
|
||||
ctx.element_position_by_id(DRIVE_INDEX_VIEW_POSITION_ID)
|
||||
{
|
||||
// First, check to see if we should scroll upwards (revealing more content at the top).
|
||||
// This computes the distance between the top of the *currently-dragging* item and the top
|
||||
// of the drive index view. If distance < 10, we emit a scroll event back to the index view.
|
||||
let pixels_from_top =
|
||||
dragged_item.min_y() - drive_index_view_position.min_y();
|
||||
if pixels_from_top < AUTOSCROLL_DETECTION_DISTANCE {
|
||||
// The speed of the autoscroll is a function of (1) how far away from the relevant border the object is
|
||||
// and (2) what the speed multiplier is.
|
||||
// Note: Scrolling upwards is decreasing the scroll value, so we multiply by -1 in this case.
|
||||
let scroll_speed = ((AUTOSCROLL_DETECTION_DISTANCE
|
||||
- pixels_from_top)
|
||||
/ AUTOSCROLL_DETECTION_DISTANCE)
|
||||
* AUTOSCROLL_SPEED_MULTIPLIER;
|
||||
ctx.dispatch_typed_action(DriveIndexAction::Autoscroll {
|
||||
delta: -scroll_speed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwize, check to see if we should scroll downwards (revealing more content at the
|
||||
// bottom).
|
||||
// This computes the distance between the bottom of the *currently-dragging* item
|
||||
// and the bottom of the drive index view. If distance < 10, emit a scroll event.
|
||||
let pixels_from_bottom =
|
||||
drive_index_view_position.max_y() - dragged_item.max_y();
|
||||
if pixels_from_bottom < AUTOSCROLL_DETECTION_DISTANCE {
|
||||
// See comment above about determining the speed of the scroll.
|
||||
let scroll_speed = ((AUTOSCROLL_DETECTION_DISTANCE
|
||||
- pixels_from_bottom)
|
||||
/ AUTOSCROLL_DETECTION_DISTANCE)
|
||||
* AUTOSCROLL_SPEED_MULTIPLIER;
|
||||
ctx.dispatch_typed_action(DriveIndexAction::Autoscroll {
|
||||
delta: scroll_speed,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
false => hoverable_item,
|
||||
};
|
||||
SavePosition::new(
|
||||
save_position_child,
|
||||
&self.item.warp_drive_id().drive_row_position_id(),
|
||||
)
|
||||
}
|
||||
WarpDriveItemId::AIFactCollection | WarpDriveItemId::MCPServerCollection => {
|
||||
SavePosition::new(
|
||||
hoverable_item,
|
||||
&self.item.warp_drive_id().drive_row_position_id(),
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_style(self, style: UiComponentStyles) -> Self {
|
||||
Self {
|
||||
styles: self.styles.merge(style),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
use crate::{
|
||||
ai::mcp::CloudMCPServer,
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{index::DriveIndexAction, CloudObjectTypeAndId, DriveObjectType},
|
||||
themes::theme::Fill,
|
||||
};
|
||||
use warpui::{elements::MouseStateHandle, AppContext, Element};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveMCPServer {
|
||||
id: CloudObjectTypeAndId,
|
||||
mcp_server: CloudMCPServer,
|
||||
}
|
||||
|
||||
impl WarpDriveMCPServer {
|
||||
pub fn new(id: CloudObjectTypeAndId, mcp_server: CloudMCPServer) -> Self {
|
||||
Self { id, mcp_server }
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveMCPServer {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
Some(self.mcp_server.model().string_model.name.clone())
|
||||
}
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.mcp_server.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::MCPServer)
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::OpenMCPServerCollection)
|
||||
}
|
||||
|
||||
fn preview(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
// TODO
|
||||
None
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.mcp_server
|
||||
.metadata
|
||||
.pending_changes_statuses
|
||||
.render_icon(sync_queue_is_dequeueing, hover_state, appearance)
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use warpui::{elements::MouseStateHandle, AppContext, Element};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{index::DriveIndexAction, DriveObjectType},
|
||||
server::ids::ClientId,
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveMCPServerCollection {
|
||||
id: ClientId,
|
||||
}
|
||||
|
||||
impl WarpDriveMCPServerCollection {
|
||||
pub fn new(id: ClientId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ClientId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveMCPServerCollection {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
Some("MCP Servers".to_string())
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::MCPServerCollection)
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::OpenMCPServerCollection)
|
||||
}
|
||||
|
||||
fn preview(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::MCPServerCollection
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
_sync_queue_is_dequeueing: bool,
|
||||
_hover_state: MouseStateHandle,
|
||||
_appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use warpui::{elements::MouseStateHandle, AppContext, Element};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::{CloudObjectMetadata, Space},
|
||||
themes::theme::Fill,
|
||||
ui_components::icons::Icon,
|
||||
};
|
||||
|
||||
use super::{
|
||||
cloud_object_styling::warp_drive_icon_color,
|
||||
index::{warp_drive_section_header_position_id, DriveIndexAction, DriveIndexSection},
|
||||
CloudObjectTypeAndId, DriveObjectType,
|
||||
};
|
||||
|
||||
pub mod ai_fact;
|
||||
pub mod ai_fact_collection;
|
||||
pub mod env_var_collection;
|
||||
pub mod folder;
|
||||
pub mod item;
|
||||
pub mod mcp_server;
|
||||
pub mod mcp_server_collection;
|
||||
pub mod notebook;
|
||||
pub mod space;
|
||||
pub mod workflow;
|
||||
|
||||
pub trait WarpDriveItem {
|
||||
/// The display name of the item. If the item is unnamed, this may return `None` - implementations
|
||||
/// should prefer this over `Some("")`, as it lets the index view use alternate styling.
|
||||
fn display_name(&self) -> Option<String>;
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata>;
|
||||
fn object_type(&self) -> Option<DriveObjectType>;
|
||||
fn secondary_icon(&self, color: Option<Fill>) -> Option<Box<dyn Element>>; // The optional icon to the right of the name
|
||||
fn click_action(&self) -> Option<DriveIndexAction>;
|
||||
fn preview(&self, appearance: &Appearance) -> Option<Box<dyn Element>>;
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId;
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>>;
|
||||
|
||||
fn icon(&self, appearance: &Appearance, color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
let object_type = self.object_type()?;
|
||||
let icon_fill = color.unwrap_or(warp_drive_icon_color(appearance, object_type).into());
|
||||
Some(Icon::from(object_type).to_warpui_icon(icon_fill).finish())
|
||||
}
|
||||
|
||||
/// If implemented, returns a string that summarizes the primary action history. For example, "Run 2 times in the last week"
|
||||
fn action_summary(&self, app: &AppContext) -> Option<String>;
|
||||
|
||||
/// Returns Some(true) if this is an open folder, Some(false) if closed folder, None if not a folder
|
||||
fn is_folder_open(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem>;
|
||||
}
|
||||
|
||||
impl WarpDriveItemId {
|
||||
pub fn drive_row_position_id(&self) -> String {
|
||||
match self {
|
||||
Self::AIFactCollection => "AI_fact_collection".to_string(),
|
||||
Self::MCPServerCollection => "MCP_server_collection".to_string(),
|
||||
Self::Object(object_id) => object_id.drive_row_position_id(),
|
||||
Self::Space(space) => {
|
||||
warp_drive_section_header_position_id(&DriveIndexSection::Space(*space))
|
||||
}
|
||||
Self::Trash => "Trash".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This uniquely identifies an item in Warp Drive index
|
||||
/// Includes spaces (which CloudObjectTypeAndId does not entail)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum WarpDriveItemId {
|
||||
AIFactCollection,
|
||||
MCPServerCollection,
|
||||
Object(CloudObjectTypeAndId),
|
||||
Space(Space),
|
||||
Trash,
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn WarpDriveItem> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use warpui::{
|
||||
elements::{Flex, MouseStateHandle, ParentElement},
|
||||
fonts::Weight,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObjectMetadata,
|
||||
drive::{index::DriveIndexAction, CloudObjectTypeAndId, DriveObjectType},
|
||||
notebooks::CloudNotebook,
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveNotebook {
|
||||
id: CloudObjectTypeAndId,
|
||||
notebook: CloudNotebook,
|
||||
is_ai_document: bool,
|
||||
}
|
||||
|
||||
impl WarpDriveNotebook {
|
||||
pub fn new(id: CloudObjectTypeAndId, notebook: CloudNotebook, is_ai_document: bool) -> Self {
|
||||
Self {
|
||||
id,
|
||||
notebook,
|
||||
is_ai_document,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveNotebook {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
if self.notebook.model().title.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.notebook.model().title.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.notebook.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
Some(DriveObjectType::Notebook {
|
||||
is_ai_document: self.is_ai_document,
|
||||
})
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
Some(DriveIndexAction::OpenObject(self.id))
|
||||
}
|
||||
|
||||
fn preview(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
let title_text = self.notebook.model().title.clone();
|
||||
let title_to_render = if title_text.is_empty() {
|
||||
"Untitled".to_string()
|
||||
} else {
|
||||
title_text
|
||||
};
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(title_to_render, true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
Some(Flex::column().with_child(title).finish())
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.notebook.metadata.pending_changes_statuses.render_icon(
|
||||
sync_queue_is_dequeueing,
|
||||
hover_state,
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use warpui::{elements::MouseStateHandle, Element};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::{CloudObjectMetadata, Space},
|
||||
drive::{index::DriveIndexAction, DriveObjectType},
|
||||
themes::theme::Fill,
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveSpace {
|
||||
space: Space,
|
||||
}
|
||||
|
||||
impl WarpDriveSpace {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(space: Space) -> Self {
|
||||
Self { space }
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveSpace {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
None
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
None
|
||||
}
|
||||
|
||||
fn preview(&self, _appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Space(self.space)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
_sync_queue_is_dequeueing: bool,
|
||||
_hover_state: MouseStateHandle,
|
||||
_appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn action_summary(&self, _app: &warpui::AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use warp_core::context_flag::ContextFlag;
|
||||
use warpui::{
|
||||
elements::{Container, Flex, MouseStateHandle, ParentElement},
|
||||
fonts::Weight,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element, SingletonEntity,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
cloud_object::{
|
||||
model::actions::{ObjectActionType, ObjectActions},
|
||||
CloudObjectMetadata,
|
||||
},
|
||||
drive::{index::DriveIndexAction, CloudObjectTypeAndId, DriveObjectType},
|
||||
themes::theme::Fill,
|
||||
workflows::{CloudWorkflow, WorkflowViewMode},
|
||||
};
|
||||
|
||||
use super::{WarpDriveItem, WarpDriveItemId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarpDriveWorkflow {
|
||||
id: CloudObjectTypeAndId,
|
||||
workflow: CloudWorkflow,
|
||||
}
|
||||
|
||||
impl WarpDriveWorkflow {
|
||||
pub fn new(id: CloudObjectTypeAndId, workflow: CloudWorkflow) -> Self {
|
||||
Self { id, workflow }
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveItem for WarpDriveWorkflow {
|
||||
fn display_name(&self) -> Option<String> {
|
||||
if self.workflow.model().data.name().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.workflow.model().data.name().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<&CloudObjectMetadata> {
|
||||
Some(&self.workflow.metadata)
|
||||
}
|
||||
|
||||
fn object_type(&self) -> Option<DriveObjectType> {
|
||||
if self.workflow.model().data.is_agent_mode_workflow() {
|
||||
Some(DriveObjectType::AgentModeWorkflow)
|
||||
} else {
|
||||
Some(DriveObjectType::Workflow)
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_icon(&self, _color: Option<Fill>) -> Option<Box<dyn Element>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn click_action(&self) -> Option<DriveIndexAction> {
|
||||
if !ContextFlag::RunWorkflow.is_enabled() {
|
||||
// If we are in a context where we can't run workflows, open it in view mode
|
||||
// by default
|
||||
Some(DriveIndexAction::OpenWorkflowInPane {
|
||||
cloud_object_type_and_id: self.id,
|
||||
open_mode: WorkflowViewMode::View,
|
||||
})
|
||||
} else {
|
||||
Some(DriveIndexAction::RunObject(self.id))
|
||||
}
|
||||
}
|
||||
|
||||
fn preview(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
let mut modal =
|
||||
Flex::column().with_cross_axis_alignment(warpui::elements::CrossAxisAlignment::Stretch);
|
||||
|
||||
let mut text = Flex::column()
|
||||
.with_child(Container::new(self.render_workflow_name(appearance)).finish());
|
||||
|
||||
if let Some(description) = self.workflow.model().data.description() {
|
||||
let description_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph(description.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(12.),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
text.add_child(
|
||||
Container::new(description_text.build().finish())
|
||||
.with_margin_top(4.)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
let content = self.render_workflow_content(appearance);
|
||||
|
||||
modal.add_children([
|
||||
Container::new(text.finish())
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
content,
|
||||
]);
|
||||
|
||||
Some(modal.finish())
|
||||
}
|
||||
|
||||
fn warp_drive_id(&self) -> WarpDriveItemId {
|
||||
WarpDriveItemId::Object(self.id)
|
||||
}
|
||||
|
||||
fn sync_status_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
self.workflow.metadata.pending_changes_statuses.render_icon(
|
||||
sync_queue_is_dequeueing,
|
||||
hover_state,
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn action_summary(&self, app: &AppContext) -> Option<String> {
|
||||
ObjectActions::as_ref(app)
|
||||
.get_action_history_summary_for_action_type(&self.id.uid(), ObjectActionType::Execute)
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn WarpDriveItem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl WarpDriveWorkflow {
|
||||
fn render_workflow_name(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(self.workflow.model().data.name().to_owned(), true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_workflow_content(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.paragraph(self.workflow.model().data.content().to_owned())
|
||||
.with_style(UiComponentStyles {
|
||||
font_family_id: Some(if self.workflow.model().data.is_agent_mode_workflow() {
|
||||
appearance.ui_font_family()
|
||||
} else {
|
||||
appearance.monospace_font_family()
|
||||
}),
|
||||
font_color: Some(theme.main_text_color(theme.surface_2()).into()),
|
||||
font_size: Some(12.),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user