zed/crates/theme/src/ui.rs
2023-08-22 16:35:56 -06:00

245 lines
6.8 KiB
Rust

use std::borrow::Cow;
use gpui::{
elements::{
ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label,
MouseEventHandler, ParentElement, Stack, Svg, SvgStyle,
},
fonts::TextStyle,
geometry::vector::Vector2F,
platform,
platform::MouseButton,
scene::MouseClick,
Action, Element, EventContext, MouseState, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{ContainedText, Interactive};
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct CheckboxStyle {
pub icon: SvgStyle,
pub label: ContainedText,
pub default: ContainerStyle,
pub checked: ContainerStyle,
pub hovered: ContainerStyle,
pub hovered_and_checked: ContainerStyle,
}
pub fn checkbox<Tag, V, F>(
label: &'static str,
style: &CheckboxStyle,
checked: bool,
id: usize,
cx: &mut ViewContext<V>,
change: F,
) -> MouseEventHandler<V>
where
Tag: 'static,
V: 'static,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
let label = Label::new(label, style.label.text.clone())
.contained()
.with_style(style.label.container);
checkbox_with_label::<Tag, _, _, _>(label, style, checked, id, cx, change)
}
pub fn checkbox_with_label<Tag, D, V, F>(
label: D,
style: &CheckboxStyle,
checked: bool,
id: usize,
cx: &mut ViewContext<V>,
change: F,
) -> MouseEventHandler<V>
where
Tag: 'static,
D: Element<V>,
V: 'static,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
MouseEventHandler::new::<Tag, _>(id, cx, |state, _| {
let indicator = if checked {
svg(&style.icon)
} else {
Empty::new()
.constrained()
.with_width(style.icon.dimensions.width)
.with_height(style.icon.dimensions.height)
};
Flex::row()
.with_child(indicator.contained().with_style(if checked {
if state.hovered() {
style.hovered_and_checked
} else {
style.checked
}
} else {
if state.hovered() {
style.hovered
} else {
style.default
}
}))
.with_child(label)
.align_children_center()
})
.on_click(platform::MouseButton::Left, move |_, view, cx| {
change(view, !checked, cx)
})
.with_cursor_style(platform::CursorStyle::PointingHand)
}
pub fn svg<V: 'static>(style: &SvgStyle) -> ConstrainedBox<V> {
Svg::new(style.asset.clone())
.with_color(style.color)
.constrained()
.with_width(style.dimensions.width)
.with_height(style.dimensions.height)
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct IconStyle {
pub icon: SvgStyle,
pub container: ContainerStyle,
}
impl IconStyle {
pub fn width(&self) -> f32 {
self.icon.dimensions.width
+ self.container.padding.left
+ self.container.padding.right
+ self.container.margin.left
+ self.container.margin.right
}
}
pub fn icon<V: 'static>(style: &IconStyle) -> Container<V> {
svg(&style.icon).contained().with_style(style.container)
}
pub fn keystroke_label<V: 'static>(
label_text: &'static str,
label_style: &ContainedText,
keystroke_style: &ContainedText,
action: Box<dyn Action>,
cx: &mut ViewContext<V>,
) -> Container<V> {
// FIXME: Put the theme in it's own global so we can
// query the keystroke style on our own
Flex::row()
.with_child(Label::new(label_text, label_style.text.clone()).contained())
.with_child(
KeystrokeLabel::new(
cx.view_id(),
action,
keystroke_style.container,
keystroke_style.text.clone(),
)
.flex_float(),
)
.contained()
.with_style(label_style.container)
}
pub type CopilotCTAButton = Interactive<ContainedText>;
pub fn cta_button<Tag, L, V, F>(
label: L,
max_width: f32,
style: &CopilotCTAButton,
cx: &mut ViewContext<V>,
f: F,
) -> MouseEventHandler<V>
where
Tag: 'static,
L: Into<Cow<'static, str>>,
V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
MouseEventHandler::new::<Tag, _>(0, cx, |state, _| {
let style = style.style_for(state);
Label::new(label, style.text.to_owned())
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_max_width(max_width)
})
.on_click(MouseButton::Left, f)
.with_cursor_style(platform::CursorStyle::PointingHand)
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ModalStyle {
close_icon: Interactive<IconStyle>,
container: ContainerStyle,
titlebar: ContainerStyle,
title_text: Interactive<TextStyle>,
dimensions: Dimensions,
}
impl ModalStyle {
pub fn dimensions(&self) -> Vector2F {
self.dimensions.to_vec()
}
}
pub fn modal<Tag, V, I, D, F>(
title: I,
style: &ModalStyle,
cx: &mut ViewContext<V>,
build_modal: F,
) -> impl Element<V>
where
Tag: 'static,
I: Into<Cow<'static, str>>,
D: Element<V>,
V: 'static,
F: FnOnce(&mut gpui::ViewContext<V>) -> D,
{
const TITLEBAR_HEIGHT: f32 = 28.;
Flex::column()
.with_child(
Stack::new()
.with_child(Label::new(
title,
style
.title_text
.style_for(&mut MouseState::default())
.clone(),
))
.with_child(
// FIXME: Get a better tag type
MouseEventHandler::new::<Tag, _>(999999, cx, |state, _cx| {
let style = style.close_icon.style_for(state);
icon(style)
})
.on_click(platform::MouseButton::Left, move |_, _, cx| {
cx.remove_window();
})
.with_cursor_style(platform::CursorStyle::PointingHand)
.aligned()
.right(),
)
.contained()
.with_style(style.titlebar)
.constrained()
.with_height(TITLEBAR_HEIGHT),
)
.with_child(
build_modal(cx)
.contained()
.with_style(style.container)
.constrained()
.with_width(style.dimensions().x())
.with_height(style.dimensions().y() - TITLEBAR_HEIGHT),
)
.constrained()
.with_height(style.dimensions().y())
}