wip: design

This commit is contained in:
2025-01-15 09:11:21 +07:00
parent e8b34ae69e
commit ec24bba69c
35 changed files with 534 additions and 1566 deletions

View File

@@ -1,123 +0,0 @@
use crate::{theme::ActiveTheme as _, Sizable, Size};
use gpui::{
div, prelude::FluentBuilder as _, relative, Div, Hsla, InteractiveElement as _, IntoElement,
ParentElement, RenderOnce, Styled,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BadgeVariant {
#[default]
Primary,
Secondary,
Outline,
Destructive,
Custom {
color: Hsla,
foreground: Hsla,
border: Hsla,
},
}
impl BadgeVariant {
fn bg(&self, cx: &gpui::WindowContext) -> Hsla {
match self {
Self::Primary => cx.theme().colors.primary,
Self::Secondary => cx.theme().secondary,
Self::Outline => gpui::transparent_black(),
Self::Destructive => cx.theme().danger,
Self::Custom { color, .. } => *color,
}
}
fn border(&self, cx: &gpui::WindowContext) -> Hsla {
match self {
Self::Primary => cx.theme().colors.primary,
Self::Secondary => cx.theme().secondary,
Self::Outline => cx.theme().border,
Self::Destructive => cx.theme().danger,
Self::Custom { border, .. } => *border,
}
}
fn fg(&self, cx: &gpui::WindowContext) -> Hsla {
match self {
Self::Primary => cx.theme().primary_foreground,
Self::Secondary => cx.theme().secondary_foreground,
Self::Outline => cx.theme().foreground,
Self::Destructive => cx.theme().danger_foreground,
Self::Custom { foreground, .. } => *foreground,
}
}
}
/// Badge is a small status indicator for UI elements.
///
/// Only support: Medium, Small
#[derive(IntoElement)]
pub struct Badge {
base: Div,
veriant: BadgeVariant,
size: Size,
}
impl Badge {
fn new() -> Self {
Self {
base: div().flex().items_center().rounded_md().border_1(),
veriant: BadgeVariant::default(),
size: Size::Medium,
}
}
pub fn with_variant(mut self, variant: BadgeVariant) -> Self {
self.veriant = variant;
self
}
pub fn primary() -> Self {
Self::new().with_variant(BadgeVariant::Primary)
}
pub fn secondary() -> Self {
Self::new().with_variant(BadgeVariant::Secondary)
}
pub fn outline() -> Self {
Self::new().with_variant(BadgeVariant::Outline)
}
pub fn destructive() -> Self {
Self::new().with_variant(BadgeVariant::Destructive)
}
pub fn custom(color: Hsla, foreground: Hsla, border: Hsla) -> Self {
Self::new().with_variant(BadgeVariant::Custom {
color,
foreground,
border,
})
}
}
impl Sizable for Badge {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl ParentElement for Badge {
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.base.extend(elements);
}
}
impl RenderOnce for Badge {
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
self.base
.line_height(relative(1.3))
.map(|this| match self.size {
Size::XSmall | Size::Small => this.text_xs().px_1p5().py_0(),
_ => this.text_xs().px_2p5().py_0p5(),
})
.bg(self.veriant.bg(cx))
.text_color(self.veriant.fg(cx))
.border_color(self.veriant.border(cx))
.hover(|this| this.opacity(0.9))
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
indicator::Indicator,
theme::{scale::ColorScaleStep, ActiveTheme, Colorize as _},
theme::{scale::ColorScaleStep, ActiveTheme},
tooltip::Tooltip,
Disableable, Icon, Selectable, Sizable, Size, StyledExt,
};
@@ -42,16 +42,6 @@ pub trait ButtonVariants: Sized {
self.with_variant(ButtonVariant::Primary)
}
/// With the danger style for the Button.
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
/// With the outline style for the Button.
fn outline(self) -> Self {
self.with_variant(ButtonVariant::Outline)
}
/// With the ghost style for the Button.
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost)
@@ -116,12 +106,10 @@ impl ButtonCustomVariant {
}
}
/// The veriant of the Button.
/// The variant of the Button.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ButtonVariant {
Primary,
Danger,
Outline,
Ghost,
Link,
Text,
@@ -406,9 +394,7 @@ impl RenderOnce for Button {
.when(normal_style.underline, |this| this.text_decoration_1())
.hover(|this| {
let hover_style = style.hovered(cx);
this.bg(hover_style.bg)
.border_color(hover_style.border)
.text_color(cx.theme().danger)
this.bg(hover_style.bg).border_color(hover_style.border)
})
.active(|this| {
let active_style = style.active(cx);
@@ -494,7 +480,6 @@ impl ButtonVariant {
fn bg_color(&self, cx: &WindowContext) -> Hsla {
match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Danger => cx.theme().danger,
ButtonVariant::Custom(colors) => colors.color,
_ => cx.theme().transparent,
}
@@ -511,9 +496,7 @@ impl ButtonVariant {
fn border_color(&self, cx: &WindowContext) -> Hsla {
match self {
ButtonVariant::Primary => cx.theme().colors.primary,
ButtonVariant::Danger => cx.theme().danger,
ButtonVariant::Outline => cx.theme().border,
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Ghost | ButtonVariant::Link | ButtonVariant::Text => {
cx.theme().transparent
}
@@ -527,7 +510,7 @@ impl ButtonVariant {
fn shadow(&self, _: &WindowContext) -> bool {
match self {
ButtonVariant::Primary | ButtonVariant::Danger => true,
ButtonVariant::Primary => true,
ButtonVariant::Custom(c) => c.shadow,
_ => false,
}
@@ -552,8 +535,6 @@ impl ButtonVariant {
fn hovered(&self, cx: &WindowContext) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Outline => cx.theme().secondary_hover,
ButtonVariant::Danger => cx.theme().danger_hover,
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
@@ -561,7 +542,7 @@ impl ButtonVariant {
};
let border = self.border_color(cx);
let fg = match self {
ButtonVariant::Link => cx.theme().link_hover,
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN),
_ => self.text_color(cx),
};
let underline = self.underline(cx);
@@ -578,26 +559,20 @@ impl ButtonVariant {
fn active(&self, cx: &WindowContext) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().primary_active,
ButtonVariant::Outline => cx.theme().secondary_active,
ButtonVariant::Ghost => {
if cx.theme().appearance.is_dark() {
cx.theme().secondary.lighten(0.2).opacity(0.8)
} else {
cx.theme().secondary.darken(0.2).opacity(0.8)
}
}
ButtonVariant::Danger => cx.theme().danger_active,
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.active,
};
let border = self.border_color(cx);
let fg = match self {
ButtonVariant::Link => cx.theme().link_active,
ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE),
ButtonVariant::Text => cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
_ => self.text_color(cx),
};
let border = self.border_color(cx);
let underline = self.underline(cx);
let shadow = self.shadow(cx);
@@ -612,26 +587,20 @@ impl ButtonVariant {
fn selected(&self, cx: &WindowContext) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Primary => cx.theme().primary_active,
ButtonVariant::Outline => cx.theme().secondary_active,
ButtonVariant::Ghost => {
if cx.theme().appearance.is_dark() {
cx.theme().secondary.lighten(0.2).opacity(0.8)
} else {
cx.theme().secondary.darken(0.2).opacity(0.8)
}
}
ButtonVariant::Danger => cx.theme().danger_active,
ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Ghost => cx.theme().base.step(cx, ColorScaleStep::FOUR),
ButtonVariant::Link => cx.theme().transparent,
ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Custom(colors) => colors.active,
};
let border = self.border_color(cx);
let fg = match self {
ButtonVariant::Link => cx.theme().link_active,
ButtonVariant::Text => cx.theme().foreground.opacity(0.7),
ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::TEN),
ButtonVariant::Text => cx.theme().accent.step(cx, ColorScaleStep::TEN),
_ => self.text_color(cx),
};
let border = self.border_color(cx);
let underline = self.underline(cx);
let shadow = self.shadow(cx);
@@ -646,26 +615,14 @@ impl ButtonVariant {
fn disabled(&self, cx: &WindowContext) -> ButtonVariantStyle {
let bg = match self {
ButtonVariant::Link
| ButtonVariant::Ghost
| ButtonVariant::Outline
| ButtonVariant::Text => cx.theme().transparent,
ButtonVariant::Primary => cx.theme().colors.primary.opacity(0.15),
ButtonVariant::Danger => cx.theme().danger.opacity(0.15),
ButtonVariant::Custom(style) => style.color.opacity(0.15),
};
let fg = match self {
ButtonVariant::Link | ButtonVariant::Text | ButtonVariant::Ghost => {
cx.theme().link.grayscale()
ButtonVariant::Link | ButtonVariant::Ghost | ButtonVariant::Text => {
cx.theme().transparent
}
_ => cx.theme().secondary_foreground.opacity(0.5).grayscale(),
};
let border = match self {
ButtonVariant::Outline => cx.theme().border.opacity(0.5),
_ => bg,
_ => cx.theme().base.step(cx, ColorScaleStep::FOUR),
};
let fg = cx.theme().base.step(cx, ColorScaleStep::ELEVEN);
let border = bg;
let underline = self.underline(cx);
let shadow = false;

View File

@@ -1,4 +1,8 @@
use crate::{h_flex, theme::ActiveTheme, v_flex, Disableable, IconName, Selectable};
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Disableable, IconName, Selectable,
};
use gpui::{
div, prelude::FluentBuilder as _, relative, svg, ElementId, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _,
@@ -65,11 +69,14 @@ impl RenderOnce for Checkbox {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (color, icon_color) = if self.disabled {
(
cx.theme().colors.primary.opacity(0.5),
cx.theme().primary_foreground.opacity(0.5),
cx.theme().base.step(cx, ColorScaleStep::THREE),
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
} else {
(cx.theme().colors.primary, cx.theme().primary_foreground)
(
cx.theme().accent.step(cx, ColorScaleStep::NINE),
cx.theme().accent.step(cx, ColorScaleStep::ONE),
)
};
h_flex()
@@ -104,21 +111,22 @@ impl RenderOnce for Checkbox {
)
.map(|this| {
if let Some(label) = self.label {
this.text_color(cx.theme().foreground).child(
div()
.w_full()
.overflow_x_hidden()
.text_ellipsis()
.line_height(relative(1.))
.child(label),
)
this.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(
div()
.w_full()
.overflow_x_hidden()
.text_ellipsis()
.line_height(relative(1.))
.child(label),
)
} else {
this
}
})
.when(self.disabled, |this| {
this.cursor_not_allowed()
.text_color(cx.theme().muted_foreground)
.text_color(cx.theme().base.step(cx, ColorScaleStep::TEN))
})
.when_some(
self.on_click.filter(|_| !self.disabled),

View File

@@ -1,10 +1,9 @@
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce,
SharedString, Styled,
};
use crate::theme::ActiveTheme;
/// A divider that can be either vertical or horizontal.
#[derive(IntoElement)]
pub struct Divider {
@@ -52,8 +51,6 @@ impl Styled for Divider {
impl RenderOnce for Divider {
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
let theme = cx.theme();
self.base
.flex()
.flex_shrink_0()
@@ -66,7 +63,9 @@ impl RenderOnce for Divider {
Axis::Vertical => this.w(px(1.)).h_full(),
Axis::Horizontal => this.h(px(1.)).w_full(),
})
.bg(self.color.unwrap_or(cx.theme().border)),
.bg(self
.color
.unwrap_or(cx.theme().base.step(cx, ColorScaleStep::THREE))),
)
.when_some(self.label, |this, label| {
this.child(
@@ -76,7 +75,6 @@ impl RenderOnce for Divider {
.mx_auto()
.text_xs()
.bg(cx.theme().background)
.text_color(theme.muted_foreground)
.child(label),
)
})

View File

@@ -1,702 +0,0 @@
use gpui::*;
use std::{
cell::Cell,
fmt::{Debug, Formatter},
rc::Rc,
sync::Arc,
};
use super::{DockArea, Panel, PanelEvent, PanelInfo, PanelState, PanelView, TabPanel, TileMeta};
use crate::{
h_flex,
scroll::{Scrollbar, ScrollbarState},
theme::ActiveTheme,
v_flex, Icon, IconName,
};
const MINIMUM_SIZE: Size<Pixels> = size(px(100.), px(100.));
const DRAG_BAR_HEIGHT: Pixels = px(30.);
const HANDLE_SIZE: Pixels = px(20.0);
#[derive(Clone, Render)]
pub struct DragMoving(EntityId);
#[derive(Clone, Render)]
pub struct DragResizing(EntityId);
#[derive(Clone)]
struct ResizeDrag {
axis: ResizeAxis,
last_position: Point<Pixels>,
last_bounds: Bounds<Pixels>,
}
#[derive(Clone, PartialEq)]
enum ResizeAxis {
Horizontal,
Vertical,
Both,
}
/// TileItem is a moveable and resizable panel that can be added to a Tiles view.
#[derive(Clone)]
pub struct TileItem {
pub(crate) panel: Arc<dyn PanelView>,
bounds: Bounds<Pixels>,
z_index: usize,
}
impl Debug for TileItem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TileItem")
.field("bounds", &self.bounds)
.field("z_index", &self.z_index)
.finish()
}
}
impl TileItem {
pub fn new(panel: Arc<dyn PanelView>, bounds: Bounds<Pixels>) -> Self {
Self {
panel,
bounds,
z_index: 0,
}
}
pub fn z_index(mut self, z_index: usize) -> Self {
self.z_index = z_index;
self
}
}
/// Tiles is a canvas that can contain multiple panels, each of which can be dragged and resized.
pub struct Tiles {
focus_handle: FocusHandle,
pub(crate) panels: Vec<TileItem>,
dragging_index: Option<usize>,
dragging_initial_mouse: Point<Pixels>,
dragging_initial_bounds: Bounds<Pixels>,
resizing_index: Option<usize>,
resizing_drag_data: Option<ResizeDrag>,
bounds: Bounds<Pixels>,
scroll_state: Rc<Cell<ScrollbarState>>,
scroll_handle: ScrollHandle,
}
impl Panel for Tiles {
fn panel_id(&self) -> SharedString {
"Tiles".into()
}
fn title(&self, _cx: &WindowContext) -> AnyElement {
"Tiles".into_any_element()
}
fn dump(&self, cx: &AppContext) -> PanelState {
let panels = self
.panels
.iter()
.map(|item: &TileItem| item.panel.dump(cx))
.collect();
let metas = self
.panels
.iter()
.map(|item: &TileItem| TileMeta {
bounds: item.bounds,
z_index: item.z_index,
})
.collect();
let mut state = PanelState::new(self);
state.panel_name = self.panel_id().to_string();
state.children = panels;
state.info = PanelInfo::Tiles { metas };
state
}
}
impl Tiles {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
panels: vec![],
dragging_index: None,
dragging_initial_mouse: Point::default(),
dragging_initial_bounds: Bounds::default(),
resizing_index: None,
resizing_drag_data: None,
bounds: Bounds::default(),
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_handle: ScrollHandle::default(),
}
}
fn sorted_panels(&self) -> Vec<TileItem> {
let mut items: Vec<(usize, TileItem)> = self.panels.iter().cloned().enumerate().collect();
items.sort_by(|a, b| a.1.z_index.cmp(&b.1.z_index).then_with(|| a.0.cmp(&b.0)));
items.into_iter().map(|(_, item)| item).collect()
}
/// Return the index of the panel.
#[inline]
pub(crate) fn index_of(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
#[allow(clippy::op_ref)]
self.panels.iter().position(|p| &p.panel == &panel)
}
/// Remove panel from the children.
pub fn remove(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.index_of(panel.clone()) {
self.panels.remove(ix);
cx.emit(PanelEvent::LayoutChanged);
}
}
fn update_initial_position(&mut self, position: Point<Pixels>, cx: &mut ViewContext<'_, Self>) {
let Some((index, item)) = self.find_at_position(position) else {
return;
};
let inner_pos = position - self.bounds.origin;
let bounds = item.bounds;
self.dragging_index = Some(index);
self.dragging_initial_mouse = inner_pos;
self.dragging_initial_bounds = bounds;
cx.notify();
}
fn update_position(&mut self, pos: Point<Pixels>, cx: &mut ViewContext<'_, Self>) {
let Some(index) = self.dragging_index else {
return;
};
let Some(item) = self.panels.get_mut(index) else {
return;
};
let adjusted_position = pos - self.bounds.origin;
let delta = adjusted_position - self.dragging_initial_mouse;
let mut new_origin = self.dragging_initial_bounds.origin + delta;
new_origin.x = new_origin.x.max(px(0.0));
new_origin.y = new_origin.y.max(px(0.0));
item.bounds.origin = round_point_to_nearest_ten(new_origin);
cx.notify();
}
fn update_resizing_drag(&mut self, drag_data: ResizeDrag, cx: &mut ViewContext<'_, Self>) {
if let Some((index, _item)) = self.find_at_position(drag_data.last_position) {
self.resizing_index = Some(index);
self.resizing_drag_data = Some(drag_data);
cx.notify();
}
}
fn resize_width(&mut self, new_width: Pixels, cx: &mut ViewContext<'_, Self>) {
if let Some(index) = self.resizing_index {
if let Some(item) = self.panels.get_mut(index) {
item.bounds.size.width = round_to_nearest_ten(new_width);
cx.notify();
}
}
}
fn resize_height(&mut self, new_height: Pixels, cx: &mut ViewContext<'_, Self>) {
if let Some(index) = self.resizing_index {
if let Some(item) = self.panels.get_mut(index) {
item.bounds.size.height = round_to_nearest_ten(new_height);
cx.notify();
}
}
}
pub fn add_item(
&mut self,
item: TileItem,
dock_area: &WeakView<DockArea>,
cx: &mut ViewContext<Self>,
) {
self.panels.push(item.clone());
cx.window_context().defer({
let panel = item.panel.clone();
let dock_area = dock_area.clone();
move |cx| {
// Subscribe to the panel's layout change event.
_ = dock_area.update(cx, |this, cx| {
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
this.subscribe_panel(&tab_panel, cx);
}
});
}
});
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
/// Find the panel at a given position, considering z-index
fn find_at_position(&self, position: Point<Pixels>) -> Option<(usize, &TileItem)> {
let inner_pos = position - self.bounds.origin;
let mut panels_with_indices: Vec<(usize, &TileItem)> =
self.panels.iter().enumerate().collect();
panels_with_indices
.sort_by(|a, b| b.1.z_index.cmp(&a.1.z_index).then_with(|| b.0.cmp(&a.0)));
for (index, item) in panels_with_indices {
let extended_bounds = Bounds::new(
item.bounds.origin,
item.bounds.size + size(HANDLE_SIZE, HANDLE_SIZE) / 2.0,
);
if extended_bounds.contains(&inner_pos) {
return Some((index, item));
}
}
None
}
#[inline]
fn reset_current_index(&mut self) {
self.dragging_index = None;
self.resizing_index = None;
}
/// Bring the panel of target_index to front, returns (old_index, new_index) if successful
fn bring_to_front(&mut self, target_index: Option<usize>) -> Option<(usize, usize)> {
if let Some(old_index) = target_index {
if old_index < self.panels.len() {
let item = self.panels.remove(old_index);
self.panels.push(item);
let new_index = self.panels.len() - 1;
self.reset_current_index();
return Some((old_index, new_index));
}
}
None
}
/// Produce a vector of AnyElement representing the three possible resize handles
fn render_resize_handles(
&mut self,
cx: &mut ViewContext<Self>,
entity_id: EntityId,
item: &TileItem,
is_occluded: impl Fn(&Bounds<Pixels>) -> bool,
) -> Vec<AnyElement> {
let panel_bounds = item.bounds;
let right_handle_bounds = Bounds::new(
panel_bounds.origin + point(panel_bounds.size.width - HANDLE_SIZE.half(), px(0.0)),
size(HANDLE_SIZE.half(), panel_bounds.size.height),
);
let bottom_handle_bounds = Bounds::new(
panel_bounds.origin + point(px(0.0), panel_bounds.size.height - HANDLE_SIZE.half()),
size(panel_bounds.size.width, HANDLE_SIZE.half()),
);
let corner_handle_bounds = Bounds::new(
panel_bounds.origin
+ point(
panel_bounds.size.width - HANDLE_SIZE.half(),
panel_bounds.size.height - HANDLE_SIZE.half(),
),
size(HANDLE_SIZE.half(), HANDLE_SIZE.half()),
);
let mut elements = Vec::new();
// Right resize handle
elements.push(if !is_occluded(&right_handle_bounds) {
div()
.id("right-resize-handle")
.cursor_col_resize()
.absolute()
.top(px(0.0))
.right(-HANDLE_SIZE.half())
.w(HANDLE_SIZE)
.h(panel_bounds.size.height)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Horizontal,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
if drag_data.axis != ResizeAxis::Horizontal {
return;
}
let pos = e.event.position;
let delta = pos.x - drag_data.last_position.x;
let new_width = (drag_data.last_bounds.size.width + delta)
.max(MINIMUM_SIZE.width);
this.resize_width(new_width, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
// Bottom resize handle
elements.push(if !is_occluded(&bottom_handle_bounds) {
div()
.id("bottom-resize-handle")
.cursor_row_resize()
.absolute()
.left(px(0.0))
.bottom(-HANDLE_SIZE.half())
.w(panel_bounds.size.width)
.h(HANDLE_SIZE)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Vertical,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
let pos = e.event.position;
let delta = pos.y - drag_data.last_position.y;
let new_height = (drag_data.last_bounds.size.height + delta)
.max(MINIMUM_SIZE.width);
this.resize_height(new_height, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
// Corner resize handle
elements.push(if !is_occluded(&corner_handle_bounds) {
div()
.id("corner-resize-handle")
.cursor_nwse_resize()
.absolute()
.right(-HANDLE_SIZE.half())
.bottom(-HANDLE_SIZE.half())
.w(HANDLE_SIZE)
.h(HANDLE_SIZE)
.child(
Icon::new(IconName::ResizeCorner)
.size(HANDLE_SIZE.half())
.text_color(cx.theme().foreground.opacity(0.3)),
)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Both,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
if drag_data.axis != ResizeAxis::Both {
return;
}
let pos = e.event.position;
let delta_x = pos.x - drag_data.last_position.x;
let delta_y = pos.y - drag_data.last_position.y;
let new_width = (drag_data.last_bounds.size.width + delta_x)
.max(MINIMUM_SIZE.width);
let new_height = (drag_data.last_bounds.size.height + delta_y)
.max(MINIMUM_SIZE.height);
this.resize_height(new_height, cx);
this.resize_width(new_width, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
elements
}
/// Produce the drag-bar element for the given panel item
fn render_drag_bar(
&mut self,
cx: &mut ViewContext<Self>,
entity_id: EntityId,
item: &TileItem,
is_occluded: &impl Fn(&Bounds<Pixels>) -> bool,
) -> AnyElement {
let drag_bar_bounds = Bounds::new(
item.bounds.origin,
Size {
width: item.bounds.size.width,
height: DRAG_BAR_HEIGHT,
},
);
if !is_occluded(&drag_bar_bounds) {
h_flex()
.id("drag-bar")
.cursor_grab()
.absolute()
.w_full()
.h(DRAG_BAR_HEIGHT)
.bg(cx.theme().transparent)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
this.update_initial_position(last_position, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.dragging_index) {
this.dragging_index = Some(new_ix);
}
}),
)
.on_drag(DragMoving(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(cx.listener(move |this, e: &DragMoveEvent<DragMoving>, cx| {
match e.drag(cx) {
DragMoving(id) => {
if *id != entity_id {
return;
}
this.update_position(e.event.position, cx);
}
}
}))
.into_any_element()
} else {
div().into_any_element()
}
}
fn render_panel(
&mut self,
item: &TileItem,
ix: usize,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let entity_id = cx.entity_id();
let panel_view = item.panel.view();
let is_occluded = {
let panels = self.panels.clone();
move |bounds: &Bounds<Pixels>| {
let this_z = panels[ix].z_index;
let this_ix = ix;
panels.iter().enumerate().any(|(sub_ix, other_item)| {
if sub_ix == this_ix {
return false;
}
let other_is_above = (other_item.z_index > this_z)
|| (other_item.z_index == this_z && sub_ix > this_ix);
other_is_above && other_item.bounds.intersects(bounds)
})
}
};
v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.absolute()
.left(item.bounds.origin.x - px(1.))
.top(item.bounds.origin.y - px(1.))
.w(item.bounds.size.width + px(1.))
.h(item.bounds.size.height + px(1.))
.child(
h_flex()
.w_full()
.h_full()
.overflow_hidden()
.child(panel_view),
)
.children(self.render_resize_handles(cx, entity_id, item, &is_occluded))
.child(self.render_drag_bar(cx, entity_id, item, &is_occluded))
}
}
#[inline]
fn round_to_nearest_ten(value: Pixels) -> Pixels {
px((value.0 / 10.0).round() * 10.0)
}
#[inline]
fn round_point_to_nearest_ten(point: Point<Pixels>) -> Point<Pixels> {
Point::new(round_to_nearest_ten(point.x), round_to_nearest_ten(point.y))
}
impl FocusableView for Tiles {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<PanelEvent> for Tiles {}
impl EventEmitter<DismissEvent> for Tiles {}
impl Render for Tiles {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone();
let view_id = view.entity_id();
let panels = self.sorted_panels();
let scroll_bounds =
self.panels
.iter()
.fold(Bounds::default(), |acc: Bounds<Pixels>, item| Bounds {
origin: Point {
x: acc.origin.x.min(item.bounds.origin.x),
y: acc.origin.y.min(item.bounds.origin.y),
},
size: Size {
width: acc.size.width.max(item.bounds.right()),
height: acc.size.height.max(item.bounds.bottom()),
},
});
let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y);
div()
.relative()
.bg(cx.theme().background)
.child(
div()
.id("tiles")
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_scroll()
.children(
panels
.into_iter()
.enumerate()
.map(|(ix, item)| self.render_panel(&item, ix, cx)),
)
.child({
canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
}),
)
.on_mouse_up(
MouseButton::Left,
cx.listener(move |this, _event: &MouseUpEvent, cx| {
if this.dragging_index.is_some()
|| this.resizing_index.is_some()
|| this.resizing_drag_data.is_some()
{
this.reset_current_index();
this.resizing_drag_data = None;
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
}),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, event: &MouseDownEvent, cx| {
if this.resizing_index.is_none() && this.dragging_index.is_none() {
let position = event.position;
if let Some((index, _)) = this.find_at_position(position) {
this.bring_to_front(Some(index));
cx.notify();
}
}
}),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::both(
view_id,
self.scroll_state.clone(),
self.scroll_handle.clone(),
scroll_size,
)),
)
.size_full()
}
}

View File

@@ -1,3 +1,10 @@
use super::{DockArea, DockItem};
use crate::{
dock_area::{panel::PanelView, tab_panel::TabPanel},
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
theme::{scale::ColorScaleStep, ActiveTheme as _},
AxisExt as _, StyledExt,
};
use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Element, Entity, InteractiveElement as _,
IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
@@ -7,14 +14,6 @@ use gpui::{
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::{
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
theme::ActiveTheme as _,
AxisExt as _, StyledExt,
};
use super::{DockArea, DockItem, PanelView, TabPanel};
#[derive(Clone, Render)]
struct ResizePanel;
@@ -190,16 +189,6 @@ impl Dock {
}
});
}
DockItem::Tiles { view, .. } => {
cx.defer({
let view = view.clone();
move |cx| {
_ = dock_area.update(cx, |this, cx| {
this.subscribe_panel(&view, cx);
});
}
});
}
DockItem::Panel { .. } => {
// Not supported
}
@@ -285,7 +274,7 @@ impl Dock {
})
.child(
div()
.bg(cx.theme().border)
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
)
@@ -383,8 +372,6 @@ impl Render for Dock {
DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => this.child(view.clone()),
DockItem::Panel { view, .. } => this.child(view.clone().view()),
// Not support to render Tiles and Tile into Dock
DockItem::Tiles { .. } => this,
})
.child(self.render_resize_handle(cx))
.child(DockElement {
@@ -416,7 +403,7 @@ impl Element for DockElement {
fn request_layout(
&mut self,
_: Option<&gpui::GlobalElementId>,
cx: &mut gpui::WindowContext,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(cx.request_layout(Style::default(), None), ())
}
@@ -426,7 +413,7 @@ impl Element for DockElement {
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut gpui::WindowContext,
_: &mut WindowContext,
) -> Self::PrepaintState {
}
@@ -436,7 +423,7 @@ impl Element for DockElement {
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut gpui::WindowContext,
cx: &mut WindowContext,
) {
cx.on_mouse_event({
let view = self.view.clone();

View File

@@ -1,11 +1,14 @@
use super::PanelEvent;
use crate::{
dock_area::panel::Panel,
dock_area::state::PanelState,
theme::{scale::ColorScaleStep, ActiveTheme},
};
use gpui::{
AppContext, EventEmitter, FocusHandle, FocusableView, ParentElement as _, Render, SharedString,
Styled as _, WindowContext,
};
use super::{Panel, PanelEvent, PanelState};
use crate::theme::ActiveTheme;
pub(crate) struct InvalidPanel {
name: SharedString,
focus_handle: FocusHandle,
@@ -27,7 +30,7 @@ impl Panel for InvalidPanel {
"InvalidPanel".into()
}
fn dump(&self, _cx: &AppContext) -> super::PanelState {
fn dump(&self, _cx: &AppContext) -> PanelState {
self.old_state.clone()
}
}
@@ -49,7 +52,7 @@ impl Render for InvalidPanel {
.flex_col()
.items_center()
.justify_center()
.text_color(cx.theme().muted_foreground)
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(format!(
"The `{}` panel type is not registered in PanelRegistry.",
self.name.clone()

View File

@@ -1,12 +1,13 @@
#[allow(clippy::module_inception)]
mod dock;
mod invalid_panel;
mod panel;
mod stack_panel;
mod state;
mod tab_panel;
mod tiles;
use crate::{
dock_area::{
dock::{Dock, DockPlacement},
panel::{Panel, PanelEvent, PanelStyle, PanelView},
stack_panel::StackPanel,
state::{DockAreaState, DockState},
tab_panel::TabPanel,
},
theme::{scale::ColorScaleStep, ActiveTheme},
};
use anyhow::Result;
use gpui::{
actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
@@ -14,16 +15,15 @@ use gpui::{
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, View, ViewContext,
VisualContext, WeakView, WindowContext,
};
use panel::PanelRegistry;
use std::sync::Arc;
pub use dock::*;
pub use panel::*;
pub use stack_panel::*;
pub use state::*;
pub use tab_panel::*;
pub use tiles::*;
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
pub mod dock;
pub mod invalid_panel;
pub mod panel;
pub mod stack_panel;
pub mod state;
pub mod tab_panel;
pub fn init(cx: &mut AppContext) {
cx.set_global(PanelRegistry::new());
@@ -88,11 +88,6 @@ pub enum DockItem {
},
/// Panel layout
Panel { view: Arc<dyn PanelView> },
/// Tiles layout
Tiles {
items: Vec<TileItem>,
view: View<Tiles>,
},
}
impl DockItem {
@@ -159,51 +154,6 @@ impl DockItem {
Self::Panel { view: panel }
}
/// Create DockItem with tiles layout
///
/// This items and metas should have the same length.
pub fn tiles(
items: Vec<DockItem>,
metas: Vec<impl Into<TileMeta> + Copy>,
dock_area: &WeakView<DockArea>,
cx: &mut WindowContext,
) -> Self {
assert!(items.len() == metas.len());
let tile_panel = cx.new_view(|cx| {
let mut tiles = Tiles::new(cx);
for (ix, item) in items.clone().into_iter().enumerate() {
match item {
DockItem::Tabs { view, .. } => {
let meta: TileMeta = metas[ix].into();
let tile_item =
TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index);
tiles.add_item(tile_item, dock_area, cx);
}
_ => {
// Ignore non-tabs items
}
}
}
tiles
});
cx.defer({
let tile_panel = tile_panel.clone();
let dock_area = dock_area.clone();
move |cx| {
_ = dock_area.update(cx, |this, cx| {
this.subscribe_panel(&tile_panel, cx);
});
}
});
Self::Tiles {
items: tile_panel.read(cx).panels.clone(),
view: tile_panel,
}
}
/// Create DockItem with tabs layout, items are displayed as tabs.
///
/// The `active_ix` is the index of the active tab, if `None` the first tab is active.
@@ -256,7 +206,6 @@ impl DockItem {
match self {
Self::Split { view, .. } => Arc::new(view.clone()),
Self::Tabs { view, .. } => Arc::new(view.clone()),
Self::Tiles { view, .. } => Arc::new(view.clone()),
Self::Panel { view, .. } => view.clone(),
}
}
@@ -269,14 +218,6 @@ impl DockItem {
}
Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(),
Self::Panel { view } => Some(view.clone()),
Self::Tiles { items, .. } => items.iter().find_map(|item| {
#[allow(clippy::op_ref)]
if &item.panel == &panel {
Some(item.panel.clone())
} else {
None
}
}),
}
}
@@ -312,7 +253,6 @@ impl DockItem {
stack_panel.add_panel(new_item.view(), None, dock_area.clone(), cx);
});
}
Self::Tiles { .. } => {}
Self::Panel { .. } => {}
}
}
@@ -330,7 +270,6 @@ impl DockItem {
item.set_collapsed(collapsed, cx);
}
}
DockItem::Tiles { .. } => {}
DockItem::Panel { .. } => {}
}
}
@@ -340,7 +279,6 @@ impl DockItem {
match self {
DockItem::Tabs { view, .. } => Some(view.clone()),
DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx),
DockItem::Tiles { .. } => None,
DockItem::Panel { .. } => None,
}
}
@@ -350,7 +288,6 @@ impl DockItem {
match self {
DockItem::Tabs { view, .. } => Some(view.clone()),
DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx),
DockItem::Tiles { .. } => None,
DockItem::Panel { .. } => None,
}
}
@@ -403,13 +340,7 @@ impl DockArea {
cx.notify();
}
// FIXME: Remove this method after 2025-01-01
#[deprecated(note = "Use `set_center` instead")]
pub fn set_root(&mut self, item: DockItem, cx: &mut ViewContext<Self>) {
self.set_center(item, cx);
}
/// The the DockItem as the center of the dock area.
/// The DockItem as the center of the dock area.
///
/// This is used to render at the Center of the DockArea.
pub fn set_center(&mut self, item: DockItem, cx: &mut ViewContext<Self>) {
@@ -722,9 +653,6 @@ impl DockArea {
DockItem::Tabs { .. } => {
// We subscribe to the tab panel event in StackPanel's insert_panel
}
DockItem::Tiles { .. } => {
// We subscribe to the tab panel event in Tiles's [`add_item`](Tiles::add_item)
}
DockItem::Panel { .. } => {
// Not supported
}
@@ -794,7 +722,6 @@ impl DockArea {
match &self.items {
DockItem::Split { view, .. } => view.clone().into_any_element(),
DockItem::Tabs { view, .. } => view.clone().into_any_element(),
DockItem::Tiles { view, .. } => view.clone().into_any_element(),
DockItem::Panel { view, .. } => view.clone().view().into_any_element(),
}
}
@@ -842,49 +769,41 @@ impl Render for DockArea {
if let Some(zoom_view) = self.zoom_view.clone() {
this.child(zoom_view)
} else {
match &self.items {
DockItem::Tiles { view, .. } => {
// render tiles
this.child(view.clone())
}
_ => {
// render dock
this.child(
// render dock
this.child(
div()
.flex()
.flex_row()
.h_full()
// Left dock
.when_some(self.left_dock.clone(), |this, dock| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::ONE))
.child(div().flex().flex_none().child(dock))
})
// Center
.child(
div()
.flex()
.flex_row()
.h_full()
// Left dock
.when_some(self.left_dock.clone(), |this, dock| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::ONE))
.child(div().flex().flex_none().child(dock))
})
// Center
.flex_1()
.flex_col()
.overflow_hidden()
// Top center
.child(
div()
.flex()
.flex_1()
.flex_col()
.overflow_hidden()
// Top center
.child(
div()
.flex_1()
.overflow_hidden()
.child(self.render_items(cx)),
)
// Bottom Dock
.when_some(self.bottom_dock.clone(), |this, dock| {
this.child(dock)
}),
.child(self.render_items(cx)),
)
// Right Dock
.when_some(self.right_dock.clone(), |this, dock| {
this.child(div().flex().flex_none().child(dock))
// Bottom Dock
.when_some(self.bottom_dock.clone(), |this, dock| {
this.child(dock)
}),
)
}
}
// Right Dock
.when_some(self.right_dock.clone(), |this, dock| {
this.child(div().flex().flex_none().child(dock))
}),
)
}
})
}

View File

@@ -1,3 +1,9 @@
use super::DockArea;
use crate::{
button::Button,
dock_area::state::{PanelInfo, PanelState},
popup_menu::PopupMenu,
};
use gpui::{
AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Global, Hsla,
IntoElement, SharedString, View, WeakView, WindowContext,
@@ -5,9 +11,6 @@ use gpui::{
use nostr_sdk::prelude::Metadata;
use std::{collections::HashMap, sync::Arc};
use super::{DockArea, PanelInfo, PanelState};
use crate::{button::Button, popup_menu::PopupMenu};
pub enum PanelEvent {
ZoomIn,
ZoomOut,

View File

@@ -1,19 +1,25 @@
use gpui::*;
use prelude::FluentBuilder;
use smallvec::SmallVec;
use std::sync::Arc;
use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel};
use super::{DockArea, PanelEvent};
use crate::{
dock::PanelInfo,
dock_area::{
panel::{Panel, PanelView},
state::{PanelInfo, PanelState},
tab_panel::TabPanel,
},
h_flex,
resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup,
},
theme::ActiveTheme,
theme::{scale::ColorScaleStep, ActiveTheme},
AxisExt as _, Placement,
};
use gpui::{
prelude::FluentBuilder, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle,
FocusableView, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription,
View, ViewContext, VisualContext as _, WeakView,
};
use smallvec::SmallVec;
use std::sync::Arc;
pub struct StackPanel {
pub(super) parent: Option<WeakView<StackPanel>>,
@@ -362,14 +368,17 @@ impl FocusableView for StackPanel {
self.focus_handle.clone()
}
}
impl EventEmitter<PanelEvent> for StackPanel {}
impl EventEmitter<DismissEvent> for StackPanel {}
impl Render for StackPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.size_full()
.overflow_hidden()
.bg(cx.theme().tab_bar)
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.child(self.panel_group.clone())
}
}

View File

@@ -1,3 +1,5 @@
use super::{invalid_panel::InvalidPanel, Dock, DockArea, DockItem, PanelRegistry};
use crate::dock_area::{dock::DockPlacement, panel::Panel};
use gpui::{
point, px, size, AppContext, Axis, Bounds, Pixels, View, VisualContext as _, WeakView,
WindowContext,
@@ -5,15 +7,11 @@ use gpui::{
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use super::{
invalid_panel::InvalidPanel, Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry,
};
/// Used to serialize and deserialize the DockArea
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct DockAreaState {
/// The version is used to mark this persisted state is compatible with the current version
/// For example, some times we many totally changed the structure of the Panel,
/// For example, sometimes we many totally changed the structure of the Panel,
/// then we can compare the version to decide whether we can use the state or ignore.
#[serde(default)]
pub version: Option<usize>,
@@ -106,8 +104,6 @@ pub enum PanelInfo {
Tabs { active_index: usize },
#[serde(rename = "panel")]
Panel(serde_json::Value),
#[serde(rename = "tiles")]
Tiles { metas: Vec<TileMeta> },
}
impl PanelInfo {
@@ -126,10 +122,6 @@ impl PanelInfo {
Self::Panel(info)
}
pub fn tiles(metas: Vec<TileMeta>) -> Self {
Self::Tiles { metas }
}
pub fn axis(&self) -> Option<Axis> {
match self {
Self::Stack { axis, .. } => Some(if *axis == 0 {
@@ -232,7 +224,6 @@ impl PanelState {
DockItem::tabs(vec![view.into()], None, &dock_area, cx)
}
PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &dock_area, cx),
}
}
}

View File

@@ -1,10 +1,14 @@
use super::{
ClosePanel, DockArea, DockPlacement, Panel, PanelEvent, PanelState, PanelStyle, PanelView,
StackPanel, ToggleZoom,
panel::PanelView, stack_panel::StackPanel, ClosePanel, DockArea, PanelEvent, PanelStyle,
ToggleZoom,
};
use crate::{
button::{Button, ButtonVariants as _},
dock::PanelInfo,
dock_area::{
dock::DockPlacement,
panel::Panel,
state::{PanelInfo, PanelState},
},
h_flex,
popup_menu::{PopupMenu, PopupMenuExt},
tab::{tab_bar::TabBar, Tab},
@@ -613,7 +617,7 @@ impl TabPanel {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().base.step(cx, ColorScaleStep::TWO))
.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE))
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, cx| {
@@ -688,7 +692,7 @@ impl TabPanel {
div()
.invisible()
.absolute()
.bg(cx.theme().drop_target)
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.map(|this| match self.will_split_placement {
Some(placement) => {
let size = DefiniteLength::Fraction(0.35);

View File

@@ -2,7 +2,7 @@ use crate::{
h_flex,
input::ClearButton,
list::{self, List, ListDelegate, ListItem},
theme::ActiveTheme,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Disableable, Icon, IconName, Sizable, Size, StyleSized, StyledExt,
};
use gpui::{
@@ -197,7 +197,7 @@ where
h_flex()
.justify_center()
.py_6()
.text_color(cx.theme().muted_foreground.opacity(0.6))
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(Icon::new(IconName::Inbox).size(px(28.)))
.into_any_element()
}
@@ -525,16 +525,18 @@ where
.when_some(self.title_prefix.clone(), |this, prefix| this.child(prefix))
.child(title.clone())
} else {
div().text_color(cx.theme().accent_foreground).child(
self.placeholder
.clone()
.unwrap_or_else(|| "Please select".into()),
)
div()
.text_color(cx.theme().accent.step(cx, ColorScaleStep::ELEVEN))
.child(
self.placeholder
.clone()
.unwrap_or_else(|| "Please select".into()),
)
};
title.when(self.disabled, |this| {
this.cursor_not_allowed()
.text_color(cx.theme().muted_foreground)
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
})
}
}
@@ -602,7 +604,7 @@ where
.justify_between()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().input)
.border_color(cx.theme().base.step(cx, ColorScaleStep::FOUR))
.rounded(px(cx.theme().radius))
.when(cx.theme().shadow, |this| this.shadow_sm())
.map(|this| {
@@ -660,8 +662,10 @@ where
Icon::new(icon)
.xsmall()
.text_color(match self.disabled {
true => cx.theme().muted_foreground.opacity(0.5),
false => cx.theme().muted_foreground,
true => cx.theme().base.step(cx, ColorScaleStep::TEN),
false => {
cx.theme().base.step(cx, ColorScaleStep::ELEVEN)
}
})
.when(self.disabled, |this| this.cursor_not_allowed()),
)
@@ -692,7 +696,9 @@ where
.mt_1p5()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.border_color(
cx.theme().base.step(cx, ColorScaleStep::FOUR),
)
.rounded(px(cx.theme().radius))
.shadow_md()
.on_mouse_down_out(|_, cx| {

View File

@@ -1,4 +1,7 @@
use crate::{theme::ActiveTheme, Sizable, Size};
use crate::{
theme::{scale::ColorScaleStep, ActiveTheme},
Sizable, Size,
};
use gpui::{
prelude::FluentBuilder as _, svg, AnyElement, Hsla, IntoElement, Radians, Render, RenderOnce,
SharedString, StyleRefinement, Styled, Svg, Transformation, View, VisualContext, WindowContext,
@@ -313,7 +316,9 @@ impl From<Icon> for AnyElement {
impl Render for Icon {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let text_color = self.text_color.unwrap_or_else(|| cx.theme().foreground);
let text_color = self
.text_color
.unwrap_or_else(|| cx.theme().base.step(cx, ColorScaleStep::ELEVEN));
svg()
.flex_none()

View File

@@ -1,10 +1,9 @@
use gpui::{Styled, WindowContext};
use crate::{
button::{Button, ButtonVariants as _},
theme::ActiveTheme as _,
theme::{scale::ColorScaleStep, ActiveTheme as _},
Icon, IconName, Sizable as _,
};
use gpui::{Styled, WindowContext};
pub(crate) struct ClearButton {}
@@ -12,7 +11,10 @@ impl ClearButton {
#[allow(clippy::new_ret_no_self)]
pub fn new(cx: &mut WindowContext) -> Button {
Button::new("clean")
.icon(Icon::new(IconName::CircleX).text_color(cx.theme().muted_foreground))
.icon(
Icon::new(IconName::CircleX)
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)),
)
.ghost()
.xsmall()
}

View File

@@ -1,5 +1,5 @@
use super::TextInput;
use crate::theme::ActiveTheme as _;
use crate::theme::{scale::ColorScaleStep, ActiveTheme as _};
use gpui::{
fill, point, px, relative, size, Bounds, Corners, Element, ElementId, ElementInputHandler,
GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, Pixels,
@@ -144,7 +144,7 @@ impl TextElement {
),
size(px(1.5), line_height),
),
cx.theme().colors.primary,
cx.theme().accent.step(cx, ColorScaleStep::NINE),
))
};
}
@@ -357,14 +357,17 @@ impl Element for TextElement {
let mut bounds = bounds;
let (display_text, text_color) = if text.is_empty() {
(placeholder, cx.theme().muted_foreground)
(
placeholder,
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
} else if input.masked {
(
"*".repeat(text.chars().count()).into(),
cx.theme().foreground,
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
)
} else {
(text, cx.theme().foreground)
(text, cx.theme().base.step(cx, ColorScaleStep::TWELVE))
};
let run = TextRun {
@@ -480,7 +483,7 @@ impl Element for TextElement {
// Paint selections
if let Some(path) = prepaint.selection_path.take() {
cx.paint_path(path, cx.theme().selection);
cx.paint_path(path, cx.theme().accent.step(cx, ColorScaleStep::FIVE));
}
// Paint multi line text

View File

@@ -23,18 +23,15 @@ use gpui::{
// - Press Up,Down to move cursor up, down line if multi-line
// - Move cursor to skip line eof empty chars.
use super::blink_cursor::BlinkCursor;
use super::change::Change;
use super::element::TextElement;
use super::ClearButton;
use super::{blink_cursor::BlinkCursor, change::Change, element::TextElement, ClearButton};
use crate::history::History;
use crate::indicator::Indicator;
use crate::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState};
use crate::theme::ActiveTheme;
use crate::Size;
use crate::StyledExt;
use crate::{Sizable, StyleSized};
use crate::{
history::History,
indicator::Indicator,
scroll::{Scrollbar, ScrollbarAxis, ScrollbarState},
theme::{scale::ColorScaleStep, ActiveTheme},
Sizable, Size, StyleSized, StyledExt,
};
actions!(
input,
@@ -55,7 +52,9 @@ actions!(
SelectAll,
Home,
End,
SelectToHome,
SelectToStartOfLine,
SelectToEndOfLine,
SelectToStart,
SelectToEnd,
ShowCharacterPalette,
Copy,
@@ -65,6 +64,8 @@ actions!(
Redo,
MoveToStartOfLine,
MoveToEndOfLine,
MoveToStart,
MoveToEnd,
TextChanged,
]
);
@@ -98,16 +99,16 @@ pub fn init(cx: &mut AppContext) {
KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
KeyBinding::new("home", Home, Some(CONTEXT)),
KeyBinding::new("end", End, Some(CONTEXT)),
KeyBinding::new("shift-home", SelectToHome, Some(CONTEXT)),
KeyBinding::new("shift-end", SelectToEnd, Some(CONTEXT)),
KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-a", SelectToHome, Some(CONTEXT)),
KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-e", SelectToEnd, Some(CONTEXT)),
KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("shift-cmd-left", SelectToHome, Some(CONTEXT)),
KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("shift-cmd-right", SelectToEnd, Some(CONTEXT)),
KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
#[cfg(target_os = "macos")]
@@ -138,6 +139,14 @@ pub fn init(cx: &mut AppContext) {
KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
@@ -145,7 +154,8 @@ pub fn init(cx: &mut AppContext) {
]);
}
type Affixes<T> = Option<Box<dyn Fn(&mut ViewContext<T>) -> AnyElement + 'static>>;
type TextInputPrefix<T> = Option<Box<dyn Fn(&mut ViewContext<T>) -> AnyElement + 'static>>;
type TextInputSuffix<T> = Option<Box<dyn Fn(&mut ViewContext<T>) -> AnyElement + 'static>>;
type Validate = Option<Box<dyn Fn(&str) -> bool + 'static>>;
pub struct TextInput {
@@ -154,8 +164,8 @@ pub struct TextInput {
multi_line: bool,
pub(super) history: History<Change>,
pub(super) blink_cursor: Model<BlinkCursor>,
pub(super) prefix: Affixes<Self>,
pub(super) suffix: Affixes<Self>,
pub(super) prefix: TextInputPrefix<Self>,
pub(super) suffix: TextInputSuffix<Self>,
pub(super) loading: bool,
pub(super) placeholder: SharedString,
pub(super) selected_range: Range<usize>,
@@ -186,6 +196,8 @@ pub struct TextInput {
scrollbar_state: Rc<Cell<ScrollbarState>>,
/// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>,
/// To remember the horizontal column (x-coordinate) of the cursor position.
preferred_x_offset: Option<Pixels>,
}
impl EventEmitter<InputEvent> for TextInput {}
@@ -228,6 +240,7 @@ impl TextInput {
scroll_handle: ScrollHandle::new(),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::size(px(0.), px(0.)),
preferred_x_offset: None,
};
// Observe the blink cursor to repaint the view when it changes.
@@ -258,6 +271,133 @@ impl TextInput {
self
}
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
fn update_preferred_x_offset(&mut self, _cx: &mut ViewContext<Self>) {
if let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) {
let offset = self.cursor_offset();
let line_height = self.last_line_height;
// Find which line and sub-line the cursor is on and its position
let (_line_index, _sub_line_index, cursor_pos) =
self.line_and_position_for_offset(offset, lines, line_height);
if let Some(pos) = cursor_pos {
// Adjust by scroll offset
let scroll_offset = bounds.origin;
self.preferred_x_offset = Some(pos.x + scroll_offset.x);
}
}
}
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
fn line_and_position_for_offset(
&self,
offset: usize,
lines: &[WrappedLine],
line_height: Pixels,
) -> (usize, usize, Option<Point<Pixels>>) {
let mut prev_lines_offset = 0;
let mut y_offset = px(0.);
for (line_index, line) in lines.iter().enumerate() {
let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, line_height) {
let sub_line_index = (pos.y.0 / line_height.0) as usize;
let adjusted_pos = point(pos.x, pos.y + y_offset);
return (line_index, sub_line_index, Some(adjusted_pos));
}
y_offset += line.size(line_height).height;
prev_lines_offset += line.len() + 1;
}
(0, 0, None)
}
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
/// direction: -1 for up, +1 for down
fn move_vertical(&mut self, direction: i32, cx: &mut ViewContext<Self>) {
if self.is_single_line() {
return;
}
let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
return;
};
let offset = self.cursor_offset();
let line_height = self.last_line_height;
let (current_line_index, current_sub_line, current_pos) =
self.line_and_position_for_offset(offset, lines, line_height);
let Some(current_pos) = current_pos else {
return;
};
let current_x = self
.preferred_x_offset
.unwrap_or_else(|| current_pos.x + bounds.origin.x);
let mut new_line_index = current_line_index;
let mut new_sub_line = current_sub_line as i32;
new_sub_line += direction;
// Handle moving above the first line
if direction == -1 && new_line_index == 0 && new_sub_line < 0 {
// Move cursor to the beginning of the text
self.move_to(0, cx);
return;
}
if new_sub_line < 0 {
if new_line_index > 0 {
new_line_index -= 1;
new_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
} else {
new_sub_line = 0;
}
} else {
let max_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
if new_sub_line > max_sub_line {
if new_line_index < lines.len() - 1 {
new_line_index += 1;
new_sub_line = 0;
} else {
new_sub_line = max_sub_line;
}
}
}
// If after adjustment, still at the same position, do not proceed
if new_line_index == current_line_index && new_sub_line == current_sub_line as i32 {
return;
}
let target_line = &lines[new_line_index];
let line_x = current_x - bounds.origin.x;
let target_sub_line = new_sub_line as usize;
let approx_pos = point(line_x, px(target_sub_line as f32 * line_height.0));
let index_res = target_line.index_for_position(approx_pos, line_height);
let new_local_index = match index_res {
Ok(i) => i + 1,
Err(i) => i,
};
let mut prev_lines_offset = 0;
for (i, l) in lines.iter().enumerate() {
if i == new_line_index {
break;
}
prev_lines_offset += l.len() + 1;
}
let new_offset = (prev_lines_offset + new_local_index).min(self.text.len());
self.selected_range = new_offset..new_offset;
self.pause_blink_cursor(cx);
cx.notify();
}
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
self.multi_line
@@ -444,9 +584,7 @@ impl TextInput {
return;
}
self.pause_blink_cursor(cx);
let offset = self.start_of_line(cx).saturating_sub(1);
self.move_to(offset, cx);
self.move_vertical(-1, cx);
}
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
@@ -454,9 +592,7 @@ impl TextInput {
return;
}
self.pause_blink_cursor(cx);
let offset = (self.end_of_line(cx) + 1).min(self.text.len());
self.move_to(offset, cx);
self.move_vertical(1, cx);
}
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
@@ -500,12 +636,30 @@ impl TextInput {
self.move_to(offset, cx);
}
fn select_to_home(&mut self, _: &SelectToHome, cx: &mut ViewContext<Self>) {
fn move_to_start(&mut self, _: &MoveToStart, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
}
fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
let end = self.text.len();
self.move_to(end, cx);
}
fn select_to_start(&mut self, _: &SelectToStart, cx: &mut ViewContext<Self>) {
self.select_to(0, cx);
}
fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
let end = self.text.len();
self.select_to(end, cx);
}
fn select_to_start_of_line(&mut self, _: &SelectToStartOfLine, cx: &mut ViewContext<Self>) {
let offset = self.start_of_line(cx);
self.select_to(offset, cx);
}
fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
fn select_to_end_of_line(&mut self, _: &SelectToEndOfLine, cx: &mut ViewContext<Self>) {
let offset = self.end_of_line(cx);
self.select_to(offset, cx);
}
@@ -594,10 +748,15 @@ impl TextInput {
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
if self.is_multi_line() {
let is_eof = self.selected_range.end == self.text.len();
self.replace_text_in_range(None, "\n", cx);
// Move cursor to the start of the next line
// TODO: To be test this line is valid
self.move_to(self.next_boundary(self.cursor_offset()) - 1, cx);
let mut new_offset = self.next_boundary(self.cursor_offset()) - 1;
if is_eof {
new_offset += 1;
}
self.move_to(new_offset, cx);
}
cx.emit(InputEvent::PressEnter);
@@ -721,6 +880,7 @@ impl TextInput {
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
self.selected_range = offset..offset;
self.pause_blink_cursor(cx);
self.update_preferred_x_offset(cx);
cx.notify()
}
@@ -848,7 +1008,9 @@ impl TextInput {
self.selected_range.end = word_range.end;
}
}
if self.selected_range.is_empty() {
self.update_preferred_x_offset(cx);
}
cx.notify()
}
@@ -1084,6 +1246,7 @@ impl ViewInputHandler for TextInput {
self.text = pending_text;
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
self.update_preferred_x_offset(cx);
cx.emit(InputEvent::Change(self.text.clone()));
cx.notify();
}
@@ -1192,11 +1355,21 @@ impl Render for TextInput {
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.when(self.multi_line, |this| {
this.on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down))
.on_action(cx.listener(Self::select_up))
.on_action(cx.listener(Self::select_down))
})
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::select_to_home))
.on_action(cx.listener(Self::select_to_end))
.on_action(cx.listener(Self::select_to_start_of_line))
.on_action(cx.listener(Self::select_to_end_of_line))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::move_to_start))
.on_action(cx.listener(Self::move_to_end))
.on_action(cx.listener(Self::select_to_start))
.on_action(cx.listener(Self::select_to_end))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::copy))
.on_action(cx.listener(Self::paste))
@@ -1214,21 +1387,13 @@ impl Render for TextInput {
.input_h(self.size)
.input_text_size(self.text_size)
.cursor_text()
.when(self.multi_line, |this| {
this.on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down))
.on_action(cx.listener(Self::select_up))
.on_action(cx.listener(Self::select_down))
.h_auto()
})
.when(self.multi_line, |this| this.h_auto())
.when(self.appearance, |this| {
this.bg(if self.disabled {
cx.theme().muted
cx.theme().base.step(cx, ColorScaleStep::FOUR)
} else {
cx.theme().background
})
.border_color(cx.theme().input)
.border_1()
.rounded(px(cx.theme().radius))
.when(cx.theme().shadow, |this| this.shadow_sm())
.when(focused, |this| this.outline(cx))
@@ -1246,7 +1411,7 @@ impl Render for TextInput {
.child(TextElement::new(cx.view().clone())),
)
.when(self.loading, |this| {
this.child(Indicator::new().color(cx.theme().muted_foreground))
this.child(Indicator::new().color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)))
})
.when(
self.cleanable && !self.loading && !self.text.is_empty() && self.is_single_line(),

View File

@@ -1,93 +0,0 @@
use crate::{h_flex, theme::ActiveTheme};
use gpui::{
div, prelude::FluentBuilder, rems, Div, IntoElement, ParentElement, RenderOnce, SharedString,
Styled, WindowContext,
};
const MASKED: &str = "";
#[derive(Default, PartialEq, Eq)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(IntoElement)]
pub struct Label {
base: Div,
label: SharedString,
align: TextAlign,
marked: bool,
}
impl Label {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: h_flex().line_height(rems(1.25)),
label: label.into(),
align: TextAlign::default(),
marked: false,
}
}
pub fn text_align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub fn text_left(mut self) -> Self {
self.align = TextAlign::Left;
self
}
pub fn text_center(mut self) -> Self {
self.align = TextAlign::Center;
self
}
pub fn text_right(mut self) -> Self {
self.align = TextAlign::Right;
self
}
pub fn masked(mut self, masked: bool) -> Self {
self.marked = masked;
self
}
}
impl Styled for Label {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Label {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let text = self.label;
let text_display = if self.marked {
MASKED.repeat(text.chars().count())
} else {
text.to_string()
};
div().text_color(cx.theme().foreground).child(
self.base
.map(|this| match self.align {
TextAlign::Left => this.justify_start(),
TextAlign::Center => this.justify_center(),
TextAlign::Right => this.justify_end(),
})
.map(|this| {
if self.align == TextAlign::Left {
this.child(div().size_full().child(text_display))
} else {
this.child(text_display)
}
}),
)
}
}

View File

@@ -1,17 +1,15 @@
pub mod animation;
pub mod badge;
pub mod button;
pub mod button_group;
pub mod checkbox;
pub mod clipboard;
pub mod context_menu;
pub mod divider;
pub mod dock;
pub mod dock_area;
pub mod dropdown;
pub mod history;
pub mod indicator;
pub mod input;
pub mod label;
pub mod list;
pub mod modal;
pub mod notification;
@@ -51,7 +49,7 @@ mod window_border;
/// You can initialize the UI module at your application's entry point.
pub fn init(cx: &mut gpui::AppContext) {
theme::init(cx);
dock::init(cx);
dock_area::init(cx);
dropdown::init(cx);
input::init(cx);
list::init(cx);

View File

@@ -1,21 +1,17 @@
use std::time::Duration;
use std::{cell::Cell, rc::Rc};
use crate::Icon;
use crate::{
input::{InputEvent, TextInput},
scroll::{Scrollbar, ScrollbarState},
theme::ActiveTheme,
v_flex, IconName, Size,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, Icon, IconName, Size,
};
use gpui::{
actions, div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity,
actions, div, prelude::FluentBuilder, px, uniform_list, AnyElement, AppContext, Entity,
FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, Length,
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
ListSizingBehavior, MouseButton, ParentElement, Render, ScrollStrategy, SharedString, Styled,
Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
};
use gpui::{px, ScrollStrategy};
use smol::Timer;
use std::{cell::Cell, rc::Rc, time::Duration};
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@@ -105,7 +101,10 @@ where
let query_input = cx.new_view(|cx| {
TextInput::new(cx)
.appearance(false)
.prefix(|cx| Icon::new(IconName::Search).text_color(cx.theme().muted_foreground))
.prefix(|cx| {
Icon::new(IconName::Search)
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
})
.placeholder("Search...")
.cleanable()
});
@@ -326,9 +325,9 @@ where
.left(px(0.))
.right(px(0.))
.bottom(px(0.))
.bg(cx.theme().list_active)
.bg(cx.theme().accent.step(cx, ColorScaleStep::SIX))
.border_1()
.border_color(cx.theme().list_active_border),
.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)),
)
})
})
@@ -341,7 +340,7 @@ where
.right(px(0.))
.bottom(px(0.))
.border_1()
.border_color(cx.theme().list_active_border),
.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)),
)
})
.on_mouse_down(
@@ -418,7 +417,7 @@ where
_ => this.py_1().px_2(),
})
.border_b_1()
.border_color(cx.theme().border)
.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE))
.child(input),
)
})

