Skip to content

Instantly share code, notes, and snippets.

@farism
Created June 23, 2023 08:22
Show Gist options
  • Save farism/1b81a24d38ca9a1979509f2d6627c985 to your computer and use it in GitHub Desktop.
Save farism/1b81a24d38ca9a1979509f2d6627c985 to your computer and use it in GitHub Desktop.
ScrollPane.tsx
import * as Jsx from "../Jsx"
import { InputActionId } from "../Document"
import { Node } from "../Node"
import { FlexDisplay, SCALING_FACTOR } from "../constants"
import { color, merge, onTick, slice9, vec3 } from "../utils"
const stylesheet = {
container: {
grow: 1,
color: color(1, 1, 1, 1),
alignItems: "stretch",
flexDirection: "row",
},
viewport: {
grow: 1,
color: color(1, 1, 1, 1),
alignItems: "stretch",
overflow: "scroll",
},
content: {},
scrollbar: {
maxWidth: 40,
grow: 1,
color: color(0.7, 0.7, 0.7, 1),
alignItems: "stretch",
},
arrow: {
width: 40,
height: 40,
grow: 0,
shrink: 0,
texture: "kenny",
flipbook: "green_button11",
justifyContent: "center",
alignItems: "center",
":hover": {
color: color(1, 1, 1, 0.8),
},
":active": {
scale: vec3(1.1, 1.1, 1),
},
},
arrowIcon: {
texture: "kenny",
width: 20,
height: 20,
},
arrowUp: {
flipbook: "grey_arrowUpWhite",
},
arrowDown: {
flipbook: "grey_arrowDownWhite",
},
track: {
grow: 1,
color: color(0.7, 0.7, 0.7, 1),
justifyContent: "start",
},
thumb: {
minHeight: 30,
texture: "kenny",
flipbook: "green_button04",
slice9: slice9(4, 8, 4, 8),
":hover": {
color: color(1, 1, 1, 0.8),
},
":active": {
scale: vec3(1.1, 1.1, 1),
},
},
} satisfies Stylesheet
export interface ScrollPaneProps {
children?: any
style?: Partial<Record<keyof typeof stylesheet, Style>>
scrollAmount?: number
ref?: (n: ScrollPane) => void
}
export class ScrollPane extends Jsx.Component<ScrollPaneProps> {
nodes: Jsx.Nodes<"container" | "viewport" | "content" | "scrollbar" | "track" | "thumb"> = {}
scrollAmount = 0
viewportHeight = 0
contentHeight = 0
trackHeight = 0
thumbHeight = 0
dragging = false
overflowing = true
private get document() {
return this.nodes.container?.document
}
constructor(props: ScrollPaneProps) {
super(props)
this.scrollAmount = props.scrollAmount ?? 100
onTick(this.initialize)
}
initialize = () => {
const { viewport, content, scrollbar, track, thumb } = this.nodes
if (viewport && content && scrollbar && track && thumb) {
this.viewportHeight = viewport.layout.height
this.contentHeight = content.layout.height
this.trackHeight = track.size.y
this.thumbHeight = (this.viewportHeight / this.contentHeight) * this.trackHeight
thumb.height = this.thumbHeight
if (this.contentHeight <= this.viewportHeight) {
this.overflowing = false
scrollbar.enabled = false
scrollbar.display = FlexDisplay.none
}
this.document?.calculateLayout()
this.document?.updateNodes()
}
}
render = () => {
const styles = merge(stylesheet, this.props.style)
return (
<box
ref={(n: Node) => (this.nodes.container = n)}
style={styles.container}
onMouseWheelUp={() => this.scrollUp()}
onMouseWheelDown={() => this.scrollDown()}
>
<box ref={(n: Node) => (this.nodes.viewport = n)} style={stylesheet.viewport}>
<box ref={(n: Node) => (this.nodes.content = n)} style={styles.content}>
{...this.props.children}
</box>
</box>
<box ref={(n: Node) => (this.nodes.scrollbar = n)} style={stylesheet.scrollbar}>
<box style={stylesheet.arrow} onMouseDown={() => this.scrollUp(2)}>
<box style={{ ...stylesheet.arrowIcon, ...stylesheet.arrowUp }} />
</box>
<box ref={(n: Node) => (this.nodes.track = n)} style={stylesheet.track} onMouseDown={this.onTrackMouseDown}>
<box
ref={(n: Node) => (this.nodes.thumb = n)}
style={stylesheet.thumb}
onMouseDown={this.onThumbMouseDown}
/>
</box>
<box style={stylesheet.arrow} onMouseDown={() => this.scrollDown(2)}>
<box style={{ ...stylesheet.arrowIcon, ...stylesheet.arrowDown }} />
</box>
</box>
</box>
)
}
setY = (node: Node, y: number, animate: boolean) => {
const clampedY = this.clampContentY(y)
if (animate) {
node.animate("position.y", clampedY, gui.EASING_OUTQUART, 0.2)
} else {
const { x, z } = node.position
node.position = vec3(x, clampedY, z)
}
}
scrollTo = (y: number, animate: boolean = true) => {
if (this.nodes.content && this.overflowing) {
this.setY(this.nodes.content, y, animate)
if (this.nodes.thumb) {
this.setY(this.nodes.thumb, this.calculateDragY(y), animate)
}
}
}
scrollBy = (delta: number, animate: boolean = true) => {
if (this.nodes.content) {
const contentY = this.clampContentY(this.nodes.content.position.y + delta)
this.scrollTo(contentY, animate)
}
}
scrollUp = (scale: number = 1) => {
this.scrollBy(-this.scrollAmount * scale)
}
scrollDown = (scale: number = 1) => {
this.scrollBy(this.scrollAmount * scale)
}
private clampContentY = (y: number) => {
const minY = -this.contentHeight / 2 + this.viewportHeight / 2
const maxY = this.contentHeight / 2 - this.viewportHeight / 2
const clampedY = math.min(maxY, math.max(minY, y))
return clampedY
}
private calculateDragY = (contentY: number) => {
return (-contentY / (this.contentHeight - this.viewportHeight)) * (this.trackHeight - this.thumbHeight)
}
private onMouseMove = (action: any) => {
if (this.dragging) {
const trackContentRatio = (this.contentHeight - this.viewportHeight) / (this.trackHeight - this.thumbHeight)
const delta = -action.dy * trackContentRatio
this.scrollBy(delta, false)
}
}
private onTrackMouseDown = (action: any) => {
if (this.nodes.track && !action.canceled) {
const trackPos = this.nodes.track.screenPosition
const trackSize = this.nodes.track.size
const percentY = (action.screen_y - trackPos.y) / (trackSize.y * SCALING_FACTOR)
const contentY = this.clampContentY((this.contentHeight - this.viewportHeight) * percentY)
this.scrollTo(-contentY, true)
}
}
private onInput = (actionId: InputActionId, action: any) => {
if (actionId === "mouseup") {
this.onMouseUp(action)
} else if (actionId === "mousemove") {
this.onMouseMove(action)
}
}
private onThumbMouseDown = (action: any) => {
action.canceled = true
this.dragging = true
this.nodes.container?.document?.addInputListener(this.onInput)
return false
}
private onMouseUp = (action: any) => {
this.dragging = false
this.document?.removeInputListener(this.onInput)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment