This commit is contained in:
Antonio Scandurra 2023-10-23 17:41:22 +02:00
commit 4d621f355d
23 changed files with 679 additions and 419 deletions

View file

@ -9,11 +9,11 @@ use refineable::Refineable;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{ use crate::{
current_platform, image_cache::ImageCache, Action, AppMetadata, AssetSource, ClipboardItem, current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AppMetadata, AssetSource,
Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, KeyBinding, ClipboardItem, Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId,
Keymap, LayoutId, MainThread, MainThreadOnly, Platform, SharedString, SubscriberSet, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, Pixels, Platform, Point,
Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
WindowContext, WindowHandle, WindowId, TextSystem, View, Window, WindowContext, WindowHandle, WindowId,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque}; use collections::{HashMap, HashSet, VecDeque};
@ -93,6 +93,7 @@ impl App {
quit_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(),
layout_id_buffer: Default::default(), layout_id_buffer: Default::default(),
propagate_event: true, propagate_event: true,
active_drag: None,
}) })
})) }))
} }
@ -171,6 +172,7 @@ pub struct AppContext {
text_system: Arc<TextSystem>, text_system: Arc<TextSystem>,
flushing_effects: bool, flushing_effects: bool,
pending_updates: usize, pending_updates: usize,
pub(crate) active_drag: Option<AnyDrag>,
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>, pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
pub(crate) executor: Executor, pub(crate) executor: Executor,
pub(crate) svg_renderer: SvgRenderer, pub(crate) svg_renderer: SvgRenderer,
@ -766,6 +768,13 @@ pub(crate) enum Effect {
}, },
} }
pub(crate) struct AnyDrag {
pub drag_handle_view: AnyView,
pub cursor_offset: Point<Pixels>,
pub state: AnyBox,
pub state_type: TypeId,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::AppContext; use super::AppContext;

View file

@ -155,6 +155,15 @@ pub fn white() -> Hsla {
} }
} }
pub fn red() -> Hsla {
Hsla {
h: 0.,
s: 1.,
l: 0.5,
a: 1.,
}
}
impl Hsla { impl Hsla {
/// Returns true if the HSLA color is fully transparent, false otherwise. /// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool { pub fn is_transparent(&self) -> bool {

View file

@ -298,7 +298,7 @@ where
style.apply_text_style(cx, |cx| { style.apply_text_style(cx, |cx| {
style.apply_overflow(bounds, cx, |cx| { style.apply_overflow(bounds, cx, |cx| {
let scroll_offset = element_state.interactive.scroll_offset(); let scroll_offset = element_state.interactive.scroll_offset();
cx.with_scroll_offset(scroll_offset, |cx| { cx.with_element_offset(scroll_offset, |cx| {
for child in &mut this.children { for child in &mut this.children {
child.paint(view_state, cx); child.paint(view_state, cx);
} }
@ -354,7 +354,7 @@ where
F: ElementFocus<V>, F: ElementFocus<V>,
V: 'static + Send + Sync, V: 'static + Send + Sync,
{ {
fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> { fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
self.interaction.as_stateless_mut() self.interaction.as_stateless_mut()
} }
} }
@ -364,7 +364,7 @@ where
F: ElementFocus<V>, F: ElementFocus<V>,
V: 'static + Send + Sync, V: 'static + Send + Sync,
{ {
fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> { fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
&mut self.interaction &mut self.interaction
} }
} }

View file

@ -156,8 +156,8 @@ where
I: ElementInteraction<V>, I: ElementInteraction<V>,
F: ElementFocus<V>, F: ElementFocus<V>,
{ {
fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> { fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
self.base.stateless_interactivity() self.base.stateless_interaction()
} }
} }
@ -166,8 +166,8 @@ where
V: 'static + Send + Sync, V: 'static + Send + Sync,
F: ElementFocus<V>, F: ElementFocus<V>,
{ {
fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> { fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
self.base.stateful_interactivity() self.base.stateful_interaction()
} }
} }

View file

@ -130,8 +130,8 @@ where
I: ElementInteraction<V>, I: ElementInteraction<V>,
F: ElementFocus<V>, F: ElementFocus<V>,
{ {
fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> { fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
self.base.stateless_interactivity() self.base.stateless_interaction()
} }
} }
@ -140,8 +140,8 @@ where
V: 'static + Send + Sync, V: 'static + Send + Sync,
F: ElementFocus<V>, F: ElementFocus<V>,
{ {
fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> { fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
self.base.stateful_interactivity() self.base.stateful_interaction()
} }
} }

View file

@ -40,6 +40,10 @@ impl Point<Pixels> {
y: self.y.scale(factor), y: self.y.scale(factor),
} }
} }
pub fn magnitude(&self) -> f64 {
((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt()
}
} }
impl<T, Rhs> Mul<Rhs> for Point<T> impl<T, Rhs> Mul<Rhs> for Point<T>

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
point, px, Action, AppContext, BorrowWindow, Bounds, DispatchContext, DispatchPhase, Element, point, px, view, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, DispatchContext,
ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow, Pixels, Point, SharedString, DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow,
Size, Style, StyleRefinement, ViewContext, Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext,
}; };
use collections::HashMap; use collections::HashMap;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
@ -11,18 +11,21 @@ use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
fmt::Debug, fmt::Debug,
marker::PhantomData,
ops::Deref, ops::Deref,
sync::Arc, sync::Arc,
}; };
const DRAG_THRESHOLD: f64 = 2.;
pub trait StatelessInteractive: Element { pub trait StatelessInteractive: Element {
fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<Self::ViewState>; fn stateless_interaction(&mut self) -> &mut StatelessInteraction<Self::ViewState>;
fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity().hover_style = f(StyleRefinement::default()); self.stateless_interaction().hover_style = f(StyleRefinement::default());
self self
} }
@ -34,7 +37,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity().group_hover_style = Some(GroupStyle { self.stateless_interaction().group_hover_style = Some(GroupStyle {
group: group_name.into(), group: group_name.into(),
style: f(StyleRefinement::default()), style: f(StyleRefinement::default()),
}); });
@ -52,7 +55,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.mouse_down_listeners .mouse_down_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble
@ -76,7 +79,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.mouse_up_listeners .mouse_up_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble
@ -100,7 +103,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.mouse_down_listeners .mouse_down_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Capture if phase == DispatchPhase::Capture
@ -124,7 +127,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.mouse_up_listeners .mouse_up_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Capture if phase == DispatchPhase::Capture
@ -147,7 +150,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.mouse_move_listeners .mouse_move_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@ -167,7 +170,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity() self.stateless_interaction()
.scroll_wheel_listeners .scroll_wheel_listeners
.push(Arc::new(move |view, event, bounds, phase, cx| { .push(Arc::new(move |view, event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@ -183,7 +186,7 @@ pub trait StatelessInteractive: Element {
C: TryInto<DispatchContext>, C: TryInto<DispatchContext>,
C::Error: Debug, C::Error: Debug,
{ {
self.stateless_interactivity().dispatch_context = self.stateless_interaction().dispatch_context =
context.try_into().expect("invalid dispatch context"); context.try_into().expect("invalid dispatch context");
self self
} }
@ -198,7 +201,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity().key_listeners.push(( self.stateless_interaction().key_listeners.push((
TypeId::of::<A>(), TypeId::of::<A>(),
Arc::new(move |view, event, _, phase, cx| { Arc::new(move |view, event, _, phase, cx| {
let event = event.downcast_ref().unwrap(); let event = event.downcast_ref().unwrap();
@ -223,7 +226,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity().key_listeners.push(( self.stateless_interaction().key_listeners.push((
TypeId::of::<KeyDownEvent>(), TypeId::of::<KeyDownEvent>(),
Arc::new(move |view, event, _, phase, cx| { Arc::new(move |view, event, _, phase, cx| {
let event = event.downcast_ref().unwrap(); let event = event.downcast_ref().unwrap();
@ -244,7 +247,7 @@ pub trait StatelessInteractive: Element {
where where
Self: Sized, Self: Sized,
{ {
self.stateless_interactivity().key_listeners.push(( self.stateless_interaction().key_listeners.push((
TypeId::of::<KeyUpEvent>(), TypeId::of::<KeyUpEvent>(),
Arc::new(move |view, event, _, phase, cx| { Arc::new(move |view, event, _, phase, cx| {
let event = event.downcast_ref().unwrap(); let event = event.downcast_ref().unwrap();
@ -254,16 +257,63 @@ pub trait StatelessInteractive: Element {
)); ));
self self
} }
fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.stateless_interaction()
.drag_over_styles
.push((TypeId::of::<S>(), f(StyleRefinement::default())));
self
}
fn group_drag_over<S: 'static>(
mut self,
group_name: impl Into<SharedString>,
f: impl FnOnce(StyleRefinement) -> StyleRefinement,
) -> Self
where
Self: Sized,
{
self.stateless_interaction().group_drag_over_styles.push((
TypeId::of::<S>(),
GroupStyle {
group: group_name.into(),
style: f(StyleRefinement::default()),
},
));
self
}
fn on_drop<S: 'static>(
mut self,
listener: impl Fn(&mut Self::ViewState, S, &mut ViewContext<Self::ViewState>)
+ Send
+ Sync
+ 'static,
) -> Self
where
Self: Sized,
{
self.stateless_interaction().drop_listeners.push((
TypeId::of::<S>(),
Arc::new(move |view, drag_state, cx| {
listener(view, *drag_state.downcast().unwrap(), cx);
}),
));
self
}
} }
pub trait StatefulInteractive: StatelessInteractive { pub trait StatefulInteractive: StatelessInteractive {
fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState>; fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState>;
fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.stateful_interactivity().active_style = f(StyleRefinement::default()); self.stateful_interaction().active_style = f(StyleRefinement::default());
self self
} }
@ -275,7 +325,7 @@ pub trait StatefulInteractive: StatelessInteractive {
where where
Self: Sized, Self: Sized,
{ {
self.stateful_interactivity().group_active_style = Some(GroupStyle { self.stateful_interaction().group_active_style = Some(GroupStyle {
group: group_name.into(), group: group_name.into(),
style: f(StyleRefinement::default()), style: f(StyleRefinement::default()),
}); });
@ -284,7 +334,7 @@ pub trait StatefulInteractive: StatelessInteractive {
fn on_click( fn on_click(
mut self, mut self,
handler: impl Fn(&mut Self::ViewState, &MouseClickEvent, &mut ViewContext<Self::ViewState>) listener: impl Fn(&mut Self::ViewState, &ClickEvent, &mut ViewContext<Self::ViewState>)
+ Send + Send
+ Sync + Sync
+ 'static, + 'static,
@ -292,9 +342,47 @@ pub trait StatefulInteractive: StatelessInteractive {
where where
Self: Sized, Self: Sized,
{ {
self.stateful_interactivity() self.stateful_interaction()
.mouse_click_listeners .click_listeners
.push(Arc::new(move |view, event, cx| handler(view, event, cx))); .push(Arc::new(move |view, event, cx| listener(view, event, cx)));
self
}
fn on_drag<S, R, E>(
mut self,
listener: impl Fn(
&mut Self::ViewState,
&mut ViewContext<Self::ViewState>,
) -> Drag<S, R, Self::ViewState, E>
+ Send
+ Sync
+ 'static,
) -> Self
where
Self: Sized,
S: 'static + Send + Sync,
R: 'static + Fn(&mut Self::ViewState, &mut ViewContext<Self::ViewState>) -> E + Send + Sync,
E: Element<ViewState = Self::ViewState>,
{
debug_assert!(
self.stateful_interaction().drag_listener.is_none(),
"calling on_drag more than once on the same element is not supported"
);
self.stateful_interaction().drag_listener =
Some(Arc::new(move |view_state, cursor_offset, cx| {
let drag = listener(view_state, cx);
let view_handle = cx.handle().upgrade().unwrap();
let drag_handle_view = view(view_handle, move |view_state, cx| {
(drag.render_drag_handle)(view_state, cx)
})
.into_any();
AnyDrag {
drag_handle_view,
cursor_offset,
state: Box::new(drag.state),
state_type: TypeId::of::<S>(),
}
}));
self self
} }
} }
@ -359,6 +447,26 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
style.refine(&stateless.hover_style); style.refine(&stateless.hover_style);
} }
if let Some(drag) = cx.active_drag.take() {
for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles {
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
if *state_type == drag.state_type
&& group_bounds.contains_point(&mouse_position)
{
style.refine(&group_drag_style.style);
}
}
}
for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
if *state_type == drag.state_type && bounds.contains_point(&mouse_position) {
style.refine(drag_over_style);
}
}
cx.active_drag = Some(drag);
}
if let Some(stateful) = self.as_stateful() { if let Some(stateful) = self.as_stateful() {
let active_state = element_state.active_state.lock(); let active_state = element_state.active_state.lock();
if active_state.group { if active_state.group {
@ -411,38 +519,104 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
.and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
if let Some(group_bounds) = hover_group_bounds { if let Some(group_bounds) = hover_group_bounds {
paint_hover_listener(group_bounds, cx); let hovered = group_bounds.contains_point(&cx.mouse_position());
cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if group_bounds.contains_point(&event.position) != hovered {
cx.notify();
}
}
});
} }
if stateless.hover_style.is_some() { if stateless.hover_style.is_some()
paint_hover_listener(bounds, cx); || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty())
{
let hovered = bounds.contains_point(&cx.mouse_position());
cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if bounds.contains_point(&event.position) != hovered {
cx.notify();
}
}
});
}
if cx.active_drag.is_some() {
let drop_listeners = stateless.drop_listeners.clone();
cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
if let Some(drag_state_type) =
cx.active_drag.as_ref().map(|drag| drag.state_type)
{
for (drop_state_type, listener) in &drop_listeners {
if *drop_state_type == drag_state_type {
let drag = cx
.active_drag
.take()
.expect("checked for type drag state type above");
listener(view, drag.state, cx);
cx.notify();
cx.stop_propagation();
}
}
}
}
});
} }
if let Some(stateful) = self.as_stateful() { if let Some(stateful) = self.as_stateful() {
let click_listeners = stateful.mouse_click_listeners.clone(); let click_listeners = stateful.click_listeners.clone();
let drag_listener = stateful.drag_listener.clone();
let pending_click = element_state.pending_click.clone(); if !click_listeners.is_empty() || drag_listener.is_some() {
let mouse_down = pending_click.lock().clone(); let pending_mouse_down = element_state.pending_mouse_down.clone();
if let Some(mouse_down) = mouse_down { let mouse_down = pending_mouse_down.lock().clone();
cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { if let Some(mouse_down) = mouse_down {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if let Some(drag_listener) = drag_listener {
let mouse_click = MouseClickEvent { let active_state = element_state.active_state.clone();
down: mouse_down.clone(),
up: event.clone(), cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
}; if cx.active_drag.is_some() {
for listener in &click_listeners { if phase == DispatchPhase::Capture {
listener(state, &mouse_click, cx); cx.notify();
}
} else if phase == DispatchPhase::Bubble
&& bounds.contains_point(&event.position)
&& (event.position - mouse_down.position).magnitude()
> DRAG_THRESHOLD
{
*active_state.lock() = ActiveState::default();
let cursor_offset = event.position - bounds.origin;
let drag = drag_listener(view_state, cursor_offset, cx);
cx.active_drag = Some(drag);
cx.notify();
cx.stop_propagation();
}
});
}
cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
{
let mouse_click = ClickEvent {
down: mouse_down.clone(),
up: event.clone(),
};
for listener in &click_listeners {
listener(view_state, &mouse_click, cx);
}
} }
} *pending_mouse_down.lock() = None;
});
*pending_click.lock() = None; } else {
}); cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
} else { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| { {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { *pending_mouse_down.lock() = Some(event.clone());
*pending_click.lock() = Some(event.clone()); }
} });
}); }
} }
let active_state = element_state.active_state.clone(); let active_state = element_state.active_state.clone();
@ -487,12 +661,12 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
if overflow.x == Overflow::Scroll { if overflow.x == Overflow::Scroll {
scroll_offset.x = scroll_offset.x =
(scroll_offset.x - delta.x).clamp(px(0.), scroll_max.width); (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
} }
if overflow.y == Overflow::Scroll { if overflow.y == Overflow::Scroll {
scroll_offset.y = scroll_offset.y =
(scroll_offset.y - delta.y).clamp(px(0.), scroll_max.height); (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
} }
if *scroll_offset != old_scroll_offset { if *scroll_offset != old_scroll_offset {
@ -506,29 +680,16 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
} }
} }
fn paint_hover_listener<V>(bounds: Bounds<Pixels>, cx: &mut ViewContext<V>)
where
V: 'static + Send + Sync,
{
let hovered = bounds.contains_point(&cx.mouse_position());
cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if bounds.contains_point(&event.position) != hovered {
cx.notify();
}
}
});
}
#[derive(Deref, DerefMut)] #[derive(Deref, DerefMut)]
pub struct StatefulInteraction<V: 'static + Send + Sync> { pub struct StatefulInteraction<V: 'static + Send + Sync> {
pub id: ElementId, pub id: ElementId,
#[deref] #[deref]
#[deref_mut] #[deref_mut]
stateless: StatelessInteraction<V>, stateless: StatelessInteraction<V>,
pub mouse_click_listeners: SmallVec<[MouseClickListener<V>; 2]>, click_listeners: SmallVec<[ClickListener<V>; 2]>,
pub active_style: StyleRefinement, active_style: StyleRefinement,
pub group_active_style: Option<GroupStyle>, group_active_style: Option<GroupStyle>,
drag_listener: Option<DragListener<V>>,
} }
impl<V> ElementInteraction<V> for StatefulInteraction<V> impl<V> ElementInteraction<V> for StatefulInteraction<V>
@ -560,13 +721,16 @@ where
Self { Self {
id, id,
stateless: StatelessInteraction::default(), stateless: StatelessInteraction::default(),
mouse_click_listeners: SmallVec::new(), click_listeners: SmallVec::new(),
drag_listener: None,
active_style: StyleRefinement::default(), active_style: StyleRefinement::default(),
group_active_style: None, group_active_style: None,
} }
} }
} }
type DropListener<V> = dyn Fn(&mut V, AnyBox, &mut ViewContext<V>) + Send + Sync;
pub struct StatelessInteraction<V> { pub struct StatelessInteraction<V> {
pub dispatch_context: DispatchContext, pub dispatch_context: DispatchContext,
pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>, pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
@ -576,6 +740,9 @@ pub struct StatelessInteraction<V> {
pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>, pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>,
pub hover_style: StyleRefinement, pub hover_style: StyleRefinement,
pub group_hover_style: Option<GroupStyle>, pub group_hover_style: Option<GroupStyle>,
drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
drop_listeners: SmallVec<[(TypeId, Arc<DropListener<V>>); 2]>,
} }
impl<V> StatelessInteraction<V> impl<V> StatelessInteraction<V>
@ -586,7 +753,8 @@ where
StatefulInteraction { StatefulInteraction {
id: id.into(), id: id.into(),
stateless: self, stateless: self,
mouse_click_listeners: SmallVec::new(), click_listeners: SmallVec::new(),
drag_listener: None,
active_style: StyleRefinement::default(), active_style: StyleRefinement::default(),
group_active_style: None, group_active_style: None,
} }
@ -642,7 +810,7 @@ impl ActiveState {
#[derive(Default)] #[derive(Default)]
pub struct InteractiveElementState { pub struct InteractiveElementState {
active_state: Arc<Mutex<ActiveState>>, active_state: Arc<Mutex<ActiveState>>,
pending_click: Arc<Mutex<Option<MouseDownEvent>>>, pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>, scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
} }
@ -665,6 +833,9 @@ impl<V> Default for StatelessInteraction<V> {
key_listeners: SmallVec::new(), key_listeners: SmallVec::new(),
hover_style: StyleRefinement::default(), hover_style: StyleRefinement::default(),
group_hover_style: None, group_hover_style: None,
drag_over_styles: SmallVec::new(),
group_drag_over_styles: SmallVec::new(),
drop_listeners: SmallVec::new(),
} }
} }
} }
@ -740,11 +911,39 @@ pub struct MouseUpEvent {
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct MouseClickEvent { pub struct ClickEvent {
pub down: MouseDownEvent, pub down: MouseDownEvent,
pub up: MouseUpEvent, pub up: MouseUpEvent,
} }
pub struct Drag<S, R, V, E>
where
S: 'static + Send + Sync,
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static + Send + Sync,
E: Element<ViewState = V>,
{
pub state: S,
pub render_drag_handle: R,
view_type: PhantomData<V>,
}
impl<S, R, V, E> Drag<S, R, V, E>
where
S: 'static + Send + Sync,
R: Fn(&mut V, &mut ViewContext<V>) -> E + Send + Sync,
V: 'static + Send + Sync,
E: Element<ViewState = V>,
{
pub fn new(state: S, render_drag_handle: R) -> Self {
Drag {
state,
render_drag_handle,
view_type: PhantomData,
}
}
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum MouseButton { pub enum MouseButton {
Left, Left,
@ -919,8 +1118,6 @@ pub type MouseUpListener<V> = Arc<
+ Sync + Sync
+ 'static, + 'static,
>; >;
pub type MouseClickListener<V> =
Arc<dyn Fn(&mut V, &MouseClickEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
pub type MouseMoveListener<V> = Arc< pub type MouseMoveListener<V> = Arc<
dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
@ -936,6 +1133,12 @@ pub type ScrollWheelListener<V> = Arc<
+ 'static, + 'static,
>; >;
pub type ClickListener<V> =
Arc<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
pub(crate) type DragListener<V> =
Arc<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + Send + Sync + 'static>;
pub type KeyListener<V> = Arc< pub type KeyListener<V> = Arc<
dyn Fn( dyn Fn(
&mut V, &mut V,

View file

@ -125,11 +125,11 @@ impl TaffyLayoutEngine {
// } // }
// println!(""); // println!("");
let started_at = std::time::Instant::now(); // let started_at = std::time::Instant::now();
self.taffy self.taffy
.compute_layout(id.into(), available_space.into()) .compute_layout(id.into(), available_space.into())
.expect(EXPECT_MESSAGE); .expect(EXPECT_MESSAGE);
println!("compute_layout took {:?}", started_at.elapsed()); // println!("compute_layout took {:?}", started_at.elapsed());
} }
pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> { pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> {

View file

@ -3,11 +3,11 @@ use crate::{
BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext,
DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId,
GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher,
Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent,
Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, MouseUpEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
@ -159,7 +159,7 @@ pub struct Window {
key_matchers: HashMap<GlobalElementId, KeyMatcher>, key_matchers: HashMap<GlobalElementId, KeyMatcher>,
z_index_stack: StackingOrder, z_index_stack: StackingOrder,
content_mask_stack: Vec<ContentMask<Pixels>>, content_mask_stack: Vec<ContentMask<Pixels>>,
scroll_offset_stack: Vec<Point<Pixels>>, element_offset_stack: Vec<Point<Pixels>>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>, mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
key_dispatch_stack: Vec<KeyDispatchStackFrame>, key_dispatch_stack: Vec<KeyDispatchStackFrame>,
freeze_key_dispatch_stack: bool, freeze_key_dispatch_stack: bool,
@ -177,7 +177,7 @@ pub struct Window {
} }
impl Window { impl Window {
pub fn new( pub(crate) fn new(
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
cx: &mut MainThread<AppContext>, cx: &mut MainThread<AppContext>,
@ -234,7 +234,7 @@ impl Window {
key_matchers: HashMap::default(), key_matchers: HashMap::default(),
z_index_stack: StackingOrder(SmallVec::new()), z_index_stack: StackingOrder(SmallVec::new()),
content_mask_stack: Vec::new(), content_mask_stack: Vec::new(),
scroll_offset_stack: Vec::new(), element_offset_stack: Vec::new(),
mouse_listeners: HashMap::default(), mouse_listeners: HashMap::default(),
key_dispatch_stack: Vec::new(), key_dispatch_stack: Vec::new(),
freeze_key_dispatch_stack: false, freeze_key_dispatch_stack: false,
@ -469,7 +469,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
.layout_engine .layout_engine
.layout_bounds(layout_id) .layout_bounds(layout_id)
.map(Into::into); .map(Into::into);
bounds.origin -= self.scroll_offset(); bounds.origin += self.element_offset();
bounds bounds
} }
@ -805,14 +805,22 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let mut root_view = cx.window.root_view.take().unwrap(); let mut root_view = cx.window.root_view.take().unwrap();
if let Some(element_id) = root_view.id() { cx.stack(0, |cx| {
cx.with_element_state(element_id, |element_state, cx| { let available_space = cx.window.content_size.map(Into::into);
let element_state = draw_with_element_state(&mut root_view, element_state, cx); draw_any_view(&mut root_view, available_space, cx);
((), element_state) });
if let Some(mut active_drag) = cx.active_drag.take() {
cx.stack(1, |cx| {
let offset = cx.mouse_position() - active_drag.cursor_offset;
cx.with_element_offset(Some(offset), |cx| {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
draw_any_view(&mut active_drag.drag_handle_view, available_space, cx);
cx.active_drag = Some(active_drag);
});
}); });
} else { }
draw_with_element_state(&mut root_view, None, cx);
};
cx.window.root_view = Some(root_view); cx.window.root_view = Some(root_view);
let scene = cx.window.scene_builder.build(); let scene = cx.window.scene_builder.build();
@ -827,20 +835,21 @@ impl<'a, 'w> WindowContext<'a, 'w> {
.detach(); .detach();
}); });
fn draw_with_element_state( fn draw_any_view(
root_view: &mut AnyView, view: &mut AnyView,
element_state: Option<AnyBox>, available_space: Size<AvailableSpace>,
cx: &mut ViewContext<()>, cx: &mut ViewContext<()>,
) -> AnyBox { ) {
let mut element_state = root_view.initialize(&mut (), element_state, cx); cx.with_optional_element_state(view.id(), |element_state, cx| {
let layout_id = root_view.layout(&mut (), &mut element_state, cx); let mut element_state = view.initialize(&mut (), element_state, cx);
let available_space = cx.window.content_size.map(Into::into); let layout_id = view.layout(&mut (), &mut element_state, cx);
cx.window cx.window
.layout_engine .layout_engine
.compute_layout(layout_id, available_space); .compute_layout(layout_id, available_space);
let bounds = cx.window.layout_engine.layout_bounds(layout_id); let bounds = cx.window.layout_engine.layout_bounds(layout_id);
root_view.paint(bounds, &mut (), &mut element_state, cx); view.paint(bounds, &mut (), &mut element_state, cx);
element_state ((), element_state)
});
} }
} }
@ -916,6 +925,12 @@ impl<'a, 'w> WindowContext<'a, 'w> {
} }
} }
if self.app.propagate_event
&& any_mouse_event.downcast_ref::<MouseUpEvent>().is_some()
{
self.active_drag = None;
}
// Just in case any handlers added new handlers, which is weird, but possible. // Just in case any handlers added new handlers, which is weird, but possible.
handlers.extend( handlers.extend(
self.window self.window
@ -1206,7 +1221,7 @@ pub trait BorrowWindow: BorrowAppContext {
result result
} }
fn with_scroll_offset<R>( fn with_element_offset<R>(
&mut self, &mut self,
offset: Option<Point<Pixels>>, offset: Option<Point<Pixels>>,
f: impl FnOnce(&mut Self) -> R, f: impl FnOnce(&mut Self) -> R,
@ -1215,16 +1230,16 @@ pub trait BorrowWindow: BorrowAppContext {
return f(self); return f(self);
}; };
let offset = self.scroll_offset() + offset; let offset = self.element_offset() + offset;
self.window_mut().scroll_offset_stack.push(offset); self.window_mut().element_offset_stack.push(offset);
let result = f(self); let result = f(self);
self.window_mut().scroll_offset_stack.pop(); self.window_mut().element_offset_stack.pop();
result result
} }
fn scroll_offset(&self) -> Point<Pixels> { fn element_offset(&self) -> Point<Pixels> {
self.window() self.window()
.scroll_offset_stack .element_offset_stack
.last() .last()
.copied() .copied()
.unwrap_or_default() .unwrap_or_default()
@ -1263,6 +1278,18 @@ pub trait BorrowWindow: BorrowAppContext {
}) })
} }
fn with_optional_element_state<S: 'static + Send + Sync, R>(
&mut self,
element_id: Option<ElementId>,
f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
) -> R {
if let Some(element_id) = element_id {
self.with_element_state(element_id, f)
} else {
f(None, self).0
}
}
fn content_mask(&self) -> ContentMask<Pixels> { fn content_mask(&self) -> ContentMask<Pixels> {
self.window() self.window()
.content_mask_stack .content_mask_stack

253
crates/ui2/src/color.rs Normal file
View file

@ -0,0 +1,253 @@
pub use crate::{theme, ButtonVariant, ElementExt, Theme};
use gpui2::{hsla, rgb, Hsla, WindowContext};
use strum::EnumIter;
#[derive(Clone, Copy)]
pub struct PlayerThemeColors {
pub cursor: Hsla,
pub selection: Hsla,
}
impl PlayerThemeColors {
pub fn new(cx: &WindowContext, ix: usize) -> Self {
let theme = theme(cx);
if ix < theme.players.len() {
Self {
cursor: theme.players[ix].cursor,
selection: theme.players[ix].selection,
}
} else {
Self {
cursor: rgb::<Hsla>(0xff00ff),
selection: rgb::<Hsla>(0xff00ff),
}
}
}
}
#[derive(Clone, Copy)]
pub struct SyntaxColor {
pub comment: Hsla,
pub string: Hsla,
pub function: Hsla,
pub keyword: Hsla,
}
impl SyntaxColor {
pub fn new(cx: &WindowContext) -> Self {
let theme = theme(cx);
Self {
comment: theme
.syntax
.get("comment")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
string: theme
.syntax
.get("string")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
function: theme
.syntax
.get("function")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
keyword: theme
.syntax
.get("keyword")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
}
}
}
/// ThemeColor is the primary interface for coloring elements in the UI.
///
/// It is a mapping layer between semantic theme colors and colors from the reference library.
///
/// While we are between zed and zed2 we use this to map semantic colors to the old theme.
#[derive(Clone, Copy)]
pub struct ThemeColor {
pub transparent: Hsla,
pub mac_os_traffic_light_red: Hsla,
pub mac_os_traffic_light_yellow: Hsla,
pub mac_os_traffic_light_green: Hsla,
pub border: Hsla,
pub border_variant: Hsla,
pub border_focused: Hsla,
pub border_transparent: Hsla,
/// The background color of an elevated surface, like a modal, tooltip or toast.
pub elevated_surface: Hsla,
pub surface: Hsla,
/// Window background color of the base app
pub background: Hsla,
/// Default background for elements like filled buttons,
/// text fields, checkboxes, radio buttons, etc.
/// - TODO: Map to step 3.
pub filled_element: Hsla,
/// The background color of a hovered element, like a button being hovered
/// with a mouse, or hovered on a touch screen.
/// - TODO: Map to step 4.
pub filled_element_hover: Hsla,
/// The background color of an active element, like a button being pressed,
/// or tapped on a touch screen.
/// - TODO: Map to step 5.
pub filled_element_active: Hsla,
/// The background color of a selected element, like a selected tab,
/// a button toggled on, or a checkbox that is checked.
pub filled_element_selected: Hsla,
pub filled_element_disabled: Hsla,
pub ghost_element: Hsla,
/// The background color of a hovered element with no default background,
/// like a ghost-style button or an interactable list item.
/// - TODO: Map to step 3.
pub ghost_element_hover: Hsla,
/// - TODO: Map to step 4.
pub ghost_element_active: Hsla,
pub ghost_element_selected: Hsla,
pub ghost_element_disabled: Hsla,
pub text: Hsla,
pub text_muted: Hsla,
pub text_placeholder: Hsla,
pub text_disabled: Hsla,
pub text_accent: Hsla,
pub icon_muted: Hsla,
pub syntax: SyntaxColor,
pub status_bar: Hsla,
pub title_bar: Hsla,
pub toolbar: Hsla,
pub tab_bar: Hsla,
/// The background of the editor
pub editor: Hsla,
pub editor_subheader: Hsla,
pub editor_active_line: Hsla,
pub terminal: Hsla,
pub image_fallback_background: Hsla,
pub git_created: Hsla,
pub git_modified: Hsla,
pub git_deleted: Hsla,
pub git_conflict: Hsla,
pub git_ignored: Hsla,
pub git_renamed: Hsla,
pub player: [PlayerThemeColors; 8],
}
impl ThemeColor {
pub fn new(cx: &WindowContext) -> Self {
let theme = theme(cx);
let transparent = hsla(0.0, 0.0, 0.0, 0.0);
let players = [
PlayerThemeColors::new(cx, 0),
PlayerThemeColors::new(cx, 1),
PlayerThemeColors::new(cx, 2),
PlayerThemeColors::new(cx, 3),
PlayerThemeColors::new(cx, 4),
PlayerThemeColors::new(cx, 5),
PlayerThemeColors::new(cx, 6),
PlayerThemeColors::new(cx, 7),
];
Self {
transparent,
mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
border: theme.lowest.base.default.border,
border_variant: theme.lowest.variant.default.border,
border_focused: theme.lowest.accent.default.border,
border_transparent: transparent,
elevated_surface: theme.lowest.base.default.background,
surface: theme.middle.base.default.background,
background: theme.lowest.base.default.background,
filled_element: theme.lowest.base.default.background,
filled_element_hover: theme.lowest.base.hovered.background,
filled_element_active: theme.lowest.base.active.background,
filled_element_selected: theme.lowest.accent.default.background,
filled_element_disabled: transparent,
ghost_element: transparent,
ghost_element_hover: theme.lowest.base.default.background,
ghost_element_active: theme.lowest.base.hovered.background,
ghost_element_selected: theme.lowest.accent.default.background,
ghost_element_disabled: transparent,
text: theme.lowest.base.default.foreground,
text_muted: theme.lowest.variant.default.foreground,
/// TODO: map this to a real value
text_placeholder: theme.lowest.negative.default.foreground,
text_disabled: theme.lowest.base.disabled.foreground,
text_accent: theme.lowest.accent.default.foreground,
icon_muted: theme.lowest.variant.default.foreground,
syntax: SyntaxColor::new(cx),
status_bar: theme.lowest.base.default.background,
title_bar: theme.lowest.base.default.background,
toolbar: theme.highest.base.default.background,
tab_bar: theme.middle.base.default.background,
editor: theme.highest.base.default.background,
editor_subheader: theme.middle.base.default.background,
terminal: theme.highest.base.default.background,
editor_active_line: theme.highest.on.default.background,
image_fallback_background: theme.lowest.base.default.background,
git_created: theme.lowest.positive.default.foreground,
git_modified: theme.lowest.accent.default.foreground,
git_deleted: theme.lowest.negative.default.foreground,
git_conflict: theme.lowest.warning.default.foreground,
git_ignored: theme.lowest.base.disabled.foreground,
git_renamed: theme.lowest.warning.default.foreground,
player: players,
}
}
}
/// Colors used exclusively for syntax highlighting.
///
/// For now we deserialize these from a theme.
/// These will be defined statically in the new theme.
#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
pub enum HighlightColor {
#[default]
Default,
Comment,
String,
Function,
Keyword,
}
impl HighlightColor {
pub fn hsla(&self, theme: &Theme) -> Hsla {
match self {
Self::Default => theme
.syntax
.get("primary")
.cloned()
.expect("Couldn't find `primary` in theme.syntax"),
Self::Comment => theme
.syntax
.get("comment")
.cloned()
.expect("Couldn't find `comment` in theme.syntax"),
Self::String => theme
.syntax
.get("string")
.cloned()
.expect("Couldn't find `string` in theme.syntax"),
Self::Function => theme
.syntax
.get("function")
.cloned()
.expect("Couldn't find `function` in theme.syntax"),
Self::Keyword => theme
.syntax
.get("keyword")
.cloned()
.expect("Couldn't find `keyword` in theme.syntax"),
}
}
}

View file

@ -36,7 +36,6 @@ impl<S: 'static + Send + Sync> Breadcrumb<S> {
cx: &mut ViewContext<S>, cx: &mut ViewContext<S>,
) -> impl Element<ViewState = S> { ) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let color = ThemeColor::new(cx);
let symbols_len = self.symbols.len(); let symbols_len = self.symbols.len();

View file

@ -159,13 +159,12 @@ impl<S: 'static + Send + Sync + Clone> Buffer<S> {
} }
fn render_row(row: BufferRow, cx: &WindowContext) -> impl Element<ViewState = S> { fn render_row(row: BufferRow, cx: &WindowContext) -> impl Element<ViewState = S> {
let system_color = SystemColor::new();
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let line_background = if row.current { let line_background = if row.current {
color.editor_active_line color.editor_active_line
} else { } else {
system_color.transparent color.transparent
}; };
let line_number_color = if row.current { let line_number_color = if row.current {

View file

@ -22,7 +22,6 @@ impl<S: 'static + Send + Sync> CollabPanel<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let color = ThemeColor::new(cx);
v_stack() v_stack()
.id(self.id.clone()) .id(self.id.clone())

View file

@ -94,8 +94,6 @@ impl<S: 'static + Send + Sync> ListHeader<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let system_color = SystemColor::new();
let color = ThemeColor::new(cx);
let is_toggleable = self.toggleable != Toggleable::NotToggleable; let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = self.toggleable.is_toggled(); let is_toggled = self.toggleable.is_toggled();
@ -373,7 +371,6 @@ impl<S: 'static + Send + Sync> ListEntry<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let system_color = SystemColor::new();
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let settings = user_settings(cx); let settings = user_settings(cx);

View file

@ -24,7 +24,6 @@ pub struct Pane<S: 'static + Send + Sync> {
impl<S: 'static + Send + Sync> Pane<S> { impl<S: 'static + Send + Sync> Pane<S> {
pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self { pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
// Fill is only here for debugging purposes, remove before release // Fill is only here for debugging purposes, remove before release
let system_color = SystemColor::new();
Self { Self {
id: id.into(), id: id.into(),

View file

@ -18,7 +18,7 @@ impl<S: 'static + Send + Sync> PlayerStack<S> {
} }
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let system_color = SystemColor::new(); let color = ThemeColor::new(cx);
let player = self.player_with_call_status.get_player(); let player = self.player_with_call_status.get_player();
self.player_with_call_status.get_call_status(); self.player_with_call_status.get_call_status();
@ -54,7 +54,7 @@ impl<S: 'static + Send + Sync> PlayerStack<S> {
.pl_1() .pl_1()
.rounded_lg() .rounded_lg()
.bg(if followers.is_none() { .bg(if followers.is_none() {
system_color.transparent color.transparent
} else { } else {
player.selection_color(cx) player.selection_color(cx)
}) })

View file

@ -21,7 +21,6 @@ impl<S: 'static + Send + Sync> ProjectPanel<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let color = ThemeColor::new(cx);
div() div()
.id(self.id.clone()) .id(self.id.clone())

View file

@ -17,6 +17,11 @@ pub struct Tab<S: 'static + Send + Sync + Clone> {
close_side: IconSide, close_side: IconSide,
} }
#[derive(Clone, Debug)]
struct TabDragState {
title: String,
}
impl<S: 'static + Send + Sync + Clone> Tab<S> { impl<S: 'static + Send + Sync + Clone> Tab<S> {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
@ -111,8 +116,19 @@ impl<S: 'static + Send + Sync + Clone> Tab<S> {
), ),
}; };
let drag_state = TabDragState {
title: self.title.clone(),
};
div() div()
.id(self.id.clone()) .id(self.id.clone())
.on_drag(move |_view, _cx| {
Drag::new(drag_state.clone(), |view, cx| div().w_8().h_4().bg(red()))
})
.drag_over::<TabDragState>(|d| d.bg(black()))
.on_drop(|_view, state: TabDragState, cx| {
dbg!(state);
})
.px_2() .px_2()
.py_0p5() .py_0p5()
.flex() .flex()
@ -148,7 +164,7 @@ impl<S: 'static + Send + Sync + Clone> Tab<S> {
} }
} }
use gpui2::ElementId; use gpui2::{black, red, Drag, ElementId};
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
pub use stories::*; pub use stories::*;