View File

@@ -1,4 +1,8 @@
use crate::{h_flex, theme::ActiveTheme, Disableable, Icon, IconName, Selectable, Sizable as _};
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
Disableable, Icon, IconName, Selectable, Sizable as _,
};
use gpui::{
div, prelude::FluentBuilder as _, AnyElement, ClickEvent, Div, ElementId, InteractiveElement,
IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce, Stateful,
@@ -123,7 +127,7 @@ impl RenderOnce for ListItem {
let is_active = self.selected || self.confirmed;
self.base
.text_color(cx.theme().foreground)
.text_color(cx.theme().base.step(cx, ColorScaleStep::TWELVE))
.relative()
.items_center()
.justify_between()
@@ -138,9 +142,11 @@ impl RenderOnce for ListItem {
this
}
})
.when(is_active, |this| this.bg(cx.theme().list_active))
.when(is_active, |this| {
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
})
.when(!is_active && !self.disabled, |this| {
this.hover(|this| this.bg(cx.theme().list_hover))
this.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::TWO)))
})
// Mouse enter
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
@@ -158,14 +164,16 @@ impl RenderOnce for ListItem {
.gap_x_1()
.child(div().w_full().children(self.children))
.when_some(self.check_icon, |this, icon| {
this.child(
div().w_5().items_center().justify_center().when(
self.confirmed,
|this| {
this.child(icon.small().text_color(cx.theme().muted_foreground))
},
),
)
this.child(div().w_5().items_center().justify_center().when(
self.confirmed,
|this| {
this.child(
icon.small().text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
),
)
},
))
}),
)
.when_some(self.suffix, |this, suffix| this.child(suffix(cx)))

