Compare commits
58 Commits
remove-bin
...
before-pai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2205430814 | ||
|
|
fa410ec150 | ||
|
|
504ea1aaea | ||
|
|
222034cacf | ||
|
|
9863b920b0 | ||
|
|
ea952b2a95 | ||
|
|
dd7eced2b6 | ||
|
|
d4922eb10b | ||
|
|
95827d4c49 | ||
|
|
2602fc47bb | ||
|
|
6d1ea782a4 | ||
|
|
870a61dd4d | ||
|
|
250b71fb44 | ||
|
|
15c4c4a308 | ||
|
|
b31df39ab0 | ||
|
|
98db7fa61e | ||
|
|
bd5473a582 | ||
|
|
1fbc04104c | ||
|
|
2f892e3523 | ||
|
|
5c3e5cc45d | ||
|
|
11a3d2b04b | ||
|
|
d75f1e64b8 | ||
|
|
1358758226 | ||
|
|
9de4dc12dd | ||
|
|
1127b1a0de | ||
|
|
7ae13c9753 | ||
|
|
c55055599a | ||
|
|
d4bae0475f | ||
|
|
72a4e525d7 | ||
|
|
15f4e93102 | ||
|
|
dbfb588e3e | ||
|
|
a202499c9a | ||
|
|
c2428f9f5d | ||
|
|
d5c5394693 | ||
|
|
8ead209099 | ||
|
|
bb97432e9a | ||
|
|
1b75f9d620 | ||
|
|
4c3178e7a8 | ||
|
|
41c8f2caa6 | ||
|
|
b9e0269991 | ||
|
|
4f2214e1d6 | ||
|
|
e25f0dfb0a | ||
|
|
3c805d4c6b | ||
|
|
eb14932950 | ||
|
|
4f1861edb6 | ||
|
|
d3756cdab3 | ||
|
|
d7becce9aa | ||
|
|
62171387f6 | ||
|
|
47ad010901 | ||
|
|
06987edadb | ||
|
|
1e1a2807db | ||
|
|
d4c54f3479 | ||
|
|
9782dd342f | ||
|
|
c024526db2 | ||
|
|
535bcfad10 | ||
|
|
c76bacb974 | ||
|
|
20554d0296 | ||
|
|
2c78cf349b |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -205,6 +205,7 @@ jobs:
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
script/draft-release-notes "$version" "$channel" > target/release-notes.md
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -248,7 +249,7 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body: ""
|
||||
body_file: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -4327,6 +4327,7 @@ dependencies = [
|
||||
"time",
|
||||
"unindent",
|
||||
"url",
|
||||
"windows 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9736,7 +9737,6 @@ dependencies = [
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
@@ -12265,6 +12265,7 @@ dependencies = [
|
||||
"any_vec",
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -12521,7 +12522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.132.0"
|
||||
version = "0.133.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -12698,6 +12699,13 @@ dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_haskell"
|
||||
version = "0.1.0"
|
||||
@@ -12735,7 +12743,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_prisma"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -110,6 +110,7 @@ members = [
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/gleam",
|
||||
"extensions/glsl",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
@@ -259,7 +260,9 @@ futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
|
||||
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
|
||||
"read-txn-no-tls",
|
||||
] }
|
||||
hex = "0.4.3"
|
||||
ignore = "0.4.22"
|
||||
indoc = "1"
|
||||
@@ -364,10 +367,16 @@ sys-locale = "0.3.1"
|
||||
version = "0.53.0"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Media",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
"restore_on_startup": "last_workspace",
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
@@ -212,6 +214,8 @@
|
||||
"scroll_debounce_ms": 50
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the project panel button in the status bar
|
||||
"button": true,
|
||||
// Default width of the project panel.
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/anthropic.rs"
|
||||
|
||||
@@ -17,6 +20,3 @@ util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1094,31 +1094,37 @@ impl AssistantPanel {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.saved_conversations_scroll_handle.clone();
|
||||
let conversation_count = self.saved_conversations.len();
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut saved_conversations = uniform_list(
|
||||
view,
|
||||
"saved_conversations",
|
||||
conversation_count,
|
||||
|this, range, cx| {
|
||||
range
|
||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
saved_conversations.layout(
|
||||
bounds.origin,
|
||||
bounds.size.map(AvailableSpace::Definite),
|
||||
cx,
|
||||
);
|
||||
saved_conversations
|
||||
},
|
||||
|_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
|
||||
)
|
||||
.size_full()
|
||||
.into_any_element()
|
||||
todo!("replace canvas")
|
||||
// canvas(
|
||||
// move |_, cx| {
|
||||
// let saved_conversations = uniform_list(
|
||||
// view.clone(),
|
||||
// "saved_conversations",
|
||||
// conversation_count,
|
||||
// |this, range, cx| {
|
||||
// range
|
||||
// .map(|ix| this.render_saved_conversation(ix, cx))
|
||||
// .collect()
|
||||
// },
|
||||
// )
|
||||
// .track_scroll(scroll_handle.clone())
|
||||
// .into_any_element();
|
||||
// saved_conversations.layout(absolute_offset, available_space, cx)
|
||||
// // compute layout for saved conversations
|
||||
// saved_conversations
|
||||
// },
|
||||
// move |bounds, saved_conversations, cx| {
|
||||
// saved_conversations.layout(
|
||||
// bounds.origin,
|
||||
// bounds.size.map(AvailableSpace::Definite),
|
||||
// cx,
|
||||
// );
|
||||
// saved_conversations
|
||||
// },
|
||||
// |_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
|
||||
// )
|
||||
// .size_full()
|
||||
// .into_any_element()
|
||||
} else if let Some(editor) = self.active_conversation_editor() {
|
||||
let editor = editor.clone();
|
||||
let conversation = editor.read(cx).conversation.clone();
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
tests::{rust_lang, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
|
||||
@@ -735,12 +736,60 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
6..9
|
||||
);
|
||||
rename.editor.update(cx, |rename_editor, cx| {
|
||||
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
||||
assert_eq!(
|
||||
rename_selection.range(),
|
||||
0..3,
|
||||
"Rename that was triggered from zero selection caret, should propose the whole word."
|
||||
);
|
||||
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
||||
rename_buffer.edit([(0..3, "THREE")], None, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.cancel(&editor::actions::Cancel, cx);
|
||||
});
|
||||
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
|
||||
editor.rename(&Rename, cx).unwrap()
|
||||
});
|
||||
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
|
||||
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
|
||||
assert_eq!(params.position, lsp::Position::new(0, 8));
|
||||
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
|
||||
lsp::Position::new(0, 6),
|
||||
lsp::Position::new(0, 9),
|
||||
))))
|
||||
})
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
prepare_rename.await.unwrap();
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
use editor::ToOffset;
|
||||
let rename = editor.pending_rename().unwrap();
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let lsp_rename_start = rename.range.start.to_offset(&buffer);
|
||||
let lsp_rename_end = rename.range.end.to_offset(&buffer);
|
||||
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
|
||||
rename.editor.update(cx, |rename_editor, cx| {
|
||||
let rename_selection = rename_editor.selections.newest::<usize>(cx);
|
||||
assert_eq!(
|
||||
rename_selection.range(),
|
||||
1..2,
|
||||
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
|
||||
);
|
||||
rename_editor.buffer().update(cx, |rename_buffer, cx| {
|
||||
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
|
||||
});
|
||||
@@ -2006,6 +2055,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
let inline_blame_off_settings = Some(InlineBlameSettings {
|
||||
enabled: false,
|
||||
delay_ms: None,
|
||||
min_column: None,
|
||||
});
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
@@ -2040,15 +2090,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
blame_entry("3a3a3a", 2..3),
|
||||
blame_entry("4c4c4c", 3..4),
|
||||
],
|
||||
permalinks: [
|
||||
("1b1b1b", "http://example.com/codehost/idx-0"),
|
||||
("0d0d0d", "http://example.com/codehost/idx-1"),
|
||||
("3a3a3a", "http://example.com/codehost/idx-2"),
|
||||
("4c4c4c", "http://example.com/codehost/idx-3"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(sha, url)| (sha.parse().unwrap(), url.parse().unwrap()))
|
||||
.collect(),
|
||||
permalinks: HashMap::default(), // This field is deprecrated
|
||||
messages: [
|
||||
("1b1b1b", "message for idx-0"),
|
||||
("0d0d0d", "message for idx-1"),
|
||||
@@ -2058,6 +2100,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.into_iter()
|
||||
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
|
||||
.collect(),
|
||||
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
|
||||
};
|
||||
client_a.fs().set_blame_for_repo(
|
||||
Path::new("/my-repo/.git"),
|
||||
@@ -2126,7 +2169,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
assert_eq!(
|
||||
details.permalink.unwrap().to_string(),
|
||||
format!("http://example.com/codehost/idx-{}", idx)
|
||||
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -305,6 +305,10 @@ impl ChannelView {
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
// Emit the edited event on the editor context so that other views can update it's state (e.g. markdown preview)
|
||||
self.editor.update(cx, |_, cx| {
|
||||
cx.emit(EditorEvent::Edited);
|
||||
});
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
|
||||
@@ -2834,34 +2834,31 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
|
||||
let thickness = px(1.);
|
||||
let color = cx.theme().colors().text;
|
||||
|
||||
canvas(
|
||||
|_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
|
||||
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
canvas(move |bounds, cx| {
|
||||
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
|
||||
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
),
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
),
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
},
|
||||
)
|
||||
),
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
})
|
||||
.w(width)
|
||||
.h(line_height)
|
||||
}
|
||||
|
||||
@@ -318,26 +318,23 @@ impl Render for CollabTitlebarItem {
|
||||
}
|
||||
|
||||
fn render_color_ribbon(color: Hsla) -> impl Element {
|
||||
canvas(
|
||||
move |_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let height = bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(height.0 / 2.0);
|
||||
let mut path = Path::new(bounds.lower_left());
|
||||
path.curve_to(
|
||||
bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
|
||||
path.curve_to(
|
||||
bounds.lower_right(),
|
||||
bounds.upper_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.lower_left());
|
||||
cx.paint_path(path, color);
|
||||
},
|
||||
)
|
||||
canvas(move |bounds, cx| {
|
||||
let height = bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(height.0 / 2.0);
|
||||
let mut path = Path::new(bounds.lower_left());
|
||||
path.curve_to(
|
||||
bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
|
||||
path.curve_to(
|
||||
bounds.lower_right(),
|
||||
bounds.upper_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.lower_left());
|
||||
cx.paint_path(path, color);
|
||||
})
|
||||
.h_1()
|
||||
.w_full()
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
}
|
||||
|
||||
async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
|
||||
dbg!(&db_path);
|
||||
log::info!("Opening main db");
|
||||
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
|
||||
@@ -131,10 +131,10 @@ use ui::{
|
||||
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::ItemHandle;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Toast;
|
||||
use workspace::{
|
||||
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
|
||||
@@ -476,8 +476,8 @@ pub struct Editor {
|
||||
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
|
||||
>,
|
||||
>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||
expect_layout_bounds_change: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1492,8 +1492,8 @@ impl Editor {
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
gutter_hovered: false,
|
||||
pixel_position_of_newest_cursor: None,
|
||||
last_bounds: None,
|
||||
expect_bounds_change: None,
|
||||
last_layout_bounds: None,
|
||||
expect_layout_bounds_change: None,
|
||||
gutter_width: Default::default(),
|
||||
style: None,
|
||||
show_cursor_names: false,
|
||||
@@ -1828,6 +1828,29 @@ impl Editor {
|
||||
old_cursor_position: &Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Copy selections to primary selection buffer
|
||||
#[cfg(target_os = "linux")]
|
||||
if local {
|
||||
let selections = self.selections.all::<usize>(cx);
|
||||
let buffer_handle = self.buffer.read(cx).read(cx);
|
||||
|
||||
let mut text = String::new();
|
||||
for (index, selection) in selections.iter().enumerate() {
|
||||
let text_for_selection = buffer_handle
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
|
||||
text.push_str(&text_for_selection);
|
||||
if index != selections.len() - 1 {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
cx.write_to_primary(ClipboardItem::new(text));
|
||||
}
|
||||
}
|
||||
|
||||
if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_active_selections(
|
||||
@@ -4920,6 +4943,25 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
let project = self.project.as_ref()?.read(cx);
|
||||
let entry = project.entry_for_path(&project_path, cx)?;
|
||||
let abs_path = project.absolute_path(&project_path, cx)?;
|
||||
let parent = if entry.is_symlink {
|
||||
abs_path.canonicalize().ok()?
|
||||
} else {
|
||||
abs_path
|
||||
}
|
||||
.parent()?
|
||||
.to_path_buf();
|
||||
Some(parent)
|
||||
}) {
|
||||
cx.dispatch_action(OpenTerminal { working_directory }.boxed_clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
@@ -8052,7 +8094,7 @@ impl Editor {
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(selection.head(), cx)?;
|
||||
let (tail_buffer, _) = self
|
||||
let (tail_buffer, cursor_buffer_position_end) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(selection.tail(), cx)?;
|
||||
@@ -8062,6 +8104,7 @@ impl Editor {
|
||||
|
||||
let snapshot = cursor_buffer.read(cx).snapshot();
|
||||
let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
|
||||
let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot);
|
||||
let prepare_rename = project.update(cx, |project, cx| {
|
||||
project.prepare_rename(cursor_buffer.clone(), cursor_buffer_offset, cx)
|
||||
});
|
||||
@@ -8090,6 +8133,8 @@ impl Editor {
|
||||
let rename_buffer_range = rename_range.to_offset(&snapshot);
|
||||
let cursor_offset_in_rename_range =
|
||||
cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
|
||||
let cursor_offset_in_rename_range_end =
|
||||
cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start);
|
||||
|
||||
this.take_rename(false, cx);
|
||||
let buffer = this.buffer.read(cx).read(cx);
|
||||
@@ -8118,7 +8163,23 @@ impl Editor {
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, old_name.clone())], None, cx)
|
||||
});
|
||||
editor.select_all(&SelectAll, cx);
|
||||
let rename_selection_range = match cursor_offset_in_rename_range
|
||||
.cmp(&cursor_offset_in_rename_range_end)
|
||||
{
|
||||
Ordering::Equal => {
|
||||
editor.select_all(&SelectAll, cx);
|
||||
return editor;
|
||||
}
|
||||
Ordering::Less => {
|
||||
cursor_offset_in_rename_range..cursor_offset_in_rename_range_end
|
||||
}
|
||||
Ordering::Greater => {
|
||||
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
|
||||
}
|
||||
};
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([rename_selection_range]);
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -8946,7 +9007,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool {
|
||||
self.show_git_blame_inline && self.has_blame_entries(cx)
|
||||
self.focus_handle.is_focused(cx) && self.show_git_blame_inline && self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
|
||||
@@ -10717,7 +10778,8 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
|
||||
let icon_size = buttons(&diagnostic, cx.block_id)
|
||||
.into_any_element()
|
||||
.measure(AvailableSpace::min_size(), cx);
|
||||
.layout(AvailableSpace::min_size(), cx)
|
||||
.size;
|
||||
|
||||
h_flex()
|
||||
.id(cx.block_id)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git::{
|
||||
blame::{Blame, BlameEntry},
|
||||
permalink::{build_commit_permalink, parse_git_remote_url},
|
||||
Oid,
|
||||
};
|
||||
use gpui::{Model, ModelContext, Subscription, Task};
|
||||
@@ -63,7 +64,8 @@ pub struct GitBlame {
|
||||
task: Task<Result<()>>,
|
||||
generated: bool,
|
||||
user_triggered: bool,
|
||||
_refresh_subscription: Subscription,
|
||||
regenerate_on_edit_task: Task<Result<()>>,
|
||||
_regenerate_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl GitBlame {
|
||||
@@ -81,7 +83,19 @@ impl GitBlame {
|
||||
&(),
|
||||
);
|
||||
|
||||
let refresh_subscription = cx.subscribe(&project, {
|
||||
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
|
||||
language::Event::DirtyChanged => {
|
||||
if !buffer.read(cx).is_dirty() {
|
||||
this.generate(cx);
|
||||
}
|
||||
}
|
||||
language::Event::Edited => {
|
||||
this.regenerate_on_edit(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let project_subscription = cx.subscribe(&project, {
|
||||
let buffer = buffer.clone();
|
||||
|
||||
move |this, _, event, cx| match event {
|
||||
@@ -116,7 +130,8 @@ impl GitBlame {
|
||||
commit_details: HashMap::default(),
|
||||
task: Task::ready(Ok(())),
|
||||
generated: false,
|
||||
_refresh_subscription: refresh_subscription,
|
||||
regenerate_on_edit_task: Task::ready(Ok(())),
|
||||
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
|
||||
};
|
||||
this.generate(cx);
|
||||
this
|
||||
@@ -272,11 +287,13 @@ impl GitBlame {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
} = blame.await?;
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, &permalinks, &languages).await;
|
||||
parse_commit_messages(messages, remote_url, &permalinks, &languages)
|
||||
.await;
|
||||
|
||||
anyhow::Ok((entries, commit_details))
|
||||
}
|
||||
@@ -310,8 +327,22 @@ impl GitBlame {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn regenerate_on_edit(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.regenerate_on_edit_task = cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.generate(cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree<GitBlameEntry> {
|
||||
let mut current_row = 0;
|
||||
let mut entries = SumTree::from_iter(
|
||||
@@ -351,13 +382,31 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
permalinks: &HashMap<Oid, Url>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
) -> HashMap<Oid, CommitDetails> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
|
||||
|
||||
for (oid, message) in messages {
|
||||
let parsed_message = parse_markdown(&message, &languages).await;
|
||||
let permalink = permalinks.get(&oid).cloned();
|
||||
|
||||
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
|
||||
Some(build_commit_permalink(
|
||||
git::permalink::BuildCommitPermalinkParams {
|
||||
remote: git_remote,
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing. This is here for backwards compatibility, so that
|
||||
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
|
||||
// we fall back to that.
|
||||
deprecated_permalinks.get(&oid).cloned()
|
||||
};
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
|
||||
@@ -1173,7 +1173,7 @@ impl SearchableItem for Editor {
|
||||
}
|
||||
|
||||
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
|
||||
self.expect_bounds_change = self.last_bounds;
|
||||
self.expect_layout_bounds_change = self.last_layout_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
use workspace::OpenInTerminal;
|
||||
|
||||
pub struct MouseContextMenu {
|
||||
pub(crate) position: Point<Pixels>,
|
||||
@@ -83,6 +84,7 @@ pub fn deploy_context_menu(
|
||||
)
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
})
|
||||
};
|
||||
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Editor {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
let original_y = scroll_position.y;
|
||||
if let Some(last_bounds) = self.expect_bounds_change.take() {
|
||||
if let Some(last_bounds) = self.expect_layout_bounds_change.take() {
|
||||
if scroll_position.y != 0. {
|
||||
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
|
||||
if scroll_position.y < 0. {
|
||||
|
||||
@@ -936,24 +936,25 @@ impl Render for ExtensionsPage {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
this.child(
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut list = uniform_list::<_, ExtensionCard, _>(
|
||||
view,
|
||||
"entries",
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.size_full()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
list.layout(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_bounds, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full(),
|
||||
// canvas(
|
||||
// move |bounds, cx| {
|
||||
// let mut list = uniform_list::<_, ExtensionCard, _>(
|
||||
// view,
|
||||
// "entries",
|
||||
// count,
|
||||
// Self::render_extensions,
|
||||
// )
|
||||
// .size_full()
|
||||
// .pb_4()
|
||||
// .track_scroll(scroll_handle)
|
||||
// .into_any_element();
|
||||
// list.layout(bounds.origin, bounds.size.into(), cx);
|
||||
// list
|
||||
// },
|
||||
// |_bounds, mut list, cx| list.paint(cx),
|
||||
// )
|
||||
// .size_full(),
|
||||
todo!("replace canvas"),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ text.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
serde.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -14,6 +14,9 @@ use time::OffsetDateTime;
|
||||
use time::UtcOffset;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub use git2 as libgit;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -21,6 +24,7 @@ pub struct Blame {
|
||||
pub entries: Vec<BlameEntry>,
|
||||
pub messages: HashMap<Oid, String>,
|
||||
pub permalinks: HashMap<Oid, Url>,
|
||||
pub remote_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
@@ -41,6 +45,8 @@ impl Blame {
|
||||
|
||||
for entry in entries.iter_mut() {
|
||||
unique_shas.insert(entry.sha);
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing.
|
||||
if let Some(remote) = parsed_remote_url.as_ref() {
|
||||
permalinks.entry(entry.sha).or_insert_with(|| {
|
||||
build_commit_permalink(BuildCommitPermalinkParams {
|
||||
@@ -59,17 +65,23 @@ impl Blame {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const GIT_BLAME_NO_COMMIT_ERROR: &'static str = "fatal: no such ref: HEAD";
|
||||
const GIT_BLAME_NO_PATH: &'static str = "fatal: no such path";
|
||||
|
||||
fn run_git_blame(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path: &Path,
|
||||
contents: &Rope,
|
||||
) -> Result<String> {
|
||||
let child = Command::new(git_binary)
|
||||
let mut child = Command::new(git_binary);
|
||||
|
||||
child
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
@@ -78,7 +90,12 @@ fn run_git_blame(
|
||||
.arg(path.as_os_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let child = child
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
@@ -98,6 +115,10 @@ fn run_git_blame(
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
return Ok(String::new());
|
||||
}
|
||||
return Err(anyhow!("git blame process failed: {}", stderr));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,25 @@ use collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
const MARKER: &'static str = "<MARKER>";
|
||||
|
||||
let output = Command::new("git")
|
||||
let mut command = Command::new("git");
|
||||
|
||||
command
|
||||
.current_dir(working_directory)
|
||||
.arg("show")
|
||||
.arg("-s")
|
||||
.arg(format!("--format=%B{}", MARKER))
|
||||
.args(shas.iter().map(ToString::to_string))
|
||||
.args(shas.iter().map(ToString::to_string));
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::ops::Range;
|
||||
use anyhow::{anyhow, Result};
|
||||
use url::Url;
|
||||
|
||||
pub(crate) enum GitHostingProvider {
|
||||
pub enum GitHostingProvider {
|
||||
Github,
|
||||
Gitlab,
|
||||
Gitee,
|
||||
@@ -90,18 +90,18 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
|
||||
Ok(permalink)
|
||||
}
|
||||
|
||||
pub(crate) struct ParsedGitRemote<'a> {
|
||||
pub struct ParsedGitRemote<'a> {
|
||||
pub provider: GitHostingProvider,
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
||||
pub(crate) struct BuildCommitPermalinkParams<'a> {
|
||||
pub struct BuildCommitPermalinkParams<'a> {
|
||||
pub remote: &'a ParsedGitRemote<'a>,
|
||||
pub sha: &'a str,
|
||||
}
|
||||
|
||||
pub(crate) fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
|
||||
pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
|
||||
let BuildCommitPermalinkParams { sha, remote } = params;
|
||||
|
||||
let ParsedGitRemote {
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url
|
||||
provider.base_url().join(&path).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
||||
pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
||||
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@github.com:")
|
||||
|
||||
@@ -115,7 +115,7 @@ wayland-protocols = { version = "0.31.2", features = [
|
||||
] }
|
||||
oo7 = "0.3.0"
|
||||
open = "5.1.2"
|
||||
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] }
|
||||
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr", "xinput"] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -547,11 +547,23 @@ impl AppContext {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Writes data to the platform clipboard.
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
/// Reads data from the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
pub fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_primary()
|
||||
}
|
||||
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
|
||||
@@ -734,7 +734,8 @@ impl VisualTestContext {
|
||||
self.update(|cx| {
|
||||
cx.with_element_context(|cx| {
|
||||
let mut element = f(cx);
|
||||
element.layout(origin, space, cx);
|
||||
element.layout(space, cx);
|
||||
cx.with_element_offset(origin, |cx| element.before_paint(cx));
|
||||
element.paint(cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
use crate::{
|
||||
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementContext,
|
||||
ElementId, LayoutId, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
|
||||
ElementId, LayoutId, Pixels, Size, ViewContext, WindowContext, ELEMENT_ARENA,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
@@ -45,33 +45,46 @@ use std::{any::Any, fmt::Debug, mem, ops::DerefMut};
|
||||
/// for more details.
|
||||
pub trait Element: 'static + IntoElement {
|
||||
/// The type of state returned from [`Element::before_layout`]. A mutable reference to this state is subsequently
|
||||
/// provided to [`Element::after_layout`] and [`Element::paint`].
|
||||
/// provided to [`Element::after_layout`], [`Element::before_paint`] and [`Element::paint`].
|
||||
type BeforeLayout: 'static;
|
||||
|
||||
/// The type of state returned from [`Element::after_layout`]. A mutable reference to this state is subsequently
|
||||
/// provided to [`Element::paint`].
|
||||
/// provided to [`Element::before_paint`] and [`Element::paint`].
|
||||
type AfterLayout: 'static;
|
||||
|
||||
/// The type of state returned from [`Element::before_paint`]. A mutable reference to this state is subsequently
|
||||
/// provided to [`Element::paint`].
|
||||
type BeforePaint: 'static;
|
||||
|
||||
/// Before an element can be painted, we need to know where it's going to be and how big it is.
|
||||
/// Use this method to request a layout from Taffy and initialize the element's state.
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout);
|
||||
|
||||
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
|
||||
/// purposes. The state argument is the same state that was returned from [`Element::before_layout()`].
|
||||
/// todo!()
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Self::AfterLayout;
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout);
|
||||
|
||||
/// Before painting an element, we need to commit its bounds to the current frame for hitbox
|
||||
/// purposes.
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Self::BeforePaint;
|
||||
|
||||
/// Once layout has been completed, this method will be called to paint the element to the screen.
|
||||
/// The state argument is the same state that was returned from [`Element::before_layout()`].
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
);
|
||||
|
||||
@@ -163,6 +176,7 @@ impl<C: RenderOnce> Component<C> {
|
||||
impl<C: RenderOnce> Element for Component<C> {
|
||||
type BeforeLayout = AnyElement;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut element = self
|
||||
@@ -176,12 +190,23 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let bounds = element.after_layout(cx);
|
||||
(bounds, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut AnyElement,
|
||||
_: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
element.after_layout(cx);
|
||||
element.before_paint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -189,6 +214,7 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
element.paint(cx)
|
||||
@@ -212,46 +238,59 @@ trait ElementObject {
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId;
|
||||
|
||||
fn after_layout(&mut self, cx: &mut ElementContext);
|
||||
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>>;
|
||||
|
||||
fn before_paint(&mut self, cx: &mut ElementContext);
|
||||
|
||||
fn paint(&mut self, cx: &mut ElementContext);
|
||||
|
||||
fn measure(
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Size<Pixels>;
|
||||
) -> ElementMeasurement;
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||
pub struct Drawable<E: Element> {
|
||||
/// The drawn element.
|
||||
pub element: E,
|
||||
phase: ElementDrawPhase<E::BeforeLayout, E::AfterLayout>,
|
||||
phase: ElementDrawPhase<E::BeforeLayout, E::AfterLayout, E::BeforePaint>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ElementDrawPhase<BeforeLayout, AfterLayout> {
|
||||
enum ElementDrawPhase<BeforeLayout, AfterLayout, BeforePaint> {
|
||||
#[default]
|
||||
Start,
|
||||
BeforeLayout {
|
||||
layout_id: LayoutId,
|
||||
before_layout: BeforeLayout,
|
||||
},
|
||||
LayoutComputed {
|
||||
layout_id: LayoutId,
|
||||
available_space: Size<AvailableSpace>,
|
||||
before_layout: BeforeLayout,
|
||||
},
|
||||
AfterLayout {
|
||||
layout_id: LayoutId,
|
||||
available_space: Option<Size<AvailableSpace>>,
|
||||
before_layout: BeforeLayout,
|
||||
after_layout: AfterLayout,
|
||||
},
|
||||
BeforePaint {
|
||||
node_id: DispatchNodeId,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: BeforeLayout,
|
||||
after_layout: AfterLayout,
|
||||
before_paint: BeforePaint,
|
||||
},
|
||||
Painted,
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
#[derive(Default)]
|
||||
pub struct ElementMeasurement {
|
||||
/// The size of the element.
|
||||
pub size: Size<Pixels>,
|
||||
/// The bounds for the focus target inside of the measured element.
|
||||
pub focus_target_bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||
impl<E: Element> Drawable<E> {
|
||||
fn new(element: E) -> Self {
|
||||
@@ -275,92 +314,133 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, cx: &mut ElementContext) {
|
||||
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::BeforeLayout {
|
||||
layout_id,
|
||||
mut before_layout,
|
||||
}
|
||||
| ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
mut before_layout,
|
||||
..
|
||||
} => {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
let node_id = cx.window.next_frame.dispatch_tree.push_node();
|
||||
let after_layout = self.element.after_layout(bounds, &mut before_layout, cx);
|
||||
let (focus_target_bounds, after_layout) =
|
||||
self.element.after_layout(bounds, &mut before_layout, cx);
|
||||
self.phase = ElementDrawPhase::AfterLayout {
|
||||
node_id,
|
||||
bounds,
|
||||
layout_id,
|
||||
available_space: None,
|
||||
before_layout,
|
||||
after_layout,
|
||||
};
|
||||
cx.window.next_frame.dispatch_tree.pop_node();
|
||||
|
||||
focus_target_bounds
|
||||
}
|
||||
_ => panic!("must call before_layout before after_layout"),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout {
|
||||
fn before_paint(&mut self, cx: &mut ElementContext) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::AfterLayout {
|
||||
node_id,
|
||||
bounds,
|
||||
layout_id,
|
||||
mut before_layout,
|
||||
mut after_layout,
|
||||
..
|
||||
} => {
|
||||
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
|
||||
self.element
|
||||
.paint(bounds, &mut before_layout, &mut after_layout, cx);
|
||||
self.phase = ElementDrawPhase::Painted;
|
||||
before_layout
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
let node_id = cx.window.next_frame.dispatch_tree.push_node();
|
||||
let before_paint =
|
||||
self.element
|
||||
.before_paint(bounds, &mut before_layout, &mut after_layout, cx);
|
||||
self.phase = ElementDrawPhase::BeforePaint {
|
||||
node_id,
|
||||
bounds,
|
||||
before_layout,
|
||||
after_layout,
|
||||
before_paint,
|
||||
};
|
||||
cx.window.next_frame.dispatch_tree.pop_node();
|
||||
}
|
||||
_ => panic!("must call after_layout before paint"),
|
||||
_ => panic!("must call after_layout before before_paint"),
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(
|
||||
fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::BeforePaint {
|
||||
node_id,
|
||||
bounds,
|
||||
mut before_layout,
|
||||
mut after_layout,
|
||||
mut before_paint,
|
||||
..
|
||||
} => {
|
||||
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
|
||||
self.element.paint(
|
||||
bounds,
|
||||
&mut before_layout,
|
||||
&mut after_layout,
|
||||
&mut before_paint,
|
||||
cx,
|
||||
);
|
||||
self.phase = ElementDrawPhase::Painted;
|
||||
before_layout
|
||||
}
|
||||
_ => panic!("must call before_paint before paint"),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Size<Pixels> {
|
||||
) -> ElementMeasurement {
|
||||
if matches!(&self.phase, ElementDrawPhase::Start) {
|
||||
self.before_layout(cx);
|
||||
}
|
||||
|
||||
let layout_id = match mem::take(&mut self.phase) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::BeforeLayout {
|
||||
layout_id,
|
||||
before_layout,
|
||||
mut before_layout,
|
||||
} => {
|
||||
cx.compute_layout(layout_id, available_space);
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
let (focus_target_bounds, after_layout) =
|
||||
self.element.after_layout(bounds, &mut before_layout, cx);
|
||||
self.phase = ElementDrawPhase::AfterLayout {
|
||||
layout_id,
|
||||
available_space,
|
||||
available_space: Some(available_space),
|
||||
before_layout,
|
||||
after_layout,
|
||||
};
|
||||
layout_id
|
||||
ElementMeasurement {
|
||||
size: bounds.size,
|
||||
focus_target_bounds,
|
||||
}
|
||||
}
|
||||
ElementDrawPhase::LayoutComputed {
|
||||
ElementDrawPhase::AfterLayout {
|
||||
layout_id,
|
||||
available_space: prev_available_space,
|
||||
before_layout,
|
||||
mut before_layout,
|
||||
..
|
||||
} => {
|
||||
if available_space != prev_available_space {
|
||||
if Some(available_space) != prev_available_space {
|
||||
cx.compute_layout(layout_id, available_space);
|
||||
}
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
let (focus_target_bounds, after_layout) =
|
||||
self.element.after_layout(bounds, &mut before_layout, cx);
|
||||
self.phase = ElementDrawPhase::AfterLayout {
|
||||
layout_id,
|
||||
available_space,
|
||||
available_space: Some(available_space),
|
||||
before_layout,
|
||||
after_layout,
|
||||
};
|
||||
layout_id
|
||||
ElementMeasurement {
|
||||
size: bounds.size,
|
||||
focus_target_bounds,
|
||||
}
|
||||
}
|
||||
_ => panic!("cannot measure after painting"),
|
||||
};
|
||||
|
||||
cx.layout_bounds(layout_id).size
|
||||
_ => panic!("cannot layout after painting"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,20 +457,24 @@ where
|
||||
Drawable::before_layout(self, cx)
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, cx: &mut ElementContext) {
|
||||
Drawable::after_layout(self, cx);
|
||||
fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
|
||||
Drawable::after_layout(self, cx)
|
||||
}
|
||||
|
||||
fn before_paint(&mut self, cx: &mut ElementContext) {
|
||||
Drawable::before_paint(self, cx);
|
||||
}
|
||||
|
||||
fn paint(&mut self, cx: &mut ElementContext) {
|
||||
Drawable::paint(self, cx);
|
||||
}
|
||||
|
||||
fn measure(
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Size<Pixels> {
|
||||
Drawable::measure(self, available_space, cx)
|
||||
) -> ElementMeasurement {
|
||||
Drawable::layout(self, available_space, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,41 +504,35 @@ impl AnyElement {
|
||||
self.0.before_layout(cx)
|
||||
}
|
||||
|
||||
/// Commits the element bounds of this [AnyElement] for hitbox purposes.
|
||||
pub fn after_layout(&mut self, cx: &mut ElementContext) {
|
||||
/// todo!()
|
||||
pub fn after_layout(&mut self, cx: &mut ElementContext) -> Option<Bounds<Pixels>> {
|
||||
self.0.after_layout(cx)
|
||||
}
|
||||
|
||||
/// Commits the element bounds of this [AnyElement] for hitbox purposes.
|
||||
pub fn before_paint(&mut self, cx: &mut ElementContext) {
|
||||
self.0.before_paint(cx)
|
||||
}
|
||||
|
||||
/// Paints the element stored in this `AnyElement`.
|
||||
pub fn paint(&mut self, cx: &mut ElementContext) {
|
||||
self.0.paint(cx)
|
||||
}
|
||||
|
||||
/// Initializes this element and performs layout within the given available space to determine its size.
|
||||
pub fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Size<Pixels> {
|
||||
self.0.measure(available_space, cx)
|
||||
}
|
||||
|
||||
/// Initializes this element, performs layout if needed and commits its bounds for hitbox purposes.
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
absolute_offset: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Size<Pixels> {
|
||||
let size = self.measure(available_space, cx);
|
||||
cx.with_absolute_element_offset(absolute_offset, |cx| self.after_layout(cx));
|
||||
size
|
||||
) -> ElementMeasurement {
|
||||
self.0.layout(available_space, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AnyElement {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let layout_id = self.before_layout(cx);
|
||||
@@ -466,8 +544,19 @@ impl Element for AnyElement {
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let bounds = self.after_layout(cx);
|
||||
(bounds, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
self.after_layout(cx)
|
||||
self.before_paint(cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -475,6 +564,7 @@ impl Element for AnyElement {
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
self.paint(cx)
|
||||
@@ -507,6 +597,7 @@ impl IntoElement for Empty {
|
||||
impl Element for Empty {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
(cx.request_layout(&crate::Style::default(), None), ())
|
||||
@@ -515,7 +606,17 @@ impl Element for Empty {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::BeforeLayout,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
@@ -525,6 +626,7 @@ impl Element for Empty {
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_before_paint: &mut Self::BeforePaint,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ impl ParentElement for Anchored {
|
||||
impl Element for Anchored {
|
||||
type BeforeLayout = AnchoredState;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) {
|
||||
let child_layout_ids = self
|
||||
@@ -91,9 +92,27 @@ impl Element for Anchored {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let mut focus_target_bounds = None;
|
||||
for child in &mut self.children {
|
||||
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
|
||||
if focus_target_bounds.is_none() {
|
||||
focus_target_bounds = Some(child_focus_target_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
(focus_target_bounds, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
if before_layout.child_layout_ids.is_empty() {
|
||||
@@ -167,7 +186,7 @@ impl Element for Anchored {
|
||||
|
||||
cx.with_element_offset(offset, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.after_layout(cx);
|
||||
child.before_paint(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -177,6 +196,7 @@ impl Element for Anchored {
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
for child in &mut self.children {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{AnyElement, Element, ElementId, IntoElement};
|
||||
use crate::{AnyElement, Bounds, Element, ElementId, IntoElement, Pixels};
|
||||
|
||||
pub use easing::*;
|
||||
|
||||
@@ -86,8 +86,8 @@ struct AnimationState {
|
||||
|
||||
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
type BeforeLayout = AnyElement;
|
||||
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(
|
||||
&mut self,
|
||||
@@ -136,18 +136,29 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
cx: &mut crate::ElementContext,
|
||||
) -> Self::AfterLayout {
|
||||
element.after_layout(cx);
|
||||
) -> (Option<Bounds<Pixels>>, ()) {
|
||||
(element.after_layout(cx), ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut crate::ElementContext,
|
||||
) -> Self::BeforePaint {
|
||||
element.before_paint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut crate::ElementContext,
|
||||
) {
|
||||
element.paint(cx);
|
||||
|
||||
@@ -4,12 +4,8 @@ use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRe
|
||||
|
||||
/// Construct a canvas element with the given paint callback.
|
||||
/// Useful for adding short term custom drawing to a view.
|
||||
pub fn canvas<T>(
|
||||
after_layout: impl 'static + FnOnce(Bounds<Pixels>, &mut ElementContext) -> T,
|
||||
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut ElementContext),
|
||||
) -> Canvas<T> {
|
||||
pub fn canvas(paint: impl 'static + FnOnce(Bounds<Pixels>, &mut ElementContext)) -> Canvas {
|
||||
Canvas {
|
||||
after_layout: Some(Box::new(after_layout)),
|
||||
paint: Some(Box::new(paint)),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
@@ -17,13 +13,12 @@ pub fn canvas<T>(
|
||||
|
||||
/// A canvas element, meant for accessing the low level paint API without defining a whole
|
||||
/// custom element
|
||||
pub struct Canvas<T> {
|
||||
after_layout: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut ElementContext) -> T>>,
|
||||
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut ElementContext)>>,
|
||||
pub struct Canvas {
|
||||
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut ElementContext)>>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl<T: 'static> IntoElement for Canvas<T> {
|
||||
impl IntoElement for Canvas {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
@@ -31,9 +26,10 @@ impl<T: 'static> IntoElement for Canvas<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Element for Canvas<T> {
|
||||
impl Element for Canvas {
|
||||
type BeforeLayout = Style;
|
||||
type AfterLayout = Option<T>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) {
|
||||
let mut style = Style::default();
|
||||
@@ -44,28 +40,35 @@ impl<T: 'static> Element for Canvas<T> {
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Style,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<T> {
|
||||
Some(self.after_layout.take().unwrap()(bounds, cx))
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
style: &mut Style,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let after_layout = after_layout.take().unwrap();
|
||||
style.paint(bounds, cx, |cx| {
|
||||
(self.paint.take().unwrap())(bounds, after_layout, cx)
|
||||
});
|
||||
style.paint(bounds, cx, |cx| (self.paint.take().unwrap())(bounds, cx));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Styled for Canvas<T> {
|
||||
impl Styled for Canvas {
|
||||
fn style(&mut self) -> &mut crate::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ impl Deferred {
|
||||
impl Element for Deferred {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) {
|
||||
let layout_id = self.child.as_mut().unwrap().before_layout(cx);
|
||||
@@ -39,6 +40,17 @@ impl Element for Deferred {
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let bounds = self.child.as_mut().unwrap().after_layout(cx);
|
||||
(bounds, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let child = self.child.take().unwrap();
|
||||
let element_offset = cx.element_offset();
|
||||
@@ -50,6 +62,7 @@ impl Element for Deferred {
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_before_paint: &mut Self::BeforePaint,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -1121,7 +1121,8 @@ impl ParentElement for Div {
|
||||
|
||||
impl Element for Div {
|
||||
type BeforeLayout = DivFrameState;
|
||||
type AfterLayout = Option<Hitbox>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Option<Hitbox>;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut child_layout_ids = SmallVec::new();
|
||||
@@ -1143,7 +1144,7 @@ impl Element for Div {
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||
let mut child_max = Point::default();
|
||||
let content_size = if before_layout.child_layout_ids.is_empty() {
|
||||
@@ -1177,25 +1178,49 @@ impl Element for Div {
|
||||
(child_max - child_min).into()
|
||||
};
|
||||
|
||||
self.interactivity.after_layout(
|
||||
let focus_target_bounds = self.interactivity.after_layout(
|
||||
bounds,
|
||||
content_size,
|
||||
cx,
|
||||
|_style, scroll_offset, hitbox, cx| {
|
||||
|_, scroll_offset, mut focus_target_bounds, cx| {
|
||||
cx.with_element_offset(scroll_offset, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.after_layout(cx);
|
||||
if let Some(child_focus_bounds) = child.after_layout(cx) {
|
||||
focus_target_bounds = Some(child_focus_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
focus_target_bounds
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
(focus_target_bounds, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.before_paint(bounds, cx, |_style, scroll_offset, hitbox, cx| {
|
||||
cx.with_element_offset(scroll_offset, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.before_paint(cx);
|
||||
}
|
||||
});
|
||||
hitbox
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
@@ -1336,13 +1361,13 @@ impl Interactivity {
|
||||
)
|
||||
}
|
||||
|
||||
/// Commit the bounds of this element according to this interactivity state's configured styles.
|
||||
/// todo!()
|
||||
pub fn after_layout<R>(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
content_size: Size<Pixels>,
|
||||
cx: &mut ElementContext,
|
||||
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut ElementContext) -> R,
|
||||
f: impl FnOnce(&Style, Point<Pixels>, Option<Bounds<Pixels>>, &mut ElementContext) -> R,
|
||||
) -> R {
|
||||
self.content_size = content_size;
|
||||
cx.with_element_state::<InteractiveElementState, _>(
|
||||
@@ -1367,6 +1392,40 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
|
||||
let scroll_offset = self.clamp_scroll_position(bounds, &style, cx);
|
||||
let focus_target_bounds =
|
||||
self.tracked_focus_handle.as_ref().and_then(|focus_handle| {
|
||||
if focus_handle.is_focused(cx) {
|
||||
Some(bounds)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let result = f(&style, scroll_offset, focus_target_bounds, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Commit the bounds of this element according to this interactivity state's configured styles.
|
||||
pub fn before_paint<R>(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut ElementContext,
|
||||
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut ElementContext) -> R,
|
||||
) -> R {
|
||||
cx.with_element_state::<InteractiveElementState, _>(
|
||||
self.element_id.clone(),
|
||||
|element_state, cx| {
|
||||
let mut element_state =
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
let style = self.compute_style_internal(None, element_state.as_mut(), cx);
|
||||
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
|
||||
let hitbox = if self.should_insert_hitbox(&style) {
|
||||
@@ -1375,7 +1434,11 @@ impl Interactivity {
|
||||
None
|
||||
};
|
||||
|
||||
let scroll_offset = self.clamp_scroll_position(bounds, &style, cx);
|
||||
let scroll_offset = self
|
||||
.scroll_offset
|
||||
.as_ref()
|
||||
.map(|scroll_offset| *scroll_offset.borrow())
|
||||
.unwrap_or_default();
|
||||
let result = f(&style, scroll_offset, hitbox, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
@@ -2263,6 +2326,7 @@ where
|
||||
{
|
||||
type BeforeLayout = E::BeforeLayout;
|
||||
type AfterLayout = E::AfterLayout;
|
||||
type BeforePaint = E::BeforePaint;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
self.element.before_layout(cx)
|
||||
@@ -2271,10 +2335,21 @@ where
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::BeforeLayout,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> E::AfterLayout {
|
||||
self.element.after_layout(bounds, state, cx)
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
self.element.after_layout(bounds, before_layout, cx)
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> E::BeforePaint {
|
||||
self.element
|
||||
.before_paint(bounds, before_layout, after_layout, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -2282,9 +2357,11 @@ where
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
self.element.paint(bounds, before_layout, after_layout, cx)
|
||||
self.element
|
||||
.paint(bounds, before_layout, after_layout, before_paint, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2346,6 +2423,7 @@ where
|
||||
{
|
||||
type BeforeLayout = E::BeforeLayout;
|
||||
type AfterLayout = E::AfterLayout;
|
||||
type BeforePaint = E::BeforePaint;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
self.element.before_layout(cx)
|
||||
@@ -2354,10 +2432,21 @@ where
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::BeforeLayout,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> E::AfterLayout {
|
||||
self.element.after_layout(bounds, state, cx)
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
self.element.after_layout(bounds, before_layout, cx)
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> E::BeforePaint {
|
||||
self.element
|
||||
.before_paint(bounds, before_layout, after_layout, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -2365,9 +2454,11 @@ where
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
self.element.paint(bounds, before_layout, after_layout, cx);
|
||||
self.element
|
||||
.paint(bounds, before_layout, after_layout, before_paint, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,8 @@ impl Img {
|
||||
|
||||
impl Element for Img {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = Option<Hitbox>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Option<Hitbox>;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
|
||||
@@ -261,16 +262,29 @@ impl Element for Img {
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
self.interactivity
|
||||
.after_layout(bounds, bounds.size, cx, |_, _, _, _| {});
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
.before_paint(bounds, cx, |_, _, hitbox, _| hitbox)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
hitbox: &mut Self::AfterLayout,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let source = self.source.clone();
|
||||
|
||||
@@ -89,17 +89,28 @@ pub enum ListSizingBehavior {
|
||||
Auto,
|
||||
}
|
||||
|
||||
struct LayoutItemsResponse {
|
||||
/// Layout information computed by the [List] element during `after_layout`.
|
||||
pub struct ListLayout {
|
||||
max_item_width: Pixels,
|
||||
scroll_top: ListOffset,
|
||||
available_item_space: Size<AvailableSpace>,
|
||||
item_elements: VecDeque<AnyElement>,
|
||||
items: VecDeque<MeasuredItem>,
|
||||
focus_target: Option<FocusTarget>,
|
||||
}
|
||||
|
||||
/// Frame state used by the [List] element after layout.
|
||||
pub struct ListAfterLayoutState {
|
||||
struct MeasuredItem {
|
||||
element: AnyElement,
|
||||
size: Size<Pixels>,
|
||||
}
|
||||
|
||||
struct FocusTarget {
|
||||
item_index: usize,
|
||||
bounds_in_item: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
/// Frame state used by the [List] element during paint.
|
||||
pub struct ListBeforePaintState {
|
||||
hitbox: Hitbox,
|
||||
layout: LayoutItemsResponse,
|
||||
layout: ListLayout,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -376,13 +387,14 @@ impl StateInner {
|
||||
available_height: Pixels,
|
||||
padding: &Edges<Pixels>,
|
||||
cx: &mut ElementContext,
|
||||
) -> LayoutItemsResponse {
|
||||
) -> ListLayout {
|
||||
let old_items = self.items.clone();
|
||||
let mut measured_items = VecDeque::new();
|
||||
let mut item_elements = VecDeque::new();
|
||||
let mut items = VecDeque::new();
|
||||
let mut rendered_height = padding.top;
|
||||
let mut max_item_width = px(0.);
|
||||
let mut scroll_top = self.logical_scroll_top();
|
||||
let mut focus_target = None;
|
||||
|
||||
let available_item_space = size(
|
||||
available_width.map_or(AvailableSpace::MinContent, |width| {
|
||||
@@ -411,10 +423,20 @@ impl StateInner {
|
||||
// If we're within the visible area or the height wasn't cached, render and measure the item's element
|
||||
if visible_height < available_height || size.is_none() {
|
||||
let mut element = (self.render_item)(scroll_top.item_ix + ix, cx);
|
||||
let element_size = element.measure(available_item_space, cx);
|
||||
size = Some(element_size);
|
||||
let element_measurement = element.layout(available_item_space, cx);
|
||||
size = Some(element_measurement.size);
|
||||
if visible_height < available_height {
|
||||
item_elements.push_back(element);
|
||||
if let Some(focus_target_bounds) = element_measurement.focus_target_bounds {
|
||||
focus_target = Some(FocusTarget {
|
||||
item_index: items.len(),
|
||||
bounds_in_item: focus_target_bounds,
|
||||
});
|
||||
}
|
||||
|
||||
items.push_back(MeasuredItem {
|
||||
element,
|
||||
size: element_measurement.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,11 +457,26 @@ impl StateInner {
|
||||
cursor.prev(&());
|
||||
if cursor.item().is_some() {
|
||||
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||
let element_size = element.measure(available_item_space, cx);
|
||||
let element_measurement = element.layout(available_item_space, cx);
|
||||
|
||||
rendered_height += element_size.height;
|
||||
measured_items.push_front(ListItem::Rendered { size: element_size });
|
||||
item_elements.push_front(element)
|
||||
rendered_height += element_measurement.size.height;
|
||||
measured_items.push_front(ListItem::Rendered {
|
||||
size: element_measurement.size,
|
||||
});
|
||||
|
||||
items.push_front(MeasuredItem {
|
||||
element,
|
||||
size: element_measurement.size,
|
||||
});
|
||||
|
||||
if let Some(focus_target_bounds) = element_measurement.focus_target_bounds {
|
||||
focus_target = Some(FocusTarget {
|
||||
item_index: 0,
|
||||
bounds_in_item: focus_target_bounds,
|
||||
});
|
||||
} else if let Some(focus_target) = focus_target.as_mut() {
|
||||
focus_target.item_index += 1;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -474,7 +511,7 @@ impl StateInner {
|
||||
*size
|
||||
} else {
|
||||
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||
element.measure(available_item_space, cx)
|
||||
element.layout(available_item_space, cx).size
|
||||
};
|
||||
|
||||
leading_overdraw += size.height;
|
||||
@@ -493,11 +530,11 @@ impl StateInner {
|
||||
|
||||
self.items = new_items;
|
||||
|
||||
LayoutItemsResponse {
|
||||
ListLayout {
|
||||
max_item_width,
|
||||
scroll_top,
|
||||
available_item_space,
|
||||
item_elements,
|
||||
items,
|
||||
focus_target,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,7 +560,8 @@ pub struct ListOffset {
|
||||
|
||||
impl Element for List {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = ListAfterLayoutState;
|
||||
type AfterLayout = Option<ListLayout>;
|
||||
type BeforePaint = ListBeforePaintState;
|
||||
|
||||
fn before_layout(
|
||||
&mut self,
|
||||
@@ -592,17 +630,15 @@ impl Element for List {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> ListAfterLayoutState {
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
state.reset = false;
|
||||
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
|
||||
// If the width of the list has changed, invalidate all cached item heights
|
||||
if state.last_layout_bounds.map_or(true, |last_bounds| {
|
||||
last_bounds.size.width != bounds.size.width
|
||||
@@ -614,48 +650,77 @@ impl Element for List {
|
||||
}
|
||||
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
let mut layout_response =
|
||||
state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
|
||||
let layout = state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
|
||||
let focus_target_bounds = layout.focus_target.as_ref().map(|focus_target| {
|
||||
let mut item_origin =
|
||||
bounds.origin + Point::new(px(0.), padding.top - layout.scroll_top.offset_in_item);
|
||||
item_origin.y -= layout.scroll_top.offset_in_item;
|
||||
for item in layout.items.iter().take(focus_target.item_index) {
|
||||
item_origin.y += item.size.height;
|
||||
}
|
||||
Bounds::new(
|
||||
item_origin + focus_target.bounds_in_item.origin,
|
||||
focus_target.bounds_in_item.size,
|
||||
)
|
||||
});
|
||||
(focus_target_bounds, Some(layout))
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> ListBeforePaintState {
|
||||
let mut layout = layout.take().unwrap();
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
state.reset = false;
|
||||
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
|
||||
// Only paint the visible items, if there is actually any space for them (taking padding into account)
|
||||
if bounds.size.height > padding.top + padding.bottom {
|
||||
// Paint the visible items
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
|
||||
item_origin.y -= layout_response.scroll_top.offset_in_item;
|
||||
for mut item_element in &mut layout_response.item_elements {
|
||||
let item_size = item_element.measure(layout_response.available_item_space, cx);
|
||||
item_element.layout(item_origin, layout_response.available_item_space, cx);
|
||||
item_origin.y += item_size.height;
|
||||
item_origin.y -= layout.scroll_top.offset_in_item;
|
||||
for item in &mut layout.items {
|
||||
cx.with_absolute_element_offset(item_origin, |cx| {
|
||||
item.element.before_paint(cx)
|
||||
});
|
||||
item_origin.y += item.size.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.last_layout_bounds = Some(bounds);
|
||||
state.last_padding = Some(padding);
|
||||
ListAfterLayoutState {
|
||||
hitbox,
|
||||
layout: layout_response,
|
||||
}
|
||||
ListBeforePaintState { hitbox, layout }
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
after_layout: &mut Self::AfterLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
before_paint: &mut Self::BeforePaint,
|
||||
cx: &mut crate::ElementContext,
|
||||
) {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
for item in &mut after_layout.layout.item_elements {
|
||||
item.paint(cx);
|
||||
for item in &mut before_paint.layout.items {
|
||||
item.element.paint(cx);
|
||||
}
|
||||
});
|
||||
|
||||
let list_state = self.state.clone();
|
||||
let height = bounds.size.height;
|
||||
let scroll_top = after_layout.layout.scroll_top;
|
||||
let hitbox_id = after_layout.hitbox.id;
|
||||
let scroll_top = before_paint.layout.scroll_top;
|
||||
let hitbox_id = before_paint.hitbox.id;
|
||||
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) {
|
||||
list_state.0.borrow_mut().scroll(
|
||||
|
||||
@@ -38,7 +38,8 @@ impl Svg {
|
||||
|
||||
impl Element for Svg {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = Option<Hitbox>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Option<Hitbox>;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let layout_id = self
|
||||
@@ -52,15 +53,28 @@ impl Element for Svg {
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
self.interactivity
|
||||
.after_layout(bounds, bounds.size, cx, |_, _, _, _| {});
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
.before_paint(bounds, cx, |_, _, hitbox, _| hitbox)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
cx: &mut ElementContext,
|
||||
) where
|
||||
|
||||
@@ -19,6 +19,7 @@ use util::ResultExt;
|
||||
impl Element for &'static str {
|
||||
type BeforeLayout = TextState;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut state = TextState::default();
|
||||
@@ -27,9 +28,19 @@ impl Element for &'static str {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
@@ -38,6 +49,7 @@ impl Element for &'static str {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut TextState,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut (),
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
@@ -64,6 +76,7 @@ impl IntoElement for String {
|
||||
impl Element for SharedString {
|
||||
type BeforeLayout = TextState;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut state = TextState::default();
|
||||
@@ -72,9 +85,19 @@ impl Element for SharedString {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
@@ -83,7 +106,8 @@ impl Element for SharedString {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let text_str: &str = self.as_ref();
|
||||
@@ -150,6 +174,7 @@ impl StyledText {
|
||||
impl Element for StyledText {
|
||||
type BeforeLayout = TextState;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut state = TextState::default();
|
||||
@@ -158,9 +183,19 @@ impl Element for StyledText {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) {
|
||||
}
|
||||
@@ -170,6 +205,7 @@ impl Element for StyledText {
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut (),
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
text_state.paint(bounds, &self.text, cx)
|
||||
@@ -403,16 +439,27 @@ impl InteractiveText {
|
||||
|
||||
impl Element for InteractiveText {
|
||||
type BeforeLayout = TextState;
|
||||
type AfterLayout = Hitbox;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Hitbox;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
self.text.before_layout(cx)
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
_cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(None, ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Hitbox {
|
||||
cx.with_element_state::<InteractiveTextState, _>(
|
||||
@@ -430,7 +477,7 @@ impl Element for InteractiveText {
|
||||
}
|
||||
}
|
||||
|
||||
self.text.after_layout(bounds, state, cx);
|
||||
self.text.before_paint(bounds, state, &mut (), cx);
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
(hitbox, interactive_state)
|
||||
},
|
||||
@@ -441,6 +488,7 @@ impl Element for InteractiveText {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Hitbox,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
@@ -577,7 +625,7 @@ impl Element for InteractiveText {
|
||||
});
|
||||
}
|
||||
|
||||
self.text.paint(bounds, text_state, &mut (), cx);
|
||||
self.text.paint(bounds, text_state, &mut (), &mut (), cx);
|
||||
|
||||
((), Some(interactive_state))
|
||||
},
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext,
|
||||
ElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render,
|
||||
ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
ElementId, ElementMeasurement, Hitbox, InteractiveElement, Interactivity, IntoElement,
|
||||
LayoutId, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -70,8 +71,9 @@ pub struct UniformList {
|
||||
|
||||
/// Frame state used by the [UniformList].
|
||||
pub struct UniformListFrameState {
|
||||
item_size: Size<Pixels>,
|
||||
item_height: Pixels,
|
||||
items: SmallVec<[AnyElement; 32]>,
|
||||
visible_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// A handle for controlling the scroll position of a uniform list.
|
||||
@@ -105,19 +107,22 @@ impl Styled for UniformList {
|
||||
|
||||
impl Element for UniformList {
|
||||
type BeforeLayout = UniformListFrameState;
|
||||
type AfterLayout = Option<Hitbox>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Option<Hitbox>;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let max_items = self.item_count;
|
||||
let item_size = self.measure_item(None, cx);
|
||||
let item_measurement = self.measure_item(None, cx);
|
||||
let layout_id = self.interactivity.before_layout(cx, |style, cx| {
|
||||
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let desired_height = item_measurement.size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_measurement.size.width
|
||||
}
|
||||
});
|
||||
|
||||
let height = match available_space.height {
|
||||
@@ -131,8 +136,9 @@ impl Element for UniformList {
|
||||
(
|
||||
layout_id,
|
||||
UniformListFrameState {
|
||||
item_size,
|
||||
item_height: Pixels::default(),
|
||||
items: SmallVec::new(),
|
||||
visible_range: 0..0,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -140,28 +146,26 @@ impl Element for UniformList {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
state: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let style = self.interactivity.compute_style(None, cx);
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||
bounds.lower_right()
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
let padded_size = size(
|
||||
bounds.size.width - border.left - padding.left - border.right - padding.right,
|
||||
bounds.size.height - border.top - padding.top - border.bottom - padding.bottom,
|
||||
);
|
||||
|
||||
let item_height = self.measure_item(Some(padded_size.width), cx).size.height;
|
||||
let content_size = Size {
|
||||
width: padded_bounds.size.width,
|
||||
height: before_layout.item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
width: padded_size.width,
|
||||
height: item_height * self.item_count + padding.top + padding.bottom,
|
||||
};
|
||||
|
||||
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
||||
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
let shared_scroll_to_item = self
|
||||
// todo!("add support for scrolling to the focused element?");
|
||||
let deferred_scroll_to_item = self
|
||||
.scroll_handle
|
||||
.as_mut()
|
||||
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
||||
@@ -170,73 +174,103 @@ impl Element for UniformList {
|
||||
bounds,
|
||||
content_size,
|
||||
cx,
|
||||
|style, mut scroll_offset, hitbox, cx| {
|
||||
|style, mut scroll_offset, mut focus_target_bounds, cx| {
|
||||
if self.item_count == 0 {
|
||||
return (focus_target_bounds, ());
|
||||
}
|
||||
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let padded_size = size(
|
||||
bounds.size.width - border.left - padding.left - border.right - padding.right,
|
||||
bounds.size.height - border.top - padding.top - border.bottom - padding.bottom,
|
||||
);
|
||||
|
||||
let content_height = item_height * self.item_count + padding.top + padding.bottom;
|
||||
let min_scroll_offset = padded_size.height - content_height;
|
||||
let is_scrolled = scroll_offset.y != px(0.);
|
||||
|
||||
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
}
|
||||
|
||||
if let Some(ix) = deferred_scroll_to_item {
|
||||
let list_height = padded_size.height;
|
||||
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||
let item_top = item_height * ix + padding.top;
|
||||
let item_bottom = item_top + item_height;
|
||||
let scroll_top = -updated_scroll_offset.y;
|
||||
if item_top < scroll_top + padding.top {
|
||||
updated_scroll_offset.y = -(item_top) + padding.top;
|
||||
} else if item_bottom > scroll_top + list_height - padding.bottom {
|
||||
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
|
||||
}
|
||||
scroll_offset = *updated_scroll_offset;
|
||||
}
|
||||
|
||||
let first_visible_element_ix =
|
||||
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
|
||||
let last_visible_element_ix =
|
||||
((-scroll_offset.y + padded_size.height) / item_height).ceil() as usize;
|
||||
let visible_range =
|
||||
first_visible_element_ix..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
for mut item in items {
|
||||
let measurement = item.layout(available_space, cx);
|
||||
if measurement.focus_target_bounds.is_some() {
|
||||
focus_target_bounds = measurement.focus_target_bounds;
|
||||
}
|
||||
state.items.push(item);
|
||||
}
|
||||
state.item_height = item_height;
|
||||
state.visible_range = visible_range;
|
||||
|
||||
(focus_target_bounds, ())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.before_paint(bounds, cx, |style, scroll_offset, hitbox, cx| {
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top),
|
||||
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||
);
|
||||
|
||||
if self.item_count > 0 {
|
||||
let content_height =
|
||||
item_height * self.item_count + padding.top + padding.bottom;
|
||||
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||
let is_scrolled = scroll_offset.y != px(0.);
|
||||
|
||||
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in state.items.iter_mut().zip(state.visible_range.clone()) {
|
||||
let item_y = state.item_height * ix + scroll_offset.y + padding.top;
|
||||
let item_origin = padded_bounds.origin + point(px(0.), item_y);
|
||||
cx.with_absolute_element_offset(item_origin, |cx| item.before_paint(cx));
|
||||
}
|
||||
|
||||
if let Some(ix) = shared_scroll_to_item {
|
||||
let list_height = padded_bounds.size.height;
|
||||
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||
let item_top = item_height * ix + padding.top;
|
||||
let item_bottom = item_top + item_height;
|
||||
let scroll_top = -updated_scroll_offset.y;
|
||||
if item_top < scroll_top + padding.top {
|
||||
updated_scroll_offset.y = -(item_top) + padding.top;
|
||||
} else if item_bottom > scroll_top + list_height - padding.bottom {
|
||||
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
|
||||
}
|
||||
scroll_offset = *updated_scroll_offset;
|
||||
}
|
||||
|
||||
let first_visible_element_ix =
|
||||
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
|
||||
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
|
||||
/ item_height)
|
||||
.ceil() as usize;
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (mut item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y + padding.top);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.layout(item_origin, available_space, cx);
|
||||
before_layout.items.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
hitbox
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
@@ -264,9 +298,13 @@ impl UniformList {
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut ElementContext) -> Size<Pixels> {
|
||||
fn measure_item(
|
||||
&self,
|
||||
list_width: Option<Pixels>,
|
||||
cx: &mut ElementContext,
|
||||
) -> ElementMeasurement {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
return ElementMeasurement::default();
|
||||
}
|
||||
|
||||
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||
@@ -278,7 +316,7 @@ impl UniformList {
|
||||
}),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
item_to_measure.measure(available_space, cx)
|
||||
item_to_measure.layout(available_space, cx)
|
||||
}
|
||||
|
||||
/// Track and render scroll state of this list with reference to the given scroll handle.
|
||||
|
||||
@@ -151,7 +151,9 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
|
||||
@@ -79,8 +79,14 @@ impl LinuxClient for HeadlessClient {
|
||||
//todo(linux)
|
||||
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ pub trait LinuxClient {
|
||||
options: WindowParams,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn run(&self);
|
||||
}
|
||||
@@ -406,7 +408,6 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
})
|
||||
}
|
||||
|
||||
//todo(linux): add trait methods for accessing the primary selection
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
||||
let url = url.to_string();
|
||||
self.background_executor().spawn(async move {
|
||||
@@ -461,10 +462,18 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.write_to_primary(item)
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.read_from_primary()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.read_from_clipboard()
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ pub(crate) struct WaylandClientState {
|
||||
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
|
||||
cursor_icon_name: String,
|
||||
cursor: Cursor,
|
||||
clipboard: Clipboard,
|
||||
primary: Primary,
|
||||
clipboard: Option<Clipboard>,
|
||||
primary: Option<Primary>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
}
|
||||
@@ -163,6 +163,12 @@ impl WaylandClientStatePtr {
|
||||
state.mouse_focused_window = Some(window);
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
// Drop the clipboard to prevent a seg fault after we've closed all Wayland connections.
|
||||
state.clipboard = None;
|
||||
state.primary = None;
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +284,8 @@ impl WaylandClient {
|
||||
cursor_icon_name: "arrow".to_string(),
|
||||
enter_token: None,
|
||||
cursor,
|
||||
clipboard,
|
||||
primary,
|
||||
clipboard: Some(clipboard),
|
||||
primary: Some(primary),
|
||||
event_loop: Some(event_loop),
|
||||
}));
|
||||
|
||||
@@ -377,14 +383,44 @@ impl LinuxClient for WaylandClient {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text);
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get_contents()
|
||||
.ok()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get_contents()
|
||||
.ok()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
|
||||
@@ -68,6 +68,7 @@ unsafe impl HasRawDisplayHandle for RawWindow {
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashSet<ObjectId>,
|
||||
@@ -90,11 +91,13 @@ pub struct WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
impl WaylandWindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
client: WaylandClientStatePtr,
|
||||
globals: Globals,
|
||||
options: WindowParams,
|
||||
@@ -132,6 +135,7 @@ impl WaylandWindowState {
|
||||
Self {
|
||||
xdg_surface,
|
||||
surface,
|
||||
decoration,
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
@@ -158,16 +162,27 @@ impl Drop for WaylandWindow {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
let surface_id = state.surface.id();
|
||||
let client = state.client.clone();
|
||||
|
||||
state.renderer.destroy();
|
||||
if let Some(decoration) = &state.decoration {
|
||||
decoration.destroy();
|
||||
}
|
||||
state.toplevel.destroy();
|
||||
if let Some(viewport) = &state.viewport {
|
||||
viewport.destroy();
|
||||
}
|
||||
state.xdg_surface.destroy();
|
||||
state.surface.destroy();
|
||||
|
||||
let state_ptr = self.0.clone();
|
||||
state.globals.executor.spawn(async move {
|
||||
state_ptr.close();
|
||||
client.drop_window(&surface_id)
|
||||
});
|
||||
state
|
||||
.globals
|
||||
.executor
|
||||
.spawn(async move {
|
||||
state_ptr.close();
|
||||
client.drop_window(&surface_id)
|
||||
})
|
||||
.detach();
|
||||
drop(state);
|
||||
}
|
||||
}
|
||||
@@ -197,13 +212,18 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
if let Some(decoration_manager) = globals.decoration_manager.as_ref() {
|
||||
let decoration =
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id());
|
||||
|
||||
// Request client side decorations if possible
|
||||
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
|
||||
}
|
||||
let decoration = globals
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
let decoration = decoration_manager.get_toplevel_decoration(
|
||||
&toplevel,
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
);
|
||||
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
|
||||
decoration
|
||||
});
|
||||
|
||||
let viewport = globals
|
||||
.viewporter
|
||||
@@ -216,8 +236,9 @@ impl WaylandWindow {
|
||||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
viewport,
|
||||
toplevel,
|
||||
decoration,
|
||||
viewport,
|
||||
client,
|
||||
globals,
|
||||
params,
|
||||
@@ -319,7 +340,7 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
result
|
||||
} else {
|
||||
false
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
@@ -481,13 +502,12 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.input_handler = Some(input_handler);
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::ConnectionExt as _;
|
||||
use x11rb::protocol::{randr, xkb, xproto, Event};
|
||||
use x11rb::protocol::{randr, xinput, xkb, xproto, Event};
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
@@ -63,6 +64,10 @@ pub struct X11ClientState {
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
|
||||
pub(crate) scroll_devices: Vec<xinput::DeviceInfo>,
|
||||
pub(crate) scroll_x: Option<f32>,
|
||||
pub(crate) scroll_y: Option<f32>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
|
||||
pub(crate) primary: X11ClipboardContext<Primary>,
|
||||
@@ -92,6 +97,19 @@ impl X11Client {
|
||||
xcb_connection
|
||||
.prefetch_extension_information(randr::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
xcb_connection
|
||||
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
|
||||
let xinput_version = xcb_connection
|
||||
.xinput_xi_query_version(2, 0)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
assert!(
|
||||
xinput_version.major_version >= 2,
|
||||
"XInput Extension v2 not supported."
|
||||
);
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
|
||||
let xkb = xcb_connection
|
||||
@@ -125,6 +143,46 @@ impl X11Client {
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
|
||||
};
|
||||
|
||||
let device_list = xcb_connection
|
||||
.xinput_list_input_devices()
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let scroll_devices = device_list
|
||||
.devices
|
||||
.iter()
|
||||
.scan(0, |class_info_idx, device_info| {
|
||||
Some(*class_info_idx + device_info.num_class_info as usize)
|
||||
})
|
||||
.zip(device_list.devices.iter())
|
||||
.map(|(class_info_idx, device_info)| {
|
||||
(
|
||||
device_info,
|
||||
device_list.infos
|
||||
[(class_info_idx - device_info.num_class_info as usize)..(class_info_idx)]
|
||||
.to_vec(),
|
||||
)
|
||||
})
|
||||
.filter(|(device_info, class_info)| {
|
||||
device_info.device_use == xinput::DeviceUse::IS_X_EXTENSION_POINTER
|
||||
&& class_info.iter().any(|class_info| match class_info.info {
|
||||
xinput::InputInfoInfo::Valuator(xinput::InputInfoInfoValuator {
|
||||
mode,
|
||||
..
|
||||
}) => mode == xinput::ValuatorMode::RELATIVE,
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.map(|(device_info, _)| *device_info)
|
||||
.collect::<Vec<_>>();
|
||||
for device in &scroll_devices {
|
||||
xcb_connection
|
||||
.xinput_open_device(device.device_id)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
|
||||
let primary = X11ClipboardContext::<Primary>::new().unwrap();
|
||||
|
||||
@@ -166,6 +224,11 @@ impl X11Client {
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
xkb: xkb_state,
|
||||
|
||||
scroll_devices,
|
||||
scroll_x: None,
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
primary,
|
||||
})))
|
||||
@@ -314,22 +377,58 @@ impl X11Client {
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
} else if event.detail >= 4 && event.detail <= 5 {
|
||||
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
|
||||
let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
|
||||
let scroll_y = SCROLL_LINES * scroll_direction;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
}));
|
||||
} else {
|
||||
log::warn!("Unknown button press: {event:?}");
|
||||
}
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
|
||||
let position = Point::new(
|
||||
(event.event_x as f32 / u16::MAX as f32).into(),
|
||||
(event.event_y as f32 / u16::MAX as f32).into(),
|
||||
);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
.iter()
|
||||
.map(|axisvalue| {
|
||||
axisvalue.integral as f32 + axisvalue.frac as f32 / u32::MAX as f32
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if event.valuator_mask[0] & 4 == 4 {
|
||||
let new_scroll = axisvalues[0];
|
||||
let old_scroll = self.0.borrow().scroll_x;
|
||||
self.0.borrow_mut().scroll_x = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = (old_scroll - new_scroll).into();
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Pixels(Point::new(delta_scroll, 0.0.into())),
|
||||
modifiers: crate::Modifiers::none(),
|
||||
touch_phase: TouchPhase::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if event.valuator_mask[0] & 8 == 8 {
|
||||
let new_scroll = axisvalues[0] / 2.0;
|
||||
let old_scroll = self.0.borrow().scroll_y;
|
||||
self.0.borrow_mut().scroll_y = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = (old_scroll - new_scroll).into();
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Pixels(Point::new(0.0.into(), delta_scroll)),
|
||||
modifiers: crate::Modifiers::none(),
|
||||
touch_phase: TouchPhase::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::ButtonRelease(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
@@ -429,6 +528,7 @@ impl LinuxClient for X11Client {
|
||||
state.x_root_index,
|
||||
x_window,
|
||||
&state.atoms,
|
||||
&state.scroll_devices,
|
||||
);
|
||||
|
||||
let screen_resources = state
|
||||
@@ -503,10 +603,26 @@ impl LinuxClient for X11Client {
|
||||
//todo(linux)
|
||||
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().primary.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.primary
|
||||
.get_contents()
|
||||
.ok()
|
||||
.map(|text| crate::ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
|
||||
@@ -13,7 +13,10 @@ use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
protocol::{
|
||||
xinput,
|
||||
xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
},
|
||||
wrapper::ConnectionExt,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
@@ -140,6 +143,7 @@ impl X11WindowState {
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
scroll_devices: &Vec<xinput::DeviceInfo>,
|
||||
) -> Self {
|
||||
let x_screen_index = params
|
||||
.display_id
|
||||
@@ -160,8 +164,6 @@ impl X11WindowState {
|
||||
| xproto::EventMask::BUTTON1_MOTION
|
||||
| xproto::EventMask::BUTTON2_MOTION
|
||||
| xproto::EventMask::BUTTON3_MOTION
|
||||
| xproto::EventMask::BUTTON4_MOTION
|
||||
| xproto::EventMask::BUTTON5_MOTION
|
||||
| xproto::EventMask::BUTTON_MOTION,
|
||||
);
|
||||
|
||||
@@ -181,6 +183,20 @@ impl X11WindowState {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for device in scroll_devices {
|
||||
xinput::ConnectionExt::xinput_xi_select_events(
|
||||
&xcb_connection,
|
||||
x_window,
|
||||
&[xinput::EventMask {
|
||||
deviceid: device.device_id as u16,
|
||||
mask: vec![xinput::XIEventMask::MOTION],
|
||||
}],
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if let Some(titlebar) = params.titlebar {
|
||||
if let Some(title) = titlebar.title {
|
||||
xcb_connection
|
||||
@@ -262,6 +278,7 @@ impl X11Window {
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
scroll_devices: &Vec<xinput::DeviceInfo>,
|
||||
) -> Self {
|
||||
X11Window {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
@@ -270,6 +287,7 @@ impl X11Window {
|
||||
x_main_screen_index,
|
||||
x_window,
|
||||
atoms,
|
||||
scroll_devices,
|
||||
))),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
|
||||
@@ -849,6 +849,8 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, _item: ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
unsafe {
|
||||
@@ -886,6 +888,10 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
unsafe {
|
||||
|
||||
@@ -337,7 +337,6 @@ struct MacWindowState {
|
||||
handle: AnyWindowHandle,
|
||||
executor: ForegroundExecutor,
|
||||
native_window: id,
|
||||
native_window_was_closed: bool,
|
||||
native_view: NonNull<Object>,
|
||||
display_link: Option<DisplayLink>,
|
||||
renderer: renderer::Renderer,
|
||||
@@ -605,6 +604,10 @@ impl MacWindow {
|
||||
registerForDraggedTypes:
|
||||
NSArray::arrayWithObject(nil, NSFilenamesPboardType)
|
||||
];
|
||||
let () = msg_send![
|
||||
native_window,
|
||||
setReleasedWhenClosed: NO
|
||||
];
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
@@ -622,7 +625,6 @@ impl MacWindow {
|
||||
handle,
|
||||
executor,
|
||||
native_window,
|
||||
native_window_was_closed: false,
|
||||
native_view: NonNull::new_unchecked(native_view),
|
||||
display_link: None,
|
||||
renderer: renderer::new_renderer(
|
||||
@@ -770,19 +772,17 @@ impl Drop for MacWindow {
|
||||
this.renderer.destroy();
|
||||
let window = this.native_window;
|
||||
this.display_link.take();
|
||||
if !this.native_window_was_closed {
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
}
|
||||
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
window.close();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
}
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
window.close();
|
||||
window.autorelease();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,7 +1592,6 @@ extern "C" fn close_window(this: &Object, _: Sel) {
|
||||
let close_callback = {
|
||||
let window_state = get_window_state(this);
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
lock.native_window_was_closed = true;
|
||||
lock.close_callback.take()
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ pub(crate) struct TestPlatform {
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
current_primary_item: Mutex<Option<ClipboardItem>>,
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
weak: Weak<Self>,
|
||||
@@ -44,6 +45,7 @@ impl TestPlatform {
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
active_window: Default::default(),
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
current_primary_item: Mutex::new(None),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
})
|
||||
@@ -125,8 +127,11 @@ impl Platform for TestPlatform {
|
||||
#[cfg(target_os = "macos")]
|
||||
return Arc::new(crate::platform::mac::MacTextSystem::new());
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
return Arc::new(crate::platform::cosmic_text::CosmicTextSystem::new());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return Arc::new(crate::platform::windows::DirectWriteTextSystem::new().unwrap());
|
||||
}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
@@ -279,10 +284,18 @@ impl Platform for TestPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.current_clipboard_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.current_primary_item.lock().clone()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod direct_write;
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod platform;
|
||||
mod util;
|
||||
mod window;
|
||||
|
||||
pub(crate) use direct_write::*;
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use platform::*;
|
||||
|
||||
1280
crates/gpui/src/platform/windows/direct_write.rs
Normal file
1280
crates/gpui/src/platform/windows/direct_write.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ pub(crate) struct WindowsPlatformInner {
|
||||
background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
text_system: Arc<CosmicTextSystem>,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
callbacks: Mutex<Callbacks>,
|
||||
pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||
pub(crate) dispatch_event: OwnedHandle,
|
||||
@@ -155,7 +155,13 @@ impl WindowsPlatform {
|
||||
let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event.to_raw()));
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let text_system = Arc::new(CosmicTextSystem::new());
|
||||
let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() {
|
||||
log::info!("Using direct write text system.");
|
||||
Arc::new(direct_write) as Arc<dyn PlatformTextSystem>
|
||||
} else {
|
||||
log::info!("Using cosmic text system.");
|
||||
Arc::new(CosmicTextSystem::new()) as Arc<dyn PlatformTextSystem>
|
||||
};
|
||||
let callbacks = Mutex::new(Callbacks::default());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let settings = RefCell::new(WindowsPlatformSystemSettings::new());
|
||||
@@ -686,6 +692,8 @@ impl Platform for WindowsPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, _item: ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
if item.text.len() > 0 {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
@@ -693,6 +701,10 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
let content = ctx.get_contents().unwrap();
|
||||
|
||||
@@ -284,7 +284,6 @@ impl WindowsWindowInner {
|
||||
WM_CHAR => self.handle_char_msg(msg, wparam, lparam),
|
||||
WM_IME_STARTCOMPOSITION => self.handle_ime_position(),
|
||||
WM_IME_COMPOSITION => self.handle_ime_composition(lparam),
|
||||
WM_IME_CHAR => self.handle_ime_char(wparam),
|
||||
WM_SETCURSOR => self.handle_set_cursor(lparam),
|
||||
_ => None,
|
||||
};
|
||||
@@ -782,7 +781,6 @@ impl WindowsWindowInner {
|
||||
let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
|
||||
let result = if string_len >= 0 {
|
||||
let mut buffer = vec![0u8; string_len as usize + 2];
|
||||
// let mut buffer = [0u8; MAX_PATH as _];
|
||||
ImmGetCompositionStringW(
|
||||
ctx,
|
||||
GCS_COMPSTR,
|
||||
@@ -812,6 +810,32 @@ impl WindowsWindowInner {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ime_compostion_result(&self) -> Option<String> {
|
||||
unsafe {
|
||||
let ctx = ImmGetContext(self.hwnd);
|
||||
let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0);
|
||||
let result = if string_len >= 0 {
|
||||
let mut buffer = vec![0u8; string_len as usize + 2];
|
||||
ImmGetCompositionStringW(
|
||||
ctx,
|
||||
GCS_RESULTSTR,
|
||||
Some(buffer.as_mut_ptr() as _),
|
||||
string_len as _,
|
||||
);
|
||||
let wstring = std::slice::from_raw_parts::<u16>(
|
||||
buffer.as_mut_ptr().cast::<u16>(),
|
||||
string_len as usize / 2,
|
||||
);
|
||||
let string = String::from_utf16_lossy(wstring);
|
||||
Some(string)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ImmReleaseContext(self.hwnd, ctx);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
|
||||
let mut ime_input = None;
|
||||
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
|
||||
@@ -840,31 +864,22 @@ impl WindowsWindowInner {
|
||||
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
|
||||
self.input_handler.set(Some(input_handler));
|
||||
}
|
||||
if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
|
||||
let Some(comp_result) = self.parse_ime_compostion_result() else {
|
||||
return None;
|
||||
};
|
||||
let Some(mut input_handler) = self.input_handler.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
input_handler.replace_text_in_range(None, &comp_result);
|
||||
self.input_handler.set(Some(input_handler));
|
||||
self.invalidate_client_area();
|
||||
return Some(0);
|
||||
}
|
||||
// currently, we don't care other stuff
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_ime_char(&self, wparam: WPARAM) -> Option<String> {
|
||||
let src = [wparam.0 as u16];
|
||||
let Ok(first_char) = char::decode_utf16(src).collect::<Vec<_>>()[0] else {
|
||||
return None;
|
||||
};
|
||||
Some(first_char.to_string())
|
||||
}
|
||||
|
||||
fn handle_ime_char(&self, wparam: WPARAM) -> Option<isize> {
|
||||
let Some(ime_char) = self.parse_ime_char(wparam) else {
|
||||
return Some(1);
|
||||
};
|
||||
let Some(mut input_handler) = self.input_handler.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
input_handler.replace_text_in_range(None, &ime_char);
|
||||
self.input_handler.set(Some(input_handler));
|
||||
self.invalidate_client_area();
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn handle_drag_drop(&self, input: PlatformInput) {
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
let Some(ref mut func) = callbacks.input else {
|
||||
|
||||
@@ -26,6 +26,29 @@ macro_rules! create_definitions {
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
/// Get the tag name list of the font OpenType features
|
||||
/// only enabled or disabled features are returned
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn tag_value_list(&self) -> Vec<(String, bool)> {
|
||||
let mut result = Vec::new();
|
||||
$(
|
||||
{
|
||||
let value = if (self.enabled & (1 << $idx)) != 0 {
|
||||
Some(true)
|
||||
} else if (self.disabled & (1 << $idx)) != 0 {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(enable) = value {
|
||||
let tag_name = stringify!($name).to_owned();
|
||||
result.push((tag_name, enable));
|
||||
}
|
||||
}
|
||||
)*
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FontFeatures {
|
||||
@@ -94,9 +117,11 @@ macro_rules! create_definitions {
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
|
||||
$(
|
||||
let feature = stringify!($name);
|
||||
if let Some(value) = self.$name() {
|
||||
map.serialize_entry(feature, &value)?;
|
||||
{
|
||||
let feature = stringify!($name);
|
||||
if let Some(value) = self.$name() {
|
||||
map.serialize_entry(feature, &value)?;
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
@@ -161,5 +186,5 @@ create_definitions!(
|
||||
(swsh, 30),
|
||||
(titl, 31),
|
||||
(tnum, 32),
|
||||
(zero, 33)
|
||||
(zero, 33),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds,
|
||||
ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||
FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render, Style,
|
||||
StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel,
|
||||
seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext,
|
||||
BeforePaintIndex, Bounds, ContentMask, Element, ElementContext, ElementId, Entity, EntityId,
|
||||
Flatten, FocusHandle, FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render,
|
||||
Style, StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use refineable::Refineable;
|
||||
@@ -24,13 +24,16 @@ impl<V> Sealed for View<V> {}
|
||||
|
||||
struct AnyViewState {
|
||||
after_layout_range: Range<AfterLayoutIndex>,
|
||||
before_paint_range: Range<BeforePaintIndex>,
|
||||
paint_range: Range<PaintIndex>,
|
||||
focus_target_bounds: Option<Bounds<Pixels>>,
|
||||
cache_key: ViewCacheKey,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ViewCacheKey {
|
||||
bounds: Bounds<Pixels>,
|
||||
after_layout_bounds: Bounds<Pixels>,
|
||||
before_paint_bounds: Bounds<Pixels>,
|
||||
content_mask: ContentMask<Pixels>,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
@@ -92,6 +95,7 @@ impl<V: 'static> View<V> {
|
||||
impl<V: Render> Element for View<V> {
|
||||
type BeforeLayout = AnyElement;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
@@ -102,14 +106,24 @@ impl<V: Render> Element for View<V> {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(element.after_layout(cx), ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
cx.set_view_id(self.entity_id());
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
element.after_layout(cx)
|
||||
element.before_paint(cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,6 +132,7 @@ impl<V: Render> Element for View<V> {
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
@@ -277,7 +292,8 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
|
||||
impl Element for AnyView {
|
||||
type BeforeLayout = Option<AnyElement>;
|
||||
type AfterLayout = Option<AnyElement>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
if let Some(style) = self.cached_style.as_ref() {
|
||||
@@ -299,20 +315,16 @@ impl Element for AnyView {
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<AnyElement> {
|
||||
cx.set_view_id(self.entity_id());
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
if self.cached_style.is_some() {
|
||||
cx.with_element_state::<AnyViewState, _>(
|
||||
let focus_target_bounds = cx.with_element_state::<AnyViewState, _>(
|
||||
Some(ElementId::View(self.entity_id())),
|
||||
|element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
||||
let content_mask = cx.content_mask();
|
||||
let text_style = cx.text_style();
|
||||
|
||||
if let Some(mut element_state) = element_state {
|
||||
if element_state.cache_key.bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
if element_state.cache_key.after_layout_bounds == bounds
|
||||
&& element_state.cache_key.text_style == text_style
|
||||
&& !cx.window.dirty_views.contains(&self.entity_id())
|
||||
&& !cx.window.refreshing
|
||||
@@ -321,34 +333,106 @@ impl Element for AnyView {
|
||||
cx.reuse_after_layout(element_state.after_layout_range.clone());
|
||||
let after_layout_end = cx.after_layout_index();
|
||||
element_state.after_layout_range = after_layout_start..after_layout_end;
|
||||
return (None, Some(element_state));
|
||||
return (element_state.focus_target_bounds, Some(element_state));
|
||||
}
|
||||
}
|
||||
|
||||
let after_layout_start = cx.after_layout_index();
|
||||
let mut element = (self.render)(self, cx);
|
||||
element.layout(bounds.origin, bounds.size.into(), cx);
|
||||
let mut rendered_element = (self.render)(self, cx);
|
||||
let element_measurement = rendered_element.layout(bounds.size.into(), cx);
|
||||
*element = Some(rendered_element);
|
||||
let focus_target_bounds =
|
||||
element_measurement
|
||||
.focus_target_bounds
|
||||
.map(|focus_target_bounds| {
|
||||
Bounds::new(
|
||||
bounds.origin + focus_target_bounds.origin,
|
||||
focus_target_bounds.size,
|
||||
)
|
||||
});
|
||||
let after_layout_end = cx.after_layout_index();
|
||||
|
||||
(
|
||||
Some(element),
|
||||
Some(AnyViewState {
|
||||
after_layout_range: after_layout_start..after_layout_end,
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
cache_key: ViewCacheKey {
|
||||
bounds,
|
||||
content_mask,
|
||||
text_style,
|
||||
},
|
||||
}),
|
||||
)
|
||||
let view_state = AnyViewState {
|
||||
after_layout_range: after_layout_start..after_layout_end,
|
||||
before_paint_range: BeforePaintIndex::default()
|
||||
..BeforePaintIndex::default(),
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
focus_target_bounds,
|
||||
cache_key: ViewCacheKey {
|
||||
after_layout_bounds: bounds,
|
||||
text_style,
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
(focus_target_bounds, Some(view_state))
|
||||
},
|
||||
);
|
||||
(focus_target_bounds, ())
|
||||
} else {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
let bounds = element.as_mut().unwrap().after_layout(cx);
|
||||
(bounds, ())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
cx.set_view_id(self.entity_id());
|
||||
if self.cached_style.is_some() {
|
||||
cx.with_element_state::<AnyViewState, _>(
|
||||
Some(ElementId::View(self.entity_id())),
|
||||
|element_state, cx| {
|
||||
let mut element_state = element_state.unwrap().unwrap();
|
||||
let content_mask = cx.content_mask();
|
||||
|
||||
if let Some(element) = element {
|
||||
let before_paint_start = cx.before_paint_index();
|
||||
cx.with_absolute_element_offset(bounds.origin, |cx| {
|
||||
element.before_paint(cx)
|
||||
});
|
||||
let before_paint_end = cx.before_paint_index();
|
||||
element_state.before_paint_range = before_paint_start..before_paint_end;
|
||||
element_state.cache_key.before_paint_bounds = bounds;
|
||||
element_state.cache_key.content_mask = content_mask;
|
||||
} else if element_state.cache_key.before_paint_bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
{
|
||||
let before_paint_start = cx.before_paint_index();
|
||||
cx.reuse_before_paint(element_state.before_paint_range.clone());
|
||||
let before_paint_end = cx.before_paint_index();
|
||||
element_state.before_paint_range = before_paint_start..before_paint_end;
|
||||
} else {
|
||||
let mut rendered_element = (self.render)(self, cx);
|
||||
let after_layout_start = cx.after_layout_index();
|
||||
rendered_element.layout(bounds.size.into(), cx);
|
||||
let after_layout_end = cx.after_layout_index();
|
||||
|
||||
let before_paint_start = cx.before_paint_index();
|
||||
cx.with_absolute_element_offset(bounds.origin, |cx| {
|
||||
rendered_element.before_paint(cx)
|
||||
});
|
||||
let before_paint_end = cx.before_paint_index();
|
||||
element_state.after_layout_range = after_layout_start..after_layout_end;
|
||||
element_state.before_paint_range = before_paint_start..before_paint_end;
|
||||
element_state.cache_key.before_paint_bounds = bounds;
|
||||
element_state.cache_key.content_mask = content_mask;
|
||||
|
||||
*element = Some(rendered_element);
|
||||
}
|
||||
|
||||
((), Some(element_state))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
let mut element = element.take().unwrap();
|
||||
element.after_layout(cx);
|
||||
Some(element)
|
||||
element.as_mut().unwrap().before_paint(cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -356,8 +440,9 @@ impl Element for AnyView {
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
element: &mut Self::AfterLayout,
|
||||
element: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
if self.cached_style.is_some() {
|
||||
|
||||
@@ -121,7 +121,7 @@ pub(crate) struct DeferredDraw {
|
||||
text_style_stack: Vec<TextStyleRefinement>,
|
||||
element: Option<AnyElement>,
|
||||
absolute_offset: Point<Pixels>,
|
||||
layout_range: Range<AfterLayoutIndex>,
|
||||
before_paint_range: Range<BeforePaintIndex>,
|
||||
paint_range: Range<PaintIndex>,
|
||||
}
|
||||
|
||||
@@ -146,6 +146,12 @@ pub(crate) struct Frame {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct AfterLayoutIndex {
|
||||
accessed_element_states_index: usize,
|
||||
line_layout_index: LineLayoutIndex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct BeforePaintIndex {
|
||||
hitboxes_index: usize,
|
||||
tooltips_index: usize,
|
||||
deferred_draws_index: usize,
|
||||
@@ -399,7 +405,8 @@ impl<'a> ElementContext<'a> {
|
||||
|
||||
// Layout all root elements.
|
||||
let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any();
|
||||
root_element.layout(Point::default(), self.window.viewport_size.into(), self);
|
||||
root_element.layout(self.window.viewport_size.into(), self);
|
||||
root_element.before_paint(self);
|
||||
|
||||
let mut sorted_deferred_draws =
|
||||
(0..self.window.next_frame.deferred_draws.len()).collect::<SmallVec<[_; 8]>>();
|
||||
@@ -411,13 +418,15 @@ impl<'a> ElementContext<'a> {
|
||||
let mut tooltip_element = None;
|
||||
if let Some(prompt) = self.window.prompt.take() {
|
||||
let mut element = prompt.view.any_view().into_any();
|
||||
element.layout(Point::default(), self.window.viewport_size.into(), self);
|
||||
element.layout(self.window.viewport_size.into(), self);
|
||||
element.before_paint(self);
|
||||
prompt_element = Some(element);
|
||||
self.window.prompt = Some(prompt);
|
||||
} else if let Some(active_drag) = self.app.active_drag.take() {
|
||||
let mut element = active_drag.view.clone().into_any();
|
||||
element.layout(AvailableSpace::min_size(), self);
|
||||
let offset = self.mouse_position() - active_drag.cursor_offset;
|
||||
element.layout(offset, AvailableSpace::min_size(), self);
|
||||
self.with_element_offset(offset, |cx| element.before_paint(cx));
|
||||
active_drag_element = Some(element);
|
||||
self.app.active_drag = Some(active_drag);
|
||||
} else {
|
||||
@@ -446,7 +455,7 @@ impl<'a> ElementContext<'a> {
|
||||
let tooltip_request = tooltip_request.unwrap();
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any();
|
||||
let mouse_position = tooltip_request.tooltip.mouse_position;
|
||||
let tooltip_size = element.measure(AvailableSpace::min_size(), self);
|
||||
let tooltip_size = element.layout(AvailableSpace::min_size(), self).size;
|
||||
|
||||
let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
|
||||
let window_bounds = Bounds {
|
||||
@@ -478,7 +487,7 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx));
|
||||
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.before_paint(cx));
|
||||
|
||||
self.window.tooltip_bounds = Some(TooltipBounds {
|
||||
id: tooltip_request.id,
|
||||
@@ -500,16 +509,16 @@ impl<'a> ElementContext<'a> {
|
||||
.dispatch_tree
|
||||
.set_active_node(deferred_draw.parent_node);
|
||||
|
||||
let layout_start = self.after_layout_index();
|
||||
let layout_start = self.before_paint_index();
|
||||
if let Some(element) = deferred_draw.element.as_mut() {
|
||||
self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| {
|
||||
element.after_layout(cx)
|
||||
element.before_paint(cx)
|
||||
});
|
||||
} else {
|
||||
self.reuse_after_layout(deferred_draw.layout_range.clone());
|
||||
self.reuse_before_paint(deferred_draw.before_paint_range.clone());
|
||||
}
|
||||
let layout_end = self.after_layout_index();
|
||||
deferred_draw.layout_range = layout_start..layout_end;
|
||||
let layout_end = self.before_paint_index();
|
||||
deferred_draw.before_paint_range = layout_start..layout_end;
|
||||
}
|
||||
assert_eq!(
|
||||
self.window.next_frame.deferred_draws.len(),
|
||||
@@ -548,6 +557,26 @@ impl<'a> ElementContext<'a> {
|
||||
|
||||
pub(crate) fn after_layout_index(&self) -> AfterLayoutIndex {
|
||||
AfterLayoutIndex {
|
||||
accessed_element_states_index: self.window.next_frame.accessed_element_states.len(),
|
||||
line_layout_index: self.window.text_system.layout_index(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reuse_after_layout(&mut self, range: Range<AfterLayoutIndex>) {
|
||||
let window = &mut self.window;
|
||||
window.next_frame.accessed_element_states.extend(
|
||||
window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
|
||||
..range.end.accessed_element_states_index]
|
||||
.iter()
|
||||
.cloned(),
|
||||
);
|
||||
window
|
||||
.text_system
|
||||
.reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);
|
||||
}
|
||||
|
||||
pub(crate) fn before_paint_index(&self) -> BeforePaintIndex {
|
||||
BeforePaintIndex {
|
||||
hitboxes_index: self.window.next_frame.hitboxes.len(),
|
||||
tooltips_index: self.window.next_frame.tooltip_requests.len(),
|
||||
deferred_draws_index: self.window.next_frame.deferred_draws.len(),
|
||||
@@ -557,7 +586,7 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reuse_after_layout(&mut self, range: Range<AfterLayoutIndex>) {
|
||||
pub(crate) fn reuse_before_paint(&mut self, range: Range<BeforePaintIndex>) {
|
||||
let window = &mut self.window;
|
||||
window.next_frame.hitboxes.extend(
|
||||
window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index]
|
||||
@@ -595,7 +624,7 @@ impl<'a> ElementContext<'a> {
|
||||
priority: deferred_draw.priority,
|
||||
element: None,
|
||||
absolute_offset: deferred_draw.absolute_offset,
|
||||
layout_range: deferred_draw.layout_range.clone(),
|
||||
before_paint_range: deferred_draw.before_paint_range.clone(),
|
||||
paint_range: deferred_draw.paint_range.clone(),
|
||||
}),
|
||||
);
|
||||
@@ -974,7 +1003,7 @@ impl<'a> ElementContext<'a> {
|
||||
assert_eq!(
|
||||
window.draw_phase,
|
||||
DrawPhase::Layout,
|
||||
"defer_draw can only be called during before_layout or after_layout"
|
||||
"defer_draw can only be called during before_layout or before_paint"
|
||||
);
|
||||
let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap();
|
||||
window.next_frame.deferred_draws.push(DeferredDraw {
|
||||
@@ -984,7 +1013,7 @@ impl<'a> ElementContext<'a> {
|
||||
priority,
|
||||
element: Some(element),
|
||||
absolute_offset,
|
||||
layout_range: AfterLayoutIndex::default()..AfterLayoutIndex::default(),
|
||||
before_paint_range: BeforePaintIndex::default()..BeforePaintIndex::default(),
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
});
|
||||
}
|
||||
@@ -1397,7 +1426,7 @@ impl<'a> ElementContext<'a> {
|
||||
bounds
|
||||
}
|
||||
|
||||
/// This method should be called during `after_layout`. You can use
|
||||
/// This method should be called during `before_paint`. You can use
|
||||
/// the returned [Hitbox] during `paint` or in an event handler
|
||||
/// to determine whether the inserted hitbox was the topmost.
|
||||
pub fn insert_hitbox(&mut self, bounds: Bounds<Pixels>, opaque: bool) -> Hitbox {
|
||||
|
||||
@@ -155,7 +155,7 @@ impl FocusableView for ImageView {
|
||||
|
||||
impl Render for ImageView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut ElementContext| {
|
||||
let checkered_background = |bounds: Bounds<Pixels>, cx: &mut ElementContext| {
|
||||
let square_size = 32.0;
|
||||
|
||||
let start_y = bounds.origin.y.0;
|
||||
@@ -190,7 +190,7 @@ impl Render for ImageView {
|
||||
}
|
||||
};
|
||||
|
||||
let checkered_background = canvas(|_, _| (), checkered_background)
|
||||
let checkered_background = canvas(checkered_background)
|
||||
.border_2()
|
||||
.border_color(cx.theme().styles.colors.border)
|
||||
.size_full()
|
||||
|
||||
@@ -15,9 +15,9 @@ pub trait ContextProvider: Send + Sync {
|
||||
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
|
||||
fn build_context(
|
||||
&self,
|
||||
_: Option<&Path>,
|
||||
_: &Location,
|
||||
_: &mut AppContext,
|
||||
_worktree_abs_path: Option<&Path>,
|
||||
_location: &Location,
|
||||
_cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
Ok(TaskVariables::default())
|
||||
}
|
||||
|
||||
@@ -363,14 +363,15 @@ impl Render for SyntaxTreeView {
|
||||
.text_bg(cx.theme().colors().background).into_any_element();
|
||||
|
||||
rendered = rendered.child(
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
list.layout(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full(),
|
||||
// canvas(
|
||||
// move |bounds, cx| {
|
||||
// list.layout(bounds.origin, bounds.size.into(), cx);
|
||||
// list
|
||||
// },
|
||||
// |_, mut list, cx| list.paint(cx),
|
||||
// )
|
||||
// .size_full(),
|
||||
todo!("replace canvas"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,13 +85,7 @@ pub fn init(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
move || {
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
move || Ok((config.clone(), load_queries($name), None)),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr) => {
|
||||
@@ -105,13 +99,7 @@ pub fn init(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
move || {
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
move || Ok((config.clone(), load_queries($name), None)),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr, $context_provider:expr) => {
|
||||
|
||||
@@ -10727,6 +10727,7 @@ fn serialize_blame_buffer_response(blame: git::blame::Blame) -> proto::BlameBuff
|
||||
entries,
|
||||
messages,
|
||||
permalinks,
|
||||
remote_url: blame.remote_url,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10775,6 +10776,7 @@ fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> gi
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url: response.remote_url,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,10 @@ pub struct InlineBlameSettings {
|
||||
///
|
||||
/// Default: 0
|
||||
pub delay_ms: Option<u64>,
|
||||
/// The minimum column number to show the inline blame information at
|
||||
///
|
||||
/// Default: 0
|
||||
pub min_column: Option<u32>,
|
||||
}
|
||||
|
||||
const fn true_value() -> bool {
|
||||
|
||||
@@ -37,7 +37,7 @@ use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::DetachAndPromptErr,
|
||||
Workspace,
|
||||
OpenInTerminal, Workspace,
|
||||
};
|
||||
|
||||
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
||||
@@ -127,7 +127,6 @@ actions!(
|
||||
CopyPath,
|
||||
CopyRelativePath,
|
||||
RevealInFinder,
|
||||
OpenInTerminal,
|
||||
Cut,
|
||||
Paste,
|
||||
Rename,
|
||||
@@ -441,9 +440,7 @@ impl ProjectPanel {
|
||||
.action("New Folder", Box::new(NewDirectory))
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
.when(is_dir, |menu| {
|
||||
menu.action("Open in Terminal…", Box::new(OpenInTerminal))
|
||||
})
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
.when(is_dir, |menu| {
|
||||
menu.separator()
|
||||
.action("Find in Folder…", Box::new(NewSearchInDirectory))
|
||||
@@ -599,7 +596,10 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
|
||||
self.expanded_dir_ids.clear();
|
||||
// By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
|
||||
// (which is it's default behaviour when there's no entry for a worktree in expanded_dir_ids).
|
||||
self.expanded_dir_ids
|
||||
.retain(|_, expanded_entries| expanded_entries.is_empty());
|
||||
self.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1128,13 +1128,20 @@ impl ProjectPanel {
|
||||
|
||||
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
let path = worktree.abs_path().join(&entry.path);
|
||||
cx.dispatch_action(
|
||||
workspace::OpenTerminal {
|
||||
working_directory: path,
|
||||
let abs_path = worktree.abs_path().join(&entry.path);
|
||||
let working_directory = if entry.is_dir() {
|
||||
Some(abs_path)
|
||||
} else {
|
||||
if entry.is_symlink {
|
||||
abs_path.canonicalize().ok()
|
||||
} else {
|
||||
Some(abs_path)
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.and_then(|path| Some(path.parent()?.to_path_buf()))
|
||||
};
|
||||
if let Some(working_directory) = working_directory {
|
||||
cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1642,7 +1649,10 @@ impl ProjectPanel {
|
||||
.child(if let Some(icon) = &icon {
|
||||
h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color))
|
||||
} else {
|
||||
h_flex().size(IconSize::default().rems()).invisible()
|
||||
h_flex()
|
||||
.size(IconSize::default().rems())
|
||||
.invisible()
|
||||
.flex_none()
|
||||
})
|
||||
.child(
|
||||
if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
|
||||
@@ -1893,8 +1903,10 @@ impl Panel for ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::FileTree)
|
||||
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||
ProjectPanelSettings::get_global(cx)
|
||||
.button
|
||||
.then(|| IconName::FileTree)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub enum ProjectPanelDockPosition {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub button: bool,
|
||||
pub default_width: Pixels,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub file_icons: bool,
|
||||
@@ -25,6 +26,10 @@ pub struct ProjectPanelSettings {
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ProjectPanelSettingsContent {
|
||||
/// Whether to show the project panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Customise default width (in pixels) taken by project panel
|
||||
///
|
||||
/// Default: 240
|
||||
|
||||
@@ -155,7 +155,7 @@ impl Render for QuickActionBar {
|
||||
}
|
||||
|
||||
menu = menu.toggleable_entry(
|
||||
"Show Git Blame",
|
||||
"Show Git Blame Inline",
|
||||
git_blame_inline_enabled,
|
||||
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
|
||||
{
|
||||
@@ -180,7 +180,9 @@ impl Render for QuickActionBar {
|
||||
quick_action_bar.toggle_settings_menu = Some(menu);
|
||||
})
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Editor Controls", cx));
|
||||
.when(self.toggle_settings_menu.is_none(), |this| {
|
||||
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id("quick action bar")
|
||||
|
||||
@@ -1963,6 +1963,7 @@ message BlameBufferResponse {
|
||||
repeated BlameEntry entries = 1;
|
||||
repeated CommitMessage messages = 2;
|
||||
repeated CommitPermalink permalinks = 3;
|
||||
optional string remote_url = 4;
|
||||
}
|
||||
|
||||
message MultiLspQuery {
|
||||
|
||||
@@ -436,6 +436,7 @@ impl BufferSearchBar {
|
||||
pub fn register(registrar: &mut impl SearchActionsRegistrar) {
|
||||
registrar.register_handler(ForDeployed(|this, _: &FocusSearch, cx| {
|
||||
this.query_editor.focus_handle(cx).focus(cx);
|
||||
this.select_query(cx);
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
|
||||
if this.supported_options().case {
|
||||
|
||||
@@ -6,6 +6,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/semantic_index.rs"
|
||||
|
||||
@@ -43,6 +46,3 @@ project = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -75,6 +75,26 @@ impl ResolvedTask {
|
||||
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
|
||||
&self.substituted_variables
|
||||
}
|
||||
|
||||
/// If the resolution produced a task with the command, returns a string, combined from its command and arguments.
|
||||
pub fn resolved_command(&self) -> Option<String> {
|
||||
self.resolved.as_ref().map(|resolved| {
|
||||
let mut command = resolved.command.clone();
|
||||
for arg in &resolved.args {
|
||||
command.push(' ');
|
||||
command.push_str(arg);
|
||||
}
|
||||
command
|
||||
})
|
||||
}
|
||||
|
||||
/// A human-readable label to display in the UI.
|
||||
pub fn display_label(&self) -> &str {
|
||||
self.resolved
|
||||
.as_ref()
|
||||
.map(|resolved| resolved.label.as_str())
|
||||
.unwrap_or_else(|| self.resolved_label.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
|
||||
|
||||
@@ -25,7 +25,6 @@ util.workspace = true
|
||||
terminal.workspace = true
|
||||
workspace.workspace = true
|
||||
language.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -168,8 +168,8 @@ fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContex
|
||||
let language_context_provider = buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.and_then(|language| language.context_provider())?;
|
||||
|
||||
.and_then(|language| language.context_provider())
|
||||
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
||||
let selection_range = selection.range();
|
||||
let start = editor_snapshot
|
||||
.display_snapshot
|
||||
|
||||
@@ -233,11 +233,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
.map(|(index, (_, candidate))| StringMatchCandidate {
|
||||
id: index,
|
||||
char_bag: candidate.resolved_label.chars().collect(),
|
||||
string: candidate
|
||||
.resolved
|
||||
.as_ref()
|
||||
.map(|resolved| resolved.label.clone())
|
||||
.unwrap_or_else(|| candidate.resolved_label.clone()),
|
||||
string: candidate.display_label().to_owned(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -306,7 +302,28 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
) -> Option<Self::ListItem> {
|
||||
let candidates = self.candidates.as_ref()?;
|
||||
let hit = &self.matches[ix];
|
||||
let (source_kind, _) = &candidates.get(hit.candidate_id)?;
|
||||
let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?;
|
||||
let template = resolved_task.original_task();
|
||||
let display_label = resolved_task.display_label();
|
||||
|
||||
let mut tooltip_label_text = if display_label != &template.label {
|
||||
template.label.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if let Some(resolved_command) = resolved_task.resolved_command() {
|
||||
if display_label != resolved_command {
|
||||
if !tooltip_label_text.trim().is_empty() {
|
||||
tooltip_label_text.push('\n');
|
||||
}
|
||||
tooltip_label_text.push_str(&resolved_command);
|
||||
}
|
||||
}
|
||||
let tooltip_label = if tooltip_label_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Tooltip::text(tooltip_label_text, cx))
|
||||
};
|
||||
|
||||
let highlighted_location = HighlightedText {
|
||||
text: hit.string.clone(),
|
||||
@@ -321,10 +338,14 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
.get_type_icon(&name.to_lowercase())
|
||||
.map(|icon_path| Icon::from_path(icon_path)),
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.when_some(tooltip_label, |list_item, item_label| {
|
||||
list_item.tooltip(move |_| item_label.clone())
|
||||
})
|
||||
.map(|item| {
|
||||
let item = if matches!(source_kind, TaskSourceKind::UserInput)
|
||||
|| Some(ix) <= self.last_used_candidate_index
|
||||
@@ -368,18 +389,10 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
}
|
||||
|
||||
fn selected_as_query(&self) -> Option<String> {
|
||||
use itertools::intersperse;
|
||||
let task_index = self.matches.get(self.selected_index())?.candidate_id;
|
||||
let tasks = self.candidates.as_ref()?;
|
||||
let (_, task) = tasks.get(task_index)?;
|
||||
task.resolved.as_ref().map(|spawn_in_terminal| {
|
||||
let mut command = spawn_in_terminal.command.clone();
|
||||
if !spawn_in_terminal.args.is_empty() {
|
||||
command.push(' ');
|
||||
command.extend(intersperse(spawn_in_terminal.args.clone(), " ".to_string()));
|
||||
}
|
||||
command
|
||||
})
|
||||
task.resolved_command()
|
||||
}
|
||||
|
||||
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
@@ -405,7 +418,11 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use language::Point;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -561,6 +578,100 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) {
|
||||
crate::tests::init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".zed": {
|
||||
"tasks.json": r#"[
|
||||
{
|
||||
"label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN",
|
||||
"command": "echo",
|
||||
"args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"]
|
||||
},
|
||||
{
|
||||
"label": "opened now: $ZED_WORKTREE_ROOT",
|
||||
"command": "echo",
|
||||
"args": ["opened", "now:", "$ZED_WORKTREE_ROOT"]
|
||||
}
|
||||
]"#,
|
||||
},
|
||||
"file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa",
|
||||
"file_with.odd_extension": "b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
Vec::<String>::new(),
|
||||
"Should list no file or worktree context-dependent when no file is open"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let _ = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/file_with.odd_extension"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"hello from …th.odd_extension:1:1".to_string(),
|
||||
"opened now: /dir".to_string()
|
||||
],
|
||||
"Second opened buffer should fill the context, labels should be trimmed if long enough"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let second_item = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/file_without_extension"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor = cx.update(|cx| second_item.act_as::<Editor>(cx)).unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5)))
|
||||
})
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"hello from …ithout_extension:2:3".to_string(),
|
||||
"opened now: /dir".to_string()
|
||||
],
|
||||
"Opened buffer should fill the context, labels should be trimmed if long enough"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
|
||||
fn open_spawn_tasks(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
@@ -569,7 +680,7 @@ mod tests {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<TasksModal>(cx)
|
||||
.unwrap()
|
||||
.expect("no task modal after `Spawn` action was dispatched")
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
|
||||
@@ -750,6 +750,11 @@ impl Terminal {
|
||||
InternalEvent::SetSelection(selection) => {
|
||||
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(selection_text) = term.selection_to_string() {
|
||||
cx.write_to_primary(ClipboardItem::new(selection_text));
|
||||
}
|
||||
|
||||
if let Some((_, head)) = selection {
|
||||
self.selection_head = Some(*head);
|
||||
}
|
||||
@@ -766,6 +771,11 @@ impl Terminal {
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(selection_text) = term.selection_to_string() {
|
||||
cx.write_to_primary(ClipboardItem::new(selection_text));
|
||||
}
|
||||
|
||||
self.selection_head = Some(point);
|
||||
cx.emit(Event::SelectionsChanged)
|
||||
}
|
||||
@@ -1192,7 +1202,12 @@ impl Terminal {
|
||||
Some(scroll_delta)
|
||||
}
|
||||
|
||||
pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
|
||||
pub fn mouse_down(
|
||||
&mut self,
|
||||
e: &MouseDownEvent,
|
||||
origin: Point<Pixels>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let position = e.position - origin;
|
||||
let point = grid_point(
|
||||
position,
|
||||
@@ -1229,6 +1244,11 @@ impl Terminal {
|
||||
self.events
|
||||
.push_back(InternalEvent::SetSelection(Some((sel, point))));
|
||||
}
|
||||
} else if e.button == MouseButton::Middle {
|
||||
if let Some(item) = cx.read_from_primary() {
|
||||
let text = item.text().to_string();
|
||||
self.input(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ impl TerminalElement {
|
||||
move |e, cx| {
|
||||
cx.focus(&focus);
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
terminal.mouse_down(&e, origin, cx);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
@@ -479,6 +479,17 @@ impl TerminalElement {
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_down(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_scroll_wheel({
|
||||
let terminal = terminal.clone();
|
||||
move |e, cx| {
|
||||
@@ -498,19 +509,8 @@ impl TerminalElement {
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_down(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -542,7 +542,7 @@ impl TerminalElement {
|
||||
|
||||
impl Element for TerminalElement {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = LayoutState;
|
||||
type BeforePaint = LayoutState;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
self.interactivity.occlude_mouse();
|
||||
@@ -556,14 +556,14 @@ impl Element for TerminalElement {
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Self::AfterLayout {
|
||||
) -> Self::BeforePaint {
|
||||
self.interactivity
|
||||
.after_layout(bounds, bounds.size, cx, |_, _, hitbox, cx| {
|
||||
.before_paint(bounds, bounds.size, cx, |_, _, hitbox, cx| {
|
||||
let hitbox = hitbox.unwrap();
|
||||
let settings = ThemeSettings::get_global(cx).clone();
|
||||
|
||||
@@ -776,7 +776,7 @@ impl Element for TerminalElement {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
layout: &mut Self::AfterLayout,
|
||||
layout: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext<'_>,
|
||||
) {
|
||||
cx.paint_quad(fill(bounds, layout.background_color));
|
||||
|
||||
@@ -98,7 +98,6 @@ impl TerminalPanel {
|
||||
.on_click(cx.listener(|pane, _, cx| {
|
||||
pane.toggle_zoom(&workspace::ToggleZoom, cx);
|
||||
}))
|
||||
// TODO kb
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
if zoomed { "Zoom Out" } else { "Zoom In" },
|
||||
@@ -292,13 +291,13 @@ impl TerminalPanel {
|
||||
action: &workspace::OpenTerminal,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let Some(this) = workspace.focus_panel::<Self>(cx) else {
|
||||
let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(Some(action.working_directory.clone()), None, cx)
|
||||
})
|
||||
terminal_panel.update(cx, |panel, cx| {
|
||||
panel.add_terminal(Some(action.working_directory.clone()), None, cx)
|
||||
});
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
}
|
||||
|
||||
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
|
||||
@@ -427,17 +426,17 @@ impl TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
///Create a new Terminal in the current working directory or the user's home directory
|
||||
/// Create a new Terminal in the current working directory or the user's home directory
|
||||
fn new_terminal(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::NewTerminal,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let Some(this) = workspace.focus_panel::<Self>(cx) else {
|
||||
let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| this.add_terminal(None, None, cx))
|
||||
terminal_panel.update(cx, |this, cx| this.add_terminal(None, None, cx));
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
}
|
||||
|
||||
fn terminals_for_task(
|
||||
@@ -598,9 +597,14 @@ impl TerminalPanel {
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn pane(&self) -> &View<Pane> {
|
||||
&self.pane
|
||||
}
|
||||
|
||||
fn has_no_terminals(&mut self, cx: &mut ViewContext<'_, Self>) -> bool {
|
||||
self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_terminals_tasks(
|
||||
@@ -713,7 +717,7 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 {
|
||||
if active && self.has_no_terminals(cx) {
|
||||
self.add_terminal(None, None, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ impl TerminalView {
|
||||
self.has_bell
|
||||
}
|
||||
|
||||
pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
|
||||
pub fn clear_bell(&mut self, cx: &mut ViewContext<TerminalView>) {
|
||||
self.has_bell = false;
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
@@ -332,7 +332,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
|
||||
self.clear_bel(cx);
|
||||
self.clear_bell(cx);
|
||||
self.terminal.update(cx, |term, _| {
|
||||
term.input(text.0.to_string());
|
||||
});
|
||||
@@ -340,7 +340,7 @@ impl TerminalView {
|
||||
|
||||
fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
|
||||
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
|
||||
self.clear_bel(cx);
|
||||
self.clear_bell(cx);
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
|
||||
});
|
||||
@@ -700,7 +700,7 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<Re
|
||||
|
||||
impl TerminalView {
|
||||
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
|
||||
self.clear_bel(cx);
|
||||
self.clear_bell(cx);
|
||||
self.pause_cursor_blinking(cx);
|
||||
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
|
||||
@@ -40,7 +40,7 @@ impl<V: 'static> TodoList<V> {
|
||||
|
||||
All of this is relatively straightforward.
|
||||
|
||||
We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to [TODO: someone who actually knows please explain why we use SharedString].
|
||||
We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to efficiently handle shared string data across multiple components and threads without the performance overhead of copying strings.
|
||||
|
||||
When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`.
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ pub struct PopoverMenuFrameState {
|
||||
|
||||
impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||
type BeforeLayout = PopoverMenuFrameState;
|
||||
type AfterLayout = Option<HitboxId>;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Option<HitboxId>;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) {
|
||||
self.with_element_state(cx, |this, element_state, cx| {
|
||||
@@ -219,14 +220,40 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||
_bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<HitboxId> {
|
||||
self.with_element_state(cx, |_this, element_state, cx| {
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
cx.with_element_id(Some(self.id.clone()), |cx| {
|
||||
let mut focus_target_bounds = None;
|
||||
|
||||
if let Some(child) = before_layout.child_element.as_mut() {
|
||||
child.after_layout(cx);
|
||||
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
|
||||
focus_target_bounds = Some(child_focus_target_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(menu) = before_layout.menu_element.as_mut() {
|
||||
menu.after_layout(cx);
|
||||
if let Some(menu_focus_target_bounds) = menu.after_layout(cx) {
|
||||
focus_target_bounds = Some(menu_focus_target_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
(focus_target_bounds, ())
|
||||
})
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<HitboxId> {
|
||||
self.with_element_state(cx, |_this, element_state, cx| {
|
||||
if let Some(child) = before_layout.child_element.as_mut() {
|
||||
child.before_paint(cx);
|
||||
}
|
||||
|
||||
if let Some(menu) = before_layout.menu_element.as_mut() {
|
||||
menu.before_paint(cx);
|
||||
}
|
||||
|
||||
before_layout.child_layout_id.map(|layout_id| {
|
||||
@@ -241,6 +268,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
||||
&mut self,
|
||||
_: Bounds<gpui::Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
child_hitbox: &mut Option<HitboxId>,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
|
||||
@@ -97,7 +97,8 @@ pub struct MenuHandleFrameState {
|
||||
|
||||
impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||
type BeforeLayout = MenuHandleFrameState;
|
||||
type AfterLayout = Hitbox;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = Hitbox;
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) {
|
||||
self.with_element_state(cx, |this, element_state, cx| {
|
||||
@@ -144,20 +145,46 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
cx.with_element_id(Some(self.id.clone()), |cx| {
|
||||
let mut focus_target_bounds = None;
|
||||
|
||||
if let Some(child) = before_layout.child_element.as_mut() {
|
||||
if let Some(child_focus_target_bounds) = child.after_layout(cx) {
|
||||
focus_target_bounds = Some(child_focus_target_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(menu) = before_layout.menu_element.as_mut() {
|
||||
if let Some(menu_focus_target_bounds) = menu.after_layout(cx) {
|
||||
focus_target_bounds = Some(menu_focus_target_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
(focus_target_bounds, ())
|
||||
})
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> Hitbox {
|
||||
cx.with_element_id(Some(self.id.clone()), |cx| {
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
|
||||
if let Some(child) = before_layout.child_element.as_mut() {
|
||||
child.after_layout(cx);
|
||||
child.before_paint(cx);
|
||||
}
|
||||
|
||||
if let Some(menu) = before_layout.menu_element.as_mut() {
|
||||
menu.after_layout(cx);
|
||||
menu.before_paint(cx);
|
||||
}
|
||||
|
||||
hitbox
|
||||
@@ -168,7 +195,8 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||
&mut self,
|
||||
_bounds: Bounds<gpui::Pixels>,
|
||||
before_layout: &mut Self::BeforeLayout,
|
||||
hitbox: &mut Self::AfterLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
hitbox: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
self.with_element_state(cx, |this, element_state, cx| {
|
||||
|
||||
@@ -205,23 +205,27 @@ impl<P> PathLikeWithPosition<P> {
|
||||
column: None,
|
||||
})
|
||||
} else {
|
||||
let maybe_col_str =
|
||||
if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
|
||||
&maybe_col_str[..maybe_col_str.len() - 1]
|
||||
} else {
|
||||
maybe_col_str
|
||||
};
|
||||
let (maybe_col_str, _) =
|
||||
maybe_col_str.split_once(':').unwrap_or((maybe_col_str, ""));
|
||||
match maybe_col_str.parse::<u32>() {
|
||||
Ok(col) => Ok(Self {
|
||||
path_like: parse_path_like_str(path_like_str)?,
|
||||
row: Some(row),
|
||||
column: Some(col),
|
||||
}),
|
||||
Err(_) => fallback(s),
|
||||
Err(_) => Ok(Self {
|
||||
path_like: parse_path_like_str(path_like_str)?,
|
||||
row: Some(row),
|
||||
column: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => fallback(s),
|
||||
Err(_) => Ok(Self {
|
||||
path_like: parse_path_like_str(path_like_str)?,
|
||||
row: None,
|
||||
column: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,23 +356,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn path_with_position_parsing_negative() {
|
||||
for input in [
|
||||
"test_file.rs:a",
|
||||
"test_file.rs:a:b",
|
||||
"test_file.rs::",
|
||||
"test_file.rs::1",
|
||||
"test_file.rs:1::",
|
||||
"test_file.rs::1:2",
|
||||
"test_file.rs:1::2",
|
||||
"test_file.rs:1:2:3",
|
||||
for (input, row, column) in [
|
||||
("test_file.rs:a", None, None),
|
||||
("test_file.rs:a:b", None, None),
|
||||
("test_file.rs::", None, None),
|
||||
("test_file.rs::1", None, None),
|
||||
("test_file.rs:1::", Some(1), None),
|
||||
("test_file.rs::1:2", None, None),
|
||||
("test_file.rs:1::2", Some(1), None),
|
||||
("test_file.rs:1:2:3", Some(1), Some(2)),
|
||||
] {
|
||||
let actual = parse_str(input);
|
||||
assert_eq!(
|
||||
actual,
|
||||
PathLikeWithPosition {
|
||||
path_like: input.to_string(),
|
||||
row: None,
|
||||
column: None,
|
||||
path_like: "test_file.rs".to_string(),
|
||||
row,
|
||||
column,
|
||||
},
|
||||
"For negative case input str '{input}', got a parse mismatch"
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ test-support = [
|
||||
anyhow.workspace = true
|
||||
any_vec.workspace = true
|
||||
async-recursion.workspace = true
|
||||
bincode = "1.2.1"
|
||||
call.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::{
|
||||
},
|
||||
toolbar::Toolbar,
|
||||
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
|
||||
NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
|
||||
NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
|
||||
SplitDirection, ToggleZoom, Workspace,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -1597,20 +1598,58 @@ impl Pane {
|
||||
);
|
||||
|
||||
if let Some(entry) = single_entry_to_resolve {
|
||||
let parent_abs_path = pane
|
||||
.update(cx, |pane, cx| {
|
||||
pane.workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
project.worktree_for_entry(entry, cx).and_then(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_id(entry)?;
|
||||
let abs_path = worktree.absolutize(&entry.path).ok()?;
|
||||
let parent = if entry.is_symlink {
|
||||
abs_path.canonicalize().ok()?
|
||||
} else {
|
||||
abs_path
|
||||
}
|
||||
.parent()?
|
||||
.to_path_buf();
|
||||
Some(parent)
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let entry_id = entry.to_proto();
|
||||
menu = menu.separator().entry(
|
||||
"Reveal In Project Panel",
|
||||
Some(Box::new(RevealInProjectPanel {
|
||||
entry_id: Some(entry_id),
|
||||
})),
|
||||
cx.handler_for(&pane, move |pane, cx| {
|
||||
pane.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(
|
||||
ProjectEntryId::from_proto(entry_id),
|
||||
))
|
||||
});
|
||||
}),
|
||||
);
|
||||
menu = menu
|
||||
.separator()
|
||||
.entry(
|
||||
"Reveal In Project Panel",
|
||||
Some(Box::new(RevealInProjectPanel {
|
||||
entry_id: Some(entry_id),
|
||||
})),
|
||||
cx.handler_for(&pane, move |pane, cx| {
|
||||
pane.project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(
|
||||
ProjectEntryId::from_proto(entry_id),
|
||||
))
|
||||
});
|
||||
}),
|
||||
)
|
||||
.when_some(parent_abs_path, |menu, abs_path| {
|
||||
menu.entry(
|
||||
"Open in Terminal",
|
||||
Some(Box::new(OpenInTerminal)),
|
||||
cx.handler_for(&pane, move |_, cx| {
|
||||
cx.dispatch_action(
|
||||
OpenTerminal {
|
||||
working_directory: abs_path.clone(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1721,16 +1760,35 @@ impl Pane {
|
||||
return;
|
||||
}
|
||||
|
||||
let edge_width = cx.rem_size() * 8;
|
||||
let cursor = event.event.position;
|
||||
let direction = if cursor.x < event.bounds.left() + edge_width {
|
||||
Some(SplitDirection::Left)
|
||||
} else if cursor.x > event.bounds.right() - edge_width {
|
||||
Some(SplitDirection::Right)
|
||||
} else if cursor.y < event.bounds.top() + edge_width {
|
||||
Some(SplitDirection::Up)
|
||||
} else if cursor.y > event.bounds.bottom() - edge_width {
|
||||
Some(SplitDirection::Down)
|
||||
let rect = event.bounds.size;
|
||||
|
||||
let size = event.bounds.size.width.min(event.bounds.size.height)
|
||||
* WorkspaceSettings::get_global(cx).drop_target_size;
|
||||
|
||||
let relative_cursor = Point::new(
|
||||
event.event.position.x - event.bounds.left(),
|
||||
event.event.position.y - event.bounds.top(),
|
||||
);
|
||||
|
||||
let direction = if relative_cursor.x < size
|
||||
|| relative_cursor.x > rect.width - size
|
||||
|| relative_cursor.y < size
|
||||
|| relative_cursor.y > rect.height - size
|
||||
{
|
||||
[
|
||||
SplitDirection::Up,
|
||||
SplitDirection::Right,
|
||||
SplitDirection::Down,
|
||||
SplitDirection::Left,
|
||||
]
|
||||
.iter()
|
||||
.min_by_key(|side| match side {
|
||||
SplitDirection::Up => relative_cursor.y,
|
||||
SplitDirection::Right => rect.width - relative_cursor.x,
|
||||
SplitDirection::Down => rect.height - relative_cursor.y,
|
||||
SplitDirection::Left => relative_cursor.x,
|
||||
})
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2013,10 +2071,7 @@ impl Render for Pane {
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.bg(theme::color_alpha(
|
||||
cx.theme().colors().drop_target_background,
|
||||
0.75,
|
||||
))
|
||||
.bg(cx.theme().colors().drop_target_background)
|
||||
.group_drag_over::<DraggedTab>("", |style| style.visible())
|
||||
.group_drag_over::<ProjectEntryId>("", |style| style.visible())
|
||||
.group_drag_over::<ExternalPaths>("", |style| style.visible())
|
||||
@@ -2032,17 +2087,22 @@ impl Render for Pane {
|
||||
.on_drop(cx.listener(move |this, paths, cx| {
|
||||
this.handle_external_paths_drop(paths, cx)
|
||||
}))
|
||||
.map(|div| match self.drag_split_direction {
|
||||
None => div.top_0().left_0().right_0().bottom_0(),
|
||||
Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
|
||||
Some(SplitDirection::Down) => {
|
||||
div.left_0().bottom_0().right_0().h_32()
|
||||
}
|
||||
Some(SplitDirection::Left) => {
|
||||
div.top_0().left_0().bottom_0().w_32()
|
||||
}
|
||||
Some(SplitDirection::Right) => {
|
||||
div.top_0().bottom_0().right_0().w_32()
|
||||
.map(|div| {
|
||||
let size = DefiniteLength::Fraction(0.5);
|
||||
match self.drag_split_direction {
|
||||
None => div.top_0().right_0().bottom_0().left_0(),
|
||||
Some(SplitDirection::Up) => {
|
||||
div.top_0().left_0().right_0().h(size)
|
||||
}
|
||||
Some(SplitDirection::Down) => {
|
||||
div.left_0().bottom_0().right_0().h(size)
|
||||
}
|
||||
Some(SplitDirection::Left) => {
|
||||
div.top_0().left_0().bottom_0().w(size)
|
||||
}
|
||||
Some(SplitDirection::Right) => {
|
||||
div.top_0().bottom_0().right_0().w(size)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -649,7 +649,7 @@ mod element {
|
||||
children: Vec<PaneAxisChildLayout>,
|
||||
}
|
||||
|
||||
struct PaneAxisChildLayout {
|
||||
pub struct PaneAxisChildLayout {
|
||||
bounds: Bounds<Pixels>,
|
||||
element: AnyElement,
|
||||
handle: Option<PaneAxisHandleLayout>,
|
||||
@@ -793,7 +793,8 @@ mod element {
|
||||
|
||||
impl Element for PaneAxisElement {
|
||||
type BeforeLayout = ();
|
||||
type AfterLayout = PaneAxisLayout;
|
||||
type AfterLayout = Vec<PaneAxisChildLayout>;
|
||||
type BeforePaint = PaneAxisLayout;
|
||||
|
||||
fn before_layout(
|
||||
&mut self,
|
||||
@@ -811,18 +812,9 @@ mod element {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::BeforeLayout,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> PaneAxisLayout {
|
||||
let dragged_handle = cx.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
|
||||
Some(self.basis.into()),
|
||||
|state, _cx| {
|
||||
let state = state
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| Rc::new(RefCell::new(None)));
|
||||
(state.clone(), Some(state))
|
||||
},
|
||||
);
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
let flexes = self.flexes.lock().clone();
|
||||
let len = self.children.len();
|
||||
debug_assert!(flexes.len() == len);
|
||||
@@ -844,13 +836,9 @@ mod element {
|
||||
let mut origin = bounds.origin;
|
||||
let space_per_flex = bounds.size.along(self.axis) / total_flex;
|
||||
|
||||
let mut bounding_boxes = self.bounding_boxes.lock();
|
||||
bounding_boxes.clear();
|
||||
let mut focus_target_bounds = None;
|
||||
|
||||
let mut layout = PaneAxisLayout {
|
||||
dragged_handle: dragged_handle.clone(),
|
||||
children: Vec::new(),
|
||||
};
|
||||
let mut children = Vec::new();
|
||||
for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() {
|
||||
let child_flex = active_pane_magnification
|
||||
.map(|magnification| {
|
||||
@@ -866,25 +854,76 @@ mod element {
|
||||
.size
|
||||
.apply_along(self.axis, |_| space_per_flex * child_flex)
|
||||
.map(|d| d.round());
|
||||
let child_bounds = Bounds::new(origin, child_size);
|
||||
|
||||
let child_bounds = Bounds {
|
||||
origin,
|
||||
size: child_size,
|
||||
};
|
||||
bounding_boxes.push(Some(child_bounds));
|
||||
child.layout(origin, child_size.into(), cx);
|
||||
let child_measurement = child.layout(child_size.into(), cx);
|
||||
if let Some(child_focus_target_bounds) = child_measurement.focus_target_bounds {
|
||||
focus_target_bounds = Some(Bounds::new(
|
||||
child_bounds.origin + child_focus_target_bounds.origin,
|
||||
child_focus_target_bounds.size,
|
||||
));
|
||||
}
|
||||
|
||||
origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
|
||||
layout.children.push(PaneAxisChildLayout {
|
||||
children.push(PaneAxisChildLayout {
|
||||
bounds: child_bounds,
|
||||
element: child,
|
||||
handle: None,
|
||||
})
|
||||
}
|
||||
|
||||
for (ix, child_layout) in layout.children.iter_mut().enumerate() {
|
||||
if active_pane_magnification.is_none() {
|
||||
if ix < len - 1 {
|
||||
(focus_target_bounds, children)
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_before_layout: &mut Self::BeforeLayout,
|
||||
children: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> PaneAxisLayout {
|
||||
let dragged_handle = cx.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
|
||||
Some(self.basis.into()),
|
||||
|state, _cx| {
|
||||
let state = state
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| Rc::new(RefCell::new(None)));
|
||||
(state.clone(), Some(state))
|
||||
},
|
||||
);
|
||||
|
||||
let magnification_value = WorkspaceSettings::get(None, cx).active_pane_magnification;
|
||||
let active_pane_magnification = if magnification_value == 1. {
|
||||
None
|
||||
} else {
|
||||
Some(magnification_value)
|
||||
};
|
||||
|
||||
let mut origin = bounds.origin;
|
||||
|
||||
let mut bounding_boxes = self.bounding_boxes.lock();
|
||||
bounding_boxes.clear();
|
||||
|
||||
let mut layout = PaneAxisLayout {
|
||||
dragged_handle: dragged_handle.clone(),
|
||||
children: Vec::new(),
|
||||
};
|
||||
for child in children {
|
||||
let child_bounds = Bounds {
|
||||
origin,
|
||||
size: child.bounds.size,
|
||||
};
|
||||
bounding_boxes.push(Some(child_bounds));
|
||||
cx.with_absolute_element_offset(origin, |cx| child.element.before_paint(cx));
|
||||
origin =
|
||||
origin.apply_along(self.axis, |val| val + child_bounds.size.along(self.axis));
|
||||
child.bounds = child_bounds;
|
||||
}
|
||||
|
||||
if active_pane_magnification.is_none() {
|
||||
let mut child_layouts = layout.children.iter_mut().peekable();
|
||||
while let Some(child_layout) = child_layouts.next() {
|
||||
if child_layouts.peek().is_some() {
|
||||
child_layout.handle =
|
||||
Some(Self::layout_handle(self.axis, child_layout.bounds, cx));
|
||||
}
|
||||
@@ -898,7 +937,8 @@ mod element {
|
||||
&mut self,
|
||||
bounds: gpui::Bounds<ui::prelude::Pixels>,
|
||||
_: &mut Self::BeforeLayout,
|
||||
layout: &mut Self::AfterLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
layout: &mut Self::BeforePaint,
|
||||
cx: &mut ui::prelude::ElementContext,
|
||||
) {
|
||||
for child in &mut layout.children {
|
||||
|
||||
@@ -125,7 +125,7 @@ define_connection! {
|
||||
//
|
||||
// workspaces(
|
||||
// workspace_id: usize, // Primary key for workspaces
|
||||
// workspace_location: Vec<PathBuf>,
|
||||
// workspace_location: Bincode<Vec<PathBuf>>,
|
||||
// dock_visible: bool, // Deprecated
|
||||
// dock_anchor: DockAnchor, // Deprecated
|
||||
// dock_pane: Option<usize>, // Deprecated
|
||||
@@ -289,64 +289,7 @@ define_connection! {
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
||||
),
|
||||
// remove bincode
|
||||
sql!(
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location TEXT UNIQUE,
|
||||
dock_visible INTEGER,
|
||||
dock_anchor TEXT,
|
||||
dock_pane INTEGER,
|
||||
left_sidebar_open INTEGER,
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB,
|
||||
left_dock_visible INTEGER,
|
||||
left_dock_active_panel TEXT,
|
||||
right_dock_visible INTEGER,
|
||||
right_dock_active_panel TEXT,
|
||||
bottom_dock_visible INTEGER,
|
||||
bottom_dock_active_panel TEXT,
|
||||
left_dock_zoom INTEGER,
|
||||
right_dock_zoom INTEGER,
|
||||
bottom_dock_zoom INTEGER,
|
||||
fullscreen INTEGER,
|
||||
centered_layout INTEGER
|
||||
) STRICT;
|
||||
INSERT INTO workspaces_2 SELECT
|
||||
workspace_id,
|
||||
json_array(cast(unhex(substr(hex(workspace_location), 33)) as text)) as workspace_location,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
dock_pane,
|
||||
left_sidebar_open,
|
||||
timestamp,
|
||||
window_state,
|
||||
window_x,
|
||||
window_y,
|
||||
window_width,
|
||||
window_height,
|
||||
display,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel,
|
||||
left_dock_zoom,
|
||||
right_dock_zoom,
|
||||
bottom_dock_zoom,
|
||||
fullscreen,
|
||||
centered_layout
|
||||
FROM workspaces
|
||||
WHERE substr(hex(workspace_location), 0, 17) == "0100000000000000";
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -48,15 +48,17 @@ impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
|
||||
impl StaticColumnCount for WorkspaceLocation {}
|
||||
impl Bind for &WorkspaceLocation {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
statement.bind(&dbg!(serde_json::to_string(&self.0)?), start_index)
|
||||
bincode::serialize(&self.0)
|
||||
.expect("Bincode serialization of paths should not fail")
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for WorkspaceLocation {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let blob = statement.column_text(start_index)?;
|
||||
let blob = statement.column_blob(start_index)?;
|
||||
Ok((
|
||||
WorkspaceLocation(serde_json::from_str(blob).context("JSON failed")?),
|
||||
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
|
||||
start_index + 1,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ actions!(
|
||||
workspace,
|
||||
[
|
||||
Open,
|
||||
OpenInTerminal,
|
||||
NewFile,
|
||||
NewWindow,
|
||||
CloseWindow,
|
||||
@@ -4006,12 +4007,9 @@ impl Render for Workspace {
|
||||
.border_color(colors.border)
|
||||
.child({
|
||||
let this = cx.view().clone();
|
||||
canvas(
|
||||
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full()
|
||||
canvas(move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds))
|
||||
.absolute()
|
||||
.size_full()
|
||||
})
|
||||
.when(self.zoomed.is_none(), |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
@@ -4061,7 +4059,7 @@ impl Render for Workspace {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
@@ -4972,6 +4970,7 @@ struct DisconnectedOverlay;
|
||||
impl Element for DisconnectedOverlay {
|
||||
type BeforeLayout = AnyElement;
|
||||
type AfterLayout = ();
|
||||
type BeforePaint = ();
|
||||
|
||||
fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
|
||||
let mut background = cx.theme().colors().elevated_surface_background;
|
||||
@@ -4995,20 +4994,31 @@ impl Element for DisconnectedOverlay {
|
||||
}
|
||||
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
overlay: &mut Self::BeforeLayout,
|
||||
cx: &mut ElementContext,
|
||||
) -> (Option<Bounds<Pixels>>, Self::AfterLayout) {
|
||||
(overlay.after_layout(cx), ())
|
||||
}
|
||||
|
||||
fn before_paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
overlay: &mut Self::BeforeLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
cx.insert_hitbox(bounds, true);
|
||||
overlay.after_layout(cx);
|
||||
overlay.before_paint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
overlay: &mut Self::BeforeLayout,
|
||||
_: &mut Self::AfterLayout,
|
||||
_after_layout: &mut Self::AfterLayout,
|
||||
_: &mut Self::BeforePaint,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
overlay.paint(cx)
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct WorkspaceSettings {
|
||||
pub show_call_status_icon: bool,
|
||||
pub autosave: AutosaveSetting,
|
||||
pub restore_on_startup: RestoreOnStartupBehaviour,
|
||||
pub drop_target_size: f32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -50,6 +51,11 @@ pub struct WorkspaceSettingsContent {
|
||||
/// Values: none, last_workspace
|
||||
/// Default: last_workspace
|
||||
pub restore_on_startup: Option<RestoreOnStartupBehaviour>,
|
||||
/// The size of the workspace split drop targets on the outer edges.
|
||||
/// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
|
||||
///
|
||||
/// Default: `0.2` (20% of the smaller dimension of the workspace)
|
||||
pub drop_target_size: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.132.0"
|
||||
version = "0.133.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -158,7 +158,7 @@ For example, to disable ligatures for a given font you can add the following to
|
||||
**Options**
|
||||
|
||||
The `left_padding` and `right_padding` options define the relative width of the
|
||||
left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.45`.
|
||||
left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`.
|
||||
|
||||
## Copilot
|
||||
|
||||
@@ -555,9 +555,14 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"git": {
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
{
|
||||
"git": {
|
||||
"git_gutter": "tracked_files",
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Git Gutter
|
||||
@@ -572,7 +577,9 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
|
||||
|
||||
```json
|
||||
{
|
||||
"git_gutter": "tracked_files"
|
||||
"git": {
|
||||
"git_gutter": "tracked_files"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -580,7 +587,52 @@ To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
|
||||
|
||||
```json
|
||||
{
|
||||
"git_gutter": "hide"
|
||||
"git": {
|
||||
"git_gutter": "hide"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inline Git Blame
|
||||
|
||||
- Description: Whether or not to show git blame information inline, on the currently focused line (requires Zed `0.132.0`).
|
||||
- Setting: `inline_blame`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
1. Disable inline git blame:
|
||||
|
||||
```json
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Only show inline git blame after a delay (that starts after cursor stops moving):
|
||||
|
||||
```json
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": false,
|
||||
"delay_ms": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -730,12 +782,13 @@ These values take in the same options as the root-level settings with the same n
|
||||
## Preview tabs
|
||||
|
||||
- Description:
|
||||
(requires Zed `0.132.x`) \
|
||||
Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \
|
||||
There are several ways to convert a preview tab into a regular tab:
|
||||
|
||||
- Double-clicking on the file
|
||||
- Double-clicking on the tab header
|
||||
- Using the 'project_panel::OpenPermanent' action
|
||||
- Using the `project_panel::OpenPermanent` action
|
||||
- Editing the file
|
||||
- Dragging the file to a different pane
|
||||
|
||||
@@ -749,8 +802,6 @@ These values take in the same options as the root-level settings with the same n
|
||||
}
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
### Enable preview from file finder
|
||||
|
||||
- Description: Determines whether to open files in preview mode when selected from the file finder.
|
||||
@@ -926,7 +977,7 @@ These values take in the same options as the root-level settings with the same n
|
||||
"font_features": null,
|
||||
"font_size": null,
|
||||
"option_as_meta": false,
|
||||
"button": false
|
||||
"button": false,
|
||||
"shell": {},
|
||||
"toolbar": {
|
||||
"title": true
|
||||
@@ -1243,6 +1294,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
|
||||
```json
|
||||
"project_panel": {
|
||||
"button": true,
|
||||
"dock": "left",
|
||||
"git_status": true,
|
||||
"default_width": "N/A - width in pixels"
|
||||
@@ -1305,6 +1357,21 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Calls
|
||||
|
||||
- Description: Customise behaviour when participating in a call
|
||||
- Setting: `calls`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"calls": {
|
||||
// Join calls with the microphone live by default
|
||||
"mute_on_join": false,
|
||||
// Share your project when you are the first to join a channel
|
||||
"share_on_join": true
|
||||
},
|
||||
```
|
||||
|
||||
## An example configuration:
|
||||
|
||||
```json
|
||||
|
||||
@@ -25,7 +25,7 @@ git submodule update --init --recursive
|
||||
rustup target add wasm32-wasi
|
||||
```
|
||||
|
||||
- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with optional component `MSVC v*** - VS YYYY C++ x64/x86 build tools`.
|
||||
- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with optional component `MSVC v*** - VS YYYY C++ x64/x86 build tools` and install Windows 11 or 10 SDK depending on your system
|
||||
|
||||
> [!NOTE]
|
||||
> `v***` is your VS version and `YYYY` is year when your VS was released.
|
||||
|
||||
@@ -17,25 +17,31 @@ You will need write access to the Zed repository to do this:
|
||||
- Run `./script/bump-zed-minor-versions` and push the tags
|
||||
and branches as instructed.
|
||||
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
|
||||
- Copy the release notes from the previous Preview release(s) to the current Stable release.
|
||||
- Write new release notes for Preview. `/script/get-preview-channel-changes` can help with this, but you'll need to edit and format the output to make it good.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the releases.
|
||||
- While you're waiting:
|
||||
- Start creating the new release notes for preview. You can start with the output of `./script/get-preview-channel-changes`.
|
||||
- Start drafting the release tweets.
|
||||
- Once the builds are ready:
|
||||
- Copy the release notes from the previous Preview release(s) to the current Stable release.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the releases on GitHub.
|
||||
- Tweet the tweets (Credentials are in 1password).
|
||||
|
||||
## Patch release process
|
||||
|
||||
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to the appropriate branch.
|
||||
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to preview.
|
||||
|
||||
You will need write access to the Zed repository to do this:
|
||||
|
||||
- Cherry pick them onto the correct branch. You can either do this manually, or leave a comment of the form `/cherry-pick v0.XXX.x` on the PR, and the GitHub bot should do it for you.
|
||||
- Run `./script/trigger-release {preview|stable}`
|
||||
- Send a PR containing your change to `main` as normal.
|
||||
- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the Github bot will send a PR to the branch.
|
||||
- In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch.
|
||||
- After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build.
|
||||
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
|
||||
- Add release notes using the `Release notes:` section of each cherry-picked PR.
|
||||
- Proof-read and edit the release notes as needed.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the release.
|
||||
|
||||
## Nightly release process
|
||||
|
||||
- Merge your changes to main
|
||||
- Run `./script/trigger-release {nightly}`
|
||||
In addition to the public releases, we also have a nightly build that we encourage employees to use.
|
||||
Nightly is released by cron once a day, and can be shipped as often as you'd like. There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`.
|
||||
|
||||
16
extensions/glsl/Cargo.toml
Normal file
16
extensions/glsl/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/glsl.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.0.6"
|
||||
@@ -1,11 +1,15 @@
|
||||
id = "glsl"
|
||||
name = "GLSL"
|
||||
description = "GLSL support."
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
schema_version = 1
|
||||
authors = ["Mikayla Maki <mikayla@zed.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.glsl_analyzer]
|
||||
name = "GLSL Analyzer LSP"
|
||||
language = "GLSL"
|
||||
|
||||
[grammars.glsl]
|
||||
repository = "https://github.com/theHamsta/tree-sitter-glsl"
|
||||
commit = "31064ce53385150f894a6c72d61b94076adf640a"
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
name = "GLSL"
|
||||
grammar = "glsl"
|
||||
path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
|
||||
path_suffixes = [
|
||||
# Traditional rasterization pipeline shaders
|
||||
"vert", "frag", "tesc", "tese", "geom",
|
||||
# Compute shaders
|
||||
"comp",
|
||||
# Ray tracing pipeline shaders
|
||||
"rgen", "rint", "rahit", "rchit", "rmiss", "rcall",
|
||||
# Other
|
||||
"glsl"
|
||||
]
|
||||
line_comments = ["// "]
|
||||
block_comment = ["/* ", " */"]
|
||||
brackets = [
|
||||
|
||||
131
extensions/glsl/src/glsl.rs
Normal file
131
extensions/glsl/src/glsl.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::fs;
|
||||
use zed::settings::LspSettings;
|
||||
use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
|
||||
|
||||
struct GlslExtension {
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
impl GlslExtension {
|
||||
fn language_server_binary_path(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<String> {
|
||||
if let Some(path) = worktree.which("glsl_analyzer") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
if let Some(path) = &self.cached_binary_path {
|
||||
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
let release = zed::latest_github_release(
|
||||
"nolanderc/glsl_analyzer",
|
||||
zed::GithubReleaseOptions {
|
||||
require_assets: true,
|
||||
pre_release: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
let (platform, arch) = zed::current_platform();
|
||||
let asset_name = format!(
|
||||
"{arch}-{os}.zip",
|
||||
arch = match arch {
|
||||
zed::Architecture::Aarch64 => "aarch64",
|
||||
zed::Architecture::X86 => "x86",
|
||||
zed::Architecture::X8664 => "x86_64",
|
||||
},
|
||||
os = match platform {
|
||||
zed::Os::Mac => "macos",
|
||||
zed::Os::Linux => "linux-musl",
|
||||
zed::Os::Windows => "windows",
|
||||
}
|
||||
);
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
|
||||
|
||||
let version_dir = format!("glsl_analyzer-{}", release.version);
|
||||
fs::create_dir_all(&version_dir)
|
||||
.map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?;
|
||||
let binary_path = format!("{version_dir}/bin/glsl_analyzer");
|
||||
|
||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
|
||||
zed::download_file(
|
||||
&asset.download_url,
|
||||
&version_dir,
|
||||
match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::Zip,
|
||||
zed::Os::Windows => zed::DownloadedFileType::Zip,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("failed to download file: {e}"))?;
|
||||
|
||||
zed::make_file_executable(&binary_path)?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
|
||||
if entry.file_name().to_str() != Some(&version_dir) {
|
||||
fs::remove_dir_all(&entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_binary_path = Some(binary_path.clone());
|
||||
Ok(binary_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for GlslExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
Ok(zed::Command {
|
||||
command: self.language_server_binary_path(language_server_id, worktree)?,
|
||||
args: vec![],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree("glsl_analyzer", worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.settings.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Some(serde_json::json!({
|
||||
"glsl_analyzer": settings
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(GlslExtension);
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_prisma"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "prisma"
|
||||
name = "Prisma"
|
||||
description = "Prisma support."
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
schema_version = 1
|
||||
authors = ["Matthew Gramigna <matthewgramigna@gmail.com>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
109
script/draft-release-notes
Executable file
109
script/draft-release-notes
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node --redirect-warnings=/dev/null
|
||||
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
let version = process.argv[2];
|
||||
let channel = process.argv[3];
|
||||
let parts = version.split(".");
|
||||
|
||||
if (
|
||||
process.argv.length != 4 ||
|
||||
parts.length != 3 ||
|
||||
parts.find((part) => isNaN(part)) != null ||
|
||||
(channel != "stable" && channel != "preview")
|
||||
) {
|
||||
console.log("Usage: draft-release-notes <version> {stable|preview}");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
|
||||
let suffix = "";
|
||||
|
||||
if (channel == "preview") {
|
||||
suffix = "-pre";
|
||||
if (parts[2] == 0) {
|
||||
priorVersion = [parts[0], parts[1] - 1, 0].join(".");
|
||||
}
|
||||
} else if (!tagExists("v${priorVersion}")) {
|
||||
console.log("Copy the release notes from preview.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
|
||||
|
||||
const newCommits = getCommits(priorTag, tag);
|
||||
|
||||
let releaseNotes = [];
|
||||
let missing = [];
|
||||
let skipped = [];
|
||||
|
||||
for (const commit of newCommits) {
|
||||
let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
|
||||
let notes = commit.releaseNotes;
|
||||
if (commit.pr == "") {
|
||||
link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
|
||||
} else if (!notes.includes("zed-industries/zed/issues")) {
|
||||
notes = notes + " ([#" + commit.pr + "](" + link + "))";
|
||||
}
|
||||
|
||||
if (commit.releaseNotes == "") {
|
||||
missing.push("- MISSING " + commit.firstLine + " " + link);
|
||||
} else if (commit.releaseNotes.startsWith("- N/A")) {
|
||||
skipped.push("- N/A " + commit.firstLine + " " + link);
|
||||
} else {
|
||||
releaseNotes.push(notes);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(releaseNotes.join("\n") + "\n");
|
||||
console.log("<!-- ");
|
||||
console.log(missing.join("\n"));
|
||||
console.log(skipped.join("\n"));
|
||||
console.log("-->");
|
||||
}
|
||||
|
||||
function getCommits(oldTag, newTag) {
|
||||
const pullRequestNumbers = execFileSync(
|
||||
"git",
|
||||
["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
|
||||
{ encoding: "utf8" },
|
||||
)
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("DIVIDER\n")
|
||||
.filter((commit) => commit.length > 0)
|
||||
.map((commit) => {
|
||||
let [hash, firstLine] = commit.split("\n")[0].split("|||");
|
||||
let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
|
||||
let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
|
||||
let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
|
||||
.split("\n\n")[0]
|
||||
.trim()
|
||||
.replace(/\n(?![\n-])/g, " ");
|
||||
|
||||
if (releaseNotes.includes("<public_issue_number_if_exists>")) {
|
||||
releaseNotes = "";
|
||||
}
|
||||
|
||||
return {
|
||||
hash,
|
||||
pr,
|
||||
cherryPick,
|
||||
releaseNotes,
|
||||
firstLine,
|
||||
};
|
||||
});
|
||||
|
||||
return pullRequestNumbers;
|
||||
}
|
||||
|
||||
function tagExists(tag) {
|
||||
try {
|
||||
execFileSync("git", ["rev-parse", "--verify", tag]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Reference in New Issue
Block a user