Skip to content

Commit f63b0ab

Browse files
0HyperCubeKeavon
authored andcommitted
Make scrollbars interactable (#328)
* Make scrollbars interactable * Add watcher for position change * Fix case of data * Fix updateHandlePosition capitalization * Clean up class name thing * Scroll bars between 0 and 1 * Allow width to be 100% * Scrollbars reflect backend * Include viewport in scrollbar * Add half viewport padding for scrollbars * Refactor scrollbar using lerp * Send messages to backend * Refactor * Use glam::DVec2 * Remove glam:: * Remove unnecessary abs * Add TrueDoctor's change * Add missing minus * Fix vue issues * Fix viewport size * Remove unnecessary log * Linear dragging
1 parent 5c36242 commit f63b0ab

File tree

8 files changed

+183
-45
lines changed

8 files changed

+183
-45
lines changed

editor/src/communication/dispatcher.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ impl Dispatcher {
2727
| Message::InputMapper(_)
2828
| Message::Documents(DocumentsMessage::Document(DocumentMessage::RenderDocument))
2929
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
30+
| Message::Frontend(FrontendMessage::UpdateScrollbars { .. })
3031
| Message::Frontend(FrontendMessage::SetCanvasZoom { .. })
3132
| Message::Frontend(FrontendMessage::SetCanvasRotation { .. })
3233
| Message::Documents(DocumentsMessage::Document(DocumentMessage::DispatchOperation { .. }))

editor/src/document/document_file.rs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,6 @@ impl From<DocumentOperation> for Message {
109109
}
110110

111111
impl DocumentMessageHandler {
112-
pub fn active_document(&self) -> &DocumentMessageHandler {
113-
self
114-
}
115-
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
116-
self
117-
}
118112
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
119113
let len = document_responses.len();
120114
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
@@ -320,9 +314,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
320314
)
321315
}
322316
SetBlendModeForSelectedLayers(blend_mode) => {
323-
let active_document = self;
324-
325-
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
317+
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
326318
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
327319
}
328320
}
@@ -407,12 +399,32 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
407399
Err(e) => log::error!("DocumentError: {:?}", e),
408400
Ok(_) => (),
409401
},
410-
RenderDocument => responses.push_back(
411-
FrontendMessage::UpdateCanvas {
412-
document: self.document.render_root(),
413-
}
414-
.into(),
415-
),
402+
RenderDocument => {
403+
responses.push_back(
404+
FrontendMessage::UpdateCanvas {
405+
document: self.document.render_root(),
406+
}
407+
.into(),
408+
);
409+
let root = self.layerdata(&[]);
410+
let viewport = ipp.viewport_bounds.size();
411+
let [bounds1, bounds2] = self.document.visible_layers_bounding_box().unwrap_or_default();
412+
let bounds1 = bounds1.min(DVec2::ZERO) - viewport * (f64::powf(2., root.scale / 3.) * 0.5);
413+
let bounds2 = bounds2.max(viewport) + viewport * (f64::powf(2., root.scale / 3.) * 0.5);
414+
let bounds_length = bounds2 - bounds1;
415+
let scrollbar_multiplier = bounds_length - viewport;
416+
let scrollbar_position = bounds1.abs() / scrollbar_multiplier;
417+
let scrollbar_size = viewport / bounds_length;
418+
responses.push_back(
419+
FrontendMessage::UpdateScrollbars {
420+
position: scrollbar_position.into(),
421+
size: scrollbar_size.into(),
422+
multiplier: scrollbar_multiplier.into(),
423+
}
424+
.into(),
425+
);
426+
}
427+
416428
NudgeSelectedLayers(x, y) => {
417429
for path in self.selected_layers().cloned() {
418430
let operation = DocumentOperation::TransformLayerInViewport {

editor/src/document/movement_handler.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub enum MovementMessage {
3030
DecreaseCanvasZoom,
3131
WheelCanvasZoom,
3232
ZoomCanvasToFitAll,
33+
TranslateCanvas(glam::DVec2),
3334
}
3435

3536
#[derive(Debug, Clone, Default, PartialEq)]
@@ -189,6 +190,12 @@ impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreproces
189190
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
190191
}
191192
}
193+
TranslateCanvas(delta) => {
194+
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
195+
196+
layerdata.translation += transformed_delta;
197+
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
198+
}
192199
}
193200
}
194201
fn actions(&self) -> ActionList {

editor/src/frontend/frontend_message_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum FrontendMessage {
1717
DisplayConfirmationToCloseDocument { document_index: usize },
1818
DisplayConfirmationToCloseAllDocuments,
1919
UpdateCanvas { document: String },
20+
UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
2021
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
2122
ExportDocument { document: String, name: String },
2223
SaveDocument { document: String, name: String },
@@ -50,6 +51,7 @@ impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
5051
ExpandFolder,
5152
SetActiveTool,
5253
UpdateCanvas,
54+
UpdateScrollbars,
5355
EnableTextInput,
5456
DisableTextInput,
5557
SetCanvasZoom,

frontend/src/components/panels/Document.vue

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,23 @@
123123
</div>
124124
</LayoutCol>
125125
<LayoutCol :class="'bar-area'">
126-
<PersistentScrollbar :direction="ScrollbarDirection.Vertical" :class="'right-scrollbar'" />
126+
<PersistentScrollbar
127+
:direction="ScrollbarDirection.Vertical"
128+
:handlePosition="scrollbarPos.y"
129+
@update:handlePosition="translateCanvasY"
130+
v-model:handleLength="scrollbarSize.y"
131+
:class="'right-scrollbar'"
132+
/>
127133
</LayoutCol>
128134
</LayoutRow>
129135
<LayoutRow :class="'bar-area'">
130-
<PersistentScrollbar :direction="ScrollbarDirection.Horizontal" :class="'bottom-scrollbar'" />
136+
<PersistentScrollbar
137+
:direction="ScrollbarDirection.Horizontal"
138+
:handlePosition="scrollbarPos.x"
139+
@update:handlePosition="translateCanvasX"
140+
v-model:handleLength="scrollbarSize.x"
141+
:class="'bottom-scrollbar'"
142+
/>
131143
</LayoutRow>
132144
</LayoutCol>
133145
</LayoutRow>
@@ -210,7 +222,7 @@
210222
<script lang="ts">
211223
import { defineComponent } from "vue";
212224
213-
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
225+
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, UpdateScrollbars, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
214226
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
215227
import { comingSoon } from "@/utilities/errors";
216228
@@ -271,6 +283,16 @@ export default defineComponent({
271283
async setRotation(newRotation: number) {
272284
(await wasm).set_rotation(newRotation * (Math.PI / 180));
273285
},
286+
async translateCanvasX(newValue: number) {
287+
const delta = newValue - this.scrollbarPos.x;
288+
this.scrollbarPos.x = newValue;
289+
(await wasm).translate_canvas(-delta * this.scrollbarMultiplier.x, 0);
290+
},
291+
async translateCanvasY(newValue: number) {
292+
const delta = newValue - this.scrollbarPos.y;
293+
this.scrollbarPos.y = newValue;
294+
(await wasm).translate_canvas(0, -delta * this.scrollbarMultiplier.y);
295+
},
274296
async selectTool(toolName: string) {
275297
(await wasm).select_tool(toolName);
276298
},
@@ -287,6 +309,15 @@ export default defineComponent({
287309
if (updateData) this.viewportSvg = updateData.document;
288310
});
289311
312+
registerResponseHandler(ResponseType.UpdateScrollbars, (responseData: Response) => {
313+
const updateData = responseData as UpdateScrollbars;
314+
if (updateData) {
315+
this.scrollbarPos = updateData.position;
316+
this.scrollbarSize = updateData.size;
317+
this.scrollbarMultiplier = updateData.multiplier;
318+
}
319+
});
320+
290321
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
291322
const toolData = responseData as SetActiveTool;
292323
if (toolData) this.activeTool = toolData.tool_name;
@@ -325,6 +356,9 @@ export default defineComponent({
325356
overlaysEnabled: true,
326357
documentRotation: 0,
327358
documentZoom: 100,
359+
scrollbarPos: { x: 0.5, y: 0.5 },
360+
scrollbarSize: { x: 0.5, y: 0.5 },
361+
scrollbarMultiplier: { x: 0, y: 0 },
328362
IncrementBehavior,
329363
IncrementDirection,
330364
MenuDirection,

frontend/src/components/widgets/scrollbars/PersistentScrollbar.vue

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
<template>
22
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
3-
<button class="arrow decrease"></button>
4-
<div class="scroll-track">
5-
<div class="scroll-click-area decrease" :style="[trackStart, preThumb, sides]"></div>
6-
<div class="scroll-thumb" :style="[thumbStart, thumbEnd, sides]"></div>
7-
<div class="scroll-click-area increase" :style="[postThumb, trackEnd, sides]"></div>
3+
<button class="arrow decrease" @mousedown="changePosition(-50)"></button>
4+
<div class="scroll-track" ref="scrollTrack" @mousedown="grabArea">
5+
<div class="scroll-thumb" @mousedown="grabHandle" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div>
86
</div>
9-
<button class="arrow increase"></button>
7+
<button class="arrow increase" @click="changePosition(50)"></button>
108
</div>
119
</template>
1210

@@ -39,6 +37,9 @@
3937
&:hover {
4038
background: var(--color-6-lowergray);
4139
}
40+
&.dragging {
41+
background: var(--color-accent-hover);
42+
}
4243
}
4344
4445
.scroll-click-area {
@@ -57,6 +58,9 @@
5758
&:hover {
5859
border-color: transparent transparent var(--color-6-lowergray) transparent;
5960
}
61+
&:active {
62+
border-color: transparent transparent var(--color-c-brightgray) transparent;
63+
}
6064
}
6165
6266
.arrow.increase {
@@ -67,6 +71,9 @@
6771
&:hover {
6872
border-color: var(--color-6-lowergray) transparent transparent transparent;
6973
}
74+
&:active {
75+
border-color: var(--color-c-brightgray) transparent transparent transparent;
76+
}
7077
}
7178
}
7279
@@ -81,6 +88,9 @@
8188
&:hover {
8289
border-color: transparent var(--color-6-lowergray) transparent transparent;
8390
}
91+
&:active {
92+
border-color: transparent var(--color-c-brightgray) transparent transparent;
93+
}
8494
}
8595
8696
.arrow.increase {
@@ -91,6 +101,9 @@
91101
&:hover {
92102
border-color: transparent transparent transparent var(--color-6-lowergray);
93103
}
104+
&:active {
105+
border-color: transparent transparent transparent var(--color-c-brightgray);
106+
}
94107
}
95108
}
96109
}
@@ -99,6 +112,15 @@
99112
<script lang="ts">
100113
import { defineComponent, PropType } from "vue";
101114
115+
// Linear Interpolation
116+
const lerp = (x: number, y: number, a: number) => x * (1 - a) + y * a;
117+
118+
// Convert the position of the handle (0-1) to the position on the track (0-1).
119+
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
120+
const handleToTrack = (handleLen: number, handlePos: number) => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
121+
122+
const mousePosition = (direction: ScrollbarDirection, e: MouseEvent) => (direction === ScrollbarDirection.Vertical ? e.clientY : e.clientX);
123+
102124
export enum ScrollbarDirection {
103125
"Horizontal" = "Horizontal",
104126
"Vertical" = "Vertical",
@@ -107,33 +129,19 @@ export enum ScrollbarDirection {
107129
export default defineComponent({
108130
props: {
109131
direction: { type: String as PropType<ScrollbarDirection>, default: ScrollbarDirection.Vertical },
132+
handlePosition: { type: Number, default: 0.5 },
133+
handleLength: { type: Number, default: 0.5 },
110134
},
111135
computed: {
112-
trackStart(): { left: string } | { top: string } {
113-
return this.direction === ScrollbarDirection.Vertical ? { top: "0%" } : { left: "0%" };
114-
},
115-
preThumb(): { right: string } | { bottom: string } {
116-
const start = 25;
117-
118-
return this.direction === ScrollbarDirection.Vertical ? { bottom: `${100 - start}%` } : { right: `${100 - start}%` };
119-
},
120136
thumbStart(): { left: string } | { top: string } {
121-
const start = 25;
137+
const start = handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
122138
123-
return this.direction === ScrollbarDirection.Vertical ? { top: `${start}%` } : { left: `${start}%` };
139+
return this.direction === ScrollbarDirection.Vertical ? { top: `${start * 100}%` } : { left: `${start * 100}%` };
124140
},
125141
thumbEnd(): { right: string } | { bottom: string } {
126-
const end = 25;
142+
const end = 1 - handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
127143
128-
return this.direction === ScrollbarDirection.Vertical ? { bottom: `${end}%` } : { right: `${end}%` };
129-
},
130-
postThumb(): { left: string } | { top: string } {
131-
const end = 25;
132-
133-
return this.direction === ScrollbarDirection.Vertical ? { top: `${100 - end}%` } : { left: `${100 - end}%` };
134-
},
135-
trackEnd(): { right: string } | { bottom: string } {
136-
return this.direction === ScrollbarDirection.Vertical ? { bottom: "0%" } : { right: "0%" };
144+
return this.direction === ScrollbarDirection.Vertical ? { bottom: `${end * 100}%` } : { right: `${end * 100}%` };
137145
},
138146
sides(): { left: string; right: string } | { top: string; bottom: string } {
139147
return this.direction === ScrollbarDirection.Vertical ? { left: "0%", right: "0%" } : { top: "0%", bottom: "0%" };
@@ -142,7 +150,58 @@ export default defineComponent({
142150
data() {
143151
return {
144152
ScrollbarDirection,
153+
dragging: false,
154+
mousePos: 0,
145155
};
146156
},
157+
mounted() {
158+
window.addEventListener("mouseup", () => {
159+
this.dragging = false;
160+
});
161+
window.addEventListener("mousemove", this.mouseMove);
162+
},
163+
methods: {
164+
trackLength(): number {
165+
const track = this.$refs.scrollTrack as HTMLElement;
166+
return this.direction === ScrollbarDirection.Vertical ? track.clientHeight - this.handleLength : track.clientWidth;
167+
},
168+
trackOffset(): number {
169+
const track = this.$refs.scrollTrack as HTMLElement;
170+
return this.direction === ScrollbarDirection.Vertical ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
171+
},
172+
clampHandlePosition(newPos: number) {
173+
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
174+
this.$emit("update:handlePosition", clampedPosition);
175+
},
176+
updateHandlePosition(e: MouseEvent) {
177+
const position = mousePosition(this.direction, e);
178+
this.clampHandlePosition(this.handlePosition + (position - this.mousePos) / (this.trackLength() * (1 - this.handleLength)));
179+
this.mousePos = position;
180+
},
181+
grabHandle(e: MouseEvent) {
182+
if (!this.dragging) {
183+
this.dragging = true;
184+
this.mousePos = mousePosition(this.direction, e);
185+
}
186+
},
187+
grabArea(e: MouseEvent) {
188+
if (!this.dragging) {
189+
this.dragging = true;
190+
this.mousePos = mousePosition(this.direction, e);
191+
this.clampHandlePosition(((this.mousePos - this.trackOffset()) / this.trackLength() - this.handleLength / 2) / (1 - this.handleLength));
192+
}
193+
},
194+
mouseUp() {
195+
this.dragging = false;
196+
},
197+
mouseMove(e: MouseEvent) {
198+
if (this.dragging) {
199+
this.updateHandlePosition(e);
200+
}
201+
},
202+
changePosition(difference: number) {
203+
this.clampHandlePosition(this.handlePosition + difference / this.trackLength());
204+
},
205+
},
147206
});
148207
</script>

0 commit comments

Comments
 (0)