View File

@@ -1,3 +1,9 @@
use crate::{
animation::cubic_bezier,
button::{Button, ButtonVariants as _},
theme::{scale::ColorScaleStep, ActiveTheme as _},
v_flex, ContextModal, IconName, Sizable as _,
};
use gpui::{
actions, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation,
AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla,
@@ -6,13 +12,6 @@ use gpui::{
};
use std::{rc::Rc, time::Duration};
use crate::{
animation::cubic_bezier,
button::{Button, ButtonVariants as _},
theme::ActiveTheme as _,
v_flex, ContextModal, IconName, Sizable as _,
};
actions!(modal, [Escape]);
const CONTEXT: &str = "Modal";
@@ -59,7 +58,7 @@ impl Modal {
let base = v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE))
.rounded_lg()
.shadow_xl()
.min_h_48()

View File

@@ -2,7 +2,7 @@ use gpui::*;
use serde::{Deserialize, Serialize};
use std::{cell::Cell, rc::Rc, time::Instant};
use crate::theme::ActiveTheme;
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
/// Scrollbar show mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
@@ -294,7 +294,7 @@ impl Scrollbar {
(
cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar,
cx.theme().border,
cx.theme().base.step(cx, ColorScaleStep::THREE),
THUMB_INSET - px(1.),
THUMB_RADIUS,
)
@@ -304,7 +304,7 @@ impl Scrollbar {
(
cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar,
cx.theme().border,
cx.theme().base.step(cx, ColorScaleStep::THREE),
THUMB_INSET - px(1.),
THUMB_RADIUS,
)

View File

@@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized {
/// Render a border with a width of 1px, color ring color
fn outline(self, cx: &WindowContext) -> Self {
self.border_color(cx.theme().ring)
self.border_color(cx.theme().accent.step(cx, ColorScaleStep::NINE))
}
/// Wraps the element in a ScrollView.

View File

@@ -1,4 +1,8 @@
use crate::{h_flex, theme::ActiveTheme, Disableable, Side, Sizable, Size};
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
Disableable, Side, Sizable, Size,
};
use gpui::{
div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, Element,
ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _,
@@ -105,8 +109,11 @@ impl Element for Switch {
let on_click = self.on_click.clone();
let (bg, toggle_bg) = match self.checked {
true => (theme.colors.primary, theme.background),
false => (theme.input, theme.background),
true => (
theme.accent.step(cx, ColorScaleStep::NINE),
theme.background,
),
false => (theme.base.step(cx, ColorScaleStep::FOUR), theme.background),
};
let (bg, toggle_bg) = match self.disabled {

View File

@@ -1,3 +1,4 @@
use crate::theme::scale::ColorScaleStep;
use crate::theme::ActiveTheme;
use crate::Selectable;
use gpui::prelude::FluentBuilder;
@@ -28,7 +29,7 @@ impl Tab {
Self {
id: id.clone(),
base: div().id(id).gap_1().py_1p5().px_3().h(px(30.)),
base: div().id(id),
label: label.into_any_element(),
metadata,
disabled: false,
@@ -85,30 +86,58 @@ impl Styled for Tab {
impl RenderOnce for Tab {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (text_color, bg_color) = match (self.selected, self.disabled) {
(true, false) => (cx.theme().tab_active_foreground, cx.theme().tab_active),
(false, false) => (cx.theme().muted_foreground, cx.theme().tab),
(true, false) => (
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
cx.theme().background,
),
(false, false) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::TWO),
),
// disabled
(true, true) => (cx.theme().muted_foreground, cx.theme().tab_active),
(false, true) => (cx.theme().muted_foreground, cx.theme().tab),
(true, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::TWO),
),
(false, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::TWO),
),
};
self.base
.h(px(30.))
.relative()
.flex()
.items_center()
.flex_shrink_0()
.cursor_pointer()
.overflow_hidden()
.text_sm()
.text_color(text_color)
.bg(bg_color)
.border_x_1()
.border_color(cx.theme().transparent)
.when(self.selected, |this| this.border_color(cx.theme().border))
.text_sm()
.when(self.selected, |this| {
this.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE))
})
.when(!self.selected, |this| {
this.child(
div()
.absolute()
.left_0()
.bottom_0()
.size_full()
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
)
})
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)
})
.child(
div()
.px_3()
.flex()
.items_center()
.gap_1()

View File

@@ -1,8 +1,8 @@
use crate::scroll::ScrollbarShow;
use colors::{default_color_scales, hsl};
use gpui::{
blue, hsla, transparent_black, AppContext, Global, Hsla, ModelContext, SharedString,
ViewContext, WindowAppearance, WindowContext,
AppContext, Global, Hsla, ModelContext, SharedString, ViewContext, WindowAppearance,
WindowContext,
};
use scale::ColorScaleSet;
use std::ops::{Deref, DerefMut};
@@ -13,249 +13,33 @@ pub mod scale;
#[derive(Debug, Clone, Copy, Default)]
pub struct ThemeColors {
pub background: Hsla,
pub border: Hsla,
pub window_border: Hsla,
pub accent: Hsla,
pub accent_foreground: Hsla,
pub card: Hsla,
pub card_foreground: Hsla,
pub danger: Hsla,
pub danger_active: Hsla,
pub danger_foreground: Hsla,
pub danger_hover: Hsla,
pub drag_border: Hsla,
pub drop_target: Hsla,
pub foreground: Hsla,
pub input: Hsla,
pub link: Hsla,
pub link_active: Hsla,
pub link_hover: Hsla,
pub list: Hsla,
pub list_active: Hsla,
pub list_active_border: Hsla,
pub list_even: Hsla,
pub list_head: Hsla,
pub list_hover: Hsla,
pub muted: Hsla,
pub muted_foreground: Hsla,
pub popover: Hsla,
pub popover_foreground: Hsla,
pub primary: Hsla,
pub primary_active: Hsla,
pub primary_foreground: Hsla,
pub primary_hover: Hsla,
pub progress_bar: Hsla,
pub ring: Hsla,
pub transparent: Hsla,
pub scrollbar: Hsla,
pub scrollbar_thumb: Hsla,
pub scrollbar_thumb_hover: Hsla,
pub secondary: Hsla,
pub secondary_active: Hsla,
pub secondary_foreground: Hsla,
pub secondary_hover: Hsla,
pub selection: Hsla,
pub skeleton: Hsla,
pub slider_bar: Hsla,
pub slider_thumb: Hsla,
pub tab: Hsla,
pub tab_active: Hsla,
pub tab_active_foreground: Hsla,
pub tab_bar: Hsla,
pub tab_foreground: Hsla,
pub title_bar: Hsla,
pub title_bar_border: Hsla,
pub window_border: Hsla,
}
impl ThemeColors {
pub fn light() -> Self {
Self {
background: hsl(0.0, 0.0, 100.),
accent: hsl(240.0, 5.0, 96.0),
accent_foreground: hsl(240.0, 5.9, 10.0),
border: hsl(240.0, 5.9, 90.0),
transparent: Hsla::transparent_black(),
window_border: hsl(240.0, 5.9, 78.0),
card: hsl(0.0, 0.0, 100.0),
card_foreground: hsl(240.0, 10.0, 3.9),
danger: hsl(0.0, 84.2, 60.2),
danger_active: hsl(0.0, 84.2, 47.0),
danger_foreground: hsl(0.0, 0.0, 98.0),
danger_hover: hsl(0.0, 84.2, 65.0),
drag_border: blue(),
drop_target: hsl(235.0, 30., 44.0).opacity(0.25),
foreground: hsl(240.0, 10., 3.9),
input: hsl(240.0, 5.9, 90.0),
link: hsl(221.0, 83.0, 53.0),
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
list: hsl(0.0, 0.0, 100.),
list_active: hsl(211.0, 97.0, 85.0).opacity(0.2),
list_active_border: hsl(211.0, 97.0, 85.0),
list_even: hsl(240.0, 5.0, 96.0),
list_head: hsl(0.0, 0.0, 100.),
list_hover: hsl(240.0, 4.8, 95.0),
muted: hsl(240.0, 4.8, 95.9),
muted_foreground: hsl(240.0, 3.8, 46.1),
popover: hsl(0.0, 0.0, 100.0),
popover_foreground: hsl(240.0, 10.0, 3.9),
primary: hsl(223.0, 5.9, 10.0),
primary_active: hsl(223.0, 1.9, 25.0),
primary_foreground: hsl(223.0, 0.0, 98.0),
primary_hover: hsl(223.0, 5.9, 15.0),
progress_bar: hsl(223.0, 5.9, 10.0),
ring: hsl(240.0, 5.9, 65.0),
scrollbar: hsl(0., 0., 97.).opacity(0.75),
scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9),
scrollbar_thumb_hover: hsl(0., 0., 59.),
secondary: hsl(240.0, 5.9, 96.9),
secondary_active: hsl(240.0, 5.9, 90.),
secondary_foreground: hsl(240.0, 59.0, 10.),
secondary_hover: hsl(240.0, 5.9, 98.),
selection: hsl(211.0, 97.0, 85.0),
skeleton: hsl(223.0, 5.9, 10.0).opacity(0.1),
slider_bar: hsl(223.0, 5.9, 10.0),
slider_thumb: hsl(0.0, 0.0, 100.0),
tab: transparent_black(),
tab_active: hsl(0.0, 0.0, 100.0),
tab_active_foreground: hsl(240.0, 10., 3.9),
tab_bar: hsl(240.0, 4.8, 95.9),
tab_foreground: hsl(240.0, 10., 3.9),
title_bar: hsl(0.0, 0.0, 98.0),
title_bar_border: hsl(220.0, 13.0, 91.0),
}
}
pub fn dark() -> Self {
Self {
background: hsl(0.0, 0.0, 8.0),
accent: hsl(240.0, 3.7, 15.9),
accent_foreground: hsl(0.0, 0.0, 78.0),
border: hsl(240.0, 3.7, 16.9),
transparent: Hsla::transparent_black(),
window_border: hsl(240.0, 3.7, 28.0),
card: hsl(0.0, 0.0, 8.0),
card_foreground: hsl(0.0, 0.0, 78.0),
danger: hsl(0.0, 62.8, 30.6),
danger_active: hsl(0.0, 62.8, 20.6),
danger_foreground: hsl(0.0, 0.0, 78.0),
danger_hover: hsl(0.0, 62.8, 35.6),
drag_border: blue(),
drop_target: hsl(235.0, 30., 44.0).opacity(0.1),
foreground: hsl(0., 0., 78.),
input: hsl(240.0, 3.7, 15.9),
link: hsl(221.0, 83.0, 53.0),
link_active: hsl(221.0, 83.0, 53.0).darken(0.2),
link_hover: hsl(221.0, 83.0, 53.0).lighten(0.2),
list: hsl(0.0, 0.0, 8.0),
list_active: hsl(240.0, 3.7, 15.0).opacity(0.2),
list_active_border: hsl(240.0, 5.9, 35.5),
list_even: hsl(240.0, 3.7, 10.0),
list_head: hsl(0.0, 0.0, 8.0),
list_hover: hsl(240.0, 3.7, 15.9),
muted: hsl(240.0, 3.7, 15.9),
muted_foreground: hsl(240.0, 5.0, 64.9),
popover: hsl(0.0, 0.0, 10.),
popover_foreground: hsl(0.0, 0.0, 78.0),
primary: hsl(223.0, 0.0, 98.0),
primary_active: hsl(223.0, 0.0, 80.0),
primary_foreground: hsl(223.0, 5.9, 10.0),
primary_hover: hsl(223.0, 0.0, 90.0),
progress_bar: hsl(223.0, 0.0, 98.0),
ring: hsl(240.0, 4.9, 83.9),
scrollbar: hsl(240., 1., 15.).opacity(0.75),
scrollbar_thumb: hsl(0., 0., 48.).opacity(0.9),
scrollbar_thumb_hover: hsl(0., 0., 68.),
secondary: hsl(240.0, 0., 13.0),
secondary_active: hsl(240.0, 0., 10.),
secondary_foreground: hsl(0.0, 0.0, 78.0),
secondary_hover: hsl(240.0, 0., 15.),
selection: hsl(211.0, 97.0, 22.0),
skeleton: hsla(223.0, 0.0, 98.0, 0.1),
slider_bar: hsl(223.0, 0.0, 98.0),
slider_thumb: hsl(0.0, 0.0, 8.0),
tab: transparent_black(),
tab_active: hsl(0.0, 0.0, 8.0),
tab_active_foreground: hsl(0., 0., 78.),
tab_bar: hsl(299.0, 0., 5.5),
tab_foreground: hsl(0., 0., 78.),
title_bar: hsl(240.0, 0.0, 10.0),
title_bar_border: hsl(240.0, 3.7, 15.9),
}
}
}
pub trait Colorize {
fn opacity(&self, opacity: f32) -> Hsla;
fn divide(&self, divisor: f32) -> Hsla;
fn invert(&self) -> Hsla;
fn invert_l(&self) -> Hsla;
fn lighten(&self, amount: f32) -> Hsla;
fn darken(&self, amount: f32) -> Hsla;
fn apply(&self, base_color: Hsla) -> Hsla;
}
impl Colorize for Hsla {
/// Returns a new color with the given opacity.
///
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
fn opacity(&self, factor: f32) -> Hsla {
Hsla {
a: self.a * factor.clamp(0.0, 1.0),
..*self
}
}
/// Returns a new color with each channel divided by the given divisor.
///
/// The divisor in range of 0.0 .. 1.0
fn divide(&self, divisor: f32) -> Hsla {
Hsla {
a: divisor,
..*self
}
}
/// Return inverted color
fn invert(&self) -> Hsla {
Hsla {
h: (self.h + 1.8) % 3.6,
s: 1.0 - self.s,
l: 1.0 - self.l,
a: self.a,
}
}
/// Return inverted lightness
fn invert_l(&self) -> Hsla {
Hsla {
l: 1.0 - self.l,
..*self
}
}
/// Return a new color with the lightness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn lighten(&self, factor: f32) -> Hsla {
let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
Hsla { l, ..*self }
}
/// Return a new color with the darkness increased by the given factor.
///
/// factor range: 0.0 .. 1.0
fn darken(&self, factor: f32) -> Hsla {
let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
Hsla { l, ..*self }
}
/// Return a new color with the same lightness and alpha but different hue and saturation.
fn apply(&self, new_color: Hsla) -> Hsla {
Hsla {
h: new_color.h,
s: new_color.s,
l: self.l,
a: self.a,
}
}
}
@@ -293,7 +77,7 @@ pub fn init(cx: &mut AppContext) {
}
pub struct Theme {
pub colors: ThemeColors,
colors: ThemeColors,
/// Base colors.
pub base: ColorScaleSet,
/// Accent colors.
@@ -304,7 +88,6 @@ pub struct Theme {
pub font_size: f32,
pub radius: f32,
pub shadow: bool,
pub transparent: Hsla,
/// Show the scrollbar mode, default: Scrolling
pub scrollbar_show: ScrollbarShow,
}
@@ -370,7 +153,6 @@ impl From<ThemeColors> for Theme {
base: color_scales.gray,
accent: color_scales.yellow,
appearance: Appearance::default(),
transparent: Hsla::transparent_black(),
font_size: 16.0,
font_family: if cfg!(target_os = "macos") {
".SystemUIFont".into()