git: Handle shift-click to stage a range of entries in the panel (#34296)
Release Notes: - git: shift-click can now be used to stage a range of entries in the git panel.
This commit is contained in:
parent
833bc6979a
commit
970a1066f5
7 changed files with 373 additions and 149 deletions
|
@ -856,6 +856,7 @@
|
||||||
"alt-shift-y": "git::UnstageFile",
|
"alt-shift-y": "git::UnstageFile",
|
||||||
"ctrl-alt-y": "git::ToggleStaged",
|
"ctrl-alt-y": "git::ToggleStaged",
|
||||||
"space": "git::ToggleStaged",
|
"space": "git::ToggleStaged",
|
||||||
|
"shift-space": "git::StageRange",
|
||||||
"tab": "git_panel::FocusEditor",
|
"tab": "git_panel::FocusEditor",
|
||||||
"shift-tab": "git_panel::FocusEditor",
|
"shift-tab": "git_panel::FocusEditor",
|
||||||
"escape": "git_panel::ToggleFocus",
|
"escape": "git_panel::ToggleFocus",
|
||||||
|
|
|
@ -930,6 +930,7 @@
|
||||||
"enter": "menu::Confirm",
|
"enter": "menu::Confirm",
|
||||||
"cmd-alt-y": "git::ToggleStaged",
|
"cmd-alt-y": "git::ToggleStaged",
|
||||||
"space": "git::ToggleStaged",
|
"space": "git::ToggleStaged",
|
||||||
|
"shift-space": "git::StageRange",
|
||||||
"cmd-y": "git::StageFile",
|
"cmd-y": "git::StageFile",
|
||||||
"cmd-shift-y": "git::UnstageFile",
|
"cmd-shift-y": "git::UnstageFile",
|
||||||
"alt-down": "git_panel::FocusEditor",
|
"alt-down": "git_panel::FocusEditor",
|
||||||
|
|
|
@ -841,6 +841,7 @@
|
||||||
"i": "git_panel::FocusEditor",
|
"i": "git_panel::FocusEditor",
|
||||||
"x": "git::ToggleStaged",
|
"x": "git::ToggleStaged",
|
||||||
"shift-x": "git::StageAll",
|
"shift-x": "git::StageAll",
|
||||||
|
"g x": "git::StageRange",
|
||||||
"shift-u": "git::UnstageAll"
|
"shift-u": "git::UnstageAll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::FakeFs;
|
use crate::{FakeFs, Fs};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use futures::future::{self, BoxFuture};
|
use futures::future::{self, BoxFuture, join_all};
|
||||||
use git::{
|
use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
repository::{
|
repository::{
|
||||||
|
@ -356,18 +356,46 @@ impl GitRepository for FakeGitRepository {
|
||||||
|
|
||||||
fn stage_paths(
|
fn stage_paths(
|
||||||
&self,
|
&self,
|
||||||
_paths: Vec<RepoPath>,
|
paths: Vec<RepoPath>,
|
||||||
_env: Arc<HashMap<String, String>>,
|
_env: Arc<HashMap<String, String>>,
|
||||||
) -> BoxFuture<'_, Result<()>> {
|
) -> BoxFuture<'_, Result<()>> {
|
||||||
unimplemented!()
|
Box::pin(async move {
|
||||||
|
let contents = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let abs_path = self.dot_git_path.parent().unwrap().join(&path);
|
||||||
|
Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let contents = join_all(contents).await;
|
||||||
|
self.with_state_async(true, move |state| {
|
||||||
|
for (path, content) in contents {
|
||||||
|
if let Some(content) = content {
|
||||||
|
state.index_contents.insert(path, content);
|
||||||
|
} else {
|
||||||
|
state.index_contents.remove(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(
|
fn unstage_paths(
|
||||||
&self,
|
&self,
|
||||||
_paths: Vec<RepoPath>,
|
paths: Vec<RepoPath>,
|
||||||
_env: Arc<HashMap<String, String>>,
|
_env: Arc<HashMap<String, String>>,
|
||||||
) -> BoxFuture<'_, Result<()>> {
|
) -> BoxFuture<'_, Result<()>> {
|
||||||
unimplemented!()
|
self.with_state_async(true, move |state| {
|
||||||
|
for path in paths {
|
||||||
|
match state.head_contents.get(&path) {
|
||||||
|
Some(content) => state.index_contents.insert(path, content.clone()),
|
||||||
|
None => state.index_contents.remove(&path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(
|
fn commit(
|
||||||
|
|
|
@ -31,8 +31,10 @@ actions!(
|
||||||
git,
|
git,
|
||||||
[
|
[
|
||||||
// per-hunk
|
// per-hunk
|
||||||
/// Toggles the staged state of the hunk at cursor.
|
/// Toggles the staged state of the hunk or status entry at cursor.
|
||||||
ToggleStaged,
|
ToggleStaged,
|
||||||
|
/// Stage status entries between an anchor entry and the cursor.
|
||||||
|
StageRange,
|
||||||
/// Stages the current hunk and moves to the next one.
|
/// Stages the current hunk and moves to the next one.
|
||||||
StageAndNext,
|
StageAndNext,
|
||||||
/// Unstages the current hunk and moves to the next one.
|
/// Unstages the current hunk and moves to the next one.
|
||||||
|
|
|
@ -30,10 +30,9 @@ use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
|
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
|
||||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||||
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
|
||||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
|
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
|
||||||
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
|
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
|
||||||
uniform_list,
|
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{Buffer, File};
|
use language::{Buffer, File};
|
||||||
|
@ -48,7 +47,7 @@ use panel::{
|
||||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||||
panel_icon_button,
|
panel_icon_button,
|
||||||
};
|
};
|
||||||
use project::git_store::RepositoryEvent;
|
use project::git_store::{RepositoryEvent, RepositoryId};
|
||||||
use project::{
|
use project::{
|
||||||
Fs, Project, ProjectPath,
|
Fs, Project, ProjectPath,
|
||||||
git_store::{GitStoreEvent, Repository},
|
git_store::{GitStoreEvent, Repository},
|
||||||
|
@ -212,14 +211,14 @@ impl GitHeaderEntry {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
enum GitListEntry {
|
enum GitListEntry {
|
||||||
GitStatusEntry(GitStatusEntry),
|
Status(GitStatusEntry),
|
||||||
Header(GitHeaderEntry),
|
Header(GitHeaderEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitListEntry {
|
impl GitListEntry {
|
||||||
fn status_entry(&self) -> Option<&GitStatusEntry> {
|
fn status_entry(&self) -> Option<&GitStatusEntry> {
|
||||||
match self {
|
match self {
|
||||||
GitListEntry::GitStatusEntry(entry) => Some(entry),
|
GitListEntry::Status(entry) => Some(entry),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,7 +322,6 @@ pub struct GitPanel {
|
||||||
pub(crate) commit_editor: Entity<Editor>,
|
pub(crate) commit_editor: Entity<Editor>,
|
||||||
conflicted_count: usize,
|
conflicted_count: usize,
|
||||||
conflicted_staged_count: usize,
|
conflicted_staged_count: usize,
|
||||||
current_modifiers: Modifiers,
|
|
||||||
add_coauthors: bool,
|
add_coauthors: bool,
|
||||||
generate_commit_message_task: Option<Task<Option<()>>>,
|
generate_commit_message_task: Option<Task<Option<()>>>,
|
||||||
entries: Vec<GitListEntry>,
|
entries: Vec<GitListEntry>,
|
||||||
|
@ -355,9 +353,16 @@ pub struct GitPanel {
|
||||||
show_placeholders: bool,
|
show_placeholders: bool,
|
||||||
local_committer: Option<GitCommitter>,
|
local_committer: Option<GitCommitter>,
|
||||||
local_committer_task: Option<Task<()>>,
|
local_committer_task: Option<Task<()>>,
|
||||||
|
bulk_staging: Option<BulkStaging>,
|
||||||
_settings_subscription: Subscription,
|
_settings_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
struct BulkStaging {
|
||||||
|
repo_id: RepositoryId,
|
||||||
|
anchor: RepoPath,
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_PANEL_EDITOR_LINES: usize = 6;
|
const MAX_PANEL_EDITOR_LINES: usize = 6;
|
||||||
|
|
||||||
pub(crate) fn commit_message_editor(
|
pub(crate) fn commit_message_editor(
|
||||||
|
@ -497,7 +502,6 @@ impl GitPanel {
|
||||||
commit_editor,
|
commit_editor,
|
||||||
conflicted_count: 0,
|
conflicted_count: 0,
|
||||||
conflicted_staged_count: 0,
|
conflicted_staged_count: 0,
|
||||||
current_modifiers: window.modifiers(),
|
|
||||||
add_coauthors: true,
|
add_coauthors: true,
|
||||||
generate_commit_message_task: None,
|
generate_commit_message_task: None,
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
|
@ -529,6 +533,7 @@ impl GitPanel {
|
||||||
entry_count: 0,
|
entry_count: 0,
|
||||||
horizontal_scrollbar,
|
horizontal_scrollbar,
|
||||||
vertical_scrollbar,
|
vertical_scrollbar,
|
||||||
|
bulk_staging: None,
|
||||||
_settings_subscription,
|
_settings_subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -735,16 +740,6 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_modifiers_changed(
|
|
||||||
&mut self,
|
|
||||||
event: &ModifiersChangedEvent,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.current_modifiers = event.modifiers;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
|
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
|
||||||
if let Some(selected_entry) = self.selected_entry {
|
if let Some(selected_entry) = self.selected_entry {
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
|
@ -1265,10 +1260,18 @@ impl GitPanel {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let (stage, repo_paths) = match entry {
|
let (stage, repo_paths) = match entry {
|
||||||
GitListEntry::GitStatusEntry(status_entry) => {
|
GitListEntry::Status(status_entry) => {
|
||||||
if status_entry.status.staging().is_fully_staged() {
|
if status_entry.status.staging().is_fully_staged() {
|
||||||
|
if let Some(op) = self.bulk_staging.clone()
|
||||||
|
&& op.anchor == status_entry.repo_path
|
||||||
|
{
|
||||||
|
self.bulk_staging = None;
|
||||||
|
}
|
||||||
|
|
||||||
(false, vec![status_entry.clone()])
|
(false, vec![status_entry.clone()])
|
||||||
} else {
|
} else {
|
||||||
|
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
|
||||||
|
|
||||||
(true, vec![status_entry.clone()])
|
(true, vec![status_entry.clone()])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1383,6 +1386,13 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(index) = self.selected_entry else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.stage_bulk(index, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
|
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(selected_entry) = self.get_selected_entry() else {
|
let Some(selected_entry) = self.get_selected_entry() else {
|
||||||
return;
|
return;
|
||||||
|
@ -2449,6 +2459,11 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let bulk_staging = self.bulk_staging.take();
|
||||||
|
let last_staged_path_prev_index = bulk_staging
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|op| self.entry_by_path(&op.anchor, cx));
|
||||||
|
|
||||||
self.entries.clear();
|
self.entries.clear();
|
||||||
self.single_staged_entry.take();
|
self.single_staged_entry.take();
|
||||||
self.single_tracked_entry.take();
|
self.single_tracked_entry.take();
|
||||||
|
@ -2465,7 +2480,7 @@ impl GitPanel {
|
||||||
let mut changed_entries = Vec::new();
|
let mut changed_entries = Vec::new();
|
||||||
let mut new_entries = Vec::new();
|
let mut new_entries = Vec::new();
|
||||||
let mut conflict_entries = Vec::new();
|
let mut conflict_entries = Vec::new();
|
||||||
let mut last_staged = None;
|
let mut single_staged_entry = None;
|
||||||
let mut staged_count = 0;
|
let mut staged_count = 0;
|
||||||
let mut max_width_item: Option<(RepoPath, usize)> = None;
|
let mut max_width_item: Option<(RepoPath, usize)> = None;
|
||||||
|
|
||||||
|
@ -2503,7 +2518,7 @@ impl GitPanel {
|
||||||
|
|
||||||
if staging.has_staged() {
|
if staging.has_staged() {
|
||||||
staged_count += 1;
|
staged_count += 1;
|
||||||
last_staged = Some(entry.clone());
|
single_staged_entry = Some(entry.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let width_estimate = Self::item_width_estimate(
|
let width_estimate = Self::item_width_estimate(
|
||||||
|
@ -2534,27 +2549,27 @@ impl GitPanel {
|
||||||
|
|
||||||
let mut pending_staged_count = 0;
|
let mut pending_staged_count = 0;
|
||||||
let mut last_pending_staged = None;
|
let mut last_pending_staged = None;
|
||||||
let mut pending_status_for_last_staged = None;
|
let mut pending_status_for_single_staged = None;
|
||||||
for pending in self.pending.iter() {
|
for pending in self.pending.iter() {
|
||||||
if pending.target_status == TargetStatus::Staged {
|
if pending.target_status == TargetStatus::Staged {
|
||||||
pending_staged_count += pending.entries.len();
|
pending_staged_count += pending.entries.len();
|
||||||
last_pending_staged = pending.entries.iter().next().cloned();
|
last_pending_staged = pending.entries.iter().next().cloned();
|
||||||
}
|
}
|
||||||
if let Some(last_staged) = &last_staged {
|
if let Some(single_staged) = &single_staged_entry {
|
||||||
if pending
|
if pending
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.any(|entry| entry.repo_path == last_staged.repo_path)
|
.any(|entry| entry.repo_path == single_staged.repo_path)
|
||||||
{
|
{
|
||||||
pending_status_for_last_staged = Some(pending.target_status);
|
pending_status_for_single_staged = Some(pending.target_status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
|
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
|
||||||
match pending_status_for_last_staged {
|
match pending_status_for_single_staged {
|
||||||
Some(TargetStatus::Staged) | None => {
|
Some(TargetStatus::Staged) | None => {
|
||||||
self.single_staged_entry = last_staged;
|
self.single_staged_entry = single_staged_entry;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -2570,11 +2585,8 @@ impl GitPanel {
|
||||||
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
||||||
header: Section::Conflict,
|
header: Section::Conflict,
|
||||||
}));
|
}));
|
||||||
self.entries.extend(
|
self.entries
|
||||||
conflict_entries
|
.extend(conflict_entries.into_iter().map(GitListEntry::Status));
|
||||||
.into_iter()
|
|
||||||
.map(GitListEntry::GitStatusEntry),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed_entries.len() > 0 {
|
if changed_entries.len() > 0 {
|
||||||
|
@ -2583,31 +2595,39 @@ impl GitPanel {
|
||||||
header: Section::Tracked,
|
header: Section::Tracked,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
self.entries.extend(
|
self.entries
|
||||||
changed_entries
|
.extend(changed_entries.into_iter().map(GitListEntry::Status));
|
||||||
.into_iter()
|
|
||||||
.map(GitListEntry::GitStatusEntry),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if new_entries.len() > 0 {
|
if new_entries.len() > 0 {
|
||||||
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
||||||
header: Section::New,
|
header: Section::New,
|
||||||
}));
|
}));
|
||||||
self.entries
|
self.entries
|
||||||
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
|
.extend(new_entries.into_iter().map(GitListEntry::Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((repo_path, _)) = max_width_item {
|
if let Some((repo_path, _)) = max_width_item {
|
||||||
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
|
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
|
||||||
GitListEntry::GitStatusEntry(git_status_entry) => {
|
GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path,
|
||||||
git_status_entry.repo_path == repo_path
|
|
||||||
}
|
|
||||||
GitListEntry::Header(_) => false,
|
GitListEntry::Header(_) => false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_counts(repo);
|
self.update_counts(repo);
|
||||||
|
|
||||||
|
let bulk_staging_anchor_new_index = bulk_staging
|
||||||
|
.as_ref()
|
||||||
|
.filter(|op| op.repo_id == repo.id)
|
||||||
|
.and_then(|op| self.entry_by_path(&op.anchor, cx));
|
||||||
|
if bulk_staging_anchor_new_index == last_staged_path_prev_index
|
||||||
|
&& let Some(index) = bulk_staging_anchor_new_index
|
||||||
|
&& let Some(entry) = self.entries.get(index)
|
||||||
|
&& let Some(entry) = entry.status_entry()
|
||||||
|
&& self.entry_staging(entry) == StageStatus::Staged
|
||||||
|
{
|
||||||
|
self.bulk_staging = bulk_staging;
|
||||||
|
}
|
||||||
|
|
||||||
self.select_first_entry_if_none(cx);
|
self.select_first_entry_if_none(cx);
|
||||||
|
|
||||||
let suggested_commit_message = self.suggest_commit_message(cx);
|
let suggested_commit_message = self.suggest_commit_message(cx);
|
||||||
|
@ -3743,7 +3763,7 @@ impl GitPanel {
|
||||||
|
|
||||||
for ix in range {
|
for ix in range {
|
||||||
match &this.entries.get(ix) {
|
match &this.entries.get(ix) {
|
||||||
Some(GitListEntry::GitStatusEntry(entry)) => {
|
Some(GitListEntry::Status(entry)) => {
|
||||||
items.push(this.render_entry(
|
items.push(this.render_entry(
|
||||||
ix,
|
ix,
|
||||||
entry,
|
entry,
|
||||||
|
@ -4000,8 +4020,6 @@ impl GitPanel {
|
||||||
let marked = self.marked_entries.contains(&ix);
|
let marked = self.marked_entries.contains(&ix);
|
||||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||||
let status = entry.status;
|
let status = entry.status;
|
||||||
let modifiers = self.current_modifiers;
|
|
||||||
let shift_held = modifiers.shift;
|
|
||||||
|
|
||||||
let has_conflict = status.is_conflicted();
|
let has_conflict = status.is_conflicted();
|
||||||
let is_modified = status.is_modified();
|
let is_modified = status.is_modified();
|
||||||
|
@ -4120,12 +4138,6 @@ impl GitPanel {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// .on_secondary_mouse_down(cx.listener(
|
|
||||||
// move |this, event: &MouseDownEvent, window, cx| {
|
|
||||||
// this.deploy_entry_context_menu(event.position, ix, window, cx);
|
|
||||||
// cx.stop_propagation();
|
|
||||||
// },
|
|
||||||
// ))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id(checkbox_wrapper_id)
|
.id(checkbox_wrapper_id)
|
||||||
|
@ -4137,46 +4149,35 @@ impl GitPanel {
|
||||||
.disabled(!has_write_access)
|
.disabled(!has_write_access)
|
||||||
.fill()
|
.fill()
|
||||||
.elevation(ElevationIndex::Surface)
|
.elevation(ElevationIndex::Surface)
|
||||||
.on_click({
|
.on_click_ext({
|
||||||
let entry = entry.clone();
|
let entry = entry.clone();
|
||||||
cx.listener(move |this, _, window, cx| {
|
let this = cx.weak_entity();
|
||||||
if !has_write_access {
|
move |_, click, window, cx| {
|
||||||
return;
|
this.update(cx, |this, cx| {
|
||||||
}
|
if !has_write_access {
|
||||||
this.toggle_staged_for_entry(
|
return;
|
||||||
&GitListEntry::GitStatusEntry(entry.clone()),
|
}
|
||||||
window,
|
if click.modifiers().shift {
|
||||||
cx,
|
this.stage_bulk(ix, cx);
|
||||||
);
|
} else {
|
||||||
cx.stop_propagation();
|
this.toggle_staged_for_entry(
|
||||||
})
|
&GitListEntry::Status(entry.clone()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cx.stop_propagation();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.tooltip(move |window, cx| {
|
.tooltip(move |window, cx| {
|
||||||
let is_staged = entry_staging.is_fully_staged();
|
let is_staged = entry_staging.is_fully_staged();
|
||||||
|
|
||||||
let action = if is_staged { "Unstage" } else { "Stage" };
|
let action = if is_staged { "Unstage" } else { "Stage" };
|
||||||
let tooltip_name = if shift_held {
|
let tooltip_name = action.to_string();
|
||||||
format!("{} section", action)
|
|
||||||
} else {
|
|
||||||
action.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let meta = if shift_held {
|
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
|
||||||
format!(
|
|
||||||
"Release shift to {} single entry",
|
|
||||||
action.to_lowercase()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("Shift click to {} section", action.to_lowercase())
|
|
||||||
};
|
|
||||||
|
|
||||||
Tooltip::with_meta(
|
|
||||||
tooltip_name,
|
|
||||||
Some(&ToggleStaged),
|
|
||||||
meta,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -4242,6 +4243,41 @@ impl GitPanel {
|
||||||
panel
|
panel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) {
|
||||||
|
let Some(op) = self.bulk_staging.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(entry) = self.entries.get(index)
|
||||||
|
&& let Some(entry) = entry.status_entry()
|
||||||
|
{
|
||||||
|
self.set_bulk_staging_anchor(entry.repo_path.clone(), cx);
|
||||||
|
}
|
||||||
|
if index < anchor_index {
|
||||||
|
std::mem::swap(&mut index, &mut anchor_index);
|
||||||
|
}
|
||||||
|
let entries = self
|
||||||
|
.entries
|
||||||
|
.get(anchor_index..=index)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| entry.status_entry().cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.change_file_stage(true, entries, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) {
|
||||||
|
let Some(repo) = self.active_repository.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.bulk_staging = Some(BulkStaging {
|
||||||
|
repo_id: repo.read(cx).id,
|
||||||
|
anchor: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||||
|
@ -4279,9 +4315,9 @@ impl Render for GitPanel {
|
||||||
.id("git_panel")
|
.id("git_panel")
|
||||||
.key_context(self.dispatch_context(window, cx))
|
.key_context(self.dispatch_context(window, cx))
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
|
||||||
.when(has_write_access && !project.is_read_only(cx), |this| {
|
.when(has_write_access && !project.is_read_only(cx), |this| {
|
||||||
this.on_action(cx.listener(Self::toggle_staged_for_selected))
|
this.on_action(cx.listener(Self::toggle_staged_for_selected))
|
||||||
|
.on_action(cx.listener(Self::stage_range))
|
||||||
.on_action(cx.listener(GitPanel::commit))
|
.on_action(cx.listener(GitPanel::commit))
|
||||||
.on_action(cx.listener(GitPanel::amend))
|
.on_action(cx.listener(GitPanel::amend))
|
||||||
.on_action(cx.listener(GitPanel::cancel))
|
.on_action(cx.listener(GitPanel::cancel))
|
||||||
|
@ -4953,7 +4989,7 @@ impl Component for PanelRepoFooter {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use git::status::StatusCode;
|
use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
|
||||||
use gpui::{TestAppContext, VisualTestContext};
|
use gpui::{TestAppContext, VisualTestContext};
|
||||||
use project::{FakeFs, WorktreeSettings};
|
use project::{FakeFs, WorktreeSettings};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -5052,13 +5088,13 @@ mod tests {
|
||||||
GitListEntry::Header(GitHeaderEntry {
|
GitListEntry::Header(GitHeaderEntry {
|
||||||
header: Section::Tracked
|
header: Section::Tracked
|
||||||
}),
|
}),
|
||||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
GitListEntry::Status(GitStatusEntry {
|
||||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||||
repo_path: "crates/gpui/gpui.rs".into(),
|
repo_path: "crates/gpui/gpui.rs".into(),
|
||||||
status: StatusCode::Modified.worktree(),
|
status: StatusCode::Modified.worktree(),
|
||||||
staging: StageStatus::Unstaged,
|
staging: StageStatus::Unstaged,
|
||||||
}),
|
}),
|
||||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
GitListEntry::Status(GitStatusEntry {
|
||||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||||
repo_path: "crates/util/util.rs".into(),
|
repo_path: "crates/util/util.rs".into(),
|
||||||
status: StatusCode::Modified.worktree(),
|
status: StatusCode::Modified.worktree(),
|
||||||
|
@ -5067,54 +5103,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO(cole) restore this once repository deduplication is implemented properly.
|
|
||||||
//cx.update_window_entity(&panel, |panel, window, cx| {
|
|
||||||
// panel.select_last(&Default::default(), window, cx);
|
|
||||||
// assert_eq!(panel.selected_entry, Some(2));
|
|
||||||
// panel.open_diff(&Default::default(), window, cx);
|
|
||||||
//});
|
|
||||||
//cx.run_until_parked();
|
|
||||||
|
|
||||||
//let worktree_roots = workspace.update(cx, |workspace, cx| {
|
|
||||||
// workspace
|
|
||||||
// .worktrees(cx)
|
|
||||||
// .map(|worktree| worktree.read(cx).abs_path())
|
|
||||||
// .collect::<Vec<_>>()
|
|
||||||
//});
|
|
||||||
//pretty_assertions::assert_eq!(
|
|
||||||
// worktree_roots,
|
|
||||||
// vec![
|
|
||||||
// Path::new(path!("/root/zed/crates/gpui")).into(),
|
|
||||||
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
|
|
||||||
// ]
|
|
||||||
//);
|
|
||||||
|
|
||||||
//project.update(cx, |project, cx| {
|
|
||||||
// let git_store = project.git_store().read(cx);
|
|
||||||
// // The repo that comes from the single-file worktree can't be selected through the UI.
|
|
||||||
// let filtered_entries = filtered_repository_entries(git_store, cx)
|
|
||||||
// .iter()
|
|
||||||
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
|
|
||||||
// .collect::<Vec<_>>();
|
|
||||||
// assert_eq!(
|
|
||||||
// filtered_entries,
|
|
||||||
// [Path::new(path!("/root/zed/crates/gpui")).into()]
|
|
||||||
// );
|
|
||||||
// // But we can select it artificially here.
|
|
||||||
// let repo_from_single_file_worktree = git_store
|
|
||||||
// .repositories()
|
|
||||||
// .values()
|
|
||||||
// .find(|repo| {
|
|
||||||
// repo.read(cx).worktree_abs_path.as_ref()
|
|
||||||
// == Path::new(path!("/root/zed/crates/util/util.rs"))
|
|
||||||
// })
|
|
||||||
// .unwrap()
|
|
||||||
// .clone();
|
|
||||||
|
|
||||||
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
|
|
||||||
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
|
|
||||||
//});
|
|
||||||
|
|
||||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||||
});
|
});
|
||||||
|
@ -5127,13 +5115,13 @@ mod tests {
|
||||||
GitListEntry::Header(GitHeaderEntry {
|
GitListEntry::Header(GitHeaderEntry {
|
||||||
header: Section::Tracked
|
header: Section::Tracked
|
||||||
}),
|
}),
|
||||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
GitListEntry::Status(GitStatusEntry {
|
||||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||||
repo_path: "crates/gpui/gpui.rs".into(),
|
repo_path: "crates/gpui/gpui.rs".into(),
|
||||||
status: StatusCode::Modified.worktree(),
|
status: StatusCode::Modified.worktree(),
|
||||||
staging: StageStatus::Unstaged,
|
staging: StageStatus::Unstaged,
|
||||||
}),
|
}),
|
||||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
GitListEntry::Status(GitStatusEntry {
|
||||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||||
repo_path: "crates/util/util.rs".into(),
|
repo_path: "crates/util/util.rs".into(),
|
||||||
status: StatusCode::Modified.worktree(),
|
status: StatusCode::Modified.worktree(),
|
||||||
|
@ -5142,4 +5130,196 @@ mod tests {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_bulk_staging(cx: &mut TestAppContext) {
|
||||||
|
use GitListEntry::*;
|
||||||
|
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.background_executor.clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"project": {
|
||||||
|
".git": {},
|
||||||
|
"src": {
|
||||||
|
"main.rs": "fn main() {}",
|
||||||
|
"lib.rs": "pub fn hello() {}",
|
||||||
|
"utils.rs": "pub fn util() {}"
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"test.rs": "fn test() {}"
|
||||||
|
},
|
||||||
|
"new_file.txt": "new content",
|
||||||
|
"another_new.rs": "// new file",
|
||||||
|
"conflict.txt": "conflicted content"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
fs.set_status_for_repo(
|
||||||
|
Path::new(path!("/root/project/.git")),
|
||||||
|
&[
|
||||||
|
(Path::new("src/main.rs"), StatusCode::Modified.worktree()),
|
||||||
|
(Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
|
||||||
|
(Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
|
||||||
|
(Path::new("new_file.txt"), FileStatus::Untracked),
|
||||||
|
(Path::new("another_new.rs"), FileStatus::Untracked),
|
||||||
|
(Path::new("src/utils.rs"), FileStatus::Untracked),
|
||||||
|
(
|
||||||
|
Path::new("conflict.txt"),
|
||||||
|
UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
|
||||||
|
let workspace =
|
||||||
|
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||||
|
|
||||||
|
cx.read(|cx| {
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.nth(0)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.as_local()
|
||||||
|
.unwrap()
|
||||||
|
.scan_complete()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let panel = workspace.update(cx, GitPanel::new).unwrap();
|
||||||
|
|
||||||
|
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||||
|
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||||
|
handle.await;
|
||||||
|
|
||||||
|
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pretty_assertions::assert_matches!(
|
||||||
|
entries.as_slice(),
|
||||||
|
&[
|
||||||
|
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::New }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let second_status_entry = entries[3].clone();
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.toggle_staged_for_entry(&second_status_entry, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.selected_entry = Some(7);
|
||||||
|
panel.stage_range(&git::StageRange, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.read(|cx| {
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.nth(0)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.as_local()
|
||||||
|
.unwrap()
|
||||||
|
.scan_complete()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||||
|
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||||
|
handle.await;
|
||||||
|
|
||||||
|
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pretty_assertions::assert_matches!(
|
||||||
|
entries.as_slice(),
|
||||||
|
&[
|
||||||
|
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::New }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let third_status_entry = entries[4].clone();
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.toggle_staged_for_entry(&third_status_entry, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.selected_entry = Some(9);
|
||||||
|
panel.stage_range(&git::StageRange, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.read(|cx| {
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.nth(0)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.as_local()
|
||||||
|
.unwrap()
|
||||||
|
.scan_complete()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||||
|
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||||
|
handle.await;
|
||||||
|
|
||||||
|
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pretty_assertions::assert_matches!(
|
||||||
|
entries.as_slice(),
|
||||||
|
&[
|
||||||
|
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Header(GitHeaderEntry { header: Section::New }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
|
AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ pub struct Checkbox {
|
||||||
toggle_state: ToggleState,
|
toggle_state: ToggleState,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
placeholder: bool,
|
placeholder: bool,
|
||||||
on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||||
filled: bool,
|
filled: bool,
|
||||||
style: ToggleStyle,
|
style: ToggleStyle,
|
||||||
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
|
||||||
|
@ -83,6 +84,16 @@ impl Checkbox {
|
||||||
pub fn on_click(
|
pub fn on_click(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_click = Some(Box::new(move |state, _, window, cx| {
|
||||||
|
handler(state, window, cx)
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_click_ext(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.on_click = Some(Box::new(handler));
|
self.on_click = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
|
@ -226,8 +237,8 @@ impl RenderOnce for Checkbox {
|
||||||
.when_some(
|
.when_some(
|
||||||
self.on_click.filter(|_| !self.disabled),
|
self.on_click.filter(|_| !self.disabled),
|
||||||
|this, on_click| {
|
|this, on_click| {
|
||||||
this.on_click(move |_, window, cx| {
|
this.on_click(move |click, window, cx| {
|
||||||
on_click(&self.toggle_state.inverse(), window, cx)
|
on_click(&self.toggle_state.inverse(), click, window, cx)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue