Add support for resizing splits and docks, as well as utilities (#3595)

Making this PR to upstream changes to util and GPUI2, resizing exists
but isn't working yet.

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2023-12-11 13:32:06 -08:00 committed by GitHub
commit 927d18b0f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 976 additions and 512 deletions

View file

@ -391,9 +391,9 @@ impl EditorElement {
let mut click_count = event.click_count; let mut click_count = event.click_count;
let modifiers = event.modifiers; let modifiers = event.modifiers;
if gutter_bounds.contains_point(&event.position) { if gutter_bounds.contains(&event.position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !text_bounds.contains_point(&event.position) { } else if !text_bounds.contains(&event.position) {
return; return;
} }
if !cx.was_top_layer(&event.position, stacking_order) { if !cx.was_top_layer(&event.position, stacking_order) {
@ -439,7 +439,7 @@ impl EditorElement {
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
if !text_bounds.contains_point(&event.position) { if !text_bounds.contains(&event.position) {
return; return;
} }
let point_for_position = position_map.point_for_position(text_bounds, event.position); let point_for_position = position_map.point_for_position(text_bounds, event.position);
@ -469,7 +469,7 @@ impl EditorElement {
if !pending_nonempty_selections if !pending_nonempty_selections
&& event.modifiers.command && event.modifiers.command
&& text_bounds.contains_point(&event.position) && text_bounds.contains(&event.position)
&& cx.was_top_layer(&event.position, stacking_order) && cx.was_top_layer(&event.position, stacking_order)
{ {
let point = position_map.point_for_position(text_bounds, event.position); let point = position_map.point_for_position(text_bounds, event.position);
@ -531,8 +531,8 @@ impl EditorElement {
); );
} }
let text_hovered = text_bounds.contains_point(&event.position); let text_hovered = text_bounds.contains(&event.position);
let gutter_hovered = gutter_bounds.contains_point(&event.position); let gutter_hovered = gutter_bounds.contains(&event.position);
let was_top = cx.was_top_layer(&event.position, stacking_order); let was_top = cx.was_top_layer(&event.position, stacking_order);
editor.set_gutter_hovered(gutter_hovered, cx); editor.set_gutter_hovered(gutter_hovered, cx);
@ -900,7 +900,7 @@ impl EditorElement {
bounds: text_bounds, bounds: text_bounds,
}), }),
|cx| { |cx| {
if text_bounds.contains_point(&cx.mouse_position()) { if text_bounds.contains(&cx.mouse_position()) {
if self if self
.editor .editor
.read(cx) .read(cx)
@ -966,7 +966,7 @@ impl EditorElement {
|fold_element_state, cx| { |fold_element_state, cx| {
if fold_element_state.is_active() { if fold_element_state.is_active() {
gpui::blue() gpui::blue()
} else if fold_bounds.contains_point(&cx.mouse_position()) { } else if fold_bounds.contains(&cx.mouse_position()) {
gpui::black() gpui::black()
} else { } else {
gpui::red() gpui::red()
@ -1377,7 +1377,7 @@ impl EditorElement {
} }
let mouse_position = cx.mouse_position(); let mouse_position = cx.mouse_position();
if track_bounds.contains_point(&mouse_position) { if track_bounds.contains(&mouse_position) {
cx.set_cursor_style(CursorStyle::Arrow); cx.set_cursor_style(CursorStyle::Arrow);
} }
@ -1405,7 +1405,7 @@ impl EditorElement {
cx.stop_propagation(); cx.stop_propagation();
} else { } else {
editor.scroll_manager.set_is_dragging_scrollbar(false, cx); editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
if track_bounds.contains_point(&event.position) { if track_bounds.contains(&event.position) {
editor.scroll_manager.show_scrollbar(cx); editor.scroll_manager.show_scrollbar(cx);
} }
} }
@ -1428,7 +1428,7 @@ impl EditorElement {
let editor = self.editor.clone(); let editor = self.editor.clone();
move |event: &MouseDownEvent, phase, cx| { move |event: &MouseDownEvent, phase, cx| {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
if track_bounds.contains_point(&event.position) { if track_bounds.contains(&event.position) {
editor.scroll_manager.set_is_dragging_scrollbar(true, cx); editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
let y = event.position.y; let y = event.position.y;
@ -2502,10 +2502,10 @@ impl EditorElement {
gutter_bounds, gutter_bounds,
&stacking_order, &stacking_order,
cx, cx,
) );
}), }),
MouseButton::Right => editor.update(cx, |editor, cx| { MouseButton::Right => editor.update(cx, |editor, cx| {
Self::mouse_right_down(editor, event, &position_map, text_bounds, cx) Self::mouse_right_down(editor, event, &position_map, text_bounds, cx);
}), }),
_ => {} _ => {}
}; };

View file

@ -513,6 +513,10 @@ impl AppContext {
self.platform.path_for_auxiliary_executable(name) self.platform.path_for_auxiliary_executable(name)
} }
pub fn double_click_interval(&self) -> Duration {
self.platform.double_click_interval()
}
pub fn prompt_for_paths( pub fn prompt_for_paths(
&self, &self,
options: PathPromptOptions, options: PathPromptOptions,

View file

@ -761,7 +761,7 @@ pub struct InteractiveBounds {
impl InteractiveBounds { impl InteractiveBounds {
pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool { pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order) self.bounds.contains(point) && cx.was_top_layer(&point, &self.stacking_order)
} }
} }
@ -860,10 +860,10 @@ impl Interactivity {
.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 {
let hovered = group_bounds.contains_point(&cx.mouse_position()); let hovered = group_bounds.contains(&cx.mouse_position());
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture { if phase == DispatchPhase::Capture {
if group_bounds.contains_point(&event.position) != hovered { if group_bounds.contains(&event.position) != hovered {
cx.notify(); cx.notify();
} }
} }
@ -875,10 +875,10 @@ impl Interactivity {
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty() || cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
{ {
let bounds = bounds.intersect(&cx.content_mask().bounds); let bounds = bounds.intersect(&cx.content_mask().bounds);
let hovered = bounds.contains_point(&cx.mouse_position()); let hovered = bounds.contains(&cx.mouse_position());
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture { if phase == DispatchPhase::Capture {
if bounds.contains_point(&event.position) != hovered { if bounds.contains(&event.position) != hovered {
cx.notify(); cx.notify();
} }
} }
@ -1068,8 +1068,8 @@ impl Interactivity {
let interactive_bounds = interactive_bounds.clone(); let interactive_bounds = interactive_bounds.clone();
cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble { if phase == DispatchPhase::Bubble {
let group = active_group_bounds let group =
.map_or(false, |bounds| bounds.contains_point(&down.position)); active_group_bounds.map_or(false, |bounds| bounds.contains(&down.position));
let element = interactive_bounds.visibly_contains(&down.position, cx); let element = interactive_bounds.visibly_contains(&down.position, cx);
if group || element { if group || element {
*active_state.borrow_mut() = ElementClickedState { group, element }; *active_state.borrow_mut() = ElementClickedState { group, element };
@ -1183,7 +1183,7 @@ impl Interactivity {
let mouse_position = cx.mouse_position(); let mouse_position = cx.mouse_position();
if let Some(group_hover) = self.group_hover_style.as_ref() { if let Some(group_hover) = self.group_hover_style.as_ref() {
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
if group_bounds.contains_point(&mouse_position) if group_bounds.contains(&mouse_position)
&& cx.was_top_layer(&mouse_position, cx.stacking_order()) && cx.was_top_layer(&mouse_position, cx.stacking_order())
{ {
style.refine(&group_hover.style); style.refine(&group_hover.style);
@ -1193,7 +1193,7 @@ impl Interactivity {
if self.hover_style.is_some() { if self.hover_style.is_some() {
if bounds if bounds
.intersect(&cx.content_mask().bounds) .intersect(&cx.content_mask().bounds)
.contains_point(&mouse_position) .contains(&mouse_position)
&& cx.was_top_layer(&mouse_position, cx.stacking_order()) && cx.was_top_layer(&mouse_position, cx.stacking_order())
{ {
style.refine(&self.hover_style); style.refine(&self.hover_style);
@ -1204,7 +1204,7 @@ impl Interactivity {
for (state_type, group_drag_style) in &self.group_drag_over_styles { for (state_type, group_drag_style) in &self.group_drag_over_styles {
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
if *state_type == drag.view.entity_type() if *state_type == drag.view.entity_type()
&& group_bounds.contains_point(&mouse_position) && group_bounds.contains(&mouse_position)
{ {
style.refine(&group_drag_style.style); style.refine(&group_drag_style.style);
} }
@ -1215,7 +1215,7 @@ impl Interactivity {
if *state_type == drag.view.entity_type() if *state_type == drag.view.entity_type()
&& bounds && bounds
.intersect(&cx.content_mask().bounds) .intersect(&cx.content_mask().bounds)
.contains_point(&mouse_position) .contains(&mouse_position)
{ {
style.refine(drag_over_style); style.refine(drag_over_style);
} }

View file

@ -253,7 +253,7 @@ impl TextState {
} }
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> { fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
if !bounds.contains_point(&position) { if !bounds.contains(&position) {
return None; return None;
} }

View file

@ -57,8 +57,12 @@ where
T: 'static, T: 'static,
E: 'static + Debug, E: 'static + Debug,
{ {
#[track_caller]
pub fn detach_and_log_err(self, cx: &mut AppContext) { pub fn detach_and_log_err(self, cx: &mut AppContext) {
cx.foreground_executor().spawn(self.log_err()).detach(); let location = core::panic::Location::caller();
cx.foreground_executor()
.spawn(self.log_tracked_err(*location))
.detach();
} }
} }

View file

@ -8,6 +8,62 @@ use std::{
ops::{Add, Div, Mul, MulAssign, Sub}, ops::{Add, Div, Mul, MulAssign, Sub},
}; };
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Axis {
Vertical,
Horizontal,
}
impl Axis {
pub fn invert(&self) -> Self {
match self {
Axis::Vertical => Axis::Horizontal,
Axis::Horizontal => Axis::Vertical,
}
}
}
pub trait Along {
type Unit;
fn along(&self, axis: Axis) -> Self::Unit;
fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self;
}
impl sqlez::bindable::StaticColumnCount for Axis {}
impl sqlez::bindable::Bind for Axis {
fn bind(
&self,
statement: &sqlez::statement::Statement,
start_index: i32,
) -> anyhow::Result<i32> {
match self {
Axis::Horizontal => "Horizontal",
Axis::Vertical => "Vertical",
}
.bind(statement, start_index)
}
}
impl sqlez::bindable::Column for Axis {
fn column(
statement: &mut sqlez::statement::Statement,
start_index: i32,
) -> anyhow::Result<(Self, i32)> {
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
Ok((
match axis_text.as_str() {
"Horizontal" => Axis::Horizontal,
"Vertical" => Axis::Vertical,
_ => anyhow::bail!("Stored serialized item kind is incorrect"),
},
next_index,
))
})
}
}
/// Describes a location in a 2D cartesian coordinate space. /// Describes a location in a 2D cartesian coordinate space.
/// ///
/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. /// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
@ -96,6 +152,30 @@ impl<T: Clone + Debug + Default> Point<T> {
} }
} }
impl<T: Clone + Debug + Default> Along for Point<T> {
type Unit = T;
fn along(&self, axis: Axis) -> T {
match axis {
Axis::Horizontal => self.x.clone(),
Axis::Vertical => self.y.clone(),
}
}
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point<T> {
match axis {
Axis::Horizontal => Point {
x: f(self.x.clone()),
y: self.y.clone(),
},
Axis::Vertical => Point {
x: self.x.clone(),
y: f(self.y.clone()),
},
}
}
}
impl Point<Pixels> { impl Point<Pixels> {
/// Scales the point by a given factor, which is typically derived from the resolution /// Scales the point by a given factor, which is typically derived from the resolution
/// of a target display to ensure proper sizing of UI elements. /// of a target display to ensure proper sizing of UI elements.
@ -373,6 +453,34 @@ impl Size<Pixels> {
} }
} }
impl<T> Along for Size<T>
where
T: Clone + Default + Debug,
{
type Unit = T;
fn along(&self, axis: Axis) -> T {
match axis {
Axis::Horizontal => self.width.clone(),
Axis::Vertical => self.height.clone(),
}
}
/// Returns the value of this size along the given axis.
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self {
match axis {
Axis::Horizontal => Size {
width: f(self.width.clone()),
height: self.height.clone(),
},
Axis::Vertical => Size {
width: self.width.clone(),
height: f(self.height.clone()),
},
}
}
}
impl<T> Size<T> impl<T> Size<T>
where where
T: PartialOrd + Clone + Default + Debug, T: PartialOrd + Clone + Default + Debug,
@ -992,7 +1100,7 @@ where
/// assert!(bounds.contains_point(&inside_point)); /// assert!(bounds.contains_point(&inside_point));
/// assert!(!bounds.contains_point(&outside_point)); /// assert!(!bounds.contains_point(&outside_point));
/// ``` /// ```
pub fn contains_point(&self, point: &Point<T>) -> bool { pub fn contains(&self, point: &Point<T>) -> bool {
point.x >= self.origin.x point.x >= self.origin.x
&& point.x <= self.origin.x.clone() + self.size.width.clone() && point.x <= self.origin.x.clone() + self.size.width.clone()
&& point.y >= self.origin.y && point.y >= self.origin.y

View file

@ -131,6 +131,12 @@ pub struct MouseMoveEvent {
pub modifiers: Modifiers, pub modifiers: Modifiers,
} }
impl MouseMoveEvent {
pub fn dragging(&self) -> bool {
self.pressed_button == Some(MouseButton::Left)
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ScrollWheelEvent { pub struct ScrollWheelEvent {
pub position: Point<Pixels>, pub position: Point<Pixels>,

View file

@ -105,6 +105,7 @@ pub(crate) trait Platform: 'static {
fn app_version(&self) -> Result<SemanticVersion>; fn app_version(&self) -> Result<SemanticVersion>;
fn app_path(&self) -> Result<PathBuf>; fn app_path(&self) -> Result<PathBuf>;
fn local_timezone(&self) -> UtcOffset; fn local_timezone(&self) -> UtcOffset;
fn double_click_interval(&self) -> Duration;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>; fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
fn set_cursor_style(&self, style: CursorStyle); fn set_cursor_style(&self, style: CursorStyle);

View file

@ -49,6 +49,7 @@ use std::{
rc::Rc, rc::Rc,
slice, str, slice, str,
sync::Arc, sync::Arc,
time::Duration,
}; };
use time::UtcOffset; use time::UtcOffset;
@ -657,6 +658,13 @@ impl Platform for MacPlatform {
"macOS" "macOS"
} }
fn double_click_interval(&self) -> Duration {
unsafe {
let double_click_interval: f64 = msg_send![class!(NSEvent), doubleClickInterval];
Duration::from_secs_f64(double_click_interval)
}
}
fn os_version(&self) -> Result<SemanticVersion> { fn os_version(&self) -> Result<SemanticVersion> {
unsafe { unsafe {
let process_info = NSProcessInfo::processInfo(nil); let process_info = NSProcessInfo::processInfo(nil);

View file

@ -12,6 +12,7 @@ use std::{
path::PathBuf, path::PathBuf,
rc::{Rc, Weak}, rc::{Rc, Weak},
sync::Arc, sync::Arc,
time::Duration,
}; };
pub struct TestPlatform { pub struct TestPlatform {
@ -274,4 +275,8 @@ impl Platform for TestPlatform {
fn delete_credentials(&self, _url: &str) -> Result<()> { fn delete_credentials(&self, _url: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn double_click_interval(&self) -> std::time::Duration {
Duration::from_millis(500)
}
} }

View file

@ -477,3 +477,12 @@ impl From<Pixels> for AvailableSpace {
AvailableSpace::Definite(pixels) AvailableSpace::Definite(pixels)
} }
} }
impl From<Size<Pixels>> for Size<AvailableSpace> {
fn from(size: Size<Pixels>) -> Self {
Size {
width: AvailableSpace::Definite(size.width),
height: AvailableSpace::Definite(size.height),
}
}
}

View file

@ -60,6 +60,16 @@ pub enum DispatchPhase {
Capture, Capture,
} }
impl DispatchPhase {
pub fn bubble(self) -> bool {
self == DispatchPhase::Bubble
}
pub fn capture(self) -> bool {
self == DispatchPhase::Capture
}
}
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>; type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>; type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>; type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>;
@ -866,7 +876,7 @@ impl<'a> WindowContext<'a> {
/// same layer as the given stacking order. /// same layer as the given stacking order.
pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool { pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
for (stack, bounds) in self.window.rendered_frame.depth_map.iter() { for (stack, bounds) in self.window.rendered_frame.depth_map.iter() {
if bounds.contains_point(point) { if bounds.contains(point) {
return level.starts_with(stack) || stack.starts_with(level); return level.starts_with(stack) || stack.starts_with(level);
} }
} }

View file

@ -137,7 +137,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right && event.button == MouseButton::Right
&& bounds.contains_point(&event.position) && bounds.contains(&event.position)
{ {
cx.stop_propagation(); cx.stop_propagation();
cx.prevent_default(); cx.prevent_default();

View file

@ -184,6 +184,11 @@ pub trait TryFutureExt {
fn log_err(self) -> LogErrorFuture<Self> fn log_err(self) -> LogErrorFuture<Self>
where where
Self: Sized; Self: Sized;
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
where
Self: Sized;
fn warn_on_err(self) -> LogErrorFuture<Self> fn warn_on_err(self) -> LogErrorFuture<Self>
where where
Self: Sized; Self: Sized;
@ -197,18 +202,29 @@ where
F: Future<Output = Result<T, E>>, F: Future<Output = Result<T, E>>,
E: std::fmt::Debug, E: std::fmt::Debug,
{ {
#[track_caller]
fn log_err(self) -> LogErrorFuture<Self> fn log_err(self) -> LogErrorFuture<Self>
where where
Self: Sized, Self: Sized,
{ {
LogErrorFuture(self, log::Level::Error) let location = Location::caller();
LogErrorFuture(self, log::Level::Error, *location)
} }
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
where
Self: Sized,
{
LogErrorFuture(self, log::Level::Error, location)
}
#[track_caller]
fn warn_on_err(self) -> LogErrorFuture<Self> fn warn_on_err(self) -> LogErrorFuture<Self>
where where
Self: Sized, Self: Sized,
{ {
LogErrorFuture(self, log::Level::Warn) let location = Location::caller();
LogErrorFuture(self, log::Level::Warn, *location)
} }
fn unwrap(self) -> UnwrapFuture<Self> fn unwrap(self) -> UnwrapFuture<Self>
@ -219,7 +235,7 @@ where
} }
} }
pub struct LogErrorFuture<F>(F, log::Level); pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
impl<F, T, E> Future for LogErrorFuture<F> impl<F, T, E> Future for LogErrorFuture<F>
where where
@ -230,12 +246,19 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let level = self.1; let level = self.1;
let location = self.2;
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
match inner.poll(cx) { match inner.poll(cx) {
Poll::Ready(output) => Poll::Ready(match output { Poll::Ready(output) => Poll::Ready(match output {
Ok(output) => Some(output), Ok(output) => Some(output),
Err(error) => { Err(error) => {
log::log!(level, "{:?}", error); log::log!(
level,
"{}:{}: {:?}",
location.file(),
location.line(),
error
);
None None
} }
}), }),

View file

@ -1,8 +1,9 @@
use crate::{status_bar::StatusItemView, Axis, Workspace}; use crate::{status_bar::StatusItemView, Workspace};
use crate::{DockClickReset, DockDragState};
use gpui::{ use gpui::{
div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled, EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -364,7 +365,7 @@ impl Dock {
this.set_open(false, cx); this.set_open(false, cx);
} }
} }
PanelEvent::Focus => todo!(), PanelEvent::Focus => {}
}), }),
]; ];
@ -485,6 +486,48 @@ impl Render for Dock {
if let Some(entry) = self.visible_entry() { if let Some(entry) = self.visible_entry() {
let size = entry.panel.size(cx); let size = entry.panel.size(cx);
let mut pre_resize_handle = None;
let mut post_resize_handle = None;
let position = self.position;
let handler = div()
.id("resize-handle")
.bg(cx.theme().colors().border)
.on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
})
.on_click(cx.listener(|v, e: &ClickEvent, cx| {
if e.down.button == MouseButton::Left {
cx.update_global(|state: &mut DockClickReset, cx| {
if state.0.is_some() {
state.0 = None;
v.resize_active_panel(None, cx)
} else {
let double_click = cx.double_click_interval();
let timer = cx.background_executor().timer(double_click);
state.0 = Some(cx.spawn(|_, mut cx| async move {
timer.await;
cx.update_global(|state: &mut DockClickReset, cx| {
state.0 = None;
})
.ok();
}));
}
})
}
}));
match self.position() {
DockPosition::Left => {
post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
}
DockPosition::Bottom => {
pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
}
DockPosition::Right => {
pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
}
}
div() div()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.map(|this| match self.position().axis() { .map(|this| match self.position().axis() {
@ -496,7 +539,9 @@ impl Render for Dock {
DockPosition::Right => this.border_l(), DockPosition::Right => this.border_l(),
DockPosition::Bottom => this.border_t(), DockPosition::Bottom => this.border_t(),
}) })
.children(pre_resize_handle)
.child(entry.panel.to_any()) .child(entry.panel.to_any())
.children(post_resize_handle)
} else { } else {
div() div()
} }

View file

@ -1046,10 +1046,11 @@ impl Pane {
{ {
pane.remove_item(item_ix, false, cx); pane.remove_item(item_ix, false, cx);
} }
})?; })
.ok();
} }
pane.update(&mut cx, |_, cx| cx.notify())?; pane.update(&mut cx, |_, cx| cx.notify()).ok();
Ok(()) Ok(())
}) })
} }

View file

@ -1,13 +1,9 @@
use crate::{AppState, FollowerState, Pane, Workspace}; use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation}; use call::{ActiveCall, ParticipantLocation};
use collections::HashMap; use collections::HashMap;
use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{ use gpui::{
point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View, point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View,
ViewContext, ViewContext,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
@ -16,42 +12,10 @@ use serde::Deserialize;
use std::sync::Arc; use std::sync::Arc;
use ui::{prelude::*, Button}; use ui::{prelude::*, Button};
const HANDLE_HITBOX_SIZE: f32 = 4.0; const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
const HORIZONTAL_MIN_SIZE: f32 = 80.; const HORIZONTAL_MIN_SIZE: f32 = 80.;
const VERTICAL_MIN_SIZE: f32 = 100.; const VERTICAL_MIN_SIZE: f32 = 100.;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Axis {
Vertical,
Horizontal,
}
impl StaticColumnCount for Axis {}
impl Bind for Axis {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
match self {
Axis::Horizontal => "Horizontal",
Axis::Vertical => "Vertical",
}
.bind(statement, start_index)
}
}
impl Column for Axis {
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
Ok((
match axis_text.as_str() {
"Horizontal" => Axis::Horizontal,
"Vertical" => Axis::Vertical,
_ => bail!("Stored serialized item kind is incorrect"),
},
next_index,
))
})
}
}
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct PaneGroup { pub struct PaneGroup {
pub(crate) root: Member, pub(crate) root: Member,
@ -612,7 +576,7 @@ impl PaneAxis {
for (idx, member) in self.members.iter().enumerate() { for (idx, member) in self.members.iter().enumerate() {
if let Some(coordinates) = bounding_boxes[idx] { if let Some(coordinates) = bounding_boxes[idx] {
if coordinates.contains_point(&coordinate) { if coordinates.contains(&coordinate) {
return match member { return match member {
Member::Pane(found) => Some(found), Member::Pane(found) => Some(found),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
@ -632,22 +596,26 @@ impl PaneAxis {
zoomed: Option<&AnyWeakView>, zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>, app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> Div { ) -> gpui::AnyElement {
debug_assert!(self.members.len() == self.flexes.lock().len()); debug_assert!(self.members.len() == self.flexes.lock().len());
let mut active_pane_ix = None;
div() pane_axis(
.flex() self.axis,
.flex_auto() basis,
.map(|s| match self.axis { self.flexes.clone(),
Axis::Vertical => s.flex_col(), self.bounding_boxes.clone(),
Axis::Horizontal => s.flex_row(), )
})
.children(self.members.iter().enumerate().map(|(ix, member)| { .children(self.members.iter().enumerate().map(|(ix, member)| {
if member.contains(active_pane) {
active_pane_ix = Some(ix);
}
match member { match member {
Member::Axis(axis) => axis Member::Axis(axis) => axis
.render( .render(
project, project,
basis, (basis + ix) * 10,
follower_states, follower_states,
active_pane, active_pane,
zoomed, zoomed,
@ -655,57 +623,15 @@ impl PaneAxis {
cx, cx,
) )
.into_any_element(), .into_any_element(),
Member::Pane(pane) => pane.clone().into_any_element(), Member::Pane(pane) => div()
.size_full()
.border()
.child(pane.clone())
.into_any_element(),
} }
})) }))
.with_active_pane(active_pane_ix)
// let mut pane_axis = PaneAxisElement::new( .into_any_element()
// self.axis,
// basis,
// self.flexes.clone(),
// self.bounding_boxes.clone(),
// );
// let mut active_pane_ix = None;
// let mut members = self.members.iter().enumerate().peekable();
// while let Some((ix, member)) = members.next() {
// let last = members.peek().is_none();
// if member.contains(active_pane) {
// active_pane_ix = Some(ix);
// }
// let mut member = member.render(
// project,
// (basis + ix) * 10,
// theme,
// follower_states,
// active_call,
// active_pane,
// zoomed,
// app_state,
// cx,
// );
// if !last {
// let mut border = theme.workspace.pane_divider;
// border.left = false;
// border.right = false;
// border.top = false;
// border.bottom = false;
// match self.axis {
// Axis::Vertical => border.bottom = true,
// Axis::Horizontal => border.right = true,
// }
// member = member.contained().with_border(border).into_any();
// }
// pane_axis = pane_axis.with_child(member.into_any());
// }
// pane_axis.set_active_pane(active_pane_ix);
// pane_axis.into_any()
} }
} }
@ -767,7 +693,281 @@ impl SplitDirection {
} }
} }
// mod element { mod element {
use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use gpui::{
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Style, WindowContext,
};
use parking_lot::Mutex;
use smallvec::SmallVec;
use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
pub fn pane_axis(
axis: Axis,
basis: usize,
flexes: Arc<Mutex<Vec<f32>>>,
bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
) -> PaneAxisElement {
PaneAxisElement {
axis,
basis,
flexes,
bounding_boxes,
children: SmallVec::new(),
active_pane_ix: None,
}
}
pub struct PaneAxisElement {
axis: Axis,
basis: usize,
flexes: Arc<Mutex<Vec<f32>>>,
bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
children: SmallVec<[AnyElement; 2]>,
active_pane_ix: Option<usize>,
}
impl PaneAxisElement {
pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
self.active_pane_ix = active_pane_ix;
self
}
fn compute_resize(
flexes: &Arc<Mutex<Vec<f32>>>,
e: &MouseMoveEvent,
ix: usize,
axis: Axis,
axis_bounds: Bounds<Pixels>,
cx: &mut WindowContext,
) {
let min_size = match axis {
Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
Axis::Vertical => px(VERTICAL_MIN_SIZE),
};
let mut flexes = flexes.lock();
debug_assert!(flex_values_in_bounds(flexes.as_slice()));
let size = move |ix, flexes: &[f32]| {
axis_bounds.size.along(axis) * (flexes[ix] / flexes.len() as f32)
};
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - px(1.) > size(ix, flexes.as_slice()) {
return;
}
let mut proposed_current_pixel_change =
(e.position - axis_bounds.origin).along(axis) - size(ix, flexes.as_slice());
let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
let flex_change = pixel_dx / axis_bounds.size.along(axis);
let current_target_flex = flexes[target_ix] + flex_change;
let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
(current_target_flex, next_target_flex)
};
let mut successors = iter::from_fn({
let forward = proposed_current_pixel_change > px(0.);
let mut ix_offset = 0;
let len = flexes.len();
move || {
let result = if forward {
(ix + 1 + ix_offset < len).then(|| ix + ix_offset)
} else {
(ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
};
ix_offset += 1;
result
}
});
while proposed_current_pixel_change.abs() > px(0.) {
let Some(current_ix) = successors.next() else {
break;
};
let next_target_size = Pixels::max(
size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
min_size,
);
let current_target_size = Pixels::max(
size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
- next_target_size,
min_size,
);
let current_pixel_change =
current_target_size - size(current_ix, flexes.as_slice());
let (current_target_flex, next_target_flex) =
flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
flexes[current_ix] = current_target_flex;
flexes[current_ix + 1] = next_target_flex;
proposed_current_pixel_change -= current_pixel_change;
}
// todo!(reserialize workspace)
// workspace.schedule_serialize(cx);
cx.notify();
}
fn push_handle(
flexes: Arc<Mutex<Vec<f32>>>,
dragged_handle: Rc<RefCell<Option<usize>>>,
axis: Axis,
ix: usize,
pane_bounds: Bounds<Pixels>,
axis_bounds: Bounds<Pixels>,
cx: &mut WindowContext,
) {
let handle_bounds = Bounds {
origin: pane_bounds.origin.apply_along(axis, |o| {
o + pane_bounds.size.along(axis) - Pixels(HANDLE_HITBOX_SIZE / 2.)
}),
size: pane_bounds
.size
.apply_along(axis, |_| Pixels(HANDLE_HITBOX_SIZE)),
};
cx.with_z_index(3, |cx| {
if handle_bounds.contains(&cx.mouse_position()) {
cx.set_cursor_style(match axis {
Axis::Vertical => CursorStyle::ResizeUpDown,
Axis::Horizontal => CursorStyle::ResizeLeftRight,
})
}
cx.add_opaque_layer(handle_bounds);
cx.on_mouse_event({
let dragged_handle = dragged_handle.clone();
move |e: &MouseDownEvent, phase, cx| {
if phase.bubble() && handle_bounds.contains(&e.position) {
dragged_handle.replace(Some(ix));
}
}
});
cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
let dragged_handle = dragged_handle.borrow();
if *dragged_handle == Some(ix) {
Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
}
});
});
}
}
impl IntoElement for PaneAxisElement {
type Element = Self;
fn element_id(&self) -> Option<ui::prelude::ElementId> {
Some(self.basis.into())
}
fn into_element(self) -> Self::Element {
self
}
}
impl Element for PaneAxisElement {
type State = Rc<RefCell<Option<usize>>>;
fn layout(
&mut self,
state: Option<Self::State>,
cx: &mut ui::prelude::WindowContext,
) -> (gpui::LayoutId, Self::State) {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
let layout_id = cx.request_layout(&style, None);
let dragged_pane = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
(layout_id, dragged_pane)
}
fn paint(
self,
bounds: gpui::Bounds<ui::prelude::Pixels>,
state: &mut Self::State,
cx: &mut ui::prelude::WindowContext,
) {
let flexes = self.flexes.lock().clone();
let len = self.children.len();
debug_assert!(flexes.len() == len);
debug_assert!(flex_values_in_bounds(flexes.as_slice()));
let mut origin = bounds.origin;
let space_per_flex = bounds.size.along(self.axis) / len as f32;
let mut bounding_boxes = self.bounding_boxes.lock();
bounding_boxes.clear();
for (ix, child) in self.children.into_iter().enumerate() {
//todo!(active_pane_magnification)
// If usign active pane magnification, need to switch to using
// 1 for all non-active panes, and then the magnification for the
// active pane.
let child_size = bounds
.size
.apply_along(self.axis, |_| space_per_flex * flexes[ix]);
let child_bounds = Bounds {
origin,
size: child_size,
};
bounding_boxes.push(Some(child_bounds));
cx.with_z_index(0, |cx| {
child.draw(origin, child_size.into(), cx);
});
cx.with_z_index(1, |cx| {
if ix < len - 1 {
Self::push_handle(
self.flexes.clone(),
state.clone(),
self.axis,
ix,
child_bounds,
bounds,
cx,
);
}
});
origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
}
cx.with_z_index(1, |cx| {
cx.on_mouse_event({
let state = state.clone();
move |e: &MouseUpEvent, phase, cx| {
if phase.bubble() {
state.replace(None);
}
}
});
})
}
}
impl ParentElement for PaneAxisElement {
fn children_mut(&mut self) -> &mut smallvec::SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
fn flex_values_in_bounds(flexes: &[f32]) -> bool {
(flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
}
// // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc}; // // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
// // use gpui::{ // // use gpui::{
@ -1166,4 +1366,4 @@ impl SplitDirection {
// }) // })
// } // }
// } // }
// } }

View file

@ -6,12 +6,12 @@ use std::path::Path;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::WindowBounds; use gpui::{Axis, WindowBounds};
use util::{unzip_option, ResultExt}; use util::{unzip_option, ResultExt};
use uuid::Uuid; use uuid::Uuid;
use crate::{Axis, WorkspaceId}; use crate::WorkspaceId;
use model::{ use model::{
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
@ -403,7 +403,7 @@ impl WorkspaceDb {
.map(|(group_id, axis, pane_id, active, flexes)| { .map(|(group_id, axis, pane_id, active, flexes)| {
if let Some((group_id, axis)) = group_id.zip(axis) { if let Some((group_id, axis)) = group_id.zip(axis) {
let flexes = flexes let flexes = flexes
.map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes)) .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
.transpose()?; .transpose()?;
Ok(SerializedPaneGroup::Group { Ok(SerializedPaneGroup::Group {

View file

@ -1,13 +1,11 @@
use crate::{ use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
item::ItemHandle, Axis, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use db::sqlez::{ use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount}, bindable::{Bind, Column, StaticColumnCount},
statement::Statement, statement::Statement,
}; };
use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds}; use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView, WindowBounds};
use project::Project; use project::Project;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},

View file

@ -29,12 +29,12 @@ use futures::{
Future, FutureExt, StreamExt, Future, FutureExt, StreamExt,
}; };
use gpui::{ use gpui::{
actions, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView, actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Point, KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools; use itertools::Itertools;
@ -227,6 +227,9 @@ pub fn init_settings(cx: &mut AppContext) {
} }
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.default_global::<DockDragState>();
cx.default_global::<DockClickReset>();
init_settings(cx); init_settings(cx);
notifications::init(cx); notifications::init(cx);
@ -480,8 +483,6 @@ struct FollowerState {
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>, items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
} }
enum WorkspaceBounds {}
impl Workspace { impl Workspace {
pub fn new( pub fn new(
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
@ -2032,7 +2033,7 @@ impl Workspace {
}; };
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
let center = match cursor { let center = match cursor {
Some(cursor) if bounding_box.contains_point(&cursor) => cursor, Some(cursor) if bounding_box.contains(&cursor) => cursor,
_ => bounding_box.center(), _ => bounding_box.center(),
}; };
@ -3571,6 +3572,16 @@ impl FocusableView for Workspace {
} }
} }
struct WorkspaceBounds(Bounds<Pixels>);
//todo!("remove this when better drag APIs are in GPUI2")
#[derive(Default)]
struct DockDragState(Option<DockPosition>);
//todo!("remove this when better double APIs are in GPUI2")
#[derive(Default)]
struct DockClickReset(Option<Task<()>>);
impl Render for Workspace { impl Render for Workspace {
type Element = Div; type Element = Div;
@ -3614,6 +3625,37 @@ impl Render for Workspace {
.border_t() .border_t()
.border_b() .border_b()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.on_mouse_up(gpui::MouseButton::Left, |_, cx| {
cx.update_global(|drag: &mut DockDragState, cx| {
drag.0 = None;
})
})
.on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
if let Some(types) = &cx.global::<DockDragState>().0 {
let workspace_bounds = cx.global::<WorkspaceBounds>().0;
match types {
DockPosition::Left => {
let size = e.position.x;
workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size.0), cx);
});
}
DockPosition::Right => {
let size = workspace_bounds.size.width - e.position.x;
workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size.0), cx);
});
}
DockPosition::Bottom => {
let size = workspace_bounds.size.height - e.position.y;
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size.0), cx);
});
}
}
}
}))
.child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
.child(self.modal_layer.clone()) .child(self.modal_layer.clone())
.child( .child(
div() div()