Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::{appearance::Appearance, themes::theme::Fill, ui_components::icons};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use warpui::{
|
||||
color::ColorU,
|
||||
elements::{
|
||||
Align, Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Dismiss, Element, Flex, MouseStateHandle, OffsetPositioning,
|
||||
ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Shrinkable, Stack, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
keymap::FixedBinding,
|
||||
ui_components::components::{Coords, UiComponent, UiComponentStyles},
|
||||
AppContext, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
pub const MODAL_CORNER_RADIUS: Radius = Radius::Pixels(8.);
|
||||
pub const MODAL_WIDTH: f32 = 440.;
|
||||
pub const MODAL_HEADER_HEIGHT: f32 = 70.;
|
||||
pub const MODAL_PADDING: f32 = 28.;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Modal<T> {
|
||||
title: Option<String>,
|
||||
header_icon: Option<icons::Icon>,
|
||||
header_icon_color: Option<Fill>,
|
||||
body: ViewHandle<T>,
|
||||
show_close_modal_button: bool,
|
||||
dismiss_on_click: bool,
|
||||
modal_styles: UiComponentStyles,
|
||||
header_styles: UiComponentStyles,
|
||||
body_styles: UiComponentStyles,
|
||||
close_modal_hover_state: MouseStateHandle,
|
||||
background_opacity: u8,
|
||||
offset_positioning: OffsetPositioning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ModalState {
|
||||
Open,
|
||||
#[default]
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Helper struct that holds the view handle and the state of the "handled" modal.
|
||||
/// It's supposed to be used within places like Input or Workspace, where there are multiple
|
||||
/// internal views for which we want the owner to decide whether it's open or closed (instead of
|
||||
/// multiplying the amount of members by adding extra booleans to hold that state).
|
||||
pub struct ModalViewState<T> {
|
||||
pub view: ViewHandle<T>,
|
||||
state: ModalState,
|
||||
}
|
||||
|
||||
impl<T: View> ModalViewState<T> {
|
||||
pub fn new(view: ViewHandle<T>) -> Self {
|
||||
Self {
|
||||
view,
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self.state, ModalState::Open)
|
||||
}
|
||||
|
||||
pub fn open(&mut self) {
|
||||
self.state = ModalState::Open;
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.state = ModalState::Closed;
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Box<dyn Element> {
|
||||
ChildView::new(&self.view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for ModalViewState<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
view: self.view.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ModalAction {
|
||||
Close,
|
||||
}
|
||||
|
||||
pub fn init(app: &mut AppContext) {
|
||||
use warpui::keymap::macros::*;
|
||||
|
||||
app.register_fixed_bindings(vec![FixedBinding::new(
|
||||
"escape",
|
||||
ModalAction::Close,
|
||||
id!("Modal"),
|
||||
)]);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ModalEvent {
|
||||
Close,
|
||||
}
|
||||
|
||||
/// A generic modal view that renders any view as its body.
|
||||
// TODO(alokedesai): Consolidate all the various modals in our app to use this view.
|
||||
impl<T: View> Modal<T> {
|
||||
pub fn new(title: Option<String>, body: ViewHandle<T>, ctx: &mut ViewContext<Self>) -> Self {
|
||||
let appearance_handle = Appearance::handle(ctx);
|
||||
ctx.observe(&appearance_handle, Self::handle_appearance_update);
|
||||
|
||||
let appearance = appearance_handle.as_ref(ctx);
|
||||
let theme = appearance.theme();
|
||||
Self {
|
||||
title,
|
||||
header_icon: None,
|
||||
header_icon_color: None,
|
||||
body,
|
||||
show_close_modal_button: true,
|
||||
dismiss_on_click: false,
|
||||
modal_styles: UiComponentStyles {
|
||||
border_radius: Some(CornerRadius::with_all(MODAL_CORNER_RADIUS)),
|
||||
border_width: Some(1.),
|
||||
border_color: Some(theme.outline().into()),
|
||||
width: Some(MODAL_WIDTH),
|
||||
height: Some(480.),
|
||||
..Default::default()
|
||||
},
|
||||
header_styles: UiComponentStyles {
|
||||
font_family_id: Some(appearance.header_font_family()),
|
||||
font_color: Some(theme.active_ui_text_color().into()),
|
||||
font_size: Some(appearance.header_font_size()),
|
||||
font_weight: Some(Weight::Normal),
|
||||
height: Some(MODAL_HEADER_HEIGHT),
|
||||
padding: Some(Coords {
|
||||
top: 20.,
|
||||
bottom: 15.,
|
||||
left: MODAL_PADDING,
|
||||
right: MODAL_PADDING,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
body_styles: UiComponentStyles {
|
||||
background: Some(theme.surface_2().into()),
|
||||
padding: Some(Coords {
|
||||
top: MODAL_PADDING,
|
||||
bottom: MODAL_PADDING,
|
||||
left: MODAL_PADDING,
|
||||
right: MODAL_PADDING,
|
||||
}),
|
||||
height: Some(410.),
|
||||
..Default::default()
|
||||
},
|
||||
close_modal_hover_state: Default::default(),
|
||||
background_opacity: 179,
|
||||
offset_positioning: Self::default_offset_positioning(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_modal_button_disabled(mut self) -> Self {
|
||||
self.show_close_modal_button = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on "Dismiss on click" behavior
|
||||
/// outside of the Modal
|
||||
pub fn with_dismiss_on_click(mut self) -> Self {
|
||||
self.dismiss_on_click = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Overwrites _some_ styles passed in `modal_style` parameter
|
||||
pub fn with_modal_style(mut self, styles: UiComponentStyles) -> Self {
|
||||
self.modal_styles = self.modal_styles.merge(styles);
|
||||
self
|
||||
}
|
||||
|
||||
/// Overwrites _some_ styles passed in `header_style` parameter
|
||||
pub fn with_header_style(mut self, styles: UiComponentStyles) -> Self {
|
||||
self.header_styles = self.header_styles.merge(styles);
|
||||
self
|
||||
}
|
||||
|
||||
/// Overwrites _some_ styles passed in `body_style` parameter
|
||||
pub fn with_body_style(mut self, styles: UiComponentStyles) -> Self {
|
||||
self.body_styles = self.body_styles.merge(styles);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_background_opacity(mut self, opacity: u8) -> Self {
|
||||
self.background_opacity = opacity;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: Option<String>) {
|
||||
self.title = title;
|
||||
}
|
||||
|
||||
pub fn set_header_icon(&mut self, icon: Option<icons::Icon>) {
|
||||
self.header_icon = icon;
|
||||
}
|
||||
|
||||
pub fn set_header_icon_color(&mut self, color: Option<Fill>) {
|
||||
self.header_icon_color = color;
|
||||
}
|
||||
|
||||
pub fn set_offset_positioning(&mut self, offset_positioning: OffsetPositioning) {
|
||||
self.offset_positioning = offset_positioning;
|
||||
}
|
||||
|
||||
fn handle_appearance_update(
|
||||
&mut self,
|
||||
handle: ModelHandle<Appearance>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let appearance = handle.as_ref(ctx);
|
||||
let theme = appearance.theme();
|
||||
// Update theme dependent styles
|
||||
self.modal_styles = self.modal_styles.merge(UiComponentStyles {
|
||||
border_color: Some(theme.outline().into()),
|
||||
..Default::default()
|
||||
});
|
||||
self.header_styles = self.header_styles.merge(UiComponentStyles {
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(theme.active_ui_text_color().into()),
|
||||
..Default::default()
|
||||
});
|
||||
self.body_styles = self.body_styles.merge(UiComponentStyles {
|
||||
background: Some(theme.surface_2().into()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn render_close_modal_button(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const BUTTON_DIAMETER: f32 = 24.;
|
||||
appearance
|
||||
.ui_builder()
|
||||
.close_button(BUTTON_DIAMETER, self.close_modal_hover_state.clone())
|
||||
.build()
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(ModalAction::Close))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_header_icon(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
if let Some(icon) = self.header_icon {
|
||||
let icon_size = self
|
||||
.header_styles
|
||||
.font_size
|
||||
.unwrap_or(appearance.header_font_size());
|
||||
// first check if there's an icon color override, otherwise fall back to the
|
||||
// header font color, otherwise fall back to the theme's active_ui_text_color
|
||||
let icon_color = self.header_icon_color.unwrap_or(
|
||||
self.header_styles
|
||||
.font_color
|
||||
.unwrap_or(appearance.theme().active_ui_text_color().into())
|
||||
.into(),
|
||||
);
|
||||
Some(
|
||||
Align::new(
|
||||
Container::new(
|
||||
ConstrainedBox::new(icon.to_warpui_icon(icon_color).finish())
|
||||
.with_width(icon_size)
|
||||
.with_height(icon_size)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(icon_size / 2.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(&self, appearance: &Appearance) -> Option<Box<dyn Element>> {
|
||||
if let Some(title) = &self.title {
|
||||
let mut header = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
if let Some(icon) = self.render_header_icon(appearance) {
|
||||
header.add_child(icon);
|
||||
}
|
||||
|
||||
header.add_child(
|
||||
Shrinkable::new(
|
||||
1.0,
|
||||
Align::new(
|
||||
Text::new_inline(
|
||||
title.clone(),
|
||||
self.header_styles
|
||||
.font_family_id
|
||||
.unwrap_or(appearance.ui_font_family()),
|
||||
self.header_styles
|
||||
.font_size
|
||||
.unwrap_or(appearance.header_font_size()),
|
||||
)
|
||||
.with_color(
|
||||
self.header_styles
|
||||
.font_color
|
||||
.unwrap_or(appearance.theme().active_ui_text_color().into()),
|
||||
)
|
||||
.with_style(
|
||||
Properties::default()
|
||||
.weight(self.header_styles.font_weight.unwrap_or(Weight::Normal)),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.left()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if self.show_close_modal_button {
|
||||
header.add_child(self.render_close_modal_button(appearance))
|
||||
}
|
||||
|
||||
let mut container = Container::new(header.finish());
|
||||
if let Some(background) = self.header_styles.background {
|
||||
container = container.with_background(background);
|
||||
}
|
||||
if let Some(border_radius) = self.header_styles.border_radius {
|
||||
container = container.with_corner_radius(border_radius.top());
|
||||
}
|
||||
|
||||
if let Some(padding) = self.header_styles.padding {
|
||||
container = container
|
||||
.with_padding_left(padding.left)
|
||||
.with_padding_top(padding.top)
|
||||
.with_padding_right(padding.right)
|
||||
.with_padding_bottom(padding.bottom);
|
||||
}
|
||||
|
||||
Some(
|
||||
ConstrainedBox::new(container.finish())
|
||||
.with_max_height(self.header_styles.height.unwrap_or(MODAL_HEADER_HEIGHT))
|
||||
.finish(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_body(&self) -> Box<dyn Element> {
|
||||
let mut container = Container::new(ChildView::new(&self.body).finish());
|
||||
|
||||
if self.title.is_some() {
|
||||
container = container
|
||||
.with_corner_radius(self.modal_styles.border_radius.unwrap_or_default().bottom());
|
||||
} else {
|
||||
// If there is no title, then the body has to have its top rounded
|
||||
container =
|
||||
container.with_corner_radius(self.modal_styles.border_radius.unwrap_or_default());
|
||||
};
|
||||
|
||||
if let Some(padding) = self.body_styles.padding {
|
||||
container = container
|
||||
.with_padding_left(padding.left)
|
||||
.with_padding_top(padding.top)
|
||||
.with_padding_right(padding.right)
|
||||
.with_padding_bottom(padding.bottom);
|
||||
}
|
||||
ConstrainedBox::new(container.finish())
|
||||
.with_max_height(self.body_styles.height.unwrap())
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> &ViewHandle<T> {
|
||||
&self.body
|
||||
}
|
||||
|
||||
fn default_offset_positioning() -> OffsetPositioning {
|
||||
OffsetPositioning::offset_from_parent(
|
||||
vec2f(0., 0.),
|
||||
ParentOffsetBounds::WindowByPosition,
|
||||
ParentAnchor::Center,
|
||||
ChildAnchor::Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: View> Entity for Modal<T> {
|
||||
type Event = ModalEvent;
|
||||
}
|
||||
|
||||
impl<T: View> TypedActionView for Modal<T> {
|
||||
type Action = ModalAction;
|
||||
|
||||
fn handle_action(&mut self, action: &ModalAction, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ModalAction::Close => {
|
||||
ctx.emit(ModalEvent::Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: View> View for Modal<T> {
|
||||
fn ui_name() -> &'static str {
|
||||
"Modal"
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
|
||||
if focus_ctx.is_self_focused() {
|
||||
ctx.focus(&self.body);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let header = self.render_header(appearance);
|
||||
let contents = if let Some(header) = header {
|
||||
Flex::column()
|
||||
.with_child(header)
|
||||
.with_child(self.render_body())
|
||||
.finish()
|
||||
} else {
|
||||
self.render_body()
|
||||
};
|
||||
|
||||
let mut modal = ConstrainedBox::new(
|
||||
Container::new(contents)
|
||||
.with_background(blended_colors::neutral_2(appearance.theme()))
|
||||
.with_corner_radius(self.modal_styles.border_radius.unwrap_or_default())
|
||||
.with_border(
|
||||
Border::all(self.modal_styles.border_width.unwrap())
|
||||
.with_border_fill(self.modal_styles.border_color.unwrap()),
|
||||
)
|
||||
.with_margin_top(35.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(self.modal_styles.width.unwrap())
|
||||
.with_max_height(self.modal_styles.height.unwrap())
|
||||
.finish();
|
||||
|
||||
if self.dismiss_on_click {
|
||||
modal = Dismiss::new(modal)
|
||||
.on_dismiss(|ctx, _app| ctx.dispatch_typed_action(ModalAction::Close))
|
||||
.finish();
|
||||
}
|
||||
|
||||
// Stack needed so that modal can get bounds information,
|
||||
// specifically to ensure no overlap with the window's traffic lights
|
||||
let mut stack = Stack::new();
|
||||
stack.add_positioned_child(modal, self.offset_positioning.clone());
|
||||
|
||||
Container::new(Align::new(stack.finish()).finish())
|
||||
.with_background_color(ColorU::new(0, 0, 0, self.background_opacity))
|
||||
.with_corner_radius(app.windows().window_corner_radius())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user