Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split widget callbacks into update and commit so only the latter adds a history state #1584

Merged
merged 4 commits into from
Feb 5, 2024

Conversation

zhiyuang
Copy link
Contributor

Closes #972

try to fix ColorButton and NumberInput widgets.

Copy link
Member

@0HyperCube 0HyperCube left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this amazing editing improvement.

return;
}

responses.add(DocumentMessage::StartTransaction);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears this is mistakenly adding undo commits when modifying the tool options (e.g. dragging the weight and stroke colour with the line tool at the top).

tool options

Copy link
Contributor Author

@zhiyuang zhiyuang Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i see. I missed this situation, maybe I need to do something like on_update callback

@Keavon Keavon changed the title Try to Split Update and Commit Layout to handle undo operations Split Update and Commit Layout to avoid spamming history steps Jan 26, 2024
@GraphiteEditor GraphiteEditor deleted a comment from github-actions bot Jan 27, 2024
Copy link

📦 Build Complete for c32103c
https://766f7f4e.graphite.pages.dev

@Keavon
Copy link
Member

Keavon commented Jan 27, 2024

I'll give this a detailed code review tomorrow, bedtime now though! Thanks @0HyperCube for the review so far.

{/if}
{@const colorInput = narrowWidgetProps(component.props, "ColorButton")}
{#if colorInput}
<ColorButton {...exclude(colorInput)} on:value={({ detail }) => updateLayout(index, detail)} />
<ColorButton {...exclude(colorInput)} on:value={({ detail }) => updateLayout(index, detail)} on:start={() => commitLayout(index)} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please help me understand why we are committing the layout upon the start of the user's dragging? Why doesn't this happen upon completion of the dragging?

Copy link
Contributor Author

@zhiyuang zhiyuang Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it also seems unreasonable to me first. After tried myself, I try to understand like this: for undo history, we always need to commit a undo history and keep a snapshot before we begin new changes. Then upon the start of dragging means commit a undo history before user begin new dragging changes(then we can properly rollback to this history).

Actually we commit the layout of a last status, not a layout changed by this widget. Maybe we could consider change a name of this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But doesn't that mean that we don't save a history step upon completing the drag? So if you change a number from 1 to 10 by dragging its NumberInput slider, that value of 10 never gets saved as a history step? So the user could move on to other things, and if they undo several times, the history jumps back two operations at once (the operation that moved from 1 to 10, and whatever the other operation was adjacent to that).

Copy link
Contributor Author

@zhiyuang zhiyuang Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But doesn't that mean that we don't save a history step upon completing the drag?

saving a history step upon completing the drag is a responsibility belongs to the next widget's commit_layout or next action that trigger Document.startTransaction. That's also why common widgets use updateAndCommitLayout.

Copy link
Contributor Author

@zhiyuang zhiyuang Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if you change a number from 1 to 10 by dragging its NumberInput slider, that value of 10 never gets saved as a history step?

e.g. if user's next action is to add a path using pen tool. During we adding a path, we will trigger Document.StartTransanction then save a history step. (Our existing history related code have handled this properly I think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see! I think part of my confusion is because I don't have a full understanding of how StartTransaction/AbortTransaction/CommitTransaction work (since the original author of that never documented them). I did test this all out and it seems to work well so I trust that you're right about this. I'll keep going with my code review.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommitTransaction is a noop. StartTransaction clones the document and stores it in a list. AbortTransaction pops a document from the list and makes it current.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting that CommitTransaction is a noop. Is there a reason we have it? Do you think we should we just remove it, then find more intuitive names for the remaining two?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your welcome to modify it.

Comment on lines 52 to 64
let layout = if let Some(layout) = self.layouts.get_mut(layout_target as usize) {
layout
} else {
warn!("CommitLayout was called referencing an invalid layout. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};

let widget_holder = if let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) {
widget_holder
} else {
warn!("CommitLayout was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
Copy link
Member

@Keavon Keavon Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be simplified with let else syntax.

Suggested change
let layout = if let Some(layout) = self.layouts.get_mut(layout_target as usize) {
layout
} else {
warn!("CommitLayout was called referencing an invalid layout. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
let widget_holder = if let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) {
widget_holder
} else {
warn!("CommitLayout was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
let Some(layout) = self.layouts.get_mut(layout_target as usize) else {
warn!("CommitLayout was called referencing an invalid layout. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else {
warn!("CommitLayout was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};

return;
};

#[remain::sorted]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of having this match statement for all the widget types exist twice (also on lines 163-291). Would you mind breaking it out into a function and combining the two use cases into one so it does the update or commit logic depending on the function's arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I could refactor it. But not that quick, I will find some time later today.

@Keavon Keavon changed the title Split Update and Commit Layout to avoid spamming history steps Split widget callbacks into update and commit so only the latter adds a history state Feb 5, 2024
@Keavon
Copy link
Member

Keavon commented Feb 5, 2024

I have a couple remaining improvements that I have in mind related to this feature but I think it's better to merge this today and save those as follow-ups. But here are my comments for those:

  • I'd like it so hovering over the entries of a DropdownMenu sends value (but not commit) updates, so dropdowns like Blend Mode could be previewed. Also, same for TextInput and TextAreaInput that should send updates as the user types but only commits when that currently happens, so some things are able to display updates live on a case-by-case basis.
  • This may pertain a bit to our discussion above about start, but I'd like to have color changes saved to history. So if you edit a color, once you release the mouse after dragging any of the sliders, it should send a commit at that time.

@Keavon Keavon merged commit 9530e55 into GraphiteEditor:master Feb 5, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make draggable widgets set only one undo/redo state upon committing a value
3 participants