editor2 rendering (#3244)

- Make tab bar visible
- Fix tab text colors
- Ensure panes cover the available space
- Make the close button close
- Fix bug when unsubscribe called after remove
- WIP: start on editor element
- Add hover behaviour to tabs
- Add PointingHand on tabs
- gpui2: Add on_hover events
- Tooltip on tabs
- MOAR TOOLTIPS
- Use an `IconButton` for the tab close button
- Remove unneeded type qualification
- Tooltips in mouse event handler & fix executor timer
- Move more tooltip logic into gpui2 & fix tooltip moving on paint
- Update tooltip code a bit
- Allow multiple subscriptions from one entity handle

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2023-11-06 11:29:42 -07:00 committed by GitHub
commit eb325fb387
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 577 additions and 164 deletions

View file

@ -37,8 +37,8 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
div, AnyElement, AppContext, BackgroundExecutor, Context, Div, Element, EventEmitter, div, AnyElement, AppContext, BackgroundExecutor, Context, Div, Element, EventEmitter,
FocusHandle, Hsla, Model, Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, FocusHandle, Hsla, Model, Pixels, Render, Styled, Subscription, Task, TextStyle, View,
VisualContext, WeakView, WindowContext, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
@ -68,6 +68,7 @@ use scroll::{
use selections_collection::{MutableSelectionsCollection, SelectionsCollection}; use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use smallvec::SmallVec;
use std::{ use std::{
any::TypeId, any::TypeId,
borrow::Cow, borrow::Cow,
@ -8347,51 +8348,51 @@ impl Editor {
// .text() // .text()
// } // }
// pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
// let mut wrap_guides = smallvec::smallvec![]; let mut wrap_guides = smallvec::smallvec![];
// if self.show_wrap_guides == Some(false) { if self.show_wrap_guides == Some(false) {
// return wrap_guides; return wrap_guides;
// } }
// let settings = self.buffer.read(cx).settings_at(0, cx); let settings = self.buffer.read(cx).settings_at(0, cx);
// if settings.show_wrap_guides { if settings.show_wrap_guides {
// if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
// wrap_guides.push((soft_wrap as usize, true)); wrap_guides.push((soft_wrap as usize, true));
// } }
// wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
// } }
// wrap_guides wrap_guides
// } }
// pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
// let settings = self.buffer.read(cx).settings_at(0, cx); let settings = self.buffer.read(cx).settings_at(0, cx);
// let mode = self let mode = self
// .soft_wrap_mode_override .soft_wrap_mode_override
// .unwrap_or_else(|| settings.soft_wrap); .unwrap_or_else(|| settings.soft_wrap);
// match mode { match mode {
// language_settings::SoftWrap::None => SoftWrap::None, language_settings::SoftWrap::None => SoftWrap::None,
// language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
// language_settings::SoftWrap::PreferredLineLength => { language_settings::SoftWrap::PreferredLineLength => {
// SoftWrap::Column(settings.preferred_line_length) SoftWrap::Column(settings.preferred_line_length)
// } }
// } }
// } }
// pub fn set_soft_wrap_mode( pub fn set_soft_wrap_mode(
// &mut self, &mut self,
// mode: language_settings::SoftWrap, mode: language_settings::SoftWrap,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) { ) {
// self.soft_wrap_mode_override = Some(mode); self.soft_wrap_mode_override = Some(mode);
// cx.notify(); cx.notify();
// } }
// pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut AppContext) -> bool { pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
// self.display_map self.display_map
// .update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
// } }
// pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) { // pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
// if self.soft_wrap_mode_override.is_some() { // if self.soft_wrap_mode_override.is_some() {
@ -9321,11 +9322,14 @@ impl EventEmitter for Editor {
} }
impl Render for Editor { impl Render for Editor {
type Element = Div<Self>; type Element = EditorElement;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
// todo!() EditorElement::new(EditorStyle {
div() text: cx.text_style(),
line_height_scalar: 1.,
theme_id: 0,
})
} }
} }

View file