View file

@ -88,7 +88,6 @@ impl TitleBar {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
let color = ThemeColor::new(cx);
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let settings = user_settings(cx); let settings = user_settings(cx);

View file

@ -1,7 +1,6 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use crate::prelude::*; use crate::prelude::*;
use crate::SystemColor;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum TrafficLightColor { enum TrafficLightColor {
@ -28,12 +27,11 @@ impl<S: 'static + Send + Sync> TrafficLight<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let system_color = SystemColor::new();
let fill = match (self.window_has_focus, self.color) { let fill = match (self.window_has_focus, self.color) {
(true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red, (true, TrafficLightColor::Red) => color.mac_os_traffic_light_red,
(true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow, (true, TrafficLightColor::Yellow) => color.mac_os_traffic_light_yellow,
(true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green, (true, TrafficLightColor::Green) => color.mac_os_traffic_light_green,
(false, _) => color.filled_element, (false, _) => color.filled_element,
}; };

View file

@ -62,7 +62,6 @@ impl<S: 'static + Send + Sync> Input<S> {
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> { fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let system_color = SystemColor::new();
let (input_bg, input_hover_bg, input_active_bg) = match self.variant { let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
InputVariant::Ghost => ( InputVariant::Ghost => (
@ -95,7 +94,7 @@ impl<S: 'static + Send + Sync> Input<S> {
.w_full() .w_full()
.px_2() .px_2()
.border() .border()
.border_color(system_color.transparent) .border_color(color.transparent)
.bg(input_bg) .bg(input_bg)
.hover(|style| style.bg(input_hover_bg)) .hover(|style| style.bg(input_hover_bg))
.active(|style| style.bg(input_active_bg)) .active(|style| style.bg(input_active_bg))

View file

@ -1,5 +1,26 @@
//! # UI Zed UI Primitives & Components
//!
//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI.
//!
//! ## Work in Progress
//!
//! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen,
//! much of the state and functionality is mocked or hard codeded, and performance has not been a focus.
//!
//! Expect some inconsistencies from component to component as we work out the best way to build these components.
//!
//! ## Getting Started
//!
//! - [ThemeColor](crate::color::ThemeColor) is your one stop shop for all colors in the UI.
//!
//! ## Design Philosophy
//!
//! Work in Progress!
//!
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
mod color;
mod components; mod components;
mod element_ext; mod element_ext;
mod elements; mod elements;

View file

@ -3,282 +3,13 @@ pub use gpui2::{
StatelessInteractive, Styled, ViewContext, WindowContext, StatelessInteractive, Styled, ViewContext, WindowContext,
}; };
pub use crate::color::*;
use crate::settings::user_settings; use crate::settings::user_settings;
pub use crate::{theme, ButtonVariant, ElementExt, Theme}; pub use crate::{theme, ButtonVariant, ElementExt, Theme};
use gpui2::{hsla, rems, rgb, Hsla, Rems}; use gpui2::{rems, Hsla, Rems};
use strum::EnumIter; use strum::EnumIter;
// TODO Remove uses in favor of ThemeColor
#[derive(Default)]
pub struct SystemColor {
pub transparent: Hsla,
pub mac_os_traffic_light_red: Hsla,
pub mac_os_traffic_light_yellow: Hsla,
pub mac_os_traffic_light_green: Hsla,
pub state_hover_background: Hsla,
pub state_active_background: Hsla,
}
impl SystemColor {
pub fn new() -> SystemColor {
SystemColor {
transparent: hsla(0.0, 0.0, 0.0, 0.0),
mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
}
}
pub fn color(&self) -> Hsla {
self.transparent
}
}
#[derive(Clone, Copy)]
pub struct PlayerThemeColors {
pub cursor: Hsla,
pub selection: Hsla,
}
impl PlayerThemeColors {
pub fn new(cx: &WindowContext, ix: usize) -> Self {
let theme = theme(cx);
if ix < theme.players.len() {
Self {
cursor: theme.players[ix].cursor,
selection: theme.players[ix].selection,
}
} else {
Self {
cursor: rgb::<Hsla>(0xff00ff),
selection: rgb::<Hsla>(0xff00ff),
}
}
}
}
#[derive(Clone, Copy)]
pub struct SyntaxColor {
pub comment: Hsla,
pub string: Hsla,
pub function: Hsla,
pub keyword: Hsla,
}
impl SyntaxColor {
pub fn new(cx: &WindowContext) -> Self {
let theme = theme(cx);
Self {
comment: theme
.syntax
.get("comment")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
string: theme
.syntax
.get("string")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
function: theme
.syntax
.get("function")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
keyword: theme
.syntax
.get("keyword")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
}
}
}
#[derive(Clone, Copy)]
pub struct ThemeColor {
pub transparent: Hsla,
pub mac_os_traffic_light_red: Hsla,
pub mac_os_traffic_light_yellow: Hsla,
pub mac_os_traffic_light_green: Hsla,
pub border: Hsla,
pub border_variant: Hsla,
pub border_focused: Hsla,
pub border_transparent: Hsla,
/// The background color of an elevated surface, like a modal, tooltip or toast.
pub elevated_surface: Hsla,
pub surface: Hsla,
/// Window background color of the base app
pub background: Hsla,
/// Default background for elements like filled buttons,
/// text fields, checkboxes, radio buttons, etc.
/// - TODO: Map to step 3.
pub filled_element: Hsla,
/// The background color of a hovered element, like a button being hovered
/// with a mouse, or hovered on a touch screen.
/// - TODO: Map to step 4.
pub filled_element_hover: Hsla,
/// The background color of an active element, like a button being pressed,
/// or tapped on a touch screen.
/// - TODO: Map to step 5.
pub filled_element_active: Hsla,
/// The background color of a selected element, like a selected tab,
/// a button toggled on, or a checkbox that is checked.
pub filled_element_selected: Hsla,
pub filled_element_disabled: Hsla,
pub ghost_element: Hsla,
/// The background color of a hovered element with no default background,
/// like a ghost-style button or an interactable list item.
/// - TODO: Map to step 3.
pub ghost_element_hover: Hsla,
/// - TODO: Map to step 4.
pub ghost_element_active: Hsla,
pub ghost_element_selected: Hsla,
pub ghost_element_disabled: Hsla,
pub text: Hsla,
pub text_muted: Hsla,
pub text_placeholder: Hsla,
pub text_disabled: Hsla,
pub text_accent: Hsla,
pub icon_muted: Hsla,
pub syntax: SyntaxColor,
pub status_bar: Hsla,
pub title_bar: Hsla,
pub toolbar: Hsla,
pub tab_bar: Hsla,
/// The background of the editor
pub editor: Hsla,
pub editor_subheader: Hsla,
pub editor_active_line: Hsla,
pub terminal: Hsla,
pub image_fallback_background: Hsla,
pub git_created: Hsla,
pub git_modified: Hsla,
pub git_deleted: Hsla,
pub git_conflict: Hsla,
pub git_ignored: Hsla,
pub git_renamed: Hsla,
pub player: [PlayerThemeColors; 8],
}
impl ThemeColor {
pub fn new(cx: &WindowContext) -> Self {
let theme = theme(cx);
let system_color = SystemColor::new();
let players = [
PlayerThemeColors::new(cx, 0),
PlayerThemeColors::new(cx, 1),
PlayerThemeColors::new(cx, 2),
PlayerThemeColors::new(cx, 3),
PlayerThemeColors::new(cx, 4),
PlayerThemeColors::new(cx, 5),
PlayerThemeColors::new(cx, 6),
PlayerThemeColors::new(cx, 7),
];
Self {
transparent: hsla(0.0, 0.0, 0.0, 0.0),
mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
border: theme.lowest.base.default.border,
border_variant: theme.lowest.variant.default.border,
border_focused: theme.lowest.accent.default.border,
border_transparent: system_color.transparent,
elevated_surface: theme.lowest.base.default.background,
surface: theme.middle.base.default.background,
background: theme.lowest.base.default.background,
filled_element: theme.lowest.base.default.background,
filled_element_hover: theme.lowest.base.hovered.background,
filled_element_active: theme.lowest.base.active.background,
filled_element_selected: theme.lowest.accent.default.background,
filled_element_disabled: system_color.transparent,
ghost_element: system_color.transparent,
ghost_element_hover: theme.lowest.base.default.background,
ghost_element_active: theme.lowest.base.hovered.background,
ghost_element_selected: theme.lowest.accent.default.background,
ghost_element_disabled: system_color.transparent,
text: theme.lowest.base.default.foreground,
text_muted: theme.lowest.variant.default.foreground,
/// TODO: map this to a real value
text_placeholder: theme.lowest.negative.default.foreground,
text_disabled: theme.lowest.base.disabled.foreground,
text_accent: theme.lowest.accent.default.foreground,
icon_muted: theme.lowest.variant.default.foreground,
syntax: SyntaxColor::new(cx),
status_bar: theme.lowest.base.default.background,
title_bar: theme.lowest.base.default.background,
toolbar: theme.highest.base.default.background,
tab_bar: theme.middle.base.default.background,
editor: theme.highest.base.default.background,
editor_subheader: theme.middle.base.default.background,
terminal: theme.highest.base.default.background,
editor_active_line: theme.highest.on.default.background,
image_fallback_background: theme.lowest.base.default.background,
git_created: theme.lowest.positive.default.foreground,
git_modified: theme.lowest.accent.default.foreground,
git_deleted: theme.lowest.negative.default.foreground,
git_conflict: theme.lowest.warning.default.foreground,
git_ignored: theme.lowest.base.disabled.foreground,
git_renamed: theme.lowest.warning.default.foreground,
player: players,
}
}
}
#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
pub enum HighlightColor {
#[default]
Default,
Comment,
String,
Function,
Keyword,
}
impl HighlightColor {
pub fn hsla(&self, theme: &Theme) -> Hsla {
let system_color = SystemColor::new();
match self {
Self::Default => theme
.syntax
.get("primary")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
Self::Comment => theme
.syntax
.get("comment")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
Self::String => theme
.syntax
.get("string")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
Self::Function => theme
.syntax
.get("function")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
Self::Keyword => theme
.syntax
.get("keyword")
.cloned()
.unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
}
}
}
pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems { pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems {
const UI_SCALE_RATIO: f32 = 0.875; const UI_SCALE_RATIO: f32 = 0.875;
@ -330,10 +61,9 @@ impl GitStatus {
pub fn hsla(&self, cx: &WindowContext) -> Hsla { pub fn hsla(&self, cx: &WindowContext) -> Hsla {
let color = ThemeColor::new(cx); let color = ThemeColor::new(cx);
let system_color = SystemColor::new();
match self { match self {
Self::None => system_color.transparent, Self::None => color.transparent,
Self::Created => color.git_created, Self::Created => color.git_created,
Self::Modified => color.git_modified, Self::Modified => color.git_modified,
Self::Deleted => color.git_deleted, Self::Deleted => color.git_deleted,