Detailed changes
@@ -3797,6 +3797,7 @@ dependencies = [
"image",
"itertools 0.10.5",
"lazy_static",
+ "linkme",
"log",
"media",
"metal",
@@ -4815,6 +4816,26 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+[[package]]
+name = "linkme"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.0.42"
@@ -8802,6 +8823,17 @@ dependencies = [
"util",
]
+[[package]]
+name = "storybook3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "settings2",
+ "theme2",
+ "ui2",
+]
+
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -10132,6 +10164,7 @@ dependencies = [
"chrono",
"gpui2",
"itertools 0.11.0",
+ "menu2",
"rand 0.8.5",
"serde",
"settings2",
@@ -95,6 +95,7 @@ members = [
"crates/sqlez_macros",
"crates/rich_text",
"crates/storybook2",
+ "crates/storybook3",
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
@@ -35,6 +35,15 @@
// "custom": 2
// },
"buffer_line_height": "comfortable",
+ // The name of a font to use for rendering text in the UI
+ "ui_font_family": "Zed Mono",
+ // The OpenType features to enable for text in the UI
+ "ui_font_features": {
+ // Disable ligatures:
+ "calt": false
+ },
+ // The default font size for text in the UI
+ "ui_font_size": 14,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
@@ -224,7 +224,7 @@ impl TestServer {
});
cx.update(|cx| {
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);
@@ -684,6 +684,7 @@ impl CollabPanel {
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
+ //todo!(collapsed_channels)
// panel.collapsed_channels = serialized_panel
// .collapsed_channels
// .unwrap_or_else(|| Vec::new());
@@ -31,9 +31,9 @@ use std::sync::Arc;
use call::ActiveCall;
use client::{Client, UserStore};
use gpui::{
- div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render,
- Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext,
- WeakView, WindowBounds,
+ div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
+ Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
+ VisualContext, WeakView, WindowBounds,
};
use project::Project;
use theme::ActiveTheme;
@@ -88,12 +88,17 @@ impl Render for CollabTitlebarItem {
h_stack()
.id("titlebar")
.justify_between()
+ .w_full()
+ .h(rems(1.75))
+ // Set a non-scaling min-height here to ensure the titlebar is
+ // always at least the height of the traffic lights.
+ .min_h(px(32.))
.when(
!matches!(cx.window_bounds(), WindowBounds::Fullscreen),
- |s| s.pl_20(),
+ // Use pixels here instead of a rem-based size because the macOS traffic
+ // lights are a static size, and don't scale with the rest of the UI.
+ |s| s.pl(px(68.)),
)
- .w_full()
- .h(rems(1.75))
.bg(cx.theme().colors().title_bar_background)
.on_click(|_, event, cx| {
if event.up.click_count == 2 {
@@ -102,6 +107,7 @@ impl Render for CollabTitlebarItem {
})
.child(
h_stack()
+ .gap_1()
// TODO - Add player menu
.child(
div()
@@ -130,14 +136,12 @@ impl Render for CollabTitlebarItem {
.color(Some(TextColor::Muted)),
)
.tooltip(move |_, cx| {
- // todo!() Replace with real action.
- #[gpui::action]
- struct NoAction {}
cx.build_view(|_| {
Tooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
"cmd-b",
- NoAction {},
+ // todo!() Replace with real action.
+ gpui::NoAction,
None,
)))
.meta("Only local branches shown")
@@ -1,9 +1,8 @@
use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
- Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
- WindowContext,
+ actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
+ ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use std::{
@@ -16,7 +15,7 @@ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt,
};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
use zed_actions::OpenZedURL;
actions!(Toggle);
@@ -47,7 +46,7 @@ impl CommandPalette {
.available_actions()
.into_iter()
.filter_map(|action| {
- let name = action.name();
+ let name = gpui::remove_the_2(action.name());
let namespace = name.split("::").next().unwrap_or("malformed action name");
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
return None;
@@ -69,10 +68,9 @@ impl CommandPalette {
}
}
-impl EventEmitter<ModalEvent> for CommandPalette {}
-impl Modal for CommandPalette {
- fn focus(&self, cx: &mut WindowContext) {
- self.picker.update(cx, |picker, cx| picker.focus(cx));
+impl ManagedView for CommandPalette {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
}
}
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette
- .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+ .update(cx, |_, cx| cx.emit(Dismiss))
.log_err();
}
@@ -456,7 +454,7 @@ mod tests {
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let app_state = AppState::test(cx);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
@@ -1051,17 +1051,15 @@ mod tests {
);
// Ensure updates to the file are reflected in the LSP.
- buffer_1
- .update(cx, |buffer, cx| {
- buffer.file_updated(
- Arc::new(File {
- abs_path: "/root/child/buffer-1".into(),
- path: Path::new("child/buffer-1").into(),
- }),
- cx,
- )
- })
- .await;
+ buffer_1.update(cx, |buffer, cx| {
+ buffer.file_updated(
+ Arc::new(File {
+ abs_path: "/root/child/buffer-1".into(),
+ path: Path::new("child/buffer-1").into(),
+ }),
+ cx,
+ )
+ });
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
- Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
+ Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
+ TextRun, UnderlineStyle, WrappedLine,
};
use inlay_map::InlayMap;
use language::{
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
})
}
- pub fn lay_out_line_for_row(
+ pub fn layout_row(
&self,
display_row: u32,
TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
editor_style,
rem_size,
}: &TextLayoutDetails,
- ) -> Line {
+ ) -> Arc<LineLayout> {
let mut runs = Vec::new();
let mut line = String::new();
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
text_system
- .layout_text(&line, font_size, &runs, None)
- .unwrap()
- .pop()
- .unwrap()
+ .layout_line(&line, font_size, &runs)
+ .expect("we expect the font to be loaded because it's rendered by the editor")
}
- pub fn x_for_point(
+ pub fn x_for_display_point(
&self,
display_point: DisplayPoint,
text_layout_details: &TextLayoutDetails,
) -> Pixels {
- let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
- layout_line.x_for_index(display_point.column() as usize)
+ let line = self.layout_row(display_point.row(), text_layout_details);
+ line.x_for_index(display_point.column() as usize)
}
- pub fn column_for_x(
+ pub fn display_column_for_x(
&self,
display_row: u32,
- x_coordinate: Pixels,
- text_layout_details: &TextLayoutDetails,
+ x: Pixels,
+ details: &TextLayoutDetails,
) -> u32 {
- let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
- layout_line.closest_index_for_x(x_coordinate) as u32
+ let layout_line = self.layout_row(display_row, details);
+ layout_line.closest_index_for_x(x) as u32
}
pub fn chars_at(
@@ -1891,6 +1891,6 @@ mod tests {
fn init_test(cx: &mut AppContext) {
let store = SettingsStore::test(cx);
cx.set_global(store);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
}
}
@@ -39,7 +39,7 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
- action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
+ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
// // .with_soft_wrap(true)
// }
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectNext {
#[serde(default)]
pub replace_newest: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct MovePageUp {
#[serde(default)]
center_cursor: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct MovePageDown {
#[serde(default)]
center_cursor: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct SelectToEndOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ConfirmCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct FoldAt {
pub buffer_row: u32,
}
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
pub struct UnfoldAt {
pub buffer_row: u32,
}
@@ -5433,7 +5433,9 @@ impl Editor {
*head.column_mut() += 1;
head = display_map.clip_point(head, Bias::Right);
let goal = SelectionGoal::HorizontalPosition(
- display_map.x_for_point(head, &text_layout_details).into(),
+ display_map
+ .x_for_display_point(head, &text_layout_details)
+ .into(),
);
selection.collapse_to(head, goal);
@@ -6379,8 +6381,8 @@ impl Editor {
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
let range = oldest_selection.display_range(&display_map).sorted();
- let start_x = display_map.x_for_point(range.start, &text_layout_details);
- let end_x = display_map.x_for_point(range.end, &text_layout_details);
+ let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
+ let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
let positions = start_x.min(end_x)..start_x.max(end_x);
selections.clear();
@@ -6419,15 +6421,16 @@ impl Editor {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let mut row = range.start.row();
- let positions = if let SelectionGoal::HorizontalRange { start, end } =
- selection.goal
- {
- px(start)..px(end)
- } else {
- let start_x = display_map.x_for_point(range.start, &text_layout_details);
- let end_x = display_map.x_for_point(range.end, &text_layout_details);
- start_x.min(end_x)..start_x.max(end_x)
- };
+ let positions =
+ if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
+ px(start)..px(end)
+ } else {
+ let start_x =
+ display_map.x_for_display_point(range.start, &text_layout_details);
+ let end_x =
+ display_map.x_for_display_point(range.end, &text_layout_details);
+ start_x.min(end_x)..start_x.max(end_x)
+ };
while row != end_row {
if above {
@@ -6980,7 +6983,7 @@ impl Editor {
let display_point = point.to_display_point(display_snapshot);
let goal = SelectionGoal::HorizontalPosition(
display_snapshot
- .x_for_point(display_point, &text_layout_details)
+ .x_for_display_point(display_point, &text_layout_details)
.into(),
);
(display_point, goal)
@@ -9367,18 +9370,16 @@ impl Render for Editor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
- EditorMode::SingleLine => {
- TextStyle {
- color: cx.theme().colors().text,
- font_family: settings.ui_font.family.clone(), // todo!()
- font_features: settings.ui_font.features,
- font_size: rems(0.875).into(),
- font_weight: FontWeight::NORMAL,
- font_style: FontStyle::Normal,
- line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
- underline: None,
- }
- }
+ EditorMode::SingleLine => TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.ui_font.family.clone(),
+ font_features: settings.ui_font.features,
+ font_size: rems(0.875).into(),
+ font_weight: FontWeight::NORMAL,
+ font_style: FontStyle::Normal,
+ line_height: relative(1.).into(),
+ underline: None,
+ },
EditorMode::AutoHeight { max_lines } => todo!(),
@@ -9749,7 +9750,8 @@ impl InputHandler for Editor {
let scroll_left = scroll_position.x * em_width;
let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
- let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
+ let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+ + self.gutter_width;
let y = line_height * (start.row() as f32 - scroll_position.y);
Some(Bounds {
@@ -8277,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
use gpui::{
div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
- ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line,
+ ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
- ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View,
- ViewContext, WindowContext,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
+ TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -476,7 +476,7 @@ impl EditorElement {
Self::paint_diff_hunks(bounds, layout, cx);
}
- for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+ for (ix, line) in layout.line_numbers.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin
+ point(
@@ -775,21 +775,21 @@ impl EditorElement {
.chars_at(cursor_position)
.next()
.and_then(|(character, _)| {
- let text = character.to_string();
+ let text = SharedString::from(character.to_string());
+ let len = text.len();
cx.text_system()
- .layout_text(
- &text,
+ .shape_line(
+ text,
cursor_row_layout.font_size,
&[TextRun {
- len: text.len(),
+ len,
font: self.style.text.font(),
color: self.style.background,
+ background_color: None,
underline: None,
}],
- None,
)
- .unwrap()
- .pop()
+ .log_err()
})
} else {
None
@@ -1244,20 +1244,20 @@ impl EditorElement {
let font_size = style.text.font_size.to_pixels(cx.rem_size());
let layout = cx
.text_system()
- .layout_text(
- " ".repeat(column).as_str(),
+ .shape_line(
+ SharedString::from(" ".repeat(column)),
font_size,
&[TextRun {
len: column,
font: style.text.font(),
color: Hsla::default(),
+ background_color: None,
underline: None,
}],
- None,
)
.unwrap();
- layout[0].width
+ layout.width
}
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
@@ -1338,7 +1338,7 @@ impl EditorElement {
relative_rows
}
- fn layout_line_numbers(
+ fn shape_line_numbers(
&self,
rows: Range<u32>,
active_rows: &BTreeMap<u32, bool>,
@@ -1347,12 +1347,12 @@ impl EditorElement {
snapshot: &EditorSnapshot,
cx: &ViewContext<Editor>,
) -> (
- Vec<Option<gpui::Line>>,
+ Vec<Option<ShapedLine>>,
Vec<Option<(FoldStatus, BufferRow, bool)>>,
) {
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let include_line_numbers = snapshot.mode == EditorMode::Full;
- let mut line_number_layouts = Vec::with_capacity(rows.len());
+ let mut shaped_line_numbers = Vec::with_capacity(rows.len());
let mut fold_statuses = Vec::with_capacity(rows.len());
let mut line_number = String::new();
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
@@ -1387,15 +1387,14 @@ impl EditorElement {
len: line_number.len(),
font: self.style.text.font(),
color,
+ background_color: None,
underline: None,
};
- let layout = cx
+ let shaped_line = cx
.text_system()
- .layout_text(&line_number, font_size, &[run], None)
- .unwrap()
- .pop()
+ .shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
- line_number_layouts.push(Some(layout));
+ shaped_line_numbers.push(Some(shaped_line));
fold_statuses.push(
is_singleton
.then(|| {
@@ -1408,17 +1407,17 @@ impl EditorElement {
}
} else {
fold_statuses.push(None);
- line_number_layouts.push(None);
+ shaped_line_numbers.push(None);
}
}
- (line_number_layouts, fold_statuses)
+ (shaped_line_numbers, fold_statuses)
}
fn layout_lines(
&mut self,
rows: Range<u32>,
- line_number_layouts: &[Option<Line>],
+ line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
cx: &ViewContext<Editor>,
) -> Vec<LineWithInvisibles> {
@@ -1439,18 +1438,17 @@ impl EditorElement {
.chain(iter::repeat(""))
.take(rows.len());
placeholder_lines
- .map(|line| {
+ .filter_map(move |line| {
let run = TextRun {
len: line.len(),
font: self.style.text.font(),
color: placeholder_color,
+ background_color: None,
underline: Default::default(),
};
cx.text_system()
- .layout_text(line, font_size, &[run], None)
- .unwrap()
- .pop()
- .unwrap()
+ .shape_line(line.to_string().into(), font_size, &[run])
+ .log_err()
})
.map(|line| LineWithInvisibles {
line,
@@ -1726,7 +1724,7 @@ impl EditorElement {
.head
});
- let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+ let (line_numbers, fold_statuses) = self.shape_line_numbers(
start_row..end_row,
&active_rows,
head_for_relative,
@@ -1740,8 +1738,7 @@ impl EditorElement {
let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
let mut max_visible_line_width = Pixels::ZERO;
- let line_layouts =
- self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+ let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
for line_with_invisibles in &line_layouts {
if line_with_invisibles.line.width > max_visible_line_width {
max_visible_line_width = line_with_invisibles.line.width;
@@ -1879,35 +1876,31 @@ impl EditorElement {
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = cx
.text_system()
- .layout_text(
- "→",
+ .shape_line(
+ "→".into(),
invisible_symbol_font_size,
&[TextRun {
len: "→".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
+ background_color: None,
underline: None,
}],
- None,
)
- .unwrap()
- .pop()
.unwrap();
let space_invisible = cx
.text_system()
- .layout_text(
- "•",
+ .shape_line(
+ "•".into(),
invisible_symbol_font_size,
&[TextRun {
len: "•".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
+ background_color: None,
underline: None,
}],
- None,
)
- .unwrap()
- .pop()
.unwrap();
LayoutState {
@@ -1939,7 +1932,7 @@ impl EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
- line_number_layouts,
+ line_numbers,
display_hunks,
blocks,
selections,
@@ -2201,7 +2194,7 @@ impl EditorElement {
#[derive(Debug)]
pub struct LineWithInvisibles {
- pub line: Line,
+ pub line: ShapedLine,
invisibles: Vec<Invisible>,
}
@@ -2211,7 +2204,7 @@ impl LineWithInvisibles {
text_style: &TextStyle,
max_line_len: usize,
max_line_count: usize,
- line_number_layouts: &[Option<Line>],
+ line_number_layouts: &[Option<ShapedLine>],
editor_mode: EditorMode,
cx: &WindowContext,
) -> Vec<Self> {
@@ -2231,11 +2224,12 @@ impl LineWithInvisibles {
}]) {
for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
if ix > 0 {
- let layout = cx
+ let shaped_line = cx
.text_system()
- .layout_text(&line, font_size, &styles, None);
+ .shape_line(line.clone().into(), font_size, &styles)
+ .unwrap();
layouts.push(Self {
- line: layout.unwrap().pop().unwrap(),
+ line: shaped_line,
invisibles: invisibles.drain(..).collect(),
});
@@ -2269,6 +2263,7 @@ impl LineWithInvisibles {
len: line_chunk.len(),
font: text_style.font(),
color: text_style.color,
+ background_color: None,
underline: text_style.underline,
});
@@ -3089,7 +3084,7 @@ pub struct LayoutState {
visible_display_row_range: Range<u32>,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,
- line_number_layouts: Vec<Option<gpui::Line>>,
+ line_numbers: Vec<Option<ShapedLine>>,
display_hunks: Vec<DisplayDiffHunk>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3102,8 +3097,8 @@ pub struct LayoutState {
code_actions_indicator: Option<CodeActionsIndicator>,
// hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
fold_indicators: Vec<Option<AnyElement<Editor>>>,
- tab_invisible: Line,
- space_invisible: Line,
+ tab_invisible: ShapedLine,
+ space_invisible: ShapedLine,
}
struct CodeActionsIndicator {
@@ -3203,7 +3198,7 @@ fn layout_line(
snapshot: &EditorSnapshot,
style: &EditorStyle,
cx: &WindowContext,
-) -> Result<Line> {
+) -> Result<ShapedLine> {
let mut line = snapshot.line(row);
if line.len() > MAX_LINE_LEN {
@@ -3215,21 +3210,17 @@ fn layout_line(
line.truncate(len);
}
- Ok(cx
- .text_system()
- .layout_text(
- &line,
- style.text.font_size.to_pixels(cx.rem_size()),
- &[TextRun {
- len: snapshot.line_len(row) as usize,
- font: style.text.font(),
- color: Hsla::default(),
- underline: None,
- }],
- None,
- )?
- .pop()
- .unwrap())
+ cx.text_system().shape_line(
+ line.into(),
+ style.text.font_size.to_pixels(cx.rem_size()),
+ &[TextRun {
+ len: snapshot.line_len(row) as usize,
+ font: style.text.font(),
+ color: Hsla::default(),
+ background_color: None,
+ underline: None,
+ }],
+ )
}
#[derive(Debug)]
@@ -3239,7 +3230,7 @@ pub struct Cursor {
line_height: Pixels,
color: Hsla,
shape: CursorShape,
- block_text: Option<Line>,
+ block_text: Option<ShapedLine>,
}
impl Cursor {
@@ -3249,7 +3240,7 @@ impl Cursor {
line_height: Pixels,
color: Hsla,
shape: CursorShape,
- block_text: Option<Line>,
+ block_text: Option<ShapedLine>,
) -> Cursor {
Cursor {
origin,
@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
@@ -797,7 +797,7 @@ impl Item for Editor {
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
- let item_id = cx.view().entity_id().as_u64() as ItemId;
+ let item_id = cx.view().item_id().as_u64() as ItemId;
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
fn serialize(
@@ -828,7 +828,7 @@ impl Item for Editor {
serialize(
buffer,
*workspace_id,
- cx.view().entity_id().as_u64() as ItemId,
+ cx.view().item_id().as_u64() as ItemId,
cx,
);
}
@@ -98,7 +98,7 @@ pub fn up_by_rows(
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
- _ => map.x_for_point(start, text_layout_details),
+ _ => map.x_for_display_point(start, text_layout_details),
};
let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
Bias::Left,
);
if point.row() < start.row() {
- *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+ *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
- _ => map.x_for_point(start, text_layout_details),
+ _ => map.x_for_display_point(start, text_layout_details),
};
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
- *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+ *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
- goal_x = map.x_for_point(point, text_layout_details)
+ goal_x = map.x_for_display_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);
@@ -313,14 +313,14 @@ impl SelectionsCollection {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
- let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+ let line = display_map.layout_row(row, &text_layout_details);
dbg!("****START COL****");
- let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
- if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
+ let start_col = line.closest_index_for_x(positions.start) as u32;
+ if start_col < line_len || (is_empty && positions.start == line.width) {
let start = DisplayPoint::new(row, start_col);
dbg!("****END COL****");
- let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+ let end_col = line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
dbg!(start_col, end_col);
@@ -2,9 +2,9 @@ use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
- actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
- ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
- WindowContext,
+ actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
+ ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
+ WeakView,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -19,7 +19,7 @@ use text::Point;
use theme::ActiveTheme;
use ui::{v_stack, HighlightedLabel, StyledExt};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
actions!(Toggle);
@@ -111,10 +111,9 @@ impl FileFinder {
}
}
-impl EventEmitter<ModalEvent> for FileFinder {}
-impl Modal for FileFinder {
- fn focus(&self, cx: &mut WindowContext) {
- self.picker.update(cx, |picker, cx| picker.focus(cx))
+impl ManagedView for FileFinder {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
}
}
impl Render for FileFinder {
@@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
.log_err();
}
}
- finder
- .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
- .ok()?;
+ finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
Some(())
})
@@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
self.file_finder
- .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+ .update(cx, |_, cx| cx.emit(Dismiss))
.log_err();
}
@@ -1763,7 +1760,7 @@ mod tests {
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
super::init(cx);
editor::init(cx);
@@ -1,13 +1,13 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
- Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+ actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+ Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
};
use text::{Bias, Point};
use theme::ActiveTheme;
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
actions!(Toggle);
@@ -23,10 +23,9 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>,
}
-impl EventEmitter<ModalEvent> for GoToLine {}
-impl Modal for GoToLine {
- fn focus(&self, cx: &mut WindowContext) {
- self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+impl ManagedView for GoToLine {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.line_editor.focus_handle(cx)
}
}
@@ -88,7 +87,7 @@ impl GoToLine {
) {
match event {
// todo!() this isn't working...
- editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
+ editor::Event::Blurred => cx.emit(Dismiss),
editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}
@@ -123,7 +122,7 @@ impl GoToLine {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(ModalEvent::Dismissed);
+ cx.emit(Dismiss);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
self.prev_scroll_position.take();
}
- cx.emit(ModalEvent::Dismissed);
+ cx.emit(Dismiss);
}
}
@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor.workspace = true
+linkme = "0.3"
derive_more.workspace = true
dhat = { version = "0.3", optional = true }
env_logger = { version = "0.9", optional = true }
@@ -1,10 +1,12 @@
use crate::SharedString;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
-use lazy_static::lazy_static;
-use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
-use serde::Deserialize;
-use std::any::{type_name, Any, TypeId};
+pub use no_action::NoAction;
+use serde_json::json;
+use std::{
+ any::{Any, TypeId},
+ ops::Deref,
+};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
/// ```rust
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
/// ```
-/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
-/// it will automatically
+/// More complex data types can also be actions. If you annotate your type with the action derive macro
+/// it will be implemented and registered automatically.
/// ```
-/// #[action]
+/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
///
-/// Any type A that satisfies the following bounds is automatically an action:
-///
-/// ```
-/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-/// ```
-///
-/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
-/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
-/// generates the code needed to register your action before `main`. Then you'll need to implement all
-/// the traits manually.
+/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
+/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
/// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
/// pub content: SharedString,
/// }
///
-/// impl std::default::Default for Paste {
-/// fn default() -> Self {
-/// Self {
-/// content: SharedString::from("🍝"),
-/// }
-/// }
+/// impl gpui::Action for Paste {
+/// ///...
/// }
/// ```
-pub trait Action: std::fmt::Debug + 'static {
- fn qualified_name() -> SharedString
- where
- Self: Sized;
- fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+pub trait Action: 'static {
+ fn boxed_clone(&self) -> Box<dyn Action>;
+ fn as_any(&self) -> &dyn Any;
+ fn partial_eq(&self, action: &dyn Action) -> bool;
+ fn name(&self) -> &str;
+
+ fn debug_name() -> &'static str
where
Self: Sized;
- fn is_registered() -> bool
+ fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
-
- fn partial_eq(&self, action: &dyn Action) -> bool;
- fn boxed_clone(&self) -> Box<dyn Action>;
- fn as_any(&self) -> &dyn Any;
}
-// Types become actions by satisfying a list of trait bounds.
-impl<A> Action for A
-where
- A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static,
-{
- fn qualified_name() -> SharedString {
- let name = type_name::<A>();
- let mut separator_matches = name.rmatch_indices("::");
- separator_matches.next().unwrap();
- let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
- // todo!() remove the 2 replacement when migration is done
- name[name_start_ix..].replace("2::", "::").into()
- }
-
- fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
- where
- Self: Sized,
- {
- let action = if let Some(params) = params {
- serde_json::from_value(params).context("failed to deserialize action")?
- } else {
- Self::default()
- };
- Ok(Box::new(action))
- }
-
- fn is_registered() -> bool {
- ACTION_REGISTRY
- .read()
- .names_by_type_id
- .get(&TypeId::of::<A>())
- .is_some()
- }
-
- fn partial_eq(&self, action: &dyn Action) -> bool {
- action
- .as_any()
- .downcast_ref::<Self>()
- .map_or(false, |a| self == a)
- }
-
- fn boxed_clone(&self) -> Box<dyn Action> {
- Box::new(self.clone())
- }
-
- fn as_any(&self) -> &dyn Any {
- self
+impl std::fmt::Debug for dyn Action {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("dyn Action")
+ .field("type_name", &self.name())
+ .finish()
}
}
@@ -119,69 +65,93 @@ impl dyn Action {
pub fn type_id(&self) -> TypeId {
self.as_any().type_id()
}
-
- pub fn name(&self) -> SharedString {
- ACTION_REGISTRY
- .read()
- .names_by_type_id
- .get(&self.type_id())
- .expect("type is not a registered action")
- .clone()
- }
}
-type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
-
-lazy_static! {
- static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
-}
+type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
-#[derive(Default)]
-struct ActionRegistry {
+pub(crate) struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
}
-/// Register an action type to allow it to be referenced in keymaps.
-pub fn register_action<A: Action>() {
- let name = A::qualified_name();
- let mut lock = ACTION_REGISTRY.write();
- lock.builders_by_name.insert(name.clone(), A::build);
- lock.names_by_type_id
- .insert(TypeId::of::<A>(), name.clone());
- lock.all_names.push(name);
+impl Default for ActionRegistry {
+ fn default() -> Self {
+ let mut this = ActionRegistry {
+ builders_by_name: Default::default(),
+ names_by_type_id: Default::default(),
+ all_names: Default::default(),
+ };
+
+ this.load_actions();
+
+ this
+ }
}
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
- let lock = ACTION_REGISTRY.read();
- let name = lock
- .names_by_type_id
- .get(type_id)
- .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
- .clone();
- drop(lock);
-
- build_action(&name, None)
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub type MacroActionBuilder = fn() -> ActionData;
+
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub struct ActionData {
+ pub name: &'static str,
+ pub type_id: TypeId,
+ pub build: ActionBuilder,
}
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
- let lock = ACTION_REGISTRY.read();
+/// This constant must be public to be accessible from other crates.
+/// But it's existence is an implementation detail and should not be used directly.
+#[doc(hidden)]
+#[linkme::distributed_slice]
+pub static __GPUI_ACTIONS: [MacroActionBuilder];
+
+impl ActionRegistry {
+ /// Load all registered actions into the registry.
+ pub(crate) fn load_actions(&mut self) {
+ for builder in __GPUI_ACTIONS {
+ let action = builder();
+ //todo(remove)
+ let name: SharedString = remove_the_2(action.name).into();
+ self.builders_by_name.insert(name.clone(), action.build);
+ self.names_by_type_id.insert(action.type_id, name.clone());
+ self.all_names.push(name);
+ }
+ }
+
+ /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+ pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
+ let name = self
+ .names_by_type_id
+ .get(type_id)
+ .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+ .clone();
- let build_action = lock
- .builders_by_name
- .get(name)
- .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
- (build_action)(params)
-}
+ self.build_action(&name, None)
+ }
+
+ /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+ pub fn build_action(
+ &self,
+ name: &str,
+ params: Option<serde_json::Value>,
+ ) -> Result<Box<dyn Action>> {
+ //todo(remove)
+ let name = remove_the_2(name);
+ let build_action = self
+ .builders_by_name
+ .get(name.deref())
+ .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+ (build_action)(params.unwrap_or_else(|| json!({})))
+ .with_context(|| format!("Attempting to build action {}", name))
+ }
-pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
- let lock = ACTION_REGISTRY.read();
- RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
- registry.all_names.as_slice()
- })
+ pub fn all_action_names(&self) -> &[SharedString] {
+ self.all_names.as_slice()
+ }
}
/// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
() => {};
( $name:ident ) => {
- #[gpui::action]
+ #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
pub struct $name;
};
@@ -200,3 +170,20 @@ macro_rules! actions {
actions!($($rest)*);
};
}
+
+//todo!(remove)
+pub fn remove_the_2(action_name: &str) -> String {
+ let mut separator_matches = action_name.rmatch_indices("::");
+ separator_matches.next().unwrap();
+ let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
+ // todo!() remove the 2 replacement when migration is done
+ action_name[name_start_ix..]
+ .replace("2::", "::")
+ .to_string()
+}
+
+mod no_action {
+ use crate as gpui;
+
+ actions!(NoAction);
+}
@@ -14,12 +14,13 @@ use smallvec::SmallVec;
pub use test_context::*;
use crate::{
- current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
- AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
- Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
- LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
- Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
- Window, WindowContext, WindowHandle, WindowId,
+ current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+ AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+ DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
+ ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
+ PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+ TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
+ WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
text_system: Arc<TextSystem>,
flushing_effects: bool,
pending_updates: usize,
+ pub(crate) actions: Rc<ActionRegistry>,
pub(crate) active_drag: Option<AnyDrag>,
pub(crate) active_tooltip: Option<AnyTooltip>,
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
platform: platform.clone(),
app_metadata,
text_system,
+ actions: Rc::new(ActionRegistry::default()),
flushing_effects: false,
pending_updates: 0,
active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
pub fn propagate(&mut self) {
self.propagate_event = true;
}
+
+ pub fn build_action(
+ &self,
+ name: &str,
+ data: Option<serde_json::Value>,
+ ) -> Result<Box<dyn Action>> {
+ self.actions.build_action(name, data)
+ }
+
+ pub fn all_action_names(&self) -> &[SharedString] {
+ self.actions.all_action_names()
+ }
}
impl Context for AppContext {
@@ -182,6 +182,10 @@ pub struct AsyncWindowContext {
}
impl AsyncWindowContext {
+ pub fn window_handle(&self) -> AnyWindowHandle {
+ self.window
+ }
+
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
Self { app, window }
}
@@ -370,10 +370,19 @@ impl<T: Send> Model<T> {
})
});
- cx.executor().run_until_parked();
- rx.try_next()
- .expect("no event received")
- .expect("model was dropped")
+ // Run other tasks until the event is emitted.
+ loop {
+ match rx.try_next() {
+ Ok(Some(event)) => return event,
+ Ok(None) => panic!("model was dropped"),
+ Err(_) => {
+ if !cx.executor().tick() {
+ break;
+ }
+ }
+ }
+ }
+ panic!("no event received")
}
}
@@ -13,7 +13,7 @@ pub trait Element<V: 'static> {
fn layout(
&mut self,
view_state: &mut V,
- previous_element_state: Option<Self::ElementState>,
+ element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> (LayoutId, Self::ElementState);
@@ -237,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
//
// if we are relying on this side-effect still, removing the debug_assert!
// likely breaks the command_palette tests.
- debug_assert!(
- A::is_registered(),
- "{:?} is not registered as an action",
- A::qualified_name()
- );
+ // debug_assert!(
+ // A::is_registered(),
+ // "{:?} is not registered as an action",
+ // A::qualified_name()
+ // );
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
Box::new(move |view, action, phase, cx| {
@@ -960,11 +960,11 @@ where
cx.background_executor().timer(TOOLTIP_DELAY).await;
view.update(&mut cx, move |view_state, cx| {
active_tooltip.borrow_mut().replace(ActiveTooltip {
- waiting: None,
tooltip: Some(AnyTooltip {
view: tooltip_builder(view_state, cx),
cursor_offset: cx.mouse_position(),
}),
+ _task: None,
});
cx.notify();
})
@@ -972,12 +972,17 @@ where
}
});
active_tooltip.borrow_mut().replace(ActiveTooltip {
- waiting: Some(task),
tooltip: None,
+ _task: Some(task),
});
}
});
+ let active_tooltip = element_state.active_tooltip.clone();
+ cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+ active_tooltip.borrow_mut().take();
+ });
+
if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
if active_tooltip.tooltip.is_some() {
cx.active_tooltip = active_tooltip.tooltip.clone()
@@ -1207,9 +1212,8 @@ pub struct InteractiveElementState {
}
pub struct ActiveTooltip {
- #[allow(unused)] // used to drop the task
- waiting: Option<Task<()>>,
tooltip: Option<AnyTooltip>,
+ _task: Option<Task<()>>,
}
/// Whether or not the element or a group that contains it is clicked by the mouse.
@@ -1,8 +1,9 @@
use smallvec::SmallVec;
+use taffy::style::{Display, Position};
use crate::{
- point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
- Size, Style,
+ point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
+ Point, Size, Style,
};
pub struct OverlayState {
@@ -14,7 +15,7 @@ pub struct Overlay<V> {
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
- // anchor_position: Option<Vector2F>,
+ anchor_position: Option<Point<Pixels>>,
// position_mode: OverlayPositionMode,
}
@@ -25,6 +26,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::SwitchAnchor,
+ anchor_position: None,
}
}
@@ -35,6 +37,13 @@ impl<V> Overlay<V> {
self
}
+ /// Sets the position in window co-ordinates
+ /// (otherwise the location the overlay is rendered is used)
+ pub fn position(mut self, anchor: Point<Pixels>) -> Self {
+ self.anchor_position = Some(anchor);
+ self
+ }
+
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = OverlayFitMode::SnapToWindow;
@@ -48,6 +57,12 @@ impl<V: 'static> ParentComponent<V> for Overlay<V> {
}
}
+impl<V: 'static> Component<V> for Overlay<V> {
+ fn render(self) -> AnyElement<V> {
+ AnyElement::new(self)
+ }
+}
+
impl<V: 'static> Element<V> for Overlay<V> {
type ElementState = OverlayState;
@@ -66,7 +81,12 @@ impl<V: 'static> Element<V> for Overlay<V> {
.iter_mut()
.map(|child| child.layout(view_state, cx))
.collect::<SmallVec<_>>();
- let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
+
+ let mut overlay_style = Style::default();
+ overlay_style.position = Position::Absolute;
+ overlay_style.display = Display::Flex;
+
+ let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
(layout_id, OverlayState { child_layout_ids })
}
@@ -90,7 +110,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
child_max = child_max.max(&child_bounds.lower_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
- let origin = bounds.origin;
+ let origin = self.anchor_position.unwrap_or(bounds.origin);
let mut desired = self.anchor_corner.get_bounds(origin, size);
let limits = Bounds {
@@ -184,6 +204,15 @@ impl AnchorCorner {
Bounds { origin, size }
}
+ pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
+ match self {
+ Self::TopLeft => bounds.origin,
+ Self::TopRight => bounds.upper_right(),
+ Self::BottomLeft => bounds.lower_left(),
+ Self::BottomRight => bounds.lower_right(),
+ }
+ }
+
fn switch_axis(self, axis: Axis) -> Self {
match axis {
Axis::Vertical => match self {
@@ -1,76 +1,39 @@
use crate::{
- AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
- Size, TextRun, ViewContext,
+ AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
+ SharedString, Size, TextRun, ViewContext, WrappedLine,
};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
-use std::{marker::PhantomData, sync::Arc};
+use std::{cell::Cell, rc::Rc, sync::Arc};
use util::ResultExt;
-impl<V: 'static> Component<V> for SharedString {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self,
- runs: None,
- state_type: PhantomData,
- }
- .render()
- }
-}
-
-impl<V: 'static> Component<V> for &'static str {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- state_type: PhantomData,
- }
- .render()
- }
-}
-
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- state_type: PhantomData,
- }
- .render()
- }
-}
-
-pub struct Text<V> {
+pub struct Text {
text: SharedString,
runs: Option<Vec<TextRun>>,
- state_type: PhantomData<V>,
}
-impl<V: 'static> Text<V> {
- /// styled renders text that has different runs of different styles.
- /// callers are responsible for setting the correct style for each run.
- ////
- /// For uniform text you can usually just pass a string as a child, and
- /// cx.text_style() will be used automatically.
+impl Text {
+ /// Renders text with runs of different styles.
+ ///
+ /// Callers are responsible for setting the correct style for each run.
+ /// For text with a uniform style, you can usually avoid calling this constructor
+ /// and just pass text directly.
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
Text {
text,
runs: Some(runs),
- state_type: Default::default(),
}
}
}
-impl<V: 'static> Component<V> for Text<V> {
+impl<V: 'static> Component<V> for Text {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
-impl<V: 'static> Element<V> for Text<V> {
- type ElementState = Arc<Mutex<Option<TextElementState>>>;
+impl<V: 'static> Element<V> for Text {
+ type ElementState = TextState;
fn element_id(&self) -> Option<crate::ElementId> {
None
@@ -103,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
let element_state = element_state.clone();
move |known_dimensions, _| {
let Some(lines) = text_system
- .layout_text(
+ .shape_text(
&text,
font_size,
&runs[..],
@@ -111,30 +74,23 @@ impl<V: 'static> Element<V> for Text<V> {
)
.log_err()
else {
- element_state.lock().replace(TextElementState {
+ element_state.lock().replace(TextStateInner {
lines: Default::default(),
line_height,
});
return Size::default();
};
- let line_count = lines
- .iter()
- .map(|line| line.wrap_count() + 1)
- .sum::<usize>();
- let size = Size {
- width: lines
- .iter()
- .map(|line| line.layout.width)
- .max()
- .unwrap()
- .ceil(),
- height: line_height * line_count,
- };
+ let mut size: Size<Pixels> = Size::default();
+ for line in &lines {
+ let line_size = line.size(line_height);
+ size.height += line_size.height;
+ size.width = size.width.max(line_size.width);
+ }
element_state
.lock()
- .replace(TextElementState { lines, line_height });
+ .replace(TextStateInner { lines, line_height });
size
}
@@ -165,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
}
}
-pub struct TextElementState {
- lines: SmallVec<[Line; 1]>,
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+impl TextState {
+ fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+ self.0.lock()
+ }
+}
+
+struct TextStateInner {
+ lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
}
+
+struct InteractiveText {
+ id: ElementId,
+ text: Text,
+}
+
+struct InteractiveTextState {
+ text_state: TextState,
+ clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+}
+
+impl<V: 'static> Element<V> for InteractiveText {
+ type ElementState = InteractiveTextState;
+
+ fn element_id(&self) -> Option<ElementId> {
+ Some(self.id.clone())
+ }
+
+ fn layout(
+ &mut self,
+ view_state: &mut V,
+ element_state: Option<Self::ElementState>,
+ cx: &mut ViewContext<V>,
+ ) -> (LayoutId, Self::ElementState) {
+ if let Some(InteractiveTextState {
+ text_state,
+ clicked_range_ixs,
+ }) = element_state
+ {
+ let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+ let element_state = InteractiveTextState {
+ text_state,
+ clicked_range_ixs,
+ };
+ (layout_id, element_state)
+ } else {
+ let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+ let element_state = InteractiveTextState {
+ text_state,
+ clicked_range_ixs: Rc::default(),
+ };
+ (layout_id, element_state)
+ }
+ }
+
+ fn paint(
+ &mut self,
+ bounds: Bounds<Pixels>,
+ view_state: &mut V,
+ element_state: &mut Self::ElementState,
+ cx: &mut ViewContext<V>,
+ ) {
+ self.text
+ .paint(bounds, view_state, &mut element_state.text_state, cx)
+ }
+}
+
+impl<V: 'static> Component<V> for SharedString {
+ fn render(self) -> AnyElement<V> {
+ Text {
+ text: self,
+ runs: None,
+ }
+ .render()
+ }
+}
+
+impl<V: 'static> Component<V> for &'static str {
+ fn render(self) -> AnyElement<V> {
+ Text {
+ text: self.into(),
+ runs: None,
+ }
+ .render()
+ }
+}
+
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+ fn render(self) -> AnyElement<V> {
+ Text {
+ text: self.into(),
+ runs: None,
+ }
+ .render()
+ }
+}
@@ -5,10 +5,11 @@ use std::{
fmt::Debug,
marker::PhantomData,
mem,
+ num::NonZeroUsize,
pin::Pin,
rc::Rc,
sync::{
- atomic::{AtomicBool, Ordering::SeqCst},
+ atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
task::{Context, Poll},
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
}
}
}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct TaskLabel(NonZeroUsize);
+
+impl TaskLabel {
+ pub fn new() -> Self {
+ static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
+ Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
+ }
+}
+
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
+
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
+
impl BackgroundExecutor {
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { dispatcher }
}
- /// Enqueues the given closure to be run on any thread. The closure returns
- /// a future which will be run to completion on any available thread.
+ /// Enqueues the given future to be run to completion on a background thread.
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
+ self.spawn_internal::<R>(Box::pin(future), None)
+ }
+
+ /// Enqueues the given future to be run to completion on a background thread.
+ /// The given label can be used to control the priority of the task in tests.
+ pub fn spawn_labeled<R>(
+ &self,
+ label: TaskLabel,
+ future: impl Future<Output = R> + Send + 'static,
+ ) -> Task<R>
+ where
+ R: Send + 'static,
+ {
+ self.spawn_internal::<R>(Box::pin(future), Some(label))
+ }
+
+ fn spawn_internal<R: Send + 'static>(
+ &self,
+ future: AnyFuture<R>,
+ label: Option<TaskLabel>,
+ ) -> Task<R> {
let dispatcher = self.dispatcher.clone();
- fn inner<R: Send + 'static>(
- dispatcher: Arc<dyn PlatformDispatcher>,
- future: AnyFuture<R>,
- ) -> Task<R> {
- let (runnable, task) =
- async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
- runnable.schedule();
- Task::Spawned(task)
- }
- inner::<R>(dispatcher, Box::pin(future))
+ let (runnable, task) =
+ async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
+ runnable.schedule();
+ Task::Spawned(task)
}
#[cfg(any(test, feature = "test-support"))]
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => return result,
Poll::Pending => {
- if !self.dispatcher.poll(background_only) {
+ if !self.dispatcher.tick(background_only) {
if awoken.swap(false, SeqCst) {
continue;
}
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
self.dispatcher.as_test().unwrap().simulate_random_delay()
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn deprioritize(&self, task_label: TaskLabel) {
+ self.dispatcher.as_test().unwrap().deprioritize(task_label)
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) {
self.dispatcher.as_test().unwrap().advance_clock(duration)
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn tick(&self) -> bool {
+ self.dispatcher.as_test().unwrap().tick(false)
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn run_until_parked(&self) {
self.dispatcher.as_test().unwrap().run_until_parked()
@@ -343,7 +343,7 @@ where
impl<T> Bounds<T>
where
- T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
+ T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
{
pub fn intersects(&self, other: &Bounds<T>) -> bool {
let my_lower_right = self.lower_right();
@@ -362,6 +362,13 @@ where
self.size.width = self.size.width.clone() + double_amount.clone();
self.size.height = self.size.height.clone() + double_amount;
}
+
+ pub fn center(&self) -> Point<T> {
+ Point {
+ x: self.origin.x.clone() + self.size.width.clone().half(),
+ y: self.origin.y.clone() + self.size.height.clone().half(),
+ }
+ }
}
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
@@ -1211,6 +1218,46 @@ impl From<()> for Length {
}
}
+pub trait Half {
+ fn half(&self) -> Self;
+}
+
+impl Half for f32 {
+ fn half(&self) -> Self {
+ self / 2.
+ }
+}
+
+impl Half for DevicePixels {
+ fn half(&self) -> Self {
+ Self(self.0 / 2)
+ }
+}
+
+impl Half for ScaledPixels {
+ fn half(&self) -> Self {
+ Self(self.0 / 2.)
+ }
+}
+
+impl Half for Pixels {
+ fn half(&self) -> Self {
+ Self(self.0 / 2.)
+ }
+}
+
+impl Half for Rems {
+ fn half(&self) -> Self {
+ Self(self.0 / 2.)
+ }
+}
+
+impl Half for GlobalPixels {
+ fn half(&self) -> Self {
+ Self(self.0 / 2.)
+ }
+}
+
pub trait IsZero {
fn is_zero(&self) -> bool;
}
@@ -49,11 +49,13 @@ pub use input::*;
pub use interactive::*;
pub use key_dispatch::*;
pub use keymap::*;
+pub use linkme;
pub use platform::*;
use private::Sealed;
pub use refineable::*;
pub use scene::*;
pub use serde;
+pub use serde_derive;
pub use serde_json;
pub use smallvec;
pub use smol::Timer;
@@ -1,6 +1,6 @@
use crate::{
- build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
- Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+ Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
+ Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
rc::Rc,
sync::Arc,
};
-use util::ResultExt;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Arc<Mutex<Keymap>>,
+ action_registry: Rc<ActionRegistry>,
}
#[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
}
impl DispatchTree {
- pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+ pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
Self {
node_stack: Vec::new(),
context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
focusable_node_ids: HashMap::default(),
keystroke_matchers: HashMap::default(),
keymap,
+ action_registry,
}
}
@@ -153,7 +154,9 @@ impl DispatchTree {
for node_id in self.dispatch_path(*node) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
- actions.extend(build_action_from_type(action_type).log_err());
+ // Intentionally silence these errors without logging.
+ // If an action cannot be built by default, it's not available.
+ actions.extend(self.action_registry.build_action_type(action_type).ok());
}
}
}
@@ -1,7 +1,10 @@
-use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
use collections::HashSet;
use smallvec::SmallVec;
-use std::{any::TypeId, collections::HashMap};
+use std::{
+ any::{Any, TypeId},
+ collections::HashMap,
+};
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
}
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
- // todo!("no action")
- // let no_action_id = (NoAction {}).id();
+ let no_action_id = &(NoAction {}).type_id();
let mut new_bindings = Vec::new();
- let has_new_disabled_keystrokes = false;
+ let mut has_new_disabled_keystrokes = false;
for binding in bindings {
- // if binding.action().id() == no_action_id {
- // has_new_disabled_keystrokes |= self
- // .disabled_keystrokes
- // .entry(binding.keystrokes)
- // .or_default()
- // .insert(binding.context_predicate);
- // } else {
- new_bindings.push(binding);
- // }
+ if binding.action.type_id() == *no_action_id {
+ has_new_disabled_keystrokes |= self
+ .disabled_keystrokes
+ .entry(binding.keystrokes)
+ .or_default()
+ .insert(binding.context_predicate);
+ } else {
+ new_bindings.push(binding);
+ }
}
if has_new_disabled_keystrokes {
@@ -8,7 +8,7 @@ use crate::{
point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
- SharedString, Size,
+ SharedString, Size, TaskLabel,
};
use anyhow::{anyhow, bail};
use async_task::Runnable;
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
pub trait PlatformDispatcher: Send + Sync {
fn is_main_thread(&self) -> bool;
- fn dispatch(&self, runnable: Runnable);
+ fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
fn dispatch_on_main_thread(&self, runnable: Runnable);
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
- fn poll(&self, background_only: bool) -> bool;
+ fn tick(&self, background_only: bool) -> bool;
fn park(&self);
fn unparker(&self) -> Unparker;
@@ -2,7 +2,7 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use objc::{
class, msg_send,
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
is_main_thread == YES
}
- fn dispatch(&self, runnable: Runnable) {
+ fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
unsafe {
dispatch_async_f(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
}
}
- fn poll(&self, _background_only: bool) -> bool {
+ fn tick(&self, _background_only: bool) -> bool {
false
}
@@ -343,10 +343,10 @@ impl MacTextSystemState {
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
{
- string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+ string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
let utf16_line_len = string.char_len() as usize;
- let mut ix_converter = StringIndexConverter::new(text);
+ let mut ix_converter = StringIndexConverter::new(text.as_ref());
for run in font_runs {
let utf8_end = ix_converter.utf8_ix + run.len;
let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
};
let font_id = self.id_for_native_font(font);
- let mut ix_converter = StringIndexConverter::new(text);
+ let mut ix_converter = StringIndexConverter::new(text.as_ref());
let mut glyphs = SmallVec::new();
for ((glyph_id, position), glyph_utf16_ix) in run
.glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
+ runs,
+ font_size,
width: typographic_bounds.width.into(),
ascent: typographic_bounds.ascent.into(),
descent: typographic_bounds.descent.into(),
- runs,
- font_size,
len: text.len(),
}
}
@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
if let Some(mut event) = event {
- let synthesized_second_event = match &mut event {
+ match &mut event {
InputEvent::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
..
},
) => {
+ // On mac, a ctrl-left click should be handled as a right click.
*event = MouseDownEvent {
button: MouseButton::Right,
modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
click_count: 1,
..*event
};
-
- Some(InputEvent::MouseDown(MouseDownEvent {
- button: MouseButton::Right,
- ..*event
- }))
}
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
// user is still holding ctrl when releasing the left mouse button
- InputEvent::MouseUp(MouseUpEvent {
- button: MouseButton::Left,
- modifiers: Modifiers { control: true, .. },
- ..
- }) => {
- lock.synthetic_drag_counter += 1;
- return;
+ InputEvent::MouseUp(
+ event @ MouseUpEvent {
+ button: MouseButton::Left,
+ modifiers: Modifiers { control: true, .. },
+ ..
+ },
+ ) => {
+ *event = MouseUpEvent {
+ button: MouseButton::Right,
+ modifiers: Modifiers {
+ control: false,
+ ..event.modifiers
+ },
+ click_count: 1,
+ ..*event
+ };
}
- _ => None,
+ _ => {}
};
match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(event);
- if let Some(event) = synthesized_second_event {
- callback(event);
- }
window_state.lock().event_callback = Some(callback);
}
}
@@ -1,7 +1,7 @@
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use backtrace::Backtrace;
-use collections::{HashMap, VecDeque};
+use collections::{HashMap, HashSet, VecDeque};
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use rand::prelude::*;
@@ -28,12 +28,14 @@ struct TestDispatcherState {
random: StdRng,
foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
background: Vec<Runnable>,
+ deprioritized_background: Vec<Runnable>,
delayed: Vec<(Duration, Runnable)>,
time: Duration,
is_main_thread: bool,
next_id: TestDispatcherId,
allow_parking: bool,
waiting_backtrace: Option<Backtrace>,
+ deprioritized_task_labels: HashSet<TaskLabel>,
}
impl TestDispatcher {
@@ -43,12 +45,14 @@ impl TestDispatcher {
random,
foreground: HashMap::default(),
background: Vec::new(),
+ deprioritized_background: Vec::new(),
delayed: Vec::new(),
time: Duration::ZERO,
is_main_thread: true,
next_id: TestDispatcherId(1),
allow_parking: false,
waiting_backtrace: None,
+ deprioritized_task_labels: Default::default(),
};
TestDispatcher {
@@ -101,8 +105,15 @@ impl TestDispatcher {
}
}
+ pub fn deprioritize(&self, task_label: TaskLabel) {
+ self.state
+ .lock()
+ .deprioritized_task_labels
+ .insert(task_label);
+ }
+
pub fn run_until_parked(&self) {
- while self.poll(false) {}
+ while self.tick(false) {}
}
pub fn parking_allowed(&self) -> bool {
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
self.state.lock().is_main_thread
}
- fn dispatch(&self, runnable: Runnable) {
- self.state.lock().background.push(runnable);
+ fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+ {
+ let mut state = self.state.lock();
+ if label.map_or(false, |label| {
+ state.deprioritized_task_labels.contains(&label)
+ }) {
+ state.deprioritized_background.push(runnable);
+ } else {
+ state.background.push(runnable);
+ }
+ }
self.unparker.unpark();
}
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
state.delayed.insert(ix, (next_time, runnable));
}
- fn poll(&self, background_only: bool) -> bool {
+ fn tick(&self, background_only: bool) -> bool {
let mut state = self.state.lock();
while let Some((deadline, _)) = state.delayed.first() {
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
};
let background_len = state.background.len();
+ let runnable;
+ let main_thread;
if foreground_len == 0 && background_len == 0 {
- return false;
- }
-
- let main_thread = state.random.gen_ratio(
- foreground_len as u32,
- (foreground_len + background_len) as u32,
- );
- let was_main_thread = state.is_main_thread;
- state.is_main_thread = main_thread;
-
- let runnable = if main_thread {
- let state = &mut *state;
- let runnables = state
- .foreground
- .values_mut()
- .filter(|runnables| !runnables.is_empty())
- .choose(&mut state.random)
- .unwrap();
- runnables.pop_front().unwrap()
+ let deprioritized_background_len = state.deprioritized_background.len();
+ if deprioritized_background_len == 0 {
+ return false;
+ }
+ let ix = state.random.gen_range(0..deprioritized_background_len);
+ main_thread = false;
+ runnable = state.deprioritized_background.swap_remove(ix);
} else {
- let ix = state.random.gen_range(0..background_len);
- state.background.swap_remove(ix)
+ main_thread = state.random.gen_ratio(
+ foreground_len as u32,
+ (foreground_len + background_len) as u32,
+ );
+ if main_thread {
+ let state = &mut *state;
+ runnable = state
+ .foreground
+ .values_mut()
+ .filter(|runnables| !runnables.is_empty())
+ .choose(&mut state.random)
+ .unwrap()
+ .pop_front()
+ .unwrap();
+ } else {
+ let ix = state.random.gen_range(0..background_len);
+ runnable = state.background.swap_remove(ix);
+ };
};
+ let was_main_thread = state.is_main_thread;
+ state.is_main_thread = main_thread;
drop(state);
runnable.run();
-
self.state.lock().is_main_thread = was_main_thread;
true
@@ -203,6 +203,7 @@ impl TextStyle {
style: self.font_style,
},
color: self.color,
+ background_color: None,
underline: self.underline.clone(),
}
}
@@ -3,20 +3,20 @@ mod line;
mod line_layout;
mod line_wrapper;
-use anyhow::anyhow;
pub use font_features::*;
pub use line::*;
pub use line_layout::*;
pub use line_wrapper::*;
-use smallvec::SmallVec;
use crate::{
px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
UnderlineStyle,
};
+use anyhow::anyhow;
use collections::HashMap;
use core::fmt;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
use std::{
cmp,
fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
}
}
- pub fn layout_text(
+ pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
+ ) -> Result<Arc<LineLayout>> {
+ let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+ for run in runs.iter() {
+ let font_id = self.font_id(&run.font)?;
+ if let Some(last_run) = font_runs.last_mut() {
+ if last_run.font_id == font_id {
+ last_run.len += run.len;
+ continue;
+ }
+ }
+ font_runs.push(FontRun {
+ len: run.len,
+ font_id,
+ });
+ }
+
+ let layout = self
+ .line_layout_cache
+ .layout_line(&text, font_size, &font_runs);
+
+ font_runs.clear();
+ self.font_runs_pool.lock().push(font_runs);
+
+ Ok(layout)
+ }
+
+ pub fn shape_line(
+ &self,
+ text: SharedString,
+ font_size: Pixels,
+ runs: &[TextRun],
+ ) -> Result<ShapedLine> {
+ debug_assert!(
+ text.find('\n').is_none(),
+ "text argument should not contain newlines"
+ );
+
+ let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+ for run in runs {
+ if let Some(last_run) = decoration_runs.last_mut() {
+ if last_run.color == run.color && last_run.underline == run.underline {
+ last_run.len += run.len as u32;
+ continue;
+ }
+ }
+ decoration_runs.push(DecorationRun {
+ len: run.len as u32,
+ color: run.color,
+ underline: run.underline.clone(),
+ });
+ }
+
+ let layout = self.layout_line(text.as_ref(), font_size, runs)?;
+
+ Ok(ShapedLine {
+ layout,
+ text,
+ decoration_runs,
+ })
+ }
+
+ pub fn shape_text(
+ &self,
+ text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
+ font_size: Pixels,
+ runs: &[TextRun],
wrap_width: Option<Pixels>,
- ) -> Result<SmallVec<[Line; 1]>> {
+ ) -> Result<SmallVec<[WrappedLine; 1]>> {
let mut runs = runs.iter().cloned().peekable();
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
@@ -210,10 +276,11 @@ impl TextSystem {
let layout = self
.line_layout_cache
- .layout_line(&line_text, font_size, &font_runs, wrap_width);
- lines.push(Line {
+ .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+ lines.push(WrappedLine {
layout,
- decorations: decoration_runs,
+ decoration_runs,
+ text: SharedString::from(line_text),
});
line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
pub len: usize,
pub font: Font,
pub color: Hsla,
+ pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
}
@@ -1,5 +1,5 @@
use crate::{
- black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+ black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
@@ -14,23 +14,51 @@ pub struct DecorationRun {
}
#[derive(Clone, Default, Debug, Deref, DerefMut)]
-pub struct Line {
+pub struct ShapedLine {
#[deref]
#[deref_mut]
- pub(crate) layout: Arc<WrappedLineLayout>,
- pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+ pub(crate) layout: Arc<LineLayout>,
+ pub text: SharedString,
+ pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
}
-impl Line {
- pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
- size(
- self.layout.width,
- line_height * (self.layout.wrap_boundaries.len() + 1),
- )
+impl ShapedLine {
+ pub fn len(&self) -> usize {
+ self.layout.len
}
- pub fn wrap_count(&self) -> usize {
- self.layout.wrap_boundaries.len()
+ pub fn paint(
+ &self,
+ origin: Point<Pixels>,
+ line_height: Pixels,
+ cx: &mut WindowContext,
+ ) -> Result<()> {
+ paint_line(
+ origin,
+ &self.layout,
+ line_height,
+ &self.decoration_runs,
+ None,
+ &[],
+ cx,
+ )?;
+
+ Ok(())
+ }
+}
+
+#[derive(Clone, Default, Debug, Deref, DerefMut)]
+pub struct WrappedLine {
+ #[deref]
+ #[deref_mut]
+ pub(crate) layout: Arc<WrappedLineLayout>,
+ pub text: SharedString,
+ pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
+}
+
+impl WrappedLine {
+ pub fn len(&self) -> usize {
+ self.layout.len()
}
pub fn paint(
@@ -39,75 +67,50 @@ impl Line {
line_height: Pixels,
cx: &mut WindowContext,
) -> Result<()> {
- let padding_top =
- (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
- let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
-
- let mut style_runs = self.decorations.iter();
- let mut wraps = self.layout.wrap_boundaries.iter().peekable();
- let mut run_end = 0;
- let mut color = black();
- let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
- let text_system = cx.text_system().clone();
-
- let mut glyph_origin = origin;
- let mut prev_glyph_position = Point::default();
- for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
- let max_glyph_size = text_system
- .bounding_box(run.font_id, self.layout.layout.font_size)?
- .size;
-
- for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
- glyph_origin.x += glyph.position.x - prev_glyph_position.x;
-
- if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
- wraps.next();
- if let Some((underline_origin, underline_style)) = current_underline.take() {
- cx.paint_underline(
- underline_origin,
- glyph_origin.x - underline_origin.x,
- &underline_style,
- )?;
- }
-
- glyph_origin.x = origin.x;
- glyph_origin.y += line_height;
- }
- prev_glyph_position = glyph.position;
-
- let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
- if glyph.index >= run_end {
- if let Some(style_run) = style_runs.next() {
- if let Some((_, underline_style)) = &mut current_underline {
- if style_run.underline.as_ref() != Some(underline_style) {
- finished_underline = current_underline.take();
- }
- }
- if let Some(run_underline) = style_run.underline.as_ref() {
- current_underline.get_or_insert((
- point(
- glyph_origin.x,
- origin.y
- + baseline_offset.y
- + (self.layout.layout.descent * 0.618),
- ),
- UnderlineStyle {
- color: Some(run_underline.color.unwrap_or(style_run.color)),
- thickness: run_underline.thickness,
- wavy: run_underline.wavy,
- },
- ));
- }
+ paint_line(
+ origin,
+ &self.layout.unwrapped_layout,
+ line_height,
+ &self.decoration_runs,
+ self.wrap_width,
+ &self.wrap_boundaries,
+ cx,
+ )?;
- run_end += style_run.len as usize;
- color = style_run.color;
- } else {
- run_end = self.layout.text.len();
- finished_underline = current_underline.take();
- }
- }
+ Ok(())
+ }
+}
- if let Some((underline_origin, underline_style)) = finished_underline {
+fn paint_line(
+ origin: Point<Pixels>,
+ layout: &LineLayout,
+ line_height: Pixels,
+ decoration_runs: &[DecorationRun],
+ wrap_width: Option<Pixels>,
+ wrap_boundaries: &[WrapBoundary],
+ cx: &mut WindowContext<'_>,
+) -> Result<()> {
+ let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
+ let baseline_offset = point(px(0.), padding_top + layout.ascent);
+ let mut decoration_runs = decoration_runs.iter();
+ let mut wraps = wrap_boundaries.iter().peekable();
+ let mut run_end = 0;
+ let mut color = black();
+ let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+ let text_system = cx.text_system().clone();
+ let mut glyph_origin = origin;
+ let mut prev_glyph_position = Point::default();
+ for (run_ix, run) in layout.runs.iter().enumerate() {
+ let max_glyph_size = text_system
+ .bounding_box(run.font_id, layout.font_size)?
+ .size;
+
+ for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+ glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+ if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+ wraps.next();
+ if let Some((underline_origin, underline_style)) = current_underline.take() {
cx.paint_underline(
underline_origin,
glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
)?;
}
- let max_glyph_bounds = Bounds {
- origin: glyph_origin,
- size: max_glyph_size,
- };
-
- let content_mask = cx.content_mask();
- if max_glyph_bounds.intersects(&content_mask.bounds) {
- if glyph.is_emoji {
- cx.paint_emoji(
- glyph_origin + baseline_offset,
- run.font_id,
- glyph.id,
- self.layout.layout.font_size,
- )?;
- } else {
- cx.paint_glyph(
- glyph_origin + baseline_offset,
- run.font_id,
- glyph.id,
- self.layout.layout.font_size,
- color,
- )?;
+ glyph_origin.x = origin.x;
+ glyph_origin.y += line_height;
+ }
+ prev_glyph_position = glyph.position;
+
+ let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+ if glyph.index >= run_end {
+ if let Some(style_run) = decoration_runs.next() {
+ if let Some((_, underline_style)) = &mut current_underline {
+ if style_run.underline.as_ref() != Some(underline_style) {
+ finished_underline = current_underline.take();
+ }
}
+ if let Some(run_underline) = style_run.underline.as_ref() {
+ current_underline.get_or_insert((
+ point(
+ glyph_origin.x,
+ origin.y + baseline_offset.y + (layout.descent * 0.618),
+ ),
+ UnderlineStyle {
+ color: Some(run_underline.color.unwrap_or(style_run.color)),
+ thickness: run_underline.thickness,
+ wavy: run_underline.wavy,
+ },
+ ));
+ }
+
+ run_end += style_run.len as usize;
+ color = style_run.color;
+ } else {
+ run_end = layout.len;
+ finished_underline = current_underline.take();
}
}
- }
- if let Some((underline_start, underline_style)) = current_underline.take() {
- let line_end_x = origin.x + self.layout.layout.width;
- cx.paint_underline(
- underline_start,
- line_end_x - underline_start.x,
- &underline_style,
- )?;
+ if let Some((underline_origin, underline_style)) = finished_underline {
+ cx.paint_underline(
+ underline_origin,
+ glyph_origin.x - underline_origin.x,
+ &underline_style,
+ )?;
+ }
+
+ let max_glyph_bounds = Bounds {
+ origin: glyph_origin,
+ size: max_glyph_size,
+ };
+
+ let content_mask = cx.content_mask();
+ if max_glyph_bounds.intersects(&content_mask.bounds) {
+ if glyph.is_emoji {
+ cx.paint_emoji(
+ glyph_origin + baseline_offset,
+ run.font_id,
+ glyph.id,
+ layout.font_size,
+ )?;
+ } else {
+ cx.paint_glyph(
+ glyph_origin + baseline_offset,
+ run.font_id,
+ glyph.id,
+ layout.font_size,
+ color,
+ )?;
+ }
+ }
}
+ }
- Ok(())
+ if let Some((underline_start, underline_style)) = current_underline.take() {
+ let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
+ cx.paint_underline(
+ underline_start,
+ line_end_x - underline_start.x,
+ &underline_style,
+ )?;
}
+
+ Ok(())
}
@@ -1,5 +1,4 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
-use derive_more::{Deref, DerefMut};
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
}
}
-#[derive(Deref, DerefMut, Default, Debug)]
+#[derive(Default, Debug)]
pub struct WrappedLineLayout {
- #[deref]
- #[deref_mut]
- pub layout: LineLayout,
- pub text: SharedString,
+ pub unwrapped_layout: Arc<LineLayout>,
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+ pub wrap_width: Option<Pixels>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
pub glyph_ix: usize,
}
+impl WrappedLineLayout {
+ pub fn len(&self) -> usize {
+ self.unwrapped_layout.len
+ }
+
+ pub fn width(&self) -> Pixels {
+ self.wrap_width
+ .unwrap_or(Pixels::MAX)
+ .min(self.unwrapped_layout.width)
+ }
+
+ pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+ Size {
+ width: self.width(),
+ height: line_height * (self.wrap_boundaries.len() + 1),
+ }
+ }
+
+ pub fn ascent(&self) -> Pixels {
+ self.unwrapped_layout.ascent
+ }
+
+ pub fn descent(&self) -> Pixels {
+ self.unwrapped_layout.descent
+ }
+
+ pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
+ &self.wrap_boundaries
+ }
+
+ pub fn font_size(&self) -> Pixels {
+ self.unwrapped_layout.font_size
+ }
+
+ pub fn runs(&self) -> &[ShapedRun] {
+ &self.unwrapped_layout.runs
+ }
+}
+
pub(crate) struct LineLayoutCache {
- prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
- curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+ previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
+ current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
+ previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+ current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
platform_text_system: Arc<dyn PlatformTextSystem>,
}
impl LineLayoutCache {
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
Self {
- prev_frame: Mutex::new(HashMap::new()),
- curr_frame: RwLock::new(HashMap::new()),
+ previous_frame: Mutex::default(),
+ current_frame: RwLock::default(),
+ previous_frame_wrapped: Mutex::default(),
+ current_frame_wrapped: RwLock::default(),
platform_text_system,
}
}
pub fn start_frame(&self) {
- let mut prev_frame = self.prev_frame.lock();
- let mut curr_frame = self.curr_frame.write();
+ let mut prev_frame = self.previous_frame.lock();
+ let mut curr_frame = self.current_frame.write();
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
curr_frame.clear();
}
- pub fn layout_line(
+ pub fn layout_wrapped_line(
&self,
- text: &SharedString,
+ text: &str,
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
runs,
wrap_width,
} as &dyn AsCacheKeyRef;
- let curr_frame = self.curr_frame.upgradable_read();
- if let Some(layout) = curr_frame.get(key) {
+
+ let current_frame = self.current_frame_wrapped.upgradable_read();
+ if let Some(layout) = current_frame.get(key) {
return layout.clone();
}
- let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
- if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
- curr_frame.insert(key, layout.clone());
+ let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+ if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
+ current_frame.insert(key, layout.clone());
layout
} else {
- let layout = self.platform_text_system.layout_line(text, font_size, runs);
- let wrap_boundaries = wrap_width
- .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
- .unwrap_or_default();
- let wrapped_line = Arc::new(WrappedLineLayout {
- layout,
- text: text.clone(),
+ let unwrapped_layout = self.layout_line(text, font_size, runs);
+ let wrap_boundaries = if let Some(wrap_width) = wrap_width {
+ unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
+ } else {
+ SmallVec::new()
+ };
+ let layout = Arc::new(WrappedLineLayout {
+ unwrapped_layout,
wrap_boundaries,
+ wrap_width,
});
-
let key = CacheKey {
- text: text.clone(),
+ text: text.into(),
font_size,
runs: SmallVec::from(runs),
wrap_width,
};
- curr_frame.insert(key, wrapped_line.clone());
- wrapped_line
+ current_frame.insert(key, layout.clone());
+ layout
+ }
+ }
+
+ pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
+ let key = &CacheKeyRef {
+ text,
+ font_size,
+ runs,
+ wrap_width: None,
+ } as &dyn AsCacheKeyRef;
+
+ let current_frame = self.current_frame.upgradable_read();
+ if let Some(layout) = current_frame.get(key) {
+ return layout.clone();
+ }
+
+ let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+ if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
+ current_frame.insert(key, layout.clone());
+ layout
+ } else {
+ let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+ let key = CacheKey {
+ text: text.into(),
+ font_size,
+ runs: SmallVec::from(runs),
+ wrap_width: None,
+ };
+ current_frame.insert(key, layout.clone());
+ layout
}
}
}
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
#[derive(Eq)]
struct CacheKey {
- text: SharedString,
+ text: String,
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,
@@ -185,10 +185,27 @@ impl Drop for FocusHandle {
}
}
+/// FocusableView allows users of your view to easily
+/// focus it (using cx.focus_view(view))
pub trait FocusableView: Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
+/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
+/// where the lifecycle of the view is handled by another view.
+pub trait ManagedView: Render {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+pub struct Dismiss;
+impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+
+impl<T: ManagedView> FocusableView for T {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.focus_handle(cx)
+ }
+}
+
// Holds the state for a specific window.
pub struct Window {
pub(crate) handle: AnyWindowHandle,
@@ -311,8 +328,8 @@ impl Window {
layout_engine: TaffyLayoutEngine::new(),
root_view: None,
element_id_stack: GlobalElementId::default(),
- previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
- current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+ previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+ current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
focus_listeners: SubscriberSet::new(),
default_prevented: true,
@@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> {
result
}
+ #[must_use]
/// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
/// layout is being requested, along with the layout ids of any children. This method is called during
/// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> {
self.window.mouse_position = mouse_move.position;
InputEvent::MouseMove(mouse_move)
}
+ InputEvent::MouseDown(mouse_down) => {
+ self.window.mouse_position = mouse_down.position;
+ InputEvent::MouseDown(mouse_down)
+ }
+ InputEvent::MouseUp(mouse_up) => {
+ self.window.mouse_position = mouse_up.position;
+ InputEvent::MouseUp(mouse_up)
+ }
// Translate dragging and dropping of external files from the operating system
// to internal drag and drop events.
InputEvent::FileDrop(file_drop) => match file_drop {
@@ -0,0 +1,45 @@
+use serde_derive::Deserialize;
+
+#[test]
+fn test_derive() {
+ use gpui2 as gpui;
+
+ #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
+ struct AnotherTestAction;
+
+ #[gpui2_macros::register_action]
+ #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+ struct RegisterableAction {}
+
+ impl gpui::Action for RegisterableAction {
+ fn boxed_clone(&self) -> Box<dyn gpui::Action> {
+ todo!()
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ todo!()
+ }
+
+ fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
+ todo!()
+ }
+
+ fn name(&self) -> &str {
+ todo!()
+ }
+
+ fn debug_name() -> &'static str
+ where
+ Self: Sized,
+ {
+ todo!()
+ }
+
+ fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
+ where
+ Self: Sized,
+ {
+ todo!()
+ }
+ }
+}
@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
proc-macro = true
[dependencies]
-syn = "1.0.72"
+syn = { version = "1.0.72", features = ["full"] }
quote = "1.0.9"
proc-macro2 = "1.0.66"
@@ -15,48 +15,81 @@
use proc_macro::TokenStream;
use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
+
+use crate::register_action::register_action;
+
+pub fn action(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
-pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
- let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
- let attrs = input
- .attrs
- .into_iter()
- .filter(|attr| !attr.path.is_ident("action"))
- .collect::<Vec<_>>();
-
- let attributes = quote! {
- #[gpui::register_action]
- #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
- #(#attrs)*
+
+ if input.generics.lt_token.is_some() {
+ return Error::new(name.span(), "Actions must be a concrete type")
+ .into_compile_error()
+ .into();
+ }
+
+ let is_unit_struct = match input.data {
+ syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
+ syn::Data::Enum(_) => false,
+ syn::Data::Union(_) => false,
+ };
+
+ let build_impl = if is_unit_struct {
+ quote! {
+ Ok(std::boxed::Box::new(Self {}))
+ }
+ } else {
+ quote! {
+ Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+ }
};
- let visibility = input.vis;
-
- let output = match input.data {
- syn::Data::Struct(ref struct_data) => match &struct_data.fields {
- syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
- let fields = &struct_data.fields;
- quote! {
- #attributes
- #visibility struct #name #fields
- }
+
+ let register_action = register_action(&name);
+
+ let output = quote! {
+ const _: fn() = || {
+ fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {}
+ assert_impl::<#name>();
+ };
+
+ impl gpui::Action for #name {
+ fn name(&self) -> &'static str
+ {
+ ::std::any::type_name::<#name>()
+ }
+
+ fn debug_name() -> &'static str
+ where
+ Self: ::std::marker::Sized
+ {
+ ::std::any::type_name::<#name>()
+ }
+
+ fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
+ where
+ Self: ::std::marker::Sized {
+ #build_impl
}
- syn::Fields::Unit => {
- quote! {
- #attributes
- #visibility struct #name;
- }
+
+ fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
+ action
+ .as_any()
+ .downcast_ref::<Self>()
+ .map_or(false, |a| self == a)
}
- },
- syn::Data::Enum(ref enum_data) => {
- let variants = &enum_data.variants;
- quote! {
- #attributes
- #visibility enum #name { #variants }
+
+ fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
+ ::std::boxed::Box::new(self.clone())
+ }
+
+ fn as_any(&self) -> &dyn ::std::any::Any {
+ self
}
}
- _ => panic!("Expected a struct or an enum."),
+
+ #register_action
};
TokenStream::from(output)
@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
style_helpers::style_helpers(args)
}
-#[proc_macro_attribute]
-pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
- action::action(attr, item)
+#[proc_macro_derive(Action)]
+pub fn action(input: TokenStream) -> TokenStream {
+ action::action(input)
}
#[proc_macro_attribute]
pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
- register_action::register_action(attr, item)
+ register_action::register_action_macro(attr, item)
}
#[proc_macro_derive(Component, attributes(component))]
@@ -12,22 +12,76 @@
// gpui2::register_action_builder::<Foo>()
// }
use proc_macro::TokenStream;
+use proc_macro2::Ident;
use quote::{format_ident, quote};
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
-pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
- let type_name = &input.ident;
- let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
+ let registration = register_action(&input.ident);
- let expanded = quote! {
+ let has_action_derive = input
+ .attrs
+ .iter()
+ .find(|attr| {
+ (|| {
+ let meta = attr.parse_meta().ok()?;
+ meta.path().is_ident("derive").then(|| match meta {
+ syn::Meta::Path(_) => None,
+ syn::Meta::NameValue(_) => None,
+ syn::Meta::List(list) => list
+ .nested
+ .iter()
+ .find(|list| match list {
+ syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
+ syn::NestedMeta::Lit(_) => false,
+ })
+ .map(|_| true),
+ })?
+ })()
+ .unwrap_or(false)
+ })
+ .is_some();
+
+ if has_action_derive {
+ return Error::new(
+ input.ident.span(),
+ "The Action derive macro has already registered this action",
+ )
+ .into_compile_error()
+ .into();
+ }
+
+ TokenStream::from(quote! {
#input
- #[allow(non_snake_case)]
- #[gpui::ctor]
- fn #ctor_fn_name() {
- gpui::register_action::<#type_name>()
- }
- };
- TokenStream::from(expanded)
+ #registration
+ })
+}
+
+pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
+ let static_slice_name =
+ format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
+
+ let action_builder_fn_name = format_ident!(
+ "__gpui_actions_builder_{}",
+ type_name.to_string().to_lowercase()
+ );
+
+ quote! {
+ #[doc(hidden)]
+ #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+ #[linkme(crate = gpui::linkme)]
+ static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
+
+ /// This is an auto generated function, do not use.
+ #[doc(hidden)]
+ fn #action_builder_fn_name() -> gpui::ActionData {
+ gpui::ActionData {
+ name: ::std::any::type_name::<#type_name>(),
+ type_id: ::std::any::TypeId::of::<#type_name>(),
+ build: <#type_name as gpui::Action>::build,
+ }
+ }
+ }
}
@@ -17,7 +17,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
pub use clock::ReplicaId;
-use futures::FutureExt as _;
+use futures::channel::oneshot;
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
use lsp::LanguageServerId;
use parking_lot::Mutex;
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -62,6 +62,7 @@ pub struct Buffer {
saved_mtime: SystemTime,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
+ reload_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
autoindent_requests: Vec<Arc<AutoindentRequest>>,
pending_autoindent: Option<Task<()>>,
@@ -509,6 +510,7 @@ impl Buffer {
saved_mtime,
saved_version: buffer.version(),
saved_version_fingerprint: buffer.as_rope().fingerprint(),
+ reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
text: buffer,
@@ -608,37 +610,52 @@ impl Buffer {
cx.notify();
}
- pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
- cx.spawn(|this, mut cx| async move {
- if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
+ pub fn reload(
+ &mut self,
+ cx: &mut ModelContext<Self>,
+ ) -> oneshot::Receiver<Option<Transaction>> {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ let prev_version = self.text.version();
+ self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+ let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
let file = this.file.as_ref()?.as_local()?;
Some((file.mtime(), file.load(cx)))
- }) {
- let new_text = new_text.await?;
- let diff = this
- .read_with(&cx, |this, cx| this.diff(new_text, cx))
- .await;
- this.update(&mut cx, |this, cx| {
- if this.version() == diff.base_version {
- this.finalize_last_transaction();
- this.apply_diff(diff, cx);
- if let Some(transaction) = this.finalize_last_transaction().cloned() {
- this.did_reload(
- this.version(),
- this.as_rope().fingerprint(),
- this.line_ending(),
- new_mtime,
- cx,
- );
- return Ok(Some(transaction));
- }
- }
- Ok(None)
- })
- } else {
- Ok(None)
- }
- })
+ }) else {
+ return Ok(());
+ };
+
+ let new_text = new_text.await?;
+ let diff = this
+ .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
+ .await;
+ this.update(&mut cx, |this, cx| {
+ if this.version() == diff.base_version {
+ this.finalize_last_transaction();
+ this.apply_diff(diff, cx);
+ tx.send(this.finalize_last_transaction().cloned()).ok();
+
+ this.did_reload(
+ this.version(),
+ this.as_rope().fingerprint(),
+ this.line_ending(),
+ new_mtime,
+ cx,
+ );
+ } else {
+ this.did_reload(
+ prev_version,
+ Rope::text_fingerprint(&new_text),
+ this.line_ending(),
+ this.saved_mtime,
+ cx,
+ );
+ }
+
+ this.reload_task.take();
+ });
+ Ok(())
+ }));
+ rx
}
pub fn did_reload(
@@ -667,13 +684,8 @@ impl Buffer {
cx.notify();
}
- pub fn file_updated(
- &mut self,
- new_file: Arc<dyn File>,
- cx: &mut ModelContext<Self>,
- ) -> Task<()> {
+ pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
let mut file_changed = false;
- let mut task = Task::ready(());
if let Some(old_file) = self.file.as_ref() {
if new_file.path() != old_file.path() {
@@ -693,8 +705,7 @@ impl Buffer {
file_changed = true;
if !self.is_dirty() {
- let reload = self.reload(cx).log_err().map(drop);
- task = cx.foreground().spawn(reload);
+ self.reload(cx).close();
}
}
}
@@ -708,7 +719,6 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
cx.notify();
}
- task
}
pub fn diff_base(&self) -> Option<&str> {
@@ -17,8 +17,9 @@ use crate::{
};
use anyhow::{anyhow, Result};
pub use clock::ReplicaId;
-use futures::FutureExt as _;
-use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
+use futures::channel::oneshot;
+use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
+use lazy_static::lazy_static;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use similar::{ChangeTag, TextDiff};
@@ -45,23 +46,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity;
+lazy_static! {
+ pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
+}
+
pub struct Buffer {
text: TextBuffer,
diff_base: Option<String>,
git_diff: git::diff::BufferDiff,
file: Option<Arc<dyn File>>,
- saved_version: clock::Global,
- saved_version_fingerprint: RopeFingerprint,
+ /// The mtime of the file when this buffer was last loaded from
+ /// or saved to disk.
saved_mtime: SystemTime,
+ /// The version vector when this buffer was last loaded from
+ /// or saved to disk.
+ saved_version: clock::Global,
+ /// A hash of the current contents of the buffer's file.
+ file_fingerprint: RopeFingerprint,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
+ reload_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
autoindent_requests: Vec<Arc<AutoindentRequest>>,
pending_autoindent: Option<Task<()>>,
@@ -421,8 +432,7 @@ impl Buffer {
.ok_or_else(|| anyhow!("missing line_ending"))?,
));
this.saved_version = proto::deserialize_version(&message.saved_version);
- this.saved_version_fingerprint =
- proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
+ this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
this.saved_mtime = message
.saved_mtime
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
@@ -438,7 +448,7 @@ impl Buffer {
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version),
- saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
+ saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
saved_mtime: Some(self.saved_mtime.into()),
}
}
@@ -508,7 +518,8 @@ impl Buffer {
Self {
saved_mtime,
saved_version: buffer.version(),
- saved_version_fingerprint: buffer.as_rope().fingerprint(),
+ file_fingerprint: buffer.as_rope().fingerprint(),
+ reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
text: buffer,
@@ -574,7 +585,7 @@ impl Buffer {
}
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
- self.saved_version_fingerprint
+ self.file_fingerprint
}
pub fn saved_mtime(&self) -> SystemTime {
@@ -602,43 +613,58 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
- self.saved_version_fingerprint = fingerprint;
+ self.file_fingerprint = fingerprint;
self.saved_mtime = mtime;
cx.emit(Event::Saved);
cx.notify();
}
- pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
- cx.spawn(|this, mut cx| async move {
- if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
+ pub fn reload(
+ &mut self,
+ cx: &mut ModelContext<Self>,
+ ) -> oneshot::Receiver<Option<Transaction>> {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ let prev_version = self.text.version();
+ self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+ let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
let file = this.file.as_ref()?.as_local()?;
Some((file.mtime(), file.load(cx)))
- })? {
- let new_text = new_text.await?;
- let diff = this
- .update(&mut cx, |this, cx| this.diff(new_text, cx))?
- .await;
- this.update(&mut cx, |this, cx| {
- if this.version() == diff.base_version {
- this.finalize_last_transaction();
- this.apply_diff(diff, cx);
- if let Some(transaction) = this.finalize_last_transaction().cloned() {
- this.did_reload(
- this.version(),
- this.as_rope().fingerprint(),
- this.line_ending(),
- new_mtime,
- cx,
- );
- return Some(transaction);
- }
- }
- None
- })
- } else {
- Ok(None)
- }
- })
+ })?
+ else {
+ return Ok(());
+ };
+
+ let new_text = new_text.await?;
+ let diff = this
+ .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
+ .await;
+ this.update(&mut cx, |this, cx| {
+ if this.version() == diff.base_version {
+ this.finalize_last_transaction();
+ this.apply_diff(diff, cx);
+ tx.send(this.finalize_last_transaction().cloned()).ok();
+
+ this.did_reload(
+ this.version(),
+ this.as_rope().fingerprint(),
+ this.line_ending(),
+ new_mtime,
+ cx,
+ );
+ } else {
+ this.did_reload(
+ prev_version,
+ Rope::text_fingerprint(&new_text),
+ this.line_ending(),
+ this.saved_mtime,
+ cx,
+ );
+ }
+
+ this.reload_task.take();
+ })
+ }));
+ rx
}
pub fn did_reload(
@@ -650,14 +676,14 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
- self.saved_version_fingerprint = fingerprint;
+ self.file_fingerprint = fingerprint;
self.text.set_line_ending(line_ending);
self.saved_mtime = mtime;
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
file.buffer_reloaded(
self.remote_id(),
&self.saved_version,
- self.saved_version_fingerprint,
+ self.file_fingerprint,
self.line_ending(),
self.saved_mtime,
cx,
@@ -667,13 +693,8 @@ impl Buffer {
cx.notify();
}
- pub fn file_updated(
- &mut self,
- new_file: Arc<dyn File>,
- cx: &mut ModelContext<Self>,
- ) -> Task<()> {
+ pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
let mut file_changed = false;
- let mut task = Task::ready(());
if let Some(old_file) = self.file.as_ref() {
if new_file.path() != old_file.path() {
@@ -693,8 +714,7 @@ impl Buffer {
file_changed = true;
if !self.is_dirty() {
- let reload = self.reload(cx).log_err().map(drop);
- task = cx.background_executor().spawn(reload);
+ self.reload(cx).close();
}
}
}
@@ -708,7 +728,6 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
cx.notify();
}
- task
}
pub fn diff_base(&self) -> Option<&str> {
@@ -1159,36 +1178,72 @@ impl Buffer {
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone();
let base_version = self.version();
- cx.background_executor().spawn(async move {
- let old_text = old_text.to_string();
- let line_ending = LineEnding::detect(&new_text);
- LineEnding::normalize(&mut new_text);
- let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
- let mut edits = Vec::new();
- let mut offset = 0;
- let empty: Arc<str> = "".into();
- for change in diff.iter_all_changes() {
- let value = change.value();
- let end_offset = offset + value.len();
- match change.tag() {
- ChangeTag::Equal => {
- offset = end_offset;
- }
- ChangeTag::Delete => {
- edits.push((offset..end_offset, empty.clone()));
- offset = end_offset;
+ cx.background_executor()
+ .spawn_labeled(*BUFFER_DIFF_TASK, async move {
+ let old_text = old_text.to_string();
+ let line_ending = LineEnding::detect(&new_text);
+ LineEnding::normalize(&mut new_text);
+
+ let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
+ let empty: Arc<str> = "".into();
+
+ let mut edits = Vec::new();
+ let mut old_offset = 0;
+ let mut new_offset = 0;
+ let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
+ for change in diff.iter_all_changes().map(Some).chain([None]) {
+ if let Some(change) = &change {
+ let len = change.value().len();
+ match change.tag() {
+ ChangeTag::Equal => {
+ old_offset += len;
+ new_offset += len;
+ }
+ ChangeTag::Delete => {
+ let old_end_offset = old_offset + len;
+ if let Some((last_old_range, _)) = &mut last_edit {
+ last_old_range.end = old_end_offset;
+ } else {
+ last_edit =
+ Some((old_offset..old_end_offset, new_offset..new_offset));
+ }
+ old_offset = old_end_offset;
+ }
+ ChangeTag::Insert => {
+ let new_end_offset = new_offset + len;
+ if let Some((_, last_new_range)) = &mut last_edit {
+ last_new_range.end = new_end_offset;
+ } else {
+ last_edit =
+ Some((old_offset..old_offset, new_offset..new_end_offset));
+ }
+ new_offset = new_end_offset;
+ }
+ }
}
- ChangeTag::Insert => {
- edits.push((offset..offset, value.into()));
+
+ if let Some((old_range, new_range)) = &last_edit {
+ if old_offset > old_range.end
+ || new_offset > new_range.end
+ || change.is_none()
+ {
+ let text = if new_range.is_empty() {
+ empty.clone()
+ } else {
+ new_text[new_range.clone()].into()
+ };
+ edits.push((old_range.clone(), text));
+ last_edit.take();
+ }
}
}
- }
- Diff {
- base_version,
- line_ending,
- edits,
- }
- })
+
+ Diff {
+ base_version,
+ line_ending,
+ edits,
+ }
+ })
}
/// Spawn a background task that searches the buffer for any whitespace
@@ -1272,12 +1327,12 @@ impl Buffer {
}
pub fn is_dirty(&self) -> bool {
- self.saved_version_fingerprint != self.as_rope().fingerprint()
+ self.file_fingerprint != self.as_rope().fingerprint()
|| self.file.as_ref().map_or(false, |file| file.is_deleted())
}
pub fn has_conflict(&self) -> bool {
- self.saved_version_fingerprint != self.as_rope().fingerprint()
+ self.file_fingerprint != self.as_rope().fingerprint()
&& self
.file
.as_ref()
@@ -10,7 +10,7 @@ path = "src/live_kit_client2.rs"
doctest = false
[[example]]
-name = "test_app"
+name = "test_app2"
[features]
test-support = [
@@ -1,7 +1,7 @@
use std::{sync::Arc, time::Duration};
use futures::StreamExt;
-use gpui::KeyBinding;
+use gpui::{Action, KeyBinding};
use live_kit_client2::{
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
};
@@ -10,7 +10,7 @@ use log::LevelFilter;
use serde_derive::Deserialize;
use simplelog::SimpleLogger;
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
struct Quit;
fn main() {
@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
- div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
- UniformListScrollHandle, View, ViewContext, WindowContext,
+ div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
+ MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
};
use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
) -> Self::ListItem;
}
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.editor.focus_handle(cx)
+ }
+}
+
impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let editor = cx.build_view(|cx| {
@@ -6190,7 +6190,7 @@ impl Project {
.log_err();
}
- buffer.file_updated(Arc::new(new_file), cx).detach();
+ buffer.file_updated(Arc::new(new_file), cx);
}
}
});
@@ -7182,7 +7182,7 @@ impl Project {
.ok_or_else(|| anyhow!("no such worktree"))?;
let file = File::from_proto(file, worktree, cx)?;
buffer.update(cx, |buffer, cx| {
- buffer.file_updated(Arc::new(file), cx).detach();
+ buffer.file_updated(Arc::new(file), cx);
});
this.detect_language_for_buffer(&buffer, cx);
}
@@ -959,7 +959,7 @@ impl LocalWorktree {
buffer_handle.update(&mut cx, |buffer, cx| {
if has_changed_file {
- buffer.file_updated(new_file, cx).detach();
+ buffer.file_updated(new_file, cx);
}
});
}
@@ -6262,7 +6262,7 @@ impl Project {
.log_err();
}
- buffer.file_updated(Arc::new(new_file), cx).detach();
+ buffer.file_updated(Arc::new(new_file), cx);
}
}
});
@@ -7256,7 +7256,7 @@ impl Project {
.ok_or_else(|| anyhow!("no such worktree"))?;
let file = File::from_proto(file, worktree, cx)?;
buffer.update(cx, |buffer, cx| {
- buffer.file_updated(Arc::new(file), cx).detach();
+ buffer.file_updated(Arc::new(file), cx);
});
this.detect_language_for_buffer(&buffer, cx);
}
@@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
}
+#[gpui::test(iterations = 30)]
+async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "file1": "the original contents",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+ let buffer = project
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+ .await
+ .unwrap();
+
+ // Simulate buffer diffs being slow, so that they don't complete before
+ // the next file change occurs.
+ cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+ // Change the buffer's file on disk, and then wait for the file change
+ // to be detected by the worktree, so that the buffer starts reloading.
+ fs.save(
+ "/dir/file1".as_ref(),
+ &"the first contents".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ worktree.next_event(cx);
+
+ // Change the buffer's file again. Depending on the random seed, the
+ // previous file change may still be in progress.
+ fs.save(
+ "/dir/file1".as_ref(),
+ &"the second contents".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ worktree.next_event(cx);
+
+ cx.executor().run_until_parked();
+ let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+ buffer.read_with(cx, |buffer, _| {
+ let buffer_text = buffer.text();
+ if buffer_text == on_disk_text {
+ assert!(
+ !buffer.is_dirty() && !buffer.has_conflict(),
+ "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
+ );
+ }
+ // If the file change occurred while the buffer was processing the first
+ // change, the buffer will be in a conflicting state.
+ else {
+ assert!(
+ buffer.is_dirty() && buffer.has_conflict(),
+ "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
+ );
+ }
+ });
+}
+
#[gpui::test]
async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -276,6 +276,7 @@ struct ShareState {
_maintain_remote_snapshot: Task<Option<()>>,
}
+#[derive(Clone)]
pub enum Event {
UpdatedEntries(UpdatedEntriesSet),
UpdatedGitRepositories(UpdatedGitRepositoriesSet),
@@ -961,7 +962,7 @@ impl LocalWorktree {
buffer_handle.update(&mut cx, |buffer, cx| {
if has_changed_file {
- buffer.file_updated(new_file, cx).detach();
+ buffer.file_updated(new_file, cx);
}
})?;
}
@@ -1579,7 +1579,7 @@ mod tests {
path::{Path, PathBuf},
sync::atomic::{self, AtomicUsize},
};
- use workspace::{pane, AppState};
+ use workspace::AppState;
#[gpui::test]
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
@@ -2785,7 +2785,7 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
init_settings(cx);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init_settings(cx);
crate::init((), cx);
@@ -2798,11 +2798,10 @@ mod tests {
fn init_test_with_editor(cx: &mut TestAppContext) {
cx.update(|cx| {
let app_state = AppState::test(cx);
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
init_settings(cx);
language::init(cx);
editor::init(cx);
- pane::init(cx);
crate::init((), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
@@ -41,6 +41,10 @@ impl Rope {
Self::default()
}
+ pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+ bromberg_sl2::hash_strict(text.as_bytes())
+ }
+
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
fn from(text: &'a str) -> Self {
Self {
text: TextSummary::from(text),
- fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+ fingerprint: Rope::text_fingerprint(text),
}
}
}
@@ -41,6 +41,10 @@ impl Rope {
Self::default()
}
+ pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+ bromberg_sl2::hash_strict(text.as_bytes())
+ }
+
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
fn from(text: &'a str) -> Self {
Self {
text: TextSummary::from(text),
- fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+ fingerprint: Rope::text_fingerprint(text),
}
}
}
@@ -73,9 +73,9 @@ impl KeymapFile {
"Expected first item in array to be a string."
)));
};
- gpui::build_action(&name, Some(data))
+ cx.build_action(&name, Some(data))
}
- Value::String(name) => gpui::build_action(&name, None),
+ Value::String(name) => cx.build_action(&name, None),
Value::Null => Ok(no_action()),
_ => {
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
.unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
+ "ui_font_family": "Courier",
+ "ui_font_features": {},
+ "ui_font_size": 14,
"buffer_font_family": "Courier",
"buffer_font_features": {},
"buffer_font_size": 14,
@@ -60,13 +60,12 @@ fn main() {
.unwrap();
cx.set_global(store);
- theme2::init(cx);
+ theme2::init(theme2::LoadThemes::All, cx);
let selector =
story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
let theme_registry = cx.global::<ThemeRegistry>();
-
let mut theme_settings = ThemeSettings::get_global(cx).clone();
theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
.flex()
.flex_col()
.size_full()
+ .font("Zed Mono")
.child(self.story.clone())
}
}
@@ -0,0 +1,17 @@
+[package]
+name = "storybook3"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook3.rs"
+
+[dependencies]
+anyhow.workspace = true
+
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+theme = { package = "theme2", path = "../theme2", features = ["stories"] }
+settings = { package = "settings2", path = "../settings2"}
@@ -0,0 +1,73 @@
+use anyhow::Result;
+use gpui::AssetSource;
+use gpui::{
+ div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
+ WindowOptions,
+};
+use settings::{default_settings, Settings, SettingsStore};
+use std::borrow::Cow;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{prelude::*, ContextMenuStory};
+
+struct Assets;
+
+impl AssetSource for Assets {
+ fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
+ todo!();
+ }
+
+ fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+ Ok(vec![])
+ }
+}
+
+fn main() {
+ let asset_source = Arc::new(Assets);
+ gpui::App::production(asset_source).run(move |cx| {
+ let mut store = SettingsStore::default();
+ store
+ .set_default_settings(default_settings().as_ref(), cx)
+ .unwrap();
+ cx.set_global(store);
+ ui::settings::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+
+ cx.open_window(
+ WindowOptions {
+ bounds: WindowBounds::Fixed(Bounds {
+ origin: Default::default(),
+ size: size(px(1500.), px(780.)).into(),
+ }),
+ ..Default::default()
+ },
+ move |cx| {
+ let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
+ cx.set_rem_size(ui_font_size);
+
+ cx.build_view(|cx| TestView {
+ story: cx.build_view(|_| ContextMenuStory).into(),
+ })
+ },
+ );
+
+ cx.activate(true);
+ })
+}
+
+struct TestView {
+ story: AnyView,
+}
+
+impl Render for TestView {
+ type Element = Div<Self>;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ div()
+ .flex()
+ .flex_col()
+ .size_full()
+ .font("Helvetica")
+ .child(self.story.clone())
+ }
+}
@@ -9,7 +9,6 @@ path = "src/terminal_view.rs"
doctest = false
[dependencies]
-# context_menu = { package = "context_menu2", path = "../context_menu2" }
editor = { package = "editor2", path = "../editor2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
@@ -1,8 +1,8 @@
// use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
// use gpui::{
-// AnyElement, AppContext, Bounds, Component, Element, HighlightStyle, Hsla, LayoutId, Line,
-// ModelContext, MouseButton, Pixels, Point, TextStyle, Underline, ViewContext, WeakModel,
-// WindowContext,
+// point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
+// FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
+// Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
// };
// use itertools::Itertools;
// use language::CursorShape;
@@ -23,6 +23,7 @@
// TerminalSize,
// };
// use theme::ThemeSettings;
+// use workspace::ElementId;
// use std::mem;
// use std::{fmt::Debug, ops::RangeInclusive};
@@ -130,23 +131,24 @@
// cx: &mut ViewContext<TerminalView>,
// ) {
// let position = {
-// let point = self.point;
-// vec2f(
-// (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
-// origin.y() + point.line as f32 * layout.size.line_height,
+// let alac_point = self.point;
+// point(
+// (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
+// origin.y + alac_point.line as f32 * layout.size.line_height,
// )
// };
-// let size = vec2f(
+// let size = point(
// (layout.size.cell_width * self.num_of_cells as f32).ceil(),
// layout.size.line_height,
-// );
+// )
+// .into();
// cx.paint_quad(
// Bounds::new(position, size),
// Default::default(),
// self.color,
// Default::default(),
-// Default::default(),
+// transparent_black(),
// );
// }
// }
@@ -281,9 +283,9 @@
// cursor_point: DisplayCursor,
// size: TerminalSize,
// text_fragment: &Line,
-// ) -> Option<(Vector2F, f32)> {
+// ) -> Option<(Point<Pixels>, Pixels)> {
// if cursor_point.line() < size.total_lines() as i32 {
-// let cursor_width = if text_fragment.width == 0. {
+// let cursor_width = if text_fragment.width == Pixels::ZERO {
// size.cell_width()
// } else {
// text_fragment.width
@@ -292,7 +294,7 @@
// //Cursor should always surround as much of the text as possible,
// //hence when on pixel boundaries round the origin down and the width up
// Some((
-// vec2f(
+// point(
// (cursor_point.col() as f32 * size.cell_width()).floor(),
// (cursor_point.line() as f32 * size.line_height()).floor(),
// ),
@@ -332,15 +334,15 @@
// let mut properties = Properties::new();
// if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
-// properties = *properties.weight(Weight::BOLD);
+// properties = *properties.weight(FontWeight::BOLD);
// }
// if indexed.flags.intersects(Flags::ITALIC) {
-// properties = *properties.style(Italic);
+// properties = *properties.style(FontStyle::Italic);
// }
// let font_id = font_cache
-// .select_font(text_style.font_family_id, &properties)
-// .unwrap_or(8text_style.font_id);
+// .select_font(text_style.font_family, &properties)
+// .unwrap_or(text_style.font_id);
// let mut result = RunStyle {
// color: fg,
@@ -366,7 +368,7 @@
// fn generic_button_handler<E>(
// connection: WeakModel<Terminal>,
// origin: Point<Pixels>,
-// f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+// f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
// ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
// move |event, _: &mut TerminalView, cx| {
// cx.focus_parent();
@@ -522,9 +524,9 @@
// fn layout(
// &mut self,
// view_state: &mut TerminalView,
-// element_state: &mut Self::ElementState,
+// element_state: Option<Self::ElementState>,
// cx: &mut ViewContext<TerminalView>,
-// ) -> LayoutId {
+// ) -> (LayoutId, Self::ElementState) {
// let settings = ThemeSettings::get_global(cx);
// let terminal_settings = TerminalSettings::get_global(cx);
@@ -569,7 +571,7 @@
// let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
// gutter = cell_width;
-// let size = constraint.max - vec2f(gutter, 0.);
+// let size = constraint.max - point(gutter, 0.);
// TerminalSize::new(line_height, cell_width, size)
// };
@@ -607,11 +609,11 @@
// cx,
// ),
// )
-// .with_position_mode(gpui::elements::OverlayPositionMode::Local)
+// .with_position_mode(gpui::OverlayPositionMode::Local)
// .into_any();
// tooltip.layout(
-// SizeConstraint::new(Vector2F::zero(), cx.window_size()),
+// SizeConstraint::new(Point::zero(), cx.window_size()),
// view_state,
// cx,
// );
@@ -735,7 +737,7 @@
// let clip_bounds = Some(visible_bounds);
// cx.paint_layer(clip_bounds, |cx| {
-// let origin = bounds.origin() + vec2f(element_state.gutter, 0.);
+// let origin = bounds.origin + point(element_state.gutter, 0.);
// // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
// self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
@@ -808,7 +810,7 @@
// });
// }
-// fn id(&self) -> Option<gpui::ElementId> {
+// fn element_id(&self) -> Option<ElementId> {
// todo!()
// }
@@ -842,12 +844,12 @@
// // ) -> Option<Bounds<Pixels>> {
// // // Use the same origin that's passed to `Cursor::paint` in the paint
// // // method bove.
-// // let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
+// // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
// // // TODO - Why is it necessary to move downward one line to get correct
// // // positioning? I would think that we'd want the same rect that is
// // // painted for the cursor.
-// // origin += vec2f(0., layout.size.line_height);
+// // origin += point(0., layout.size.line_height);
// // Some(layout.cursor.as_ref()?.bounding_rect(origin))
// // }
@@ -886,7 +888,7 @@
// range: &RangeInclusive<AlacPoint>,
// layout: &LayoutState,
// origin: Point<Pixels>,
-// ) -> Option<(f32, Vec<HighlightedRangeLine>)> {
+// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
// // Step 1. Normalize the points to be viewport relative.
// // When display_offset = 1, here's how the grid is arranged:
// //-2,0 -2,1...
@@ -937,8 +939,8 @@
// }
// highlighted_range_lines.push(HighlightedRangeLine {
-// start_x: origin.x() + line_start as f32 * layout.size.cell_width,
-// end_x: origin.x() + line_end as f32 * layout.size.cell_width,
+// start_x: origin.x + line_start as f32 * layout.size.cell_width,
+// end_x: origin.x + line_end as f32 * layout.size.cell_width,
// });
// }
@@ -304,13 +304,13 @@ impl TerminalPanel {
.pane
.read(cx)
.items()
- .map(|item| item.id().as_u64())
+ .map(|item| item.item_id().as_u64())
.collect::<Vec<_>>();
let active_item_id = self
.pane
.read(cx)
.active_item()
- .map(|item| item.id().as_u64());
+ .map(|item| item.item_id().as_u64());
let height = self.height;
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
@@ -7,27 +7,17 @@ pub mod terminal_panel;
// todo!()
// use crate::terminal_element::TerminalElement;
-use anyhow::Context;
-use dirs::home_dir;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
+ actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
- InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, ParentComponent, Pixels,
- Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
+ InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
+ ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+ WeakView,
};
use language::Bias;
use persistence::TERMINAL_DB;
use project::{search::SearchQuery, LocalWorktree, Project};
-use serde::Deserialize;
-use settings::Settings;
-use smol::Timer;
-use std::{
- ops::RangeInclusive,
- path::{Path, PathBuf},
- sync::Arc,
- time::Duration,
-};
use terminal::{
alacritty_terminal::{
index::Point,
@@ -42,7 +32,21 @@ use workspace::{
notifications::NotifyResultExt,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem},
- NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+ ui::{ContextMenu, Label},
+ CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use anyhow::Context;
+use dirs::home_dir;
+use serde::Deserialize;
+use settings::Settings;
+use smol::Timer;
+
+use std::{
+ ops::RangeInclusive,
+ path::{Path, PathBuf},
+ sync::Arc,
+ time::Duration,
};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -51,17 +55,16 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
pub struct SendText(String);
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
pub struct SendKeystroke(String);
actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
pub fn init(cx: &mut AppContext) {
+ workspace::ui::init(cx);
terminal_panel::init(cx);
terminal::init(cx);
@@ -82,7 +85,7 @@ pub struct TerminalView {
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
- // context_menu: View<ContextMenu>,
+ context_menu: Option<View<ContextMenu>>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
@@ -265,8 +268,7 @@ impl TerminalView {
has_new_content: true,
has_bell: false,
focus_handle: cx.focus_handle(),
- // todo!()
- // context_menu: cx.build_view(|cx| ContextMenu::new(view_id, cx)),
+ context_menu: None,
blink_state: true,
blinking_on: false,
blinking_paused: false,
@@ -293,18 +295,24 @@ impl TerminalView {
cx.emit(Event::Wakeup);
}
- pub fn deploy_context_menu(&mut self, _position: Point<Pixels>, _cx: &mut ViewContext<Self>) {
- //todo!(context_menu)
- // let menu_entries = vec![
- // ContextMenuItem::action("Clear", Clear),
- // ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
- // ];
-
- // self.context_menu.update(cx, |menu, cx| {
- // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
- // });
-
- // cx.notify();
+ pub fn deploy_context_menu(
+ &mut self,
+ position: gpui::Point<Pixels>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.context_menu = Some(cx.build_view(|cx| {
+ ContextMenu::new(cx)
+ .entry(Label::new("Clear"), Box::new(Clear))
+ .entry(
+ Label::new("Close"),
+ Box::new(CloseActiveItem { save_intent: None }),
+ )
+ }));
+ dbg!(&position);
+ // todo!()
+ // self.context_menu
+ // .show(position, AnchorCorner::TopLeft, menu_entries, cx);
+ // cx.notify();
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
@@ -541,28 +549,41 @@ impl Render for TerminalView {
let focused = self.focus_handle.is_focused(cx);
div()
+ .relative()
+ .child(
+ div()
+ .z_index(0)
+ .absolute()
+ .on_key_down(Self::key_down)
+ .on_action(TerminalView::send_text)
+ .on_action(TerminalView::send_keystroke)
+ .on_action(TerminalView::copy)
+ .on_action(TerminalView::paste)
+ .on_action(TerminalView::clear)
+ .on_action(TerminalView::show_character_palette)
+ .on_action(TerminalView::select_all)
+ // todo!()
+ .child(
+ "TERMINAL HERE", // TerminalElement::new(
+ // terminal_handle,
+ // focused,
+ // self.should_show_cursor(focused, cx),
+ // self.can_navigate_to_selected_word,
+ // )
+ )
+ .on_mouse_down(MouseButton::Right, |this, event, cx| {
+ this.deploy_context_menu(event.position, cx);
+ cx.notify();
+ }),
+ )
+ .children(
+ self.context_menu
+ .clone()
+ .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
+ )
.track_focus(&self.focus_handle)
.on_focus_in(Self::focus_in)
.on_focus_out(Self::focus_out)
- .on_key_down(Self::key_down)
- .on_action(TerminalView::send_text)
- .on_action(TerminalView::send_keystroke)
- .on_action(TerminalView::copy)
- .on_action(TerminalView::paste)
- .on_action(TerminalView::clear)
- .on_action(TerminalView::show_character_palette)
- .on_action(TerminalView::select_all)
- // todo!()
- .child(
- "TERMINAL HERE", // TerminalElement::new(
- // terminal_handle,
- // focused,
- // self.should_show_cursor(focused, cx),
- // self.can_navigate_to_selected_word,
- // )
- )
- // todo!()
- // .child(ChildView::new(&self.context_menu, cx))
}
}
@@ -1107,7 +1128,7 @@ mod tests {
pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
let params = cx.update(AppState::test);
cx.update(|cx| {
- theme::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);
});
@@ -100,6 +100,11 @@ impl ThemeRegistry {
.ok_or_else(|| anyhow!("theme not found: {}", name))
.cloned()
}
+
+ pub fn load_user_themes(&mut self) {
+ #[cfg(not(feature = "importing-themes"))]
+ self.insert_user_theme_familes(crate::all_user_themes());
+ }
}
impl Default for ThemeRegistry {
@@ -110,9 +115,6 @@ impl Default for ThemeRegistry {
this.insert_theme_families([one_family()]);
- #[cfg(not(feature = "importing-themes"))]
- this.insert_user_theme_familes(crate::all_user_themes());
-
this
}
}
@@ -34,6 +34,10 @@ pub struct ThemeSettingsContent {
#[serde(default)]
pub ui_font_size: Option<f32>,
#[serde(default)]
+ pub ui_font_family: Option<String>,
+ #[serde(default)]
+ pub ui_font_features: Option<FontFeatures>,
+ #[serde(default)]
pub buffer_font_family: Option<String>,
#[serde(default)]
pub buffer_font_size: Option<f32>,
@@ -117,13 +121,13 @@ impl settings::Settings for ThemeSettings {
user_values: &[&Self::FileContent],
cx: &mut AppContext,
) -> Result<Self> {
- let themes = cx.default_global::<Arc<ThemeRegistry>>();
+ let themes = cx.default_global::<ThemeRegistry>();
let mut this = Self {
- ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+ ui_font_size: defaults.ui_font_size.unwrap().into(),
ui_font: Font {
- family: "Helvetica".into(),
- features: Default::default(),
+ family: defaults.ui_font_family.clone().unwrap().into(),
+ features: defaults.ui_font_features.clone().unwrap(),
weight: Default::default(),
style: Default::default(),
},
@@ -149,6 +153,13 @@ impl settings::Settings for ThemeSettings {
this.buffer_font.features = value;
}
+ if let Some(value) = value.ui_font_family {
+ this.ui_font.family = value.into();
+ }
+ if let Some(value) = value.ui_font_features {
+ this.ui_font.features = value;
+ }
+
if let Some(value) = &value.theme {
if let Some(theme) = themes.get(value).log_err() {
this.active_theme = theme;
@@ -31,8 +31,25 @@ pub enum Appearance {
Dark,
}
-pub fn init(cx: &mut AppContext) {
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LoadThemes {
+ /// Only load the base theme.
+ ///
+ /// No user themes will be loaded.
+ JustBase,
+
+ /// Load all of the built-in themes.
+ All,
+}
+
+pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
cx.set_global(ThemeRegistry::default());
+
+ match themes_to_load {
+ LoadThemes::JustBase => (),
+ LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
+ }
+
ThemeSettings::register(cx);
}
@@ -9,6 +9,7 @@ anyhow.workspace = true
chrono = "0.4"
gpui = { package = "gpui2", path = "../gpui2" }
itertools = { version = "0.11.0", optional = true }
+menu = { package = "menu2", path = "../menu2"}
serde.workspace = true
settings2 = { path = "../settings2" }
smallvec.workspace = true
@@ -178,6 +178,7 @@ impl<V: 'static> Button<V> {
.text_ui()
.rounded_md()
.bg(self.variant.bg_color(cx))
+ .cursor_pointer()
.hover(|style| style.bg(self.variant.bg_color_hover(cx)))
.active(|style| style.bg(self.variant.bg_color_active(cx)));
@@ -1,60 +1,255 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::prelude::*;
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+ overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
+ FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+};
-pub enum ContextMenuItem {
- Header(SharedString),
- Entry(Label),
- Separator,
+pub struct ContextMenu {
+ items: Vec<ListItem>,
+ focus_handle: FocusHandle,
}
-impl ContextMenuItem {
- fn to_list_item<V: 'static>(self) -> ListItem {
- match self {
- ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
- ContextMenuItem::Entry(label) => {
- ListEntry::new(label).variant(ListItemVariant::Inset).into()
- }
- ContextMenuItem::Separator => ListSeparator::new().into(),
+impl ManagedView for ContextMenu {
+ fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl ContextMenu {
+ pub fn new(cx: &mut WindowContext) -> Self {
+ Self {
+ items: Default::default(),
+ focus_handle: cx.focus_handle(),
}
}
- pub fn header(label: impl Into<SharedString>) -> Self {
- Self::Header(label.into())
+ pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+ self.items.push(ListItem::Header(ListSubHeader::new(title)));
+ self
}
- pub fn separator() -> Self {
- Self::Separator
+ pub fn separator(mut self) -> Self {
+ self.items.push(ListItem::Separator(ListSeparator));
+ self
}
- pub fn entry(label: Label) -> Self {
- Self::Entry(label)
+ pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+ self.items.push(ListEntry::new(label).action(action).into());
+ self
+ }
+
+ pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+ // todo!()
+ cx.emit(Dismiss);
+ }
+
+ pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+ cx.emit(Dismiss);
}
}
-#[derive(Component)]
-pub struct ContextMenu {
- items: Vec<ContextMenuItem>,
+impl Render for ContextMenu {
+ type Element = Div<Self>;
+ // todo!()
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ div().elevation_2(cx).flex().flex_row().child(
+ v_stack()
+ .min_w(px(200.))
+ .track_focus(&self.focus_handle)
+ .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+ // .on_action(ContextMenu::select_first)
+ // .on_action(ContextMenu::select_last)
+ // .on_action(ContextMenu::select_next)
+ // .on_action(ContextMenu::select_prev)
+ .on_action(ContextMenu::confirm)
+ .on_action(ContextMenu::cancel)
+ .flex_none()
+ // .bg(cx.theme().colors().elevated_surface_background)
+ // .border()
+ // .border_color(cx.theme().colors().border)
+ .child(List::new(self.items.clone())),
+ )
+ }
}
-impl ContextMenu {
- pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
- Self {
- items: items.into_iter().collect(),
+pub struct MenuHandle<V: 'static, M: ManagedView> {
+ id: Option<ElementId>,
+ child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
+ menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
+
+ anchor: Option<AnchorCorner>,
+ attach: Option<AnchorCorner>,
+}
+
+impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
+ pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+ self.id = Some(id.into());
+ self
+ }
+
+ pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+ self.menu_builder = Some(Rc::new(f));
+ self
+ }
+
+ pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+ self.child_builder = Some(Box::new(|b| f(b).render()));
+ self
+ }
+
+ /// anchor defines which corner of the menu to anchor to the attachment point
+ /// (by default the cursor position, but see attach)
+ pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+ self.anchor = Some(anchor);
+ self
+ }
+
+ /// attach defines which corner of the handle to attach the menu's anchor to
+ pub fn attach(mut self, attach: AnchorCorner) -> Self {
+ self.attach = Some(attach);
+ self
+ }
+}
+
+pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+ MenuHandle {
+ id: None,
+ child_builder: None,
+ menu_builder: None,
+ anchor: None,
+ attach: None,
+ }
+}
+
+pub struct MenuHandleState<V, M> {
+ menu: Rc<RefCell<Option<View<M>>>>,
+ position: Rc<RefCell<Point<Pixels>>>,
+ child_layout_id: Option<LayoutId>,
+ child_element: Option<AnyElement<V>>,
+ menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
+ type ElementState = MenuHandleState<V, M>;
+
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ Some(self.id.clone().expect("menu_handle must have an id()"))
+ }
+
+ fn layout(
+ &mut self,
+ view_state: &mut V,
+ element_state: Option<Self::ElementState>,
+ cx: &mut crate::ViewContext<V>,
+ ) -> (gpui::LayoutId, Self::ElementState) {
+ let (menu, position) = if let Some(element_state) = element_state {
+ (element_state.menu, element_state.position)
+ } else {
+ (Rc::default(), Rc::default())
+ };
+
+ let mut menu_layout_id = None;
+
+ let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+ let mut overlay = overlay::<V>().snap_to_window();
+ if let Some(anchor) = self.anchor {
+ overlay = overlay.anchor(anchor);
+ }
+ overlay = overlay.position(*position.borrow());
+
+ let mut view = overlay.child(menu.clone()).render();
+ menu_layout_id = Some(view.layout(view_state, cx));
+ view
+ });
+
+ let mut child_element = self
+ .child_builder
+ .take()
+ .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+ let child_layout_id = child_element
+ .as_mut()
+ .map(|child_element| child_element.layout(view_state, cx));
+
+ let layout_id = cx.request_layout(
+ &gpui::Style::default(),
+ menu_layout_id.into_iter().chain(child_layout_id),
+ );
+
+ (
+ layout_id,
+ MenuHandleState {
+ menu,
+ position,
+ child_element,
+ child_layout_id,
+ menu_element,
+ },
+ )
+ }
+
+ fn paint(
+ &mut self,
+ bounds: Bounds<gpui::Pixels>,
+ view_state: &mut V,
+ element_state: &mut Self::ElementState,
+ cx: &mut crate::ViewContext<V>,
+ ) {
+ if let Some(child) = element_state.child_element.as_mut() {
+ child.paint(view_state, cx);
}
+
+ if let Some(menu) = element_state.menu_element.as_mut() {
+ menu.paint(view_state, cx);
+ return;
+ }
+
+ let Some(builder) = self.menu_builder.clone() else {
+ return;
+ };
+ let menu = element_state.menu.clone();
+ let position = element_state.position.clone();
+ let attach = self.attach.clone();
+ let child_layout_id = element_state.child_layout_id.clone();
+
+ cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+ if phase == DispatchPhase::Bubble
+ && event.button == MouseButton::Right
+ && bounds.contains_point(&event.position)
+ {
+ cx.stop_propagation();
+ cx.prevent_default();
+
+ let new_menu = (builder)(view_state, cx);
+ let menu2 = menu.clone();
+ cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+ &Dismiss => {
+ *menu2.borrow_mut() = None;
+ cx.notify();
+ }
+ })
+ .detach();
+ *menu.borrow_mut() = Some(new_menu);
+
+ *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+ attach
+ .unwrap()
+ .corner(cx.layout_bounds(child_layout_id.unwrap()))
+ } else {
+ cx.mouse_position()
+ };
+ cx.notify();
+ }
+ });
}
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .flex()
- .bg(cx.theme().colors().elevated_surface_background)
- .border()
- .border_color(cx.theme().colors().border)
- .child(List::new(
- self.items
- .into_iter()
- .map(ContextMenuItem::to_list_item::<V>)
- .collect(),
- ))
+impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
+ fn render(self) -> AnyElement<V> {
+ AnyElement::new(self)
}
}
@@ -65,7 +260,18 @@ pub use stories::*;
mod stories {
use super::*;
use crate::story::Story;
- use gpui::{Div, Render};
+ use gpui::{actions, Div, Render, VisualContext};
+
+ actions!(PrintCurrentDate);
+
+ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+ cx.build_view(|cx| {
+ ContextMenu::new(cx).header(header).separator().entry(
+ Label::new("Print current time"),
+ PrintCurrentDate.boxed_clone(),
+ )
+ })
+ }
pub struct ContextMenuStory;
@@ -74,13 +280,83 @@ mod stories {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
- .child(Story::title_for::<_, ContextMenu>(cx))
- .child(Story::label(cx, "Default"))
- .child(ContextMenu::new([
- ContextMenuItem::header("Section header"),
- ContextMenuItem::Separator,
- ContextMenuItem::entry(Label::new("Some entry")),
- ]))
+ .on_action(|_, _: &PrintCurrentDate, _| {
+ if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
+ println!("Current Unix time is {:?}", unix_time.as_secs());
+ }
+ })
+ .flex()
+ .flex_row()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle()
+ .id("test2")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ .render()
+ })
+ .menu(move |_, cx| build_menu(cx, "top left")),
+ )
+ .child(
+ menu_handle()
+ .id("test1")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ .render()
+ })
+ .anchor(AnchorCorner::BottomLeft)
+ .attach(AnchorCorner::TopLeft)
+ .menu(move |_, cx| build_menu(cx, "bottom left")),
+ ),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle()
+ .id("test3")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ .render()
+ })
+ .anchor(AnchorCorner::TopRight)
+ .menu(move |_, cx| build_menu(cx, "top right")),
+ )
+ .child(
+ menu_handle()
+ .id("test4")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ .render()
+ })
+ .anchor(AnchorCorner::BottomRight)
+ .attach(AnchorCorner::TopRight)
+ .menu(move |_, cx| build_menu(cx, "bottom right")),
+ ),
+ )
}
}
}
@@ -1,5 +1,5 @@
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, AnyView, MouseButton};
+use gpui::{prelude::*, Action, AnyView, MouseButton};
use std::sync::Arc;
struct IconButtonHandlers<V: 'static> {
@@ -19,6 +19,7 @@ pub struct IconButton<V: 'static> {
color: TextColor,
variant: ButtonVariant,
state: InteractionState,
+ selected: bool,
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
handlers: IconButtonHandlers<V>,
}
@@ -31,6 +32,7 @@ impl<V: 'static> IconButton<V> {
color: TextColor::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
+ selected: false,
tooltip: None,
handlers: IconButtonHandlers::default(),
}
@@ -56,6 +58,11 @@ impl<V: 'static> IconButton<V> {
self
}
+ pub fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+
pub fn tooltip(
mut self,
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
@@ -69,14 +76,18 @@ impl<V: 'static> IconButton<V> {
self
}
+ pub fn action(self, action: Box<dyn Action>) -> Self {
+ self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+ }
+
fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => TextColor::Disabled,
- (InteractionState::Active, _) => TextColor::Error,
+ (InteractionState::Active, _) => TextColor::Selected,
_ => self.color,
};
- let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+ let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
ButtonVariant::Filled => (
cx.theme().colors().element_background,
cx.theme().colors().element_hover,
@@ -89,27 +100,32 @@ impl<V: 'static> IconButton<V> {
),
};
+ if self.selected {
+ bg_color = bg_hover_color;
+ }
+
let mut button = h_stack()
.id(self.id.clone())
.justify_center()
.rounded_md()
.p_1()
.bg(bg_color)
+ .cursor_pointer()
.hover(|style| style.bg(bg_hover_color))
.active(|style| style.bg(bg_active_color))
.child(IconElement::new(self.icon).color(icon_color));
if let Some(click_handler) = self.handlers.click.clone() {
- button = button
- .on_mouse_down(MouseButton::Left, move |state, event, cx| {
- cx.stop_propagation();
- click_handler(state, cx);
- })
- .cursor_pointer();
+ button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+ cx.stop_propagation();
+ click_handler(state, cx);
+ })
}
if let Some(tooltip) = self.tooltip.take() {
- button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+ if !self.selected {
+ button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+ }
}
button
@@ -81,13 +81,12 @@ pub use stories::*;
mod stories {
use super::*;
use crate::Story;
- use gpui::{action, Div, Render};
+ use gpui::{actions, Div, Render};
use itertools::Itertools;
pub struct KeybindingStory;
- #[action]
- struct NoAction {}
+ actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
@@ -60,7 +60,7 @@ pub enum LineHeightStyle {
UILabel,
}
-#[derive(Component)]
+#[derive(Clone, Component)]
pub struct Label {
label: SharedString,
size: LabelSize,
@@ -1,11 +1,10 @@
-use gpui::div;
+use gpui::{div, Action};
-use crate::prelude::*;
use crate::settings::user_settings;
use crate::{
- disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
- TextColor, Toggle,
+ disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
};
+use crate::{prelude::*, GraphicSlot};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
@@ -118,7 +117,7 @@ impl ListHeader {
}
}
-#[derive(Component)]
+#[derive(Component, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -173,7 +172,7 @@ pub enum ListEntrySize {
Medium,
}
-#[derive(Component)]
+#[derive(Component, Clone)]
pub enum ListItem {
Entry(ListEntry),
Separator(ListSeparator),
@@ -232,6 +231,25 @@ pub struct ListEntry {
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
+ on_click: Option<Box<dyn Action>>,
+}
+
+impl Clone for ListEntry {
+ fn clone(&self) -> Self {
+ Self {
+ disabled: self.disabled,
+ // TODO: Reintroduce this
+ // disclosure_control_style: DisclosureControlVisibility,
+ indent_level: self.indent_level,
+ label: self.label.clone(),
+ left_slot: self.left_slot.clone(),
+ overflow: self.overflow,
+ size: self.size,
+ toggle: self.toggle,
+ variant: self.variant,
+ on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+ }
+ }
}
impl ListEntry {
@@ -245,9 +263,15 @@ impl ListEntry {
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
+ on_click: Default::default(),
}
}
+ pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+ self.on_click = Some(action.into());
+ self
+ }
+
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
@@ -303,9 +327,21 @@ impl ListEntry {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
-
div()
.relative()
+ .hover(|mut style| {
+ style.background = Some(cx.theme().colors().editor_background.into());
+ style
+ })
+ .on_mouse_down(gpui::MouseButton::Left, {
+ let action = self.on_click.map(|action| action.boxed_clone());
+
+ move |entry: &mut V, event, cx| {
+ if let Some(action) = action.as_ref() {
+ cx.dispatch_action(action.boxed_clone());
+ }
+ }
+ })
.group("")
.bg(cx.theme().colors().surface_background)
// TODO: Add focus state
@@ -401,7 +437,7 @@ impl List {
v_stack()
.w_full()
.py_1()
- .children(self.header)
+ .children(self.header.map(|header| header))
.child(list_content)
}
}
@@ -12,7 +12,6 @@ impl Story {
.flex_col()
.pt_2()
.px_4()
- .font("Zed Mono")
.bg(cx.theme().colors().background)
}
@@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize};
fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
this.bg(cx.theme().colors().elevated_surface_background)
+ .z_index(index.z_index())
.rounded_lg()
.border()
.border_color(cx.theme().colors().border_variant)
@@ -1,13 +1,14 @@
use crate::{status_bar::StatusItemView, Axis, Workspace};
use gpui::{
- div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
- FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
- WeakView, WindowContext,
+ div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+ EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+ Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
-use ui::{h_stack, IconButton, InteractionState, Tooltip};
+use theme2::ActiveTheme;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
pub enum PanelEvent {
ChangePosition,
@@ -216,11 +217,11 @@ impl Dock {
// .map_or(false, |panel| panel.has_focus(cx))
// }
- // pub fn panel<T: Panel>(&self) -> Option<View<T>> {
- // self.panel_entries
- // .iter()
- // .find_map(|entry| entry.panel.as_any().clone().downcast())
- // }
+ pub fn panel<T: Panel>(&self) -> Option<View<T>> {
+ self.panel_entries
+ .iter()
+ .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
+ }
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries
@@ -416,23 +417,13 @@ impl Dock {
}
}
- // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
- // todo!()
- // if let Some(active_entry) = self.visible_entry() {
- // Empty::new()
- // .into_any()
- // .contained()
- // .with_style(self.style(cx))
- // .resizable::<WorkspaceBounds>(
- // self.position.to_resize_handle_side(),
- // active_entry.panel.size(cx),
- // |_, _, _| {},
- // )
- // .into_any()
- // } else {
- // Empty::new().into_any()
- // }
- // }
+ pub fn toggle_action(&self) -> Box<dyn Action> {
+ match self.position {
+ DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+ DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+ DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+ }
+ }
}
impl Render for Dock {
@@ -443,10 +434,16 @@ impl Render for Dock {
let size = entry.panel.size(cx);
div()
+ .border_color(cx.theme().colors().border)
.map(|this| match self.position().axis() {
Axis::Horizontal => this.w(px(size)).h_full(),
Axis::Vertical => this.h(px(size)).w_full(),
})
+ .map(|this| match self.position() {
+ DockPosition::Left => this.border_r(),
+ DockPosition::Right => this.border_l(),
+ DockPosition::Bottom => this.border_t(),
+ })
.child(entry.panel.to_any())
} else {
div()
@@ -454,40 +451,6 @@ impl Render for Dock {
}
}
-// todo!()
-// impl View for Dock {
-// fn ui_name() -> &'static str {
-// "Dock"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// if let Some(active_entry) = self.visible_entry() {
-// let style = self.style(cx);
-// ChildView::new(active_entry.panel.as_any(), cx)
-// .contained()
-// .with_style(style)
-// .resizable::<WorkspaceBounds>(
-// self.position.to_resize_handle_side(),
-// active_entry.panel.size(cx),
-// |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
-// )
-// .into_any()
-// } else {
-// Empty::new().into_any()
-// }
-// }
-
-// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-// if cx.is_self_focused() {
-// if let Some(active_entry) = self.visible_entry() {
-// cx.focus(active_entry.panel.as_any());
-// } else {
-// cx.focus_parent();
-// }
-// }
-// }
-// }
-
impl PanelButtons {
pub fn new(
dock: View<Dock>,
@@ -648,6 +611,7 @@ impl PanelButtons {
// }
// }
+// here be kittens
impl Render for PanelButtons {
type Element = Div<Self>;
@@ -657,6 +621,13 @@ impl Render for PanelButtons {
let active_index = dock.active_panel_index;
let is_open = dock.is_open;
+ let (menu_anchor, menu_attach) = match dock.position {
+ DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
+ DockPosition::Bottom | DockPosition::Right => {
+ (AnchorCorner::BottomRight, AnchorCorner::TopRight)
+ }
+ };
+
let buttons = dock
.panel_entries
.iter()
@@ -664,18 +635,36 @@ impl Render for PanelButtons {
.filter_map(|(i, panel)| {
let icon = panel.panel.icon(cx)?;
let name = panel.panel.persistent_name();
- let action = panel.panel.toggle_action(cx);
- let action2 = action.boxed_clone();
-
- let mut button = IconButton::new(panel.panel.persistent_name(), icon)
- .when(i == active_index, |el| el.state(InteractionState::Active))
- .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
- .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
- Some(button)
+ let mut button: IconButton<Self> = if i == active_index && is_open {
+ let action = dock.toggle_action();
+ let tooltip: SharedString =
+ format!("Close {} dock", dock.position.to_label()).into();
+ IconButton::new(name, icon)
+ .state(InteractionState::Active)
+ .action(action.boxed_clone())
+ .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+ } else {
+ let action = panel.panel.toggle_action(cx);
+
+ IconButton::new(name, icon)
+ .action(action.boxed_clone())
+ .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+ };
+
+ Some(
+ menu_handle()
+ .id(name)
+ .menu(move |_, cx| {
+ cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+ })
+ .anchor(menu_anchor)
+ .attach(menu_attach)
+ .child(|is_open| button.selected(is_open)),
+ )
});
- h_stack().children(buttons)
+ h_stack().gap_0p5().children(buttons)
}
}
@@ -240,7 +240,7 @@ pub trait ItemHandle: 'static + Send {
fn deactivated(&self, cx: &mut WindowContext);
fn workspace_deactivated(&self, cx: &mut WindowContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
- fn id(&self) -> EntityId;
+ fn item_id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
@@ -399,7 +399,7 @@ impl<T: Item> ItemHandle for View<T> {
if workspace
.panes_by_item
- .insert(self.id(), pane.downgrade())
+ .insert(self.item_id(), pane.downgrade())
.is_none()
{
let mut pending_autosave = DelayedDebouncedEditAction::new();
@@ -410,7 +410,7 @@ impl<T: Item> ItemHandle for View<T> {
Some(cx.subscribe(self, move |workspace, item, event, cx| {
let pane = if let Some(pane) = workspace
.panes_by_item
- .get(&item.id())
+ .get(&item.item_id())
.and_then(|pane| pane.upgrade())
{
pane
@@ -463,7 +463,7 @@ impl<T: Item> ItemHandle for View<T> {
match event {
ItemEvent::CloseItem => {
pane.update(cx, |pane, cx| {
- pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
+ pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
})
.detach_and_log_err(cx);
return;
@@ -502,7 +502,7 @@ impl<T: Item> ItemHandle for View<T> {
// })
// .detach();
- let item_id = self.id();
+ let item_id = self.item_id();
cx.observe_release(self, move |workspace, _, _| {
workspace.panes_by_item.remove(&item_id);
event_subscription.take();
@@ -527,7 +527,7 @@ impl<T: Item> ItemHandle for View<T> {
self.update(cx, |this, cx| this.navigate(data, cx))
}
- fn id(&self) -> EntityId {
+ fn item_id(&self) -> EntityId {
self.entity_id()
}
@@ -712,7 +712,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
self.read(cx).remote_id().or_else(|| {
client.peer_id().map(|creator| ViewId {
creator,
- id: self.id().as_u64(),
+ id: self.item_id().as_u64(),
})
})
}
@@ -1,6 +1,6 @@
use gpui::{
- div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
- ViewContext, WindowContext,
+ div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
+ ViewContext,
};
use ui::{h_stack, v_stack};
@@ -15,14 +15,6 @@ pub struct ModalLayer {
active_modal: Option<ActiveModal>,
}
-pub trait Modal: Render + EventEmitter<ModalEvent> {
- fn focus(&self, cx: &mut WindowContext);
-}
-
-pub enum ModalEvent {
- Dismissed,
-}
-
impl ModalLayer {
pub fn new() -> Self {
Self { active_modal: None }
@@ -30,7 +22,7 @@ impl ModalLayer {
pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
where
- V: Modal,
+ V: ManagedView,
B: FnOnce(&mut ViewContext<V>) -> V,
{
if let Some(active_modal) = &self.active_modal {
@@ -46,17 +38,15 @@ impl ModalLayer {
pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
where
- V: Modal,
+ V: ManagedView,
{
self.active_modal = Some(ActiveModal {
modal: new_modal.clone().into(),
- subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
- ModalEvent::Dismissed => this.hide_modal(cx),
- }),
+ subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
previous_focus_handle: cx.focused(),
focus_handle: cx.focus_handle(),
});
- new_modal.update(cx, |modal, cx| modal.focus(cx));
+ cx.focus_view(&new_modal);
cx.notify();
}
@@ -7,9 +7,9 @@ use crate::{
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
- actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
- EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
- ViewContext, VisualContext, WeakView, WindowContext,
+ actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+ EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
+ Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
@@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize);
// pub pane: WeakView<Pane>,
// }
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
#[serde(rename_all = "camelCase")]
pub struct CloseActiveItem {
pub save_intent: Option<SaveIntent>,
}
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
#[serde(rename_all = "camelCase")]
pub struct CloseAllItems {
pub save_intent: Option<SaveIntent>,
@@ -104,29 +102,6 @@ actions!(
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
-pub fn init(cx: &mut AppContext) {
- // todo!()
- // cx.add_action(Pane::toggle_zoom);
- // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
- // pane.activate_item(action.0, true, true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
- // pane.activate_item(pane.items.len() - 1, true, true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
- // pane.activate_prev_item(true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
- // pane.activate_next_item(true, cx);
- // });
- // cx.add_async_action(Pane::close_active_item);
- // cx.add_async_action(Pane::close_inactive_items);
- // cx.add_async_action(Pane::close_clean_items);
- // cx.add_async_action(Pane::close_items_to_the_left);
- // cx.add_async_action(Pane::close_items_to_the_right);
- // cx.add_async_action(Pane::close_all_items);
-}
-
pub enum Event {
AddItem { item: Box<dyn ItemHandle> },
ActivateItem { local: bool },
@@ -142,7 +117,10 @@ pub enum Event {
impl fmt::Debug for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- Event::AddItem { item } => f.debug_struct("AddItem").field("item", &item.id()).finish(),
+ Event::AddItem { item } => f
+ .debug_struct("AddItem")
+ .field("item", &item.item_id())
+ .finish(),
Event::ActivateItem { local } => f
.debug_struct("ActivateItem")
.field("local", local)
@@ -526,7 +504,7 @@ impl Pane {
.0
.lock()
.paths_by_item
- .insert(item.id(), (project_path, abs_path));
+ .insert(item.item_id(), (project_path, abs_path));
}
}
}
@@ -550,7 +528,7 @@ impl Pane {
};
let existing_item_index = self.items.iter().position(|existing_item| {
- if existing_item.id() == item.id() {
+ if existing_item.item_id() == item.item_id() {
true
} else if existing_item.is_singleton(cx) {
existing_item
@@ -615,21 +593,21 @@ impl Pane {
self.items.iter()
}
- // pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
- // self.items
- // .iter()
- // .filter_map(|item| item.as_any().clone().downcast())
- // }
+ pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
+ self.items
+ .iter()
+ .filter_map(|item| item.to_any().downcast().ok())
+ }
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
self.items.get(self.active_item_index).cloned()
}
- // pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
- // self.items
- // .get(self.active_item_index)?
- // .pixel_position_of_cursor(cx)
- // }
+ pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
+ self.items
+ .get(self.active_item_index)?
+ .pixel_position_of_cursor(cx)
+ }
pub fn item_for_entry(
&self,
@@ -646,24 +624,26 @@ impl Pane {
}
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
- self.items.iter().position(|i| i.id() == item.id())
+ self.items
+ .iter()
+ .position(|i| i.item_id() == item.item_id())
}
- // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
- // // Potentially warn the user of the new keybinding
- // let workspace_handle = self.workspace().clone();
- // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
- // .detach();
-
- // if self.zoomed {
- // cx.emit(Event::ZoomOut);
- // } else if !self.items.is_empty() {
- // if !self.has_focus {
- // cx.focus_self();
- // }
- // cx.emit(Event::ZoomIn);
+ // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+ // // Potentially warn the user of the new keybinding
+ // let workspace_handle = self.workspace().clone();
+ // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+ // .detach();
+
+ // if self.zoomed {
+ // cx.emit(Event::ZoomOut);
+ // } else if !self.items.is_empty() {
+ // if !self.has_focus {
+ // cx.focus_self();
// }
+ // cx.emit(Event::ZoomIn);
// }
+ // }
pub fn activate_item(
&mut self,
@@ -691,9 +671,9 @@ impl Pane {
if let Some(newly_active_item) = self.items.get(index) {
self.activation_history
.retain(|&previously_active_item_id| {
- previously_active_item_id != newly_active_item.id()
+ previously_active_item_id != newly_active_item.item_id()
});
- self.activation_history.push(newly_active_item.id());
+ self.activation_history.push(newly_active_item.item_id());
}
self.update_toolbar(cx);
@@ -707,25 +687,25 @@ impl Pane {
}
}
- // pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
- // let mut index = self.active_item_index;
- // if index > 0 {
- // index -= 1;
- // } else if !self.items.is_empty() {
- // index = self.items.len() - 1;
- // }
- // self.activate_item(index, activate_pane, activate_pane, cx);
- // }
+ pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+ let mut index = self.active_item_index;
+ if index > 0 {
+ index -= 1;
+ } else if !self.items.is_empty() {
+ index = self.items.len() - 1;
+ }
+ self.activate_item(index, activate_pane, activate_pane, cx);
+ }
- // pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
- // let mut index = self.active_item_index;
- // if index + 1 < self.items.len() {
- // index += 1;
- // } else {
- // index = 0;
- // }
- // self.activate_item(index, activate_pane, activate_pane, cx);
- // }
+ pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+ let mut index = self.active_item_index;
+ if index + 1 < self.items.len() {
+ index += 1;
+ } else {
+ index = 0;
+ }
+ self.activate_item(index, activate_pane, activate_pane, cx);
+ }
pub fn close_active_item(
&mut self,
@@ -735,7 +715,7 @@ impl Pane {
if self.items.is_empty() {
return None;
}
- let active_item_id = self.items[self.active_item_index].id();
+ let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_item_by_id(
active_item_id,
action.save_intent.unwrap_or(SaveIntent::Close),
@@ -752,106 +732,106 @@ impl Pane {
self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
}
- // pub fn close_inactive_items(
- // &mut self,
- // _: &CloseInactiveItems,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // if self.items.is_empty() {
- // return None;
- // }
+ pub fn close_inactive_items(
+ &mut self,
+ _: &CloseInactiveItems,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
- // let active_item_id = self.items[self.active_item_index].id();
- // Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
- // item_id != active_item_id
- // }))
- // }
+ let active_item_id = self.items[self.active_item_index].item_id();
+ Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+ item_id != active_item_id
+ }))
+ }
- // pub fn close_clean_items(
- // &mut self,
- // _: &CloseCleanItems,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let item_ids: Vec<_> = self
- // .items()
- // .filter(|item| !item.is_dirty(cx))
- // .map(|item| item.id())
- // .collect();
- // Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
- // item_ids.contains(&item_id)
- // }))
- // }
+ pub fn close_clean_items(
+ &mut self,
+ _: &CloseCleanItems,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let item_ids: Vec<_> = self
+ .items()
+ .filter(|item| !item.is_dirty(cx))
+ .map(|item| item.item_id())
+ .collect();
+ Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+ item_ids.contains(&item_id)
+ }))
+ }
- // pub fn close_items_to_the_left(
- // &mut self,
- // _: &CloseItemsToTheLeft,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // if self.items.is_empty() {
- // return None;
- // }
- // let active_item_id = self.items[self.active_item_index].id();
- // Some(self.close_items_to_the_left_by_id(active_item_id, cx))
- // }
+ pub fn close_items_to_the_left(
+ &mut self,
+ _: &CloseItemsToTheLeft,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].item_id();
+ Some(self.close_items_to_the_left_by_id(active_item_id, cx))
+ }
- // pub fn close_items_to_the_left_by_id(
- // &mut self,
- // item_id: usize,
- // cx: &mut ViewContext<Self>,
- // ) -> Task<Result<()>> {
- // let item_ids: Vec<_> = self
- // .items()
- // .take_while(|item| item.id() != item_id)
- // .map(|item| item.id())
- // .collect();
- // self.close_items(cx, SaveIntent::Close, move |item_id| {
- // item_ids.contains(&item_id)
- // })
- // }
+ pub fn close_items_to_the_left_by_id(
+ &mut self,
+ item_id: EntityId,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let item_ids: Vec<_> = self
+ .items()
+ .take_while(|item| item.item_id() != item_id)
+ .map(|item| item.item_id())
+ .collect();
+ self.close_items(cx, SaveIntent::Close, move |item_id| {
+ item_ids.contains(&item_id)
+ })
+ }
- // pub fn close_items_to_the_right(
- // &mut self,
- // _: &CloseItemsToTheRight,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // if self.items.is_empty() {
- // return None;
- // }
- // let active_item_id = self.items[self.active_item_index].id();
- // Some(self.close_items_to_the_right_by_id(active_item_id, cx))
- // }
+ pub fn close_items_to_the_right(
+ &mut self,
+ _: &CloseItemsToTheRight,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].item_id();
+ Some(self.close_items_to_the_right_by_id(active_item_id, cx))
+ }
- // pub fn close_items_to_the_right_by_id(
- // &mut self,
- // item_id: usize,
- // cx: &mut ViewContext<Self>,
- // ) -> Task<Result<()>> {
- // let item_ids: Vec<_> = self
- // .items()
- // .rev()
- // .take_while(|item| item.id() != item_id)
- // .map(|item| item.id())
- // .collect();
- // self.close_items(cx, SaveIntent::Close, move |item_id| {
- // item_ids.contains(&item_id)
- // })
- // }
+ pub fn close_items_to_the_right_by_id(
+ &mut self,
+ item_id: EntityId,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let item_ids: Vec<_> = self
+ .items()
+ .rev()
+ .take_while(|item| item.item_id() != item_id)
+ .map(|item| item.item_id())
+ .collect();
+ self.close_items(cx, SaveIntent::Close, move |item_id| {
+ item_ids.contains(&item_id)
+ })
+ }
- // pub fn close_all_items(
- // &mut self,
- // action: &CloseAllItems,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // if self.items.is_empty() {
- // return None;
- // }
+ pub fn close_all_items(
+ &mut self,
+ action: &CloseAllItems,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ if self.items.is_empty() {
+ return None;
+ }
- // Some(
- // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
- // true
- // }),
- // )
- // }
+ Some(
+ self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+ true
+ }),
+ )
+ }
pub(super) fn file_names_for_prompt(
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
@@ -898,7 +878,7 @@ impl Pane {
let mut items_to_close = Vec::new();
let mut dirty_items = Vec::new();
for item in &self.items {
- if should_close(item.id()) {
+ if should_close(item.item_id()) {
items_to_close.push(item.boxed_clone());
if item.is_dirty(cx) {
dirty_items.push(item.boxed_clone());
@@ -951,7 +931,7 @@ impl Pane {
for item in workspace.items(cx) {
if !items_to_close
.iter()
- .any(|item_to_close| item_to_close.id() == item.id())
+ .any(|item_to_close| item_to_close.item_id() == item.item_id())
{
let other_project_item_ids = item.project_item_model_ids(cx);
project_item_ids.retain(|id| !other_project_item_ids.contains(id));
@@ -979,7 +959,11 @@ impl Pane {
// Remove the item from the pane.
pane.update(&mut cx, |pane, cx| {
- if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
+ if let Some(item_ix) = pane
+ .items
+ .iter()
+ .position(|i| i.item_id() == item.item_id())
+ {
pane.remove_item(item_ix, false, cx);
}
})?;
@@ -997,7 +981,7 @@ impl Pane {
cx: &mut ViewContext<Self>,
) {
self.activation_history
- .retain(|&history_entry| history_entry != self.items[item_index].id());
+ .retain(|&history_entry| history_entry != self.items[item_index].item_id());
if item_index == self.active_item_index {
let index_to_activate = self
@@ -1005,7 +989,7 @@ impl Pane {
.pop()
.and_then(|last_activated_item| {
self.items.iter().enumerate().find_map(|(index, item)| {
- (item.id() == last_activated_item).then_some(index)
+ (item.item_id() == last_activated_item).then_some(index)
})
})
// We didn't have a valid activation history entry, so fallback
@@ -1022,7 +1006,9 @@ impl Pane {
let item = self.items.remove(item_index);
- cx.emit(Event::RemoveItem { item_id: item.id() });
+ cx.emit(Event::RemoveItem {
+ item_id: item.item_id(),
+ });
if self.items.is_empty() {
item.deactivated(cx);
self.update_toolbar(cx);
@@ -1043,16 +1029,20 @@ impl Pane {
.0
.lock()
.paths_by_item
- .get(&item.id())
+ .get(&item.item_id())
.and_then(|(_, abs_path)| abs_path.clone());
self.nav_history
.0
.lock()
.paths_by_item
- .insert(item.id(), (path, abs_path));
+ .insert(item.item_id(), (path, abs_path));
} else {
- self.nav_history.0.lock().paths_by_item.remove(&item.id());
+ self.nav_history
+ .0
+ .lock()
+ .paths_by_item
+ .remove(&item.item_id());
}
if self.items.is_empty() && self.zoomed {
@@ -1325,7 +1315,7 @@ impl Pane {
) -> Option<()> {
let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
- Some((i, item.id()))
+ Some((i, item.item_id()))
} else {
None
}
@@ -1356,10 +1346,10 @@ impl Pane {
) -> impl Component<Self> {
let label = item.tab_content(Some(detail), cx);
let close_icon = || {
- let id = item.id();
+ let id = item.item_id();
div()
- .id(item.id())
+ .id(item.item_id())
.invisible()
.group_hover("", |style| style.visible())
.child(IconButton::new("close_tab", Icon::Close).on_click(
@@ -1389,7 +1379,7 @@ impl Pane {
div()
.group("")
- .id(item.id())
+ .id(item.item_id())
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |div, text| {
div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
@@ -1916,8 +1906,27 @@ impl Render for Pane {
.on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
.on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
.on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
+ // cx.add_action(Pane::toggle_zoom);
+ // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
+ // pane.activate_item(action.0, true, true, cx);
+ // });
+ // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+ // pane.activate_item(pane.items.len() - 1, true, true, cx);
+ // });
+ // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+ // pane.activate_prev_item(true, cx);
+ // });
+ // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+ // pane.activate_next_item(true, cx);
+ // });
+ // cx.add_async_action(Pane::close_active_item);
+ // cx.add_async_action(Pane::close_inactive_items);
+ // cx.add_async_action(Pane::close_clean_items);
+ // cx.add_async_action(Pane::close_items_to_the_left);
+ // cx.add_async_action(Pane::close_items_to_the_right);
+ // cx.add_async_action(Pane::close_all_items);
.size_full()
- .on_action(|pane: &mut Self, action, cx| {
+ .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
pane.close_active_item(action, cx)
.map(|task| task.detach_and_log_err(cx));
})
@@ -240,7 +240,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyView {
impl PartialEq for Box<dyn SearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
- self.id() == other.id()
+ self.item_id() == other.item_id()
}
}
@@ -29,11 +29,12 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
- actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
- AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
- FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
- ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
- ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+ actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+ AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
+ FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
+ ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+ Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
+ WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -49,7 +50,7 @@ pub use persistence::{
WorkspaceDb, DB,
};
use postage::stream::Stream;
-use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
+use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
use settings2::Settings;
use status_bar::StatusBar;
@@ -57,7 +58,7 @@ pub use status_bar::StatusItemView;
use std::{
any::TypeId,
borrow::Cow,
- env,
+ cmp, env,
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
time::Duration,
@@ -84,8 +85,8 @@ lazy_static! {
.and_then(parse_pixel_position_env_var);
}
-// #[derive(Clone, PartialEq)]
-// pub struct RemoveWorktreeFromProject(pub WorktreeId);
+#[derive(Clone, PartialEq)]
+pub struct RemoveWorktreeFromProject(pub WorktreeId);
actions!(
Open,
@@ -114,40 +115,40 @@ actions!(
CloseAllDocks,
);
-// #[derive(Clone, PartialEq)]
-// pub struct OpenPaths {
-// pub paths: Vec<PathBuf>,
-// }
+#[derive(Clone, PartialEq)]
+pub struct OpenPaths {
+ pub paths: Vec<PathBuf>,
+}
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePane(pub usize);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePane(pub usize);
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePaneInDirection(pub SplitDirection);
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct SwapPaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct SwapPaneInDirection(pub SplitDirection);
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct NewFileInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct NewFileInDirection(pub SplitDirection);
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct SaveAll {
-// pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+ pub save_intent: Option<SaveIntent>,
+}
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct Save {
-// pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+ pub save_intent: Option<SaveIntent>,
+}
-// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-// #[serde(rename_all = "camelCase")]
-// pub struct CloseAllItemsAndPanes {
-// pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+ pub save_intent: Option<SaveIntent>,
+}
#[derive(Deserialize)]
pub struct Toast {
@@ -194,26 +195,11 @@ impl Clone for Toast {
}
}
-#[register_action]
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)]
pub struct OpenTerminal {
pub working_directory: PathBuf,
}
-// impl_actions!(
-// workspace,
-// [
-// ActivatePane,
-// ActivatePaneInDirection,
-// SwapPaneInDirection,
-// NewFileInDirection,
-// Toast,
-// SaveAll,
-// Save,
-// CloseAllItemsAndPanes,
-// ]
-// );
-
pub type WorkspaceId = i64;
pub fn init_settings(cx: &mut AppContext) {
@@ -223,7 +209,6 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
- pane::init(cx);
notifications::init(cx);
// cx.add_global_action({
@@ -355,7 +340,7 @@ impl AppState {
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
- theme2::init(cx);
+ theme2::init(theme2::LoadThemes::JustBase, cx);
client2::init(&client, cx);
crate::init_settings(cx);
@@ -424,6 +409,7 @@ pub enum Event {
}
pub struct Workspace {
+ window_self: WindowHandle<Self>,
weak_self: WeakView<Self>,
workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
zoomed: Option<AnyWeakView>,
@@ -456,6 +442,8 @@ pub struct Workspace {
pane_history_timestamp: Arc<AtomicUsize>,
}
+impl EventEmitter<Event> for Workspace {}
+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: PeerId,
@@ -533,8 +521,8 @@ impl Workspace {
)
});
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
- // todo!()
- // cx.focus(¢er_pane);
+
+ cx.focus_view(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
@@ -637,10 +625,16 @@ impl Workspace {
this.serialize_workspace(cx);
cx.notify();
}),
+ cx.on_release(|this, cx| {
+ this.app_state.workspace_store.update(cx, |store, _| {
+ store.workspaces.remove(&this.window_self);
+ })
+ }),
];
cx.defer(|this, cx| this.update_window_title(cx));
Workspace {
+ window_self: window_handle,
weak_self: weak_handle.clone(),
zoomed: None,
zoomed_position: None,
@@ -780,19 +774,6 @@ impl Workspace {
})?
};
- // todo!() Ask how to do this
- // let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
- // let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
-
- // (app_state.initialize_workspace)(
- // weak_view,
- // serialized_workspace.is_some(),
- // app_state.clone(),
- // async_cx,
- // )
- // .await
- // .log_err();
-
window
.update(&mut cx, |_, cx| cx.activate_window())
.log_err();
@@ -965,12 +946,12 @@ impl Workspace {
if let Some((project_entry_id, build_item)) = task.log_err() {
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
- pane.active_item().map(|p| p.id())
+ pane.active_item().map(|p| p.item_id())
})?;
pane.update(&mut cx, |pane, cx| {
let item = pane.open_item(project_entry_id, true, cx, build_item);
- navigated |= Some(item.id()) != prev_active_item_id;
+ navigated |= Some(item.item_id()) != prev_active_item_id;
pane.nav_history_mut().set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
navigated |= item.navigate(data, cx);
@@ -1078,35 +1059,40 @@ impl Workspace {
}
}
- // pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
- // cx.spawn(|mut cx| async move {
- // let window = cx
- // .windows()
- // .into_iter()
- // .find(|window| window.is_active(&cx).unwrap_or(false));
- // if let Some(window) = window {
- // //This can only get called when the window's project connection has been lost
- // //so we don't need to prompt the user for anything and instead just close the window
- // window.remove(&mut cx);
- // }
- // })
- // .detach();
- // }
+ // todo!(Non-window-actions)
+ pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
+ cx.windows().iter().find(|window| {
+ window
+ .update(cx, |_, window| {
+ if window.is_window_active() {
+ //This can only get called when the window's project connection has been lost
+ //so we don't need to prompt the user for anything and instead just close the window
+ window.remove_window();
+ true
+ } else {
+ false
+ }
+ })
+ .unwrap_or(false)
+ });
+ }
- // pub fn close(
- // &mut self,
- // _: &CloseWindow,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let window = cx.window();
- // let prepare = self.prepare_to_close(false, cx);
- // Some(cx.spawn(|_, mut cx| async move {
- // if prepare.await? {
- // window.remove(&mut cx);
- // }
- // Ok(())
- // }))
- // }
+ pub fn close(
+ &mut self,
+ _: &CloseWindow,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let window = cx.window_handle();
+ let prepare = self.prepare_to_close(false, cx);
+ Some(cx.spawn(|_, mut cx| async move {
+ if prepare.await? {
+ window.update(&mut cx, |_, cx| {
+ cx.remove_window();
+ })?;
+ }
+ Ok(())
+ }))
+ }
pub fn prepare_to_close(
&mut self,
@@ -1114,184 +1100,177 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
//todo!(saveing)
- // let active_call = self.active_call().cloned();
- // let window = cx.window();
+ let active_call = self.active_call().cloned();
+ let window = cx.window_handle();
cx.spawn(|this, mut cx| async move {
- // let workspace_count = cx
- // .windows()
- // .into_iter()
- // .filter(|window| window.root_is::<Workspace>())
- // .count();
-
- // if let Some(active_call) = active_call {
- // if !quitting
- // && workspace_count == 1
- // && active_call.read_with(&cx, |call, _| call.room().is_some())
- // {
- // let answer = window.prompt(
- // PromptLevel::Warning,
- // "Do you want to leave the current call?",
- // &["Close window and hang up", "Cancel"],
- // &mut cx,
- // );
-
- // if let Some(mut answer) = answer {
- // if answer.next().await == Some(1) {
- // return anyhow::Ok(false);
- // } else {
- // active_call
- // .update(&mut cx, |call, cx| call.hang_up(cx))
- // .await
- // .log_err();
- // }
- // }
- // }
- // }
+ let workspace_count = cx.update(|_, cx| {
+ cx.windows()
+ .iter()
+ .filter(|window| window.downcast::<Workspace>().is_some())
+ .count()
+ })?;
- Ok(
- false, // this
- // .update(&mut cx, |this, cx| {
- // this.save_all_internal(SaveIntent::Close, cx)
- // })?
- // .await?
- )
+ if let Some(active_call) = active_call {
+ if !quitting
+ && workspace_count == 1
+ && active_call.read_with(&cx, |call, _| call.room().is_some())?
+ {
+ let answer = window.update(&mut cx, |_, cx| {
+ cx.prompt(
+ PromptLevel::Warning,
+ "Do you want to leave the current call?",
+ &["Close window and hang up", "Cancel"],
+ )
+ })?;
+
+ if answer.await.log_err() == Some(1) {
+ return anyhow::Ok(false);
+ } else {
+ active_call
+ .update(&mut cx, |call, cx| call.hang_up(cx))?
+ .await
+ .log_err();
+ }
+ }
+ }
+
+ Ok(this
+ .update(&mut cx, |this, cx| {
+ this.save_all_internal(SaveIntent::Close, cx)
+ })?
+ .await?)
})
}
- // fn save_all(
- // &mut self,
- // action: &SaveAll,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let save_all =
- // self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
- // Some(cx.foreground().spawn(async move {
- // save_all.await?;
- // Ok(())
- // }))
- // }
+ fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
+ let save_all = self
+ .save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
+ .detach_and_log_err(cx);
+ }
- // fn save_all_internal(
- // &mut self,
- // mut save_intent: SaveIntent,
- // cx: &mut ViewContext<Self>,
- // ) -> Task<Result<bool>> {
- // if self.project.read(cx).is_read_only() {
- // return Task::ready(Ok(true));
- // }
- // let dirty_items = self
- // .panes
- // .iter()
- // .flat_map(|pane| {
- // pane.read(cx).items().filter_map(|item| {
- // if item.is_dirty(cx) {
- // Some((pane.downgrade(), item.boxed_clone()))
- // } else {
- // None
- // }
- // })
- // })
- // .collect::<Vec<_>>();
-
- // let project = self.project.clone();
- // cx.spawn(|workspace, mut cx| async move {
- // // Override save mode and display "Save all files" prompt
- // if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
- // let mut answer = workspace.update(&mut cx, |_, cx| {
- // let prompt = Pane::file_names_for_prompt(
- // &mut dirty_items.iter().map(|(_, handle)| handle),
- // dirty_items.len(),
- // cx,
- // );
- // cx.prompt(
- // PromptLevel::Warning,
- // &prompt,
- // &["Save all", "Discard all", "Cancel"],
- // )
- // })?;
- // match answer.next().await {
- // Some(0) => save_intent = SaveIntent::SaveAll,
- // Some(1) => save_intent = SaveIntent::Skip,
- // _ => {}
- // }
- // }
- // for (pane, item) in dirty_items {
- // let (singleton, project_entry_ids) =
- // cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
- // if singleton || !project_entry_ids.is_empty() {
- // if let Some(ix) =
- // pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
- // {
- // if !Pane::save_item(
- // project.clone(),
- // &pane,
- // ix,
- // &*item,
- // save_intent,
- // &mut cx,
- // )
- // .await?
- // {
- // return Ok(false);
- // }
- // }
- // }
- // }
- // Ok(true)
- // })
- // }
+ fn save_all_internal(
+ &mut self,
+ mut save_intent: SaveIntent,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<bool>> {
+ if self.project.read(cx).is_read_only() {
+ return Task::ready(Ok(true));
+ }
+ let dirty_items = self
+ .panes
+ .iter()
+ .flat_map(|pane| {
+ pane.read(cx).items().filter_map(|item| {
+ if item.is_dirty(cx) {
+ Some((pane.downgrade(), item.boxed_clone()))
+ } else {
+ None
+ }
+ })
+ })
+ .collect::<Vec<_>>();
- // pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
- // let mut paths = cx.prompt_for_paths(PathPromptOptions {
- // files: true,
- // directories: true,
- // multiple: true,
- // });
+ let project = self.project.clone();
+ cx.spawn(|workspace, mut cx| async move {
+ // Override save mode and display "Save all files" prompt
+ if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+ let mut answer = workspace.update(&mut cx, |_, cx| {
+ let prompt = Pane::file_names_for_prompt(
+ &mut dirty_items.iter().map(|(_, handle)| handle),
+ dirty_items.len(),
+ cx,
+ );
+ cx.prompt(
+ PromptLevel::Warning,
+ &prompt,
+ &["Save all", "Discard all", "Cancel"],
+ )
+ })?;
+ match answer.await.log_err() {
+ Some(0) => save_intent = SaveIntent::SaveAll,
+ Some(1) => save_intent = SaveIntent::Skip,
+ _ => {}
+ }
+ }
+ for (pane, item) in dirty_items {
+ let (singleton, project_entry_ids) =
+ cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
+ if singleton || !project_entry_ids.is_empty() {
+ if let Some(ix) =
+ pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
+ {
+ if !Pane::save_item(
+ project.clone(),
+ &pane,
+ ix,
+ &*item,
+ save_intent,
+ &mut cx,
+ )
+ .await?
+ {
+ return Ok(false);
+ }
+ }
+ }
+ }
+ Ok(true)
+ })
+ }
- // Some(cx.spawn(|this, mut cx| async move {
- // if let Some(paths) = paths.recv().await.flatten() {
- // if let Some(task) = this
- // .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
- // .log_err()
- // {
- // task.await?
- // }
- // }
- // Ok(())
- // }))
- // }
+ pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
+ let mut paths = cx.prompt_for_paths(PathPromptOptions {
+ files: true,
+ directories: true,
+ multiple: true,
+ });
- // pub fn open_workspace_for_paths(
- // &mut self,
- // paths: Vec<PathBuf>,
- // cx: &mut ViewContext<Self>,
- // ) -> Task<Result<()>> {
- // let window = cx.window().downcast::<Self>();
- // let is_remote = self.project.read(cx).is_remote();
- // let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
- // let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
- // let close_task = if is_remote || has_worktree || has_dirty_items {
- // None
- // } else {
- // Some(self.prepare_to_close(false, cx))
- // };
- // let app_state = self.app_state.clone();
+ cx.spawn(|this, mut cx| async move {
+ let Some(paths) = paths.await.log_err().flatten() else {
+ return;
+ };
- // cx.spawn(|_, mut cx| async move {
- // let window_to_replace = if let Some(close_task) = close_task {
- // if !close_task.await? {
- // return Ok(());
- // }
- // window
- // } else {
- // None
- // };
- // cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
- // .await?;
- // Ok(())
- // })
- // }
+ if let Some(task) = this
+ .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
+ .log_err()
+ {
+ task.await.log_err();
+ }
+ })
+ .detach()
+ }
+
+ pub fn open_workspace_for_paths(
+ &mut self,
+ paths: Vec<PathBuf>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let window = cx.window_handle().downcast::<Self>();
+ let is_remote = self.project.read(cx).is_remote();
+ let has_worktree = self.project.read(cx).worktrees().next().is_some();
+ let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
+ let close_task = if is_remote || has_worktree || has_dirty_items {
+ None
+ } else {
+ Some(self.prepare_to_close(false, cx))
+ };
+ let app_state = self.app_state.clone();
+
+ cx.spawn(|_, mut cx| async move {
+ let window_to_replace = if let Some(close_task) = close_task {
+ if !close_task.await? {
+ return Ok(());
+ }
+ window
+ } else {
+ None
+ };
+ cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
+ .await?;
+ Ok(())
+ })
+ }
#[allow(clippy::type_complexity)]
pub fn open_paths(
@@ -1369,25 +1348,25 @@ impl Workspace {
})
}
- // fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
- // let mut paths = cx.prompt_for_paths(PathPromptOptions {
- // files: false,
- // directories: true,
- // multiple: true,
- // });
- // cx.spawn(|this, mut cx| async move {
- // if let Some(paths) = paths.recv().await.flatten() {
- // let results = this
- // .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
- // .await;
- // for result in results.into_iter().flatten() {
- // result.log_err();
- // }
- // }
- // anyhow::Ok(())
- // })
- // .detach_and_log_err(cx);
- // }
+ fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
+ let mut paths = cx.prompt_for_paths(PathPromptOptions {
+ files: false,
+ directories: true,
+ multiple: true,
+ });
+ cx.spawn(|this, mut cx| async move {
+ if let Some(paths) = paths.await.log_err().flatten() {
+ let results = this
+ .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+ .await;
+ for result in results.into_iter().flatten() {
+ result.log_err();
+ }
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
fn project_path_for_path(
project: Model<Project>,
@@ -1418,18 +1397,18 @@ impl Workspace {
self.panes.iter().flat_map(|pane| pane.read(cx).items())
}
- // pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
- // self.items_of_type(cx).max_by_key(|item| item.id())
- // }
+ pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
+ self.items_of_type(cx).max_by_key(|item| item.item_id())
+ }
- // pub fn items_of_type<'a, T: Item>(
- // &'a self,
- // cx: &'a AppContext,
- // ) -> impl 'a + Iterator<Item = View<T>> {
- // self.panes
- // .iter()
- // .flat_map(|pane| pane.read(cx).items_of_type())
- // }
+ pub fn items_of_type<'a, T: Item>(
+ &'a self,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = View<T>> {
+ self.panes
+ .iter()
+ .flat_map(|pane| pane.read(cx).items_of_type())
+ }
pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.active_pane().read(cx).active_item()
@@ -1466,68 +1445,70 @@ impl Workspace {
})
}
- // pub fn close_inactive_items_and_panes(
- // &mut self,
- // _: &CloseInactiveTabsAndPanes,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // self.close_all_internal(true, SaveIntent::Close, cx)
- // }
+ pub fn close_inactive_items_and_panes(
+ &mut self,
+ _: &CloseInactiveTabsAndPanes,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.close_all_internal(true, SaveIntent::Close, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }
- // pub fn close_all_items_and_panes(
- // &mut self,
- // action: &CloseAllItemsAndPanes,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
- // }
+ pub fn close_all_items_and_panes(
+ &mut self,
+ action: &CloseAllItemsAndPanes,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }
- // fn close_all_internal(
- // &mut self,
- // retain_active_pane: bool,
- // save_intent: SaveIntent,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let current_pane = self.active_pane();
+ fn close_all_internal(
+ &mut self,
+ retain_active_pane: bool,
+ save_intent: SaveIntent,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let current_pane = self.active_pane();
- // let mut tasks = Vec::new();
+ let mut tasks = Vec::new();
- // if retain_active_pane {
- // if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
- // pane.close_inactive_items(&CloseInactiveItems, cx)
- // }) {
- // tasks.push(current_pane_close);
- // };
- // }
+ if retain_active_pane {
+ if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+ pane.close_inactive_items(&CloseInactiveItems, cx)
+ }) {
+ tasks.push(current_pane_close);
+ };
+ }
- // for pane in self.panes() {
- // if retain_active_pane && pane.id() == current_pane.id() {
- // continue;
- // }
+ for pane in self.panes() {
+ if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
+ continue;
+ }
- // if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
- // pane.close_all_items(
- // &CloseAllItems {
- // save_intent: Some(save_intent),
- // },
- // cx,
- // )
- // }) {
- // tasks.push(close_pane_items)
- // }
- // }
+ if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
+ pane.close_all_items(
+ &CloseAllItems {
+ save_intent: Some(save_intent),
+ },
+ cx,
+ )
+ }) {
+ tasks.push(close_pane_items)
+ }
+ }
- // if tasks.is_empty() {
- // None
- // } else {
- // Some(cx.spawn(|_, _| async move {
- // for task in tasks {
- // task.await?
- // }
- // Ok(())
- // }))
- // }
- // }
+ if tasks.is_empty() {
+ None
+ } else {
+ Some(cx.spawn(|_, _| async move {
+ for task in tasks {
+ task.await?
+ }
+ Ok(())
+ }))
+ }
+ }
pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
let dock = match dock_side {
@@ -1635,15 +1616,15 @@ impl Workspace {
None
}
- // pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
- // for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
- // let dock = dock.read(cx);
- // if let Some(panel) = dock.panel::<T>() {
- // return Some(panel);
- // }
- // }
- // None
- // }
+ pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
+ for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+ let dock = dock.read(cx);
+ if let Some(panel) = dock.panel::<T>() {
+ return Some(panel);
+ }
+ }
+ None
+ }
fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for pane in &self.panes {
@@ -1954,81 +1935,89 @@ impl Workspace {
}
}
- // fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
- // let panes = self.center.panes();
- // if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
- // cx.focus(&pane);
- // } else {
- // self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
- // }
- // }
+ fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
+ let panes = self.center.panes();
+ if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
+ cx.focus_view(&pane);
+ } else {
+ self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
+ }
+ }
- // pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
- // let panes = self.center.panes();
- // if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
- // let next_ix = (ix + 1) % panes.len();
- // let next_pane = panes[next_ix].clone();
- // cx.focus(&next_pane);
- // }
- // }
+ pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
+ let panes = self.center.panes();
+ if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+ let next_ix = (ix + 1) % panes.len();
+ let next_pane = panes[next_ix].clone();
+ cx.focus_view(&next_pane);
+ }
+ }
- // pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
- // let panes = self.center.panes();
- // if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
- // let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
- // let prev_pane = panes[prev_ix].clone();
- // cx.focus(&prev_pane);
- // }
- // }
+ pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
+ let panes = self.center.panes();
+ if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+ let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
+ let prev_pane = panes[prev_ix].clone();
+ cx.focus_view(&prev_pane);
+ }
+ }
- // pub fn activate_pane_in_direction(
- // &mut self,
- // direction: SplitDirection,
- // cx: &mut ViewContext<Self>,
- // ) {
- // if let Some(pane) = self.find_pane_in_direction(direction, cx) {
- // cx.focus(pane);
- // }
- // }
+ pub fn activate_pane_in_direction(
+ &mut self,
+ direction: SplitDirection,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+ cx.focus_view(pane);
+ }
+ }
- // pub fn swap_pane_in_direction(
- // &mut self,
- // direction: SplitDirection,
- // cx: &mut ViewContext<Self>,
- // ) {
- // if let Some(to) = self
- // .find_pane_in_direction(direction, cx)
- // .map(|pane| pane.clone())
- // {
- // self.center.swap(&self.active_pane.clone(), &to);
- // cx.notify();
- // }
- // }
+ pub fn swap_pane_in_direction(
+ &mut self,
+ direction: SplitDirection,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(to) = self
+ .find_pane_in_direction(direction, cx)
+ .map(|pane| pane.clone())
+ {
+ self.center.swap(&self.active_pane.clone(), &to);
+ cx.notify();
+ }
+ }
- // fn find_pane_in_direction(
- // &mut self,
- // direction: SplitDirection,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<&View<Pane>> {
- // let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
- // return None;
- // };
- // let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
- // let center = match cursor {
- // Some(cursor) if bounding_box.contains_point(cursor) => cursor,
- // _ => bounding_box.center(),
- // };
+ fn find_pane_in_direction(
+ &mut self,
+ direction: SplitDirection,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<&View<Pane>> {
+ let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+ return None;
+ };
+ let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
+ let center = match cursor {
+ Some(cursor) if bounding_box.contains_point(&cursor) => cursor,
+ _ => bounding_box.center(),
+ };
- // let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
+ let distance_to_next = 1.; //todo(pane dividers styling)
- // let target = match direction {
- // SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
- // SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
- // SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
- // SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
- // };
- // self.center.pane_at_pixel_position(target)
- // }
+ let target = match direction {
+ SplitDirection::Left => {
+ Point::new(bounding_box.origin.x - distance_to_next.into(), center.y)
+ }
+ SplitDirection::Right => {
+ Point::new(bounding_box.right() + distance_to_next.into(), center.y)
+ }
+ SplitDirection::Up => {
+ Point::new(center.x, bounding_box.origin.y - distance_to_next.into())
+ }
+ SplitDirection::Down => {
+ Point::new(center.x, bounding_box.top() + distance_to_next.into())
+ }
+ };
+ self.center.pane_at_pixel_position(target)
+ }
fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
if self.active_pane != pane {
@@ -11,7 +11,7 @@ path = "src/zed2.rs"
doctest = false
[[bin]]
-name = "Zed"
+name = "Zed2"
path = "src/main.rs"
[dependencies]
@@ -27,7 +27,6 @@ collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
collections = { path = "../collections" }
command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
-# context_menu = { path = "../context_menu" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
@@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter {
&self,
cx: &mut AppContext,
) -> BoxFuture<'static, serde_json::Value> {
- let action_names = gpui::all_action_names();
+ let action_names = cx.all_action_names();
let staff_mode = cx.is_staff();
let language_names = &self.languages.language_names();
let settings_schema = cx.global::<SettingsStore>().json_schema(
@@ -140,8 +140,7 @@ fn main() {
cx.set_global(client.clone());
- theme::init(cx);
- // context_menu::init(cx);
+ theme::init(theme::LoadThemes::All, cx);
project::Project::init(&client, cx);
client::init(&client, cx);
command_palette::init(cx);
@@ -1,4 +1,5 @@
-use gpui::action;
+use gpui::Action;
+use serde::Deserialize;
// If the zed binary doesn't use anything in this crate, it will be optimized away
// and the actions won't initialize. So we just provide an empty initialization function
@@ -9,12 +10,12 @@ use gpui::action;
// https://github.com/mmastrac/rust-ctor/issues/280
pub fn init() {}
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
pub struct OpenBrowser {
pub url: String,
}
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
pub struct OpenZedURL {
pub url: String,
}