OpenTUI is here (#2685)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { batch, createContext, createEffect, Show, useContext, type JSX, type ParentProps } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { Renderable, RGBA } from "@opentui/core"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createEventBus } from "@solid-primitives/event-bus"
|
||||
|
||||
const Border = {
|
||||
topLeft: "┃",
|
||||
topRight: "┃",
|
||||
bottomLeft: "┃",
|
||||
bottomRight: "┃",
|
||||
horizontal: "",
|
||||
vertical: "┃",
|
||||
topT: "+",
|
||||
bottomT: "+",
|
||||
leftT: "+",
|
||||
rightT: "+",
|
||||
cross: "+",
|
||||
}
|
||||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
size?: "medium" | "large"
|
||||
onClose: () => void
|
||||
}>,
|
||||
) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<box
|
||||
onMouseUp={async () => {
|
||||
props.onClose?.()
|
||||
}}
|
||||
width={dimensions().width}
|
||||
height={dimensions().height}
|
||||
alignItems="center"
|
||||
position="absolute"
|
||||
paddingTop={dimensions().height / 4}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
|
||||
>
|
||||
<box
|
||||
onMouseUp={async (e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
customBorderChars={Border}
|
||||
width={props.size === "large" ? 80 : 60}
|
||||
maxWidth={dimensions().width - 2}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
borderColor={theme.border}
|
||||
paddingTop={1}
|
||||
>
|
||||
{props.children}
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function init() {
|
||||
const [store, setStore] = createStore({
|
||||
stack: [] as {
|
||||
element: JSX.Element
|
||||
onClose?: () => void
|
||||
}[],
|
||||
size: "medium" as "medium" | "large",
|
||||
})
|
||||
const allClosedEvent = createEventBus<void>()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "escape" && store.stack.length > 0) {
|
||||
const current = store.stack.at(-1)!
|
||||
current.onClose?.()
|
||||
setStore("stack", store.stack.slice(0, -1))
|
||||
evt.preventDefault()
|
||||
refocus()
|
||||
}
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
let focus: Renderable | null
|
||||
function refocus() {
|
||||
setTimeout(() => {
|
||||
if (!focus) return
|
||||
if (focus.isDestroyed) return
|
||||
function find(item: Renderable) {
|
||||
for (const child of item.getChildren()) {
|
||||
if (child === focus) return true
|
||||
if (find(child)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const found = find(renderer.root)
|
||||
if (!found) return
|
||||
focus.focus()
|
||||
}, 1)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (store.stack.length === 0) {
|
||||
allClosedEvent.emit()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
clear() {
|
||||
for (const item of store.stack) {
|
||||
if (item.onClose) item.onClose()
|
||||
}
|
||||
batch(() => {
|
||||
setStore("size", "medium")
|
||||
setStore("stack", [])
|
||||
})
|
||||
refocus()
|
||||
},
|
||||
replace(input: any, onClose?: () => void) {
|
||||
if (store.stack.length === 0) focus = renderer.currentFocusedRenderable
|
||||
for (const item of store.stack) {
|
||||
if (item.onClose) item.onClose()
|
||||
}
|
||||
setStore("size", "medium")
|
||||
setStore("stack", [
|
||||
{
|
||||
element: input,
|
||||
onClose,
|
||||
},
|
||||
])
|
||||
},
|
||||
get stack() {
|
||||
return store.stack
|
||||
},
|
||||
get size() {
|
||||
return store.size
|
||||
},
|
||||
setSize(size: "medium" | "large") {
|
||||
setStore("size", size)
|
||||
},
|
||||
get allClosedEvent() {
|
||||
return allClosedEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DialogContext = ReturnType<typeof init>
|
||||
|
||||
const ctx = createContext<DialogContext>()
|
||||
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const value = init()
|
||||
return (
|
||||
<ctx.Provider value={value}>
|
||||
{props.children}
|
||||
<box position="absolute">
|
||||
<Show when={value.stack.length}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||
{value.stack.at(-1)!.element}
|
||||
</Dialog>
|
||||
</Show>
|
||||
</box>
|
||||
</ctx.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const value = useContext(ctx)
|
||||
if (!value) {
|
||||
throw new Error("useDialog must be used within a DialogProvider")
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user