@ -3,17 +3,18 @@ use super::{
}; };
use crate::{ use crate::{
display_map::{BlockStyle, DisplaySnapshot}, display_map::{BlockStyle, DisplaySnapshot},
EditorStyle, EditorMode, EditorStyle, SoftWrap,
}; };
use anyhow::Result; use anyhow::Result;
use gpui::{ use gpui::{
black, px, relative, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, black, point, px, relative, size, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style,
TextSystem, TextRun, TextSystem, ViewContext,
}; };
use language::{CursorShape, Selection}; use language::{CursorShape, Selection};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ops::Range, sync::Arc}; use std::{cmp, ops::Range, sync::Arc};
use sum_tree::Bias; use sum_tree::Bias;
use theme::ActiveTheme;
enum FoldMarkers {} enum FoldMarkers {}
@ -1321,29 +1322,31 @@ impl EditorElement {
// } // }
// } // }
// fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> f32 { fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> Pixels {
// let style = &self.style; let style = &self.style;
let font_size = style.text.font_size * cx.rem_size();
let layout = cx
.text_system()
.layout_text(
" ".repeat(column).as_str(),
font_size,
&[TextRun {
len: column,
font: style.text.font(),
color: Hsla::default(),
underline: None,
}],
None,
)
.unwrap();
// cx.text_layout_cache() layout[0].width
// .layout_str( }
// " ".repeat(column).as_str(),
// style.text.font_size,
// &[(
// column,
// RunStyle {
// font_id: style.text.font_id,
// color: Color::black(),
// underline: Default::default(),
// },
// )],
// )
// .width()
// }
// fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 { fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
// let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
// self.column_pixels(digit_count, cx) self.column_pixels(digit_count, cx)
// } }
//Folds contained in a hunk are ignored apart from shrinking visual size //Folds contained in a hunk are ignored apart from shrinking visual size
//If a fold contains any hunks then that fold line is marked as modified //If a fold contains any hunks then that fold line is marked as modified
@ -2002,6 +2005,7 @@ impl Element<Editor> for EditorElement {
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut gpui::ViewContext<Editor>, cx: &mut gpui::ViewContext<Editor>,
) -> gpui::LayoutId { ) -> gpui::LayoutId {
let rem_size = cx.rem_size();
let mut style = Style::default(); let mut style = Style::default();
style.size.width = relative(1.).into(); style.size.width = relative(1.).into();
style.size.height = relative(1.).into(); style.size.height = relative(1.).into();
@ -2011,18 +2015,125 @@ impl Element<Editor> for EditorElement {
fn paint( fn paint(
&mut self, &mut self,
bounds: Bounds<gpui::Pixels>, bounds: Bounds<gpui::Pixels>,
view_state: &mut Editor, editor: &mut Editor,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut gpui::ViewContext<Editor>, cx: &mut gpui::ViewContext<Editor>,
) { ) {
let text_style = cx.text_style(); // let mut size = constraint.max;
// if size.x().is_infinite() {
// unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
// }
let layout_text = cx.text_system().layout_text( let snapshot = editor.snapshot(cx);
"hello world", let style = self.style.clone();
text_style.font_size * cx.rem_size(), let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
&[text_style.to_run("hello world".len())], let font_size = style.text.font_size * cx.rem_size();
None, let line_height = (font_size * style.line_height_scalar).round();
); let em_width = cx
.text_system()
.typographic_bounds(font_id, font_size, 'm')
.unwrap()
.size
.width;
let em_advance = cx
.text_system()
.advance(font_id, font_size, 'm')
.unwrap()
.width;
let gutter_padding;
let gutter_width;
let gutter_margin;
if snapshot.show_gutter {
let descent = cx.text_system().descent(font_id, font_size).unwrap();
let gutter_padding_factor = 3.5;
gutter_padding = (em_width * gutter_padding_factor).round();
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
gutter_margin = -descent;
} else {
gutter_padding = px(0.0);
gutter_width = px(0.0);
gutter_margin = px(0.0);
};
let text_width = bounds.size.width - gutter_width;
let overscroll = point(em_width, px(0.));
let snapshot = {
editor.set_visible_line_count((bounds.size.height / line_height).into(), cx);
let editor_width = text_width - gutter_margin - overscroll.x - em_width;
let wrap_width = match editor.soft_wrap_mode(cx) {
SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
SoftWrap::EditorWidth => editor_width,
SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
};
if editor.set_wrap_width(Some(wrap_width), cx) {
editor.snapshot(cx)
} else {
snapshot
}
};
let wrap_guides = editor
.wrap_guides(cx)
.iter()
.map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
.collect::<SmallVec<[_; 2]>>();
let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
// todo!("this should happen during layout")
if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
todo!()
// size.set_y(
// scroll_height
// .min(constraint.max_along(Axis::Vertical))
// .max(constraint.min_along(Axis::Vertical))
// .max(line_height)
// .min(line_height * max_lines as f32),
// )
} else if let EditorMode::SingleLine = snapshot.mode {
todo!()
// size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
}
// todo!()
// else if size.y().is_infinite() {
// // size.set_y(scroll_height);
// }
//
let gutter_size = size(gutter_width, bounds.size.height);
let text_size = size(text_width, bounds.size.height);
let autoscroll_horizontally =
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
let mut snapshot = editor.snapshot(cx);
let scroll_position = snapshot.scroll_position();
// The scroll position is a fractional point, the whole number of which represents
// the top of the window in terms of display rows.
let start_row = scroll_position.y as u32;
let height_in_lines = f32::from(bounds.size.height / line_height);
let max_row = snapshot.max_point().row();
// Add 1 to ensure selections bleed off screen
let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
dbg!(start_row..end_row);
// let text_style = cx.text_style();
// let layout_text = cx.text_system().layout_text(
// "hello world",
// text_style.font_size * cx.rem_size(),
// &[text_style.to_run("hello world".len())],
// None,
// );
// let line_height = text_style
// .line_height
// .to_pixels(text_style.font_size.into(), cx.rem_size());
// layout_text.unwrap()[0]
// .paint(bounds.origin, line_height, cx)
// .unwrap();
} }
} }

View file

@ -578,18 +578,24 @@ impl Item for Editor {
fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> { fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
let theme = cx.theme(); let theme = cx.theme();
AnyElement::new( AnyElement::new(
div() div()
.flex() .flex()
.flex_row() .flex_row()
.items_center() .items_center()
.bg(gpui::white()) .gap_2()
.text_color(gpui::white())
.child(self.title(cx).to_string()) .child(self.title(cx).to_string())
.children(detail.and_then(|detail| { .children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?; let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy(); let description = path.to_string_lossy();
Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
Some(
div()
.text_color(theme.colors().text_muted)
.text_xs()
.child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)),
)
})), })),
) )
} }
@ -625,8 +631,7 @@ impl Item for Editor {
fn deactivated(&mut self, cx: &mut ViewContext<Self>) { fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
let selection = self.selections.newest_anchor(); let selection = self.selections.newest_anchor();
todo!() self.push_to_nav_history(selection.head(), None, cx);
// self.push_to_nav_history(selection.head(), None, cx);
} }
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) { fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {

View file

@ -303,20 +303,20 @@ impl Editor {
self.scroll_manager.visible_line_count self.scroll_manager.visible_line_count
} }
// pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) { pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
// let opened_first_time = self.scroll_manager.visible_line_count.is_none(); let opened_first_time = self.scroll_manager.visible_line_count.is_none();
// self.scroll_manager.visible_line_count = Some(lines); self.scroll_manager.visible_line_count = Some(lines);
// if opened_first_time { if opened_first_time {
// cx.spawn(|editor, mut cx| async move { cx.spawn(|editor, mut cx| async move {
// editor editor
// .update(&mut cx, |editor, cx| { .update(&mut cx, |editor, cx| {
// editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
// }) })
// .ok() .ok()
// }) })
// .detach() .detach()
// } }
// } }
pub fn set_scroll_position( pub fn set_scroll_position(
&mut self, &mut self,

View file

@ -48,11 +48,11 @@ impl AutoscrollStrategy {
impl Editor { impl Editor {
pub fn autoscroll_vertically( pub fn autoscroll_vertically(
&mut self, &mut self,
viewport_height: f32, viewport_height: Pixels,
line_height: f32, line_height: Pixels,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) -> bool {
let visible_lines = viewport_height / line_height; let visible_lines = f32::from(viewport_height / line_height);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {

View file

@ -20,6 +20,7 @@ fn generate_dispatch_bindings() {
.header("src/platform/mac/dispatch.h") .header("src/platform/mac/dispatch.h")
.allowlist_var("_dispatch_main_q") .allowlist_var("_dispatch_main_q")
.allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT") .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
.allowlist_var("DISPATCH_TIME_NOW")
.allowlist_function("dispatch_get_global_queue") .allowlist_function("dispatch_get_global_queue")
.allowlist_function("dispatch_async_f") .allowlist_function("dispatch_async_f")
.allowlist_function("dispatch_after_f") .allowlist_function("dispatch_after_f")

View file

@ -161,6 +161,7 @@ pub struct AppContext {
flushing_effects: bool, flushing_effects: bool,
pending_updates: usize, pending_updates: usize,
pub(crate) active_drag: Option<AnyDrag>, pub(crate) active_drag: Option<AnyDrag>,
pub(crate) active_tooltip: Option<AnyTooltip>,
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>, pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>, pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>,
pub(crate) background_executor: BackgroundExecutor, pub(crate) background_executor: BackgroundExecutor,
@ -219,6 +220,7 @@ impl AppContext {
flushing_effects: false, flushing_effects: false,
pending_updates: 0, pending_updates: 0,
active_drag: None, active_drag: None,
active_tooltip: None,
next_frame_callbacks: HashMap::default(), next_frame_callbacks: HashMap::default(),
frame_consumers: HashMap::default(), frame_consumers: HashMap::default(),
background_executor: executor, background_executor: executor,
@ -900,3 +902,9 @@ pub(crate) struct AnyDrag {
pub view: AnyView, pub view: AnyView,
pub cursor_offset: Point<Pixels>, pub cursor_offset: Point<Pixels>,
} }
#[derive(Clone)]
pub(crate) struct AnyTooltip {
pub view: AnyView,
pub cursor_offset: Point<Pixels>,
}

View file

@ -3,7 +3,7 @@ use crate::{
ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement,
Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, Visibility,
}; };
use refineable::Refineable; use refineable::Refineable;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -249,11 +249,22 @@ where
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) { ) {
self.with_element_id(cx, |this, _global_id, cx| { self.with_element_id(cx, |this, _global_id, cx| {
let style = this.compute_style(bounds, element_state, cx);
if style.visibility == Visibility::Hidden {
return;
}
if let Some(mouse_cursor) = style.mouse_cursor {
let hovered = bounds.contains_point(&cx.mouse_position());
if hovered {
cx.set_cursor_style(mouse_cursor);
}
}
if let Some(group) = this.group.clone() { if let Some(group) = this.group.clone() {
GroupBounds::push(group, bounds, cx); GroupBounds::push(group, bounds, cx);
} }
let style = this.compute_style(bounds, element_state, cx);
let z_index = style.z_index.unwrap_or(0); let z_index = style.z_index.unwrap_or(0);
let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_min = point(Pixels::MAX, Pixels::MAX);

View file

@ -21,7 +21,7 @@ pub fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> {
} }
impl<T: Clone + Debug + Default> Point<T> { impl<T: Clone + Debug + Default> Point<T> {
pub fn new(x: T, y: T) -> Self { pub const fn new(x: T, y: T) -> Self {
Self { x, y } Self { x, y }
} }
@ -825,6 +825,12 @@ impl From<Pixels> for u32 {
} }
} }
impl From<u32> for Pixels {
fn from(pixels: u32) -> Self {
Pixels(pixels as f32)
}
}
impl From<Pixels> for usize { impl From<Pixels> for usize {
fn from(pixels: Pixels) -> Self { fn from(pixels: Pixels) -> Self {
pixels.0 as usize pixels.0 as usize

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
div, point, px, Action, AnyDrag, AnyView, AppContext, BorrowWindow, Bounds, Component, div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds,
DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, Keystroke, Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch,
Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, View, Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style,
ViewContext, StyleRefinement, Task, View, ViewContext,
}; };
use collections::HashMap; use collections::HashMap;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
@ -17,9 +17,12 @@ use std::{
ops::Deref, ops::Deref,
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
time::Duration,
}; };
const DRAG_THRESHOLD: f64 = 2.; const DRAG_THRESHOLD: f64 = 2.;
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
pub trait StatelessInteractive<V: 'static>: Element<V> { pub trait StatelessInteractive<V: 'static>: Element<V> {
fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>; fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>;
@ -333,6 +336,37 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
})); }));
self self
} }
fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
where
Self: Sized,
{
debug_assert!(
self.stateful_interaction().hover_listener.is_none(),
"calling on_hover more than once on the same element is not supported"
);
self.stateful_interaction().hover_listener = Some(Box::new(listener));
self
}
fn tooltip<W>(
mut self,
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
) -> Self
where
Self: Sized,
W: 'static + Render,
{
debug_assert!(
self.stateful_interaction().tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
self.stateful_interaction().tooltip_builder = Some(Arc::new(move |view_state, cx| {
build_tooltip(view_state, cx).into()
}));
self
}
} }
pub trait ElementInteraction<V: 'static>: 'static { pub trait ElementInteraction<V: 'static>: 'static {
@ -568,6 +602,77 @@ pub trait ElementInteraction<V: 'static>: 'static {
} }
} }
if let Some(hover_listener) = stateful.hover_listener.take() {
let was_hovered = element_state.hover_state.clone();
let has_mouse_down = element_state.pending_mouse_down.clone();
cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let is_hovered =
bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
let mut was_hovered = was_hovered.lock();
if is_hovered != was_hovered.clone() {
*was_hovered = is_hovered;
drop(was_hovered);
hover_listener(view_state, is_hovered, cx);
}
});
}
if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
let active_tooltip = element_state.active_tooltip.clone();
let pending_mouse_down = element_state.pending_mouse_down.clone();
cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let is_hovered = bounds.contains_point(&event.position)
&& pending_mouse_down.lock().is_none();
if !is_hovered {
active_tooltip.lock().take();
return;
}
if active_tooltip.lock().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
move |view, mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
view.update(&mut cx, move |view_state, cx| {
active_tooltip.lock().replace(ActiveTooltip {
waiting: None,
tooltip: Some(AnyTooltip {
view: tooltip_builder(view_state, cx),
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
}),
});
cx.notify();
})
.ok();
}
});
active_tooltip.lock().replace(ActiveTooltip {
waiting: Some(task),
tooltip: None,
});
}
});
if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
if active_tooltip.tooltip.is_some() {
cx.active_tooltip = active_tooltip.tooltip.clone()
}
}
}
let active_state = element_state.active_state.clone(); let active_state = element_state.active_state.clone();
if active_state.lock().is_none() { if active_state.lock().is_none() {
let active_group_bounds = stateful let active_group_bounds = stateful
@ -639,6 +744,8 @@ pub struct StatefulInteraction<V> {
active_style: StyleRefinement, active_style: StyleRefinement,
group_active_style: Option<GroupStyle>, group_active_style: Option<GroupStyle>,
drag_listener: Option<DragListener<V>>, drag_listener: Option<DragListener<V>>,
hover_listener: Option<HoverListener<V>>,
tooltip_builder: Option<TooltipBuilder<V>>,
} }
impl<V: 'static> ElementInteraction<V> for StatefulInteraction<V> { impl<V: 'static> ElementInteraction<V> for StatefulInteraction<V> {
@ -666,6 +773,8 @@ impl<V> From<ElementId> for StatefulInteraction<V> {
stateless: StatelessInteraction::default(), stateless: StatelessInteraction::default(),
click_listeners: SmallVec::new(), click_listeners: SmallVec::new(),
drag_listener: None, drag_listener: None,
hover_listener: None,
tooltip_builder: None,
active_style: StyleRefinement::default(), active_style: StyleRefinement::default(),
group_active_style: None, group_active_style: None,
} }
@ -695,6 +804,8 @@ impl<V> StatelessInteraction<V> {
stateless: self, stateless: self,
click_listeners: SmallVec::new(), click_listeners: SmallVec::new(),
drag_listener: None, drag_listener: None,
hover_listener: None,
tooltip_builder: None,
active_style: StyleRefinement::default(), active_style: StyleRefinement::default(),
group_active_style: None, group_active_style: None,
} }
@ -746,8 +857,16 @@ impl ActiveState {
#[derive(Default)] #[derive(Default)]
pub struct InteractiveElementState { pub struct InteractiveElementState {
active_state: Arc<Mutex<ActiveState>>, active_state: Arc<Mutex<ActiveState>>,
hover_state: Arc<Mutex<bool>>,
pending_mouse_down: 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>>>>,
active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
}
struct ActiveTooltip {
#[allow(unused)] // used to drop the task
waiting: Option<Task<()>>,
tooltip: Option<AnyTooltip>,
} }
impl InteractiveElementState { impl InteractiveElementState {
@ -1097,6 +1216,10 @@ pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>)
pub(crate) type DragListener<V> = pub(crate) type DragListener<V> =
Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>; Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
pub type KeyListener<V> = Box< pub type KeyListener<V> = Box<
dyn Fn( dyn Fn(
&mut V, &mut V,

View file

@ -11,11 +11,7 @@ use objc::{
}; };
use parking::{Parker, Unparker}; use parking::{Parker, Unparker};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{ffi::c_void, sync::Arc, time::Duration};
ffi::c_void,
sync::Arc,
time::{Duration, SystemTime},
};
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
@ -62,16 +58,10 @@ impl PlatformDispatcher for MacDispatcher {
} }
fn dispatch_after(&self, duration: Duration, runnable: Runnable) { fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
let now = SystemTime::now();
let after_duration = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
+ duration.as_nanos() as u64;
unsafe { unsafe {
let queue = let queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0); dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0);
let when = dispatch_time(0, after_duration as i64); let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
dispatch_after_f( dispatch_after_f(
when, when,
queue, queue,

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems, Result, Rgba, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems,
SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, Result, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
}; };
use refineable::{Cascade, Refineable}; use refineable::{Cascade, Refineable};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -19,6 +19,9 @@ pub struct Style {
/// What layout strategy should be used? /// What layout strategy should be used?
pub display: Display, pub display: Display,
/// Should the element be painted on screen?
pub visibility: Visibility,
// Overflow properties // Overflow properties
/// How children overflowing their container should affect layout /// How children overflowing their container should affect layout
#[refineable] #[refineable]
@ -98,6 +101,9 @@ pub struct Style {
/// TEXT /// TEXT
pub text: TextStyleRefinement, pub text: TextStyleRefinement,
/// The mouse cursor style shown when the mouse pointer is over an element.
pub mouse_cursor: Option<CursorStyle>,
pub z_index: Option<u32>, pub z_index: Option<u32>,
} }
@ -107,6 +113,13 @@ impl Styled for StyleRefinement {
} }
} }
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)]
pub enum Visibility {
#[default]
Visible,
Hidden,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BoxShadow { pub struct BoxShadow {
pub color: Hsla, pub color: Hsla,
@ -297,6 +310,7 @@ impl Default for Style {
fn default() -> Self { fn default() -> Self {
Style { Style {
display: Display::Block, display: Display::Block,
visibility: Visibility::Visible,
overflow: Point { overflow: Point {
x: Overflow::Visible, x: Overflow::Visible,
y: Overflow::Visible, y: Overflow::Visible,
@ -328,6 +342,7 @@ impl Default for Style {
corner_radii: Corners::default(), corner_radii: Corners::default(),
box_shadow: Default::default(), box_shadow: Default::default(),
text: TextStyleRefinement::default(), text: TextStyleRefinement::default(),
mouse_cursor: None,
z_index: None, z_index: None,
} }
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
self as gpui2, hsla, point, px, relative, rems, AlignItems, DefiniteLength, Display, Fill, self as gpui2, hsla, point, px, relative, rems, AlignItems, CursorStyle, DefiniteLength,
FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString,
StyleRefinement, Visibility,
}; };
use crate::{BoxShadow, TextStyleRefinement}; use crate::{BoxShadow, TextStyleRefinement};
use smallvec::smallvec; use smallvec::smallvec;
@ -60,6 +61,54 @@ pub trait Styled {
self self
} }
/// Sets the visibility of the element to `visible`.
/// [Docs](https://tailwindcss.com/docs/visibility)
fn visible(mut self) -> Self
where
Self: Sized,
{
self.style().visibility = Some(Visibility::Visible);
self
}
/// Sets the visibility of the element to `hidden`.
/// [Docs](https://tailwindcss.com/docs/visibility)
fn invisible(mut self) -> Self
where
Self: Sized,
{
self.style().visibility = Some(Visibility::Hidden);
self
}
fn cursor(mut self, cursor: CursorStyle) -> Self
where
Self: Sized,
{
self.style().mouse_cursor = Some(cursor);
self
}
/// Sets the cursor style when hovering an element to `default`.
/// [Docs](https://tailwindcss.com/docs/cursor)
fn cursor_default(mut self) -> Self
where
Self: Sized,
{
self.style().mouse_cursor = Some(CursorStyle::Arrow);
self
}
/// Sets the cursor style when hovering an element to `pointer`.
/// [Docs](https://tailwindcss.com/docs/cursor)
fn cursor_pointer(mut self) -> Self
where
Self: Sized,
{
self.style().mouse_cursor = Some(CursorStyle::PointingHand);
self
}
/// Sets the flex direction of the element to `column`. /// Sets the flex direction of the element to `column`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column) /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
fn flex_col(mut self) -> Self fn flex_col(mut self) -> Self

View file

@ -14,7 +14,7 @@ impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
} }
struct SubscriberSetState<EmitterKey, Callback> { struct SubscriberSetState<EmitterKey, Callback> {
subscribers: BTreeMap<EmitterKey, BTreeMap<usize, Callback>>, subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Callback>>>,
dropped_subscribers: BTreeSet<(EmitterKey, usize)>, dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
next_subscriber_id: usize, next_subscriber_id: usize,
} }
@ -38,12 +38,18 @@ where
lock.subscribers lock.subscribers
.entry(emitter_key.clone()) .entry(emitter_key.clone())
.or_default() .or_default()
.get_or_insert_with(|| Default::default())
.insert(subscriber_id, callback); .insert(subscriber_id, callback);
let this = self.0.clone(); let this = self.0.clone();
Subscription { Subscription {
unsubscribe: Some(Box::new(move || { unsubscribe: Some(Box::new(move || {
let mut lock = this.lock(); let mut lock = this.lock();
if let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) { let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else {
// remove was called with this emitter_key
return;
};
if let Some(subscribers) = subscribers {
subscribers.remove(&subscriber_id); subscribers.remove(&subscriber_id);
if subscribers.is_empty() { if subscribers.is_empty() {
lock.subscribers.remove(&emitter_key); lock.subscribers.remove(&emitter_key);
@ -62,34 +68,43 @@ where
pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> { pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
let subscribers = self.0.lock().subscribers.remove(&emitter); let subscribers = self.0.lock().subscribers.remove(&emitter);
subscribers.unwrap_or_default().into_values() subscribers
.unwrap_or_default()
.map(|s| s.into_values())
.into_iter()
.flatten()
} }
pub fn retain<F>(&self, emitter: &EmitterKey, mut f: F) pub fn retain<F>(&self, emitter: &EmitterKey, mut f: F)
where where
F: FnMut(&mut Callback) -> bool, F: FnMut(&mut Callback) -> bool,
{ {
let entry = self.0.lock().subscribers.remove_entry(emitter); let Some(mut subscribers) = self
if let Some((emitter, mut subscribers)) = entry { .0
subscribers.retain(|_, callback| f(callback)); .lock()
let mut lock = self.0.lock(); .subscribers
.get_mut(emitter)
.and_then(|s| s.take())
else {
return;
};
// Add any new subscribers that were added while invoking the callback. subscribers.retain(|_, callback| f(callback));
if let Some(new_subscribers) = lock.subscribers.remove(&emitter) { let mut lock = self.0.lock();
subscribers.extend(new_subscribers);
}
// Remove any dropped subscriptions that were dropped while invoking the callback. // Add any new subscribers that were added while invoking the callback.
for (dropped_emitter, dropped_subscription_id) in if let Some(Some(new_subscribers)) = lock.subscribers.remove(&emitter) {
mem::take(&mut lock.dropped_subscribers) subscribers.extend(new_subscribers);
{ }
debug_assert_eq!(emitter, dropped_emitter);
subscribers.remove(&dropped_subscription_id);
}
if !subscribers.is_empty() { // Remove any dropped subscriptions that were dropped while invoking the callback.
lock.subscribers.insert(emitter, subscribers); for (dropped_emitter, dropped_subscription_id) in mem::take(&mut lock.dropped_subscribers) {
} debug_assert_eq!(*emitter, dropped_emitter);
subscribers.remove(&dropped_subscription_id);
}
if !subscribers.is_empty() {
lock.subscribers.insert(emitter.clone(), Some(subscribers));
} }
} }
} }

View file

@ -1,14 +1,14 @@
use crate::{ use crate::{
px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId,
Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId,
Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch,
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton,
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::HashMap; use collections::HashMap;
@ -190,6 +190,7 @@ pub struct Window {
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>, pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
default_prevented: bool, default_prevented: bool,
mouse_position: Point<Pixels>, mouse_position: Point<Pixels>,
requested_cursor_style: Option<CursorStyle>,
scale_factor: f32, scale_factor: f32,
bounds: WindowBounds, bounds: WindowBounds,
bounds_observers: SubscriberSet<(), AnyObserver>, bounds_observers: SubscriberSet<(), AnyObserver>,
@ -283,6 +284,7 @@ impl Window {
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
default_prevented: true, default_prevented: true,
mouse_position, mouse_position,
requested_cursor_style: None,
scale_factor, scale_factor,
bounds, bounds,
bounds_observers: SubscriberSet::new(), bounds_observers: SubscriberSet::new(),
@ -669,6 +671,10 @@ impl<'a> WindowContext<'a> {
self.window.mouse_position self.window.mouse_position
} }
pub fn set_cursor_style(&mut self, style: CursorStyle) {
self.window.requested_cursor_style = Some(style)
}
/// Called during painting to invoke the given closure in a new stacking context. The given /// Called during painting to invoke the given closure in a new stacking context. The given
/// z-index is interpreted relative to the previous call to `stack`. /// z-index is interpreted relative to the previous call to `stack`.
pub fn stack<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { pub fn stack<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
@ -981,12 +987,27 @@ impl<'a> WindowContext<'a> {
cx.active_drag = Some(active_drag); cx.active_drag = Some(active_drag);
}); });
}); });
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
self.stack(1, |cx| {
cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
active_tooltip.view.draw(available_space, cx);
});
});
} }
self.window.root_view = Some(root_view); self.window.root_view = Some(root_view);
let scene = self.window.scene_builder.build(); let scene = self.window.scene_builder.build();
self.window.platform_window.draw(scene); self.window.platform_window.draw(scene);
let cursor_style = self
.window
.requested_cursor_style
.take()
.unwrap_or(CursorStyle::Arrow);
self.platform.set_cursor_style(cursor_style);
self.window.dirty = false; self.window.dirty = false;
} }

View file

@ -23,6 +23,7 @@ mod tab;
mod toast; mod toast;
mod toggle; mod toggle;
mod tool_divider; mod tool_divider;
mod tooltip;
pub use avatar::*; pub use avatar::*;
pub use button::*; pub use button::*;
@ -49,3 +50,4 @@ pub use tab::*;
pub use toast::*; pub use toast::*;
pub use toggle::*; pub use toggle::*;
pub use tool_divider::*; pub use tool_divider::*;
pub use tooltip::*;

View file

@ -186,7 +186,6 @@ impl IconElement {
} }
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let fill = self.color.color(cx);
let svg_size = match self.size { let svg_size = match self.size {
IconSize::Small => rems(0.75), IconSize::Small => rems(0.75),
IconSize::Medium => rems(0.9375), IconSize::Medium => rems(0.9375),
@ -196,7 +195,7 @@ impl IconElement {
.size(svg_size) .size(svg_size)
.flex_none() .flex_none()
.path(self.icon.path()) .path(self.icon.path())
.text_color(fill) .text_color(self.color.color(cx))
} }
} }

View file

@ -0,0 +1,31 @@
use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext};
use theme2::ActiveTheme;
#[derive(Clone, Debug)]
pub struct TextTooltip {
title: SharedString,
}
impl TextTooltip {
pub fn new(str: SharedString) -> Self {
Self { title: str }
}
}
impl Render for TextTooltip {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let theme = cx.theme();
div()
.bg(theme.colors().background)
.rounded(px(8.))
.border()
.font("Zed Sans")
.border_color(theme.colors().border)
.text_color(theme.colors().text)
.pl_2()
.pr_2()
.child(self.title.clone())
}
}

View file

@ -26,7 +26,7 @@ use std::{
}, },
}; };
use ui::v_stack; use ui::v_stack;
use ui::{prelude::*, Icon, IconButton, IconColor, IconElement}; use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip};
use util::truncate_and_remove_front; use util::truncate_and_remove_front;
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@ -1359,15 +1359,30 @@ impl Pane {
cx: &mut ViewContext<'_, Pane>, cx: &mut ViewContext<'_, Pane>,
) -> impl Component<Self> { ) -> impl Component<Self> {
let label = item.tab_content(Some(detail), cx); let label = item.tab_content(Some(detail), cx);
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); let close_icon = || {
let id = item.id();
let (tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { div()
.id(item.id())
.invisible()
.group_hover("", |style| style.visible())
.child(IconButton::new("close_tab", Icon::Close).on_click(
move |pane: &mut Self, cx| {
pane.close_item_by_id(id, SaveIntent::Close, cx)
.detach_and_log_err(cx);
},
))
};
let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
false => ( false => (
cx.theme().colors().text_muted,
cx.theme().colors().tab_inactive_background, cx.theme().colors().tab_inactive_background,
cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active, cx.theme().colors().ghost_element_active,
), ),
true => ( true => (
cx.theme().colors().text,
cx.theme().colors().tab_active_background, cx.theme().colors().tab_active_background,
cx.theme().colors().element_hover, cx.theme().colors().element_hover,
cx.theme().colors().element_active, cx.theme().colors().element_active,
@ -1377,7 +1392,12 @@ impl Pane {
let close_right = ItemSettings::get_global(cx).close_position.right(); let close_right = ItemSettings::get_global(cx).close_position.right();
div() div()
.group("")
.id(item.id()) .id(item.id())
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |div, text| {
div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
})
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target)) // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
// .on_drop(|_view, state: View<DraggedTab>, cx| { // .on_drop(|_view, state: View<DraggedTab>, cx| {
@ -1397,6 +1417,7 @@ impl Pane {
.flex() .flex()
.items_center() .items_center()
.gap_1p5() .gap_1p5()
.text_color(text_color)
.children(if item.has_conflict(cx) { .children(if item.has_conflict(cx) {
Some( Some(
IconElement::new(Icon::ExclamationTriangle) IconElement::new(Icon::ExclamationTriangle)
@ -1457,7 +1478,7 @@ impl Pane {
), ),
) )
.child( .child(
div().w_0().flex_1().h_full().child( div().flex_1().h_full().child(
div().id("tabs").flex().overflow_x_scroll().children( div().id("tabs").flex().overflow_x_scroll().children(
self.items self.items
.iter() .iter()
@ -1888,13 +1909,14 @@ impl Render for Pane {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
v_stack() v_stack()
.size_full()
.child(self.render_tab_bar(cx)) .child(self.render_tab_bar(cx))
.child(div() /* toolbar */) .child(div() /* todo!(toolbar) */)
.child(if let Some(item) = self.active_item() { .child(if let Some(item) = self.active_item() {
item.to_any().render() div().flex_1().child(item.to_any())
} else { } else {
// todo!() // todo!()
div().child("Empty Pane").render() div().child("Empty Pane")
}) })
// enum MouseNavigationHandler {} // enum MouseNavigationHandler {}

View file

@ -201,7 +201,7 @@ impl Member {
// Some(pane) // Some(pane)
// }; // };
div().child(pane.clone()).render() div().size_full().child(pane.clone()).render()
// Stack::new() // Stack::new()
// .with_child(pane_element.contained().with_border(leader_border)) // .with_child(pane_element.contained().with_border(leader_border))

View file

@ -44,6 +44,7 @@ impl Render for StatusBar {
.items_center() .items_center()
.justify_between() .justify_between()
.w_full() .w_full()
.h_8()
.bg(cx.theme().colors().status_bar_background) .bg(cx.theme().colors().status_bar_background)
.child(self.render_left_tools(cx)) .child(self.render_left_tools(cx))
.child(self.render_right_tools(cx)) .child(self.render_right_tools(cx))

View file

@ -208,7 +208,6 @@ fn main() {
if stdout_is_a_pty() { if stdout_is_a_pty() {
cx.activate(true); cx.activate(true);
let urls = collect_url_args(); let urls = collect_url_args();
dbg!(&urls);
if !urls.is_empty() { if !urls.is_empty() {
listener.open_urls(urls) listener.open_urls(urls)
} }