Ruxp: React components for declarative UXP entry points

hey all. while writing my plug-in for photoshop i got tired of “Controllers” and the ugly API for manipulating menu items so i wrote ruxp. it lets you define your plug-in and entry points (especially panel menus) in terms of React components and manipulate them reactively. you might like it if you’re already using React. it looks like this:

import { useState } from "react"
import { createRoot } from "react-dom/client"
import { Plugin, Panel, Item, Command } from "ruxp"

const MyPlugin = () => {
    const [good, setGood] = useState(true)
    const doGood = () => setGood(true)
    const doBad = () => setGood(false)

    return (
        <Plugin>
            <Panel id="nicePanel">
                <NicePanel good={good} />
                <Item label="Do something...">
                    <Item label="...good" checked={good} onInvoke={doGood} />
                    <Item label="...bad" checked={!good} onInvoke={doBad} />
                </Item>
            </Panel>
            <Command id="doGood" onInvoke={doGood} />
            <Command id="doBad" onInvoke={doBad} />
        </Plugin>
    )
}

// (you could just put your state and menu items down here)
const NicePanel = ({ good }) => <p>i'm {good ? "good" : "bad"}</p>

createRoot(document.querySelector("#root")).render(<MyPlugin />)

i think my favorite feature is not having to provide menu IDs. it is a pretty simple wrapper and i’m already successfully using it in my plug-in so there shouldn’t be major issues (except being untested outside of photoshop), but feel free to contribute. you can find it on github and npm.

6 Likes

Wow, nice! I’ll have to give this a try when I get my PC back to working state :smiley:

1 Like

I like the idea behind this. I have to check how it works in some use cases.

Can it handle multiple panels within single plugin? When I tried <uxp-panel> in react render() I got into issues that my panels were blank.

So instead I had to put placeholder <uxp-panel> into HTML entrypoint and then use react portal to put content into designated placeholders… but react alone does not touch those <uxp-panel>

Does it mean you solved this issue with panels not loading?

1 Like

hey! yup, the components were developed with the idea of multiple panels in a single plug-in. indeed rendering a uxp-panel from React doesn’t work* because, by the time the component is mounted, the entry points have already been set up and UXP has already grabbed its panel roots. the way it solves this is by creating these uxp-panel roots at start-up and later portaling into them. so essentially what you were trying to do is already built into the component, but i wonder if you ran into any issues that led you to do it in the first place?

*though i do find it weird that nesting a uxp-panel inside of the pre-existing one doesn’t work, but i guess that’s just how UXP handles that custom element. given that it’s already handled by the Panel component, you should just avoid uxp-panel.

Thanks for explanation. I think you solved it similar way as I did. Making panels before react kicks in, not touching panel elements and portaling into them.

The difference is that I used HTML file as entrypoint in manifest meanwhile you really on JS file… but maybe both could work?

I am trying it now… if that would work it would be better than what I have.

Maybe you could write your own types as well. Those UXP generated are not good at all :smiley:

How about core.suppressResizeGripper({target: "main", type: "panel", value: true}); Could you add resize gripper supression? Or maybe disable it by default with possibility to turn this on?

1 Like

oh well personally i use an HTML entry point too, it’s just mostly empty and links to the JS file so it’s almost like a JS entry point. but do you mean that the library should be able to pick up on existing elements (i.e. ones already in the HTML doc)? that should be pretty easy to do, but it seems a little weird given how the “mental model” of the library is that you’re rendering a React tree from scratch, so it’s almost like you really do own the uxp-panels (so they can share a managed common parent, etc). and yeah UXP types have been the bane of my existence, i’ve been thinking about at least forking an existing set of typings and fixing it up but even that would be a bit of effort haha.

the gripper suppression feature is a great idea, i had no clue it was a thing! i can implement it right now. do you think <Panel gripper id=...> (i.e. defaults to no gripper) would be fine?

Default to no gripper sounds good to me. I am not even sure why does it exist since you can resize panel with and without it.

I removed HTML file from my project. I liked that for the clarity before. Because you open it and see what HTML is before React kicks in. But now that syntax moved into React as well so that is not such issue.

The only thing I had to change is to add some root element before Ruxp starts. This is the only hidden part.

Overall I like it a lot. I think I will use it. But I might do some modifications. Hopefully it will be bug free :smiley:

I was thinking about fixing UXP types as well… but thinking about 100+ hours working for free does not make me happy :smiley: And if I do that while working for client I have to keep it private. :expressionless:

Example of what I work with right now: https://developer.adobe.com/xd/uxp/uxp/reference-js/Global%20Members/Data%20Transfers/Clipboard/#writetexttext documentation says it accepts object instead of text. MDN documentation says it accepts texts.

UXP types says it accepts any which is somewhat useless. But the worst part is that writeText is part of Navigator and when you type navigator.writeText it doesn’t even know what navigator is. So we have type but I cannot use it?

I had a chance to improve official PS types while working within DOM contract. Having chance to help with official UXP types would be nice too.

gripper is out with v1.1.1. thanks for adopting my library! i’m glad you liked it. if you have any more improvements or feature requests feel free to tell me or file an issue/PR. i did want to warn you that the library currently uses an internal property (entrypoints._pluginInfo.manifest, which really shouldn’t be internal but whatever), because it needs to know the entry point IDs to call setup() on start-up. do you think this will be an issue?

i feel you with regards to the typings. before settling on some random guy’s UXP typings i tried to make do with the official ones. imagine my horror when i realized that they completely re-declare the web/DOM API typings rather than just building on top of the existing ones. that seems to be what you’re suffering with as well. even beyond that, all of the typings and docs are riddled with mistakes that clearly nobody at Adobe wants to fix and maintain. i wish i could help too! but yeah, unpaid work is not very viable right now haha.

Thanks.

Maybe you could use const manifestFile = await fsProvider.getEntryWithUrl("plugin:/manifest.json");

https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/uxp/Persistent%20File%20Storage/FileSystemProvider/

And then const text = await manifestFile.read();
https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-js/Modules/uxp/Persistent%20File%20Storage/File/#readoptions

This uses the oldest API available and should be possible to do this without any extra permission. Also manifest file position should be guaranteed. Make sure to read file only once at start.

The only problem I can think of is that it is asynchronous and I remember something about limited time window during plugin initalization but not sure what exactly it was.*

Regarding to typings… the problem is you have to re-declare it. Because UXP is not browser. Some features are there, some are missing and some support only cetain data types in arguments or have other quirks. Like a clipboard API. It has API that browser does not. Otherwise you would need to use a lot of Pick and Omit and also add new stuff… that would be messy as well. Not sure if that is even possible. https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys

I know it looks unmaintained… but on the other hand we had nothing at all for years… so this is still some improvement.

*Edit: I somewhat take it back… it is not so easy to locate entrypoints in manifest correctly. entryPoints is valid but entrypoints as well. It can be in manifest root… but schema says it could be also within host which is object but manifest says it could be also array. And for hosthosts is also valid alias. Right now only one host app is supported but it could be more in future. So you can get quite some combinations and also manifest updates in future.

1 Like

One more idea. For render property I would rather have render?: ReactNode. Because for FunctionComponent type… I have hard time to use it with some good syntax… :smiley:

hmm okay, yeah i guess reading the file would generally be a good idea but with all of the entry points possibilities you mentioned i’d rather hold off until it’s clear that doing so would be necessary! (the setup timeout is ~20ms; according to some hacky testing i conducted before landing on the current solution, reading a file fits comfortably in this time span, but ya never know)

about the typings, you can extend existing interfaces using module augmentation/declaration merging, e.g. i did this little snippet when dealing with UXP menus

interface HTMLMenuElement {
    /** Render the menu at the _x_, _y_ coordinates. */
    popupAt(x: number, y: number): void
}

and removing stuff could be done by creating new interfaces (or using Omit like you say) and just patching the return/property/etc. types wherever they show up. it’s not like UXP is so far removed from the Chromium it’s based on that you’d need to redefine the API as a whole (even though a whole bunch of stuff is missing). re-declaring everything makes for auto-generated docs that aren’t very useful (because they don’t only tell you what’s new; you have to find it among the piles of existing API) and virtually unmaintainable typings based on copy/pasting (at least you should fork the DOM typings). i guess i might try making my own.

anyway, regarding render with ReactNodes, why not just use children directly? instead of passing it as a prop like <Panel render={<MyNode />} /> just pass it as a child node like <Panel><MyNode /></Panel>. i’m guessing you use class components, meaning you can’t pass the component directly as a prop and have to wrap it in a lambda, which is inconvenient. i’m gonna replace FunctionComponent with ComponentType so that you can pass any kind of component instead.

1 Like

What do you think about events… ?

uxpshowpanel
uxphidepanel

E.g.

<Panel id="nicePanel" onShow={fn1} onHide={fn2}>

It would be cool if we could just add to the panel event listeners on what to do when you open/close panel. It would be great if this could be per panel specific.

Sometimes I update panel UI as user works with PS. But when panel is not visible it is good to suspend updates.

1 Like

yeah, i wanted to implement lifecycle events from the start but as you obviously know show() and hide() don’t work at all so i just ditched it. thanks for letting me know about the very much undocumented uxp*panel command… event… things. they seem to work fine but as you probably also already know they don’t tell which panel was hidden/shown. do you have any way to work around this? haven’t been able to find even a way to check which panels are currently visible.

I do… but first you have to ask yourself philosofical questions… what is your relationship with other Adobe apps? :smiley: Is this mainly for Photoshop?

haha well ruxp is definitely intended to be cross-app but given that most people (and i) are going to be using it for PS, and i’ve already implemented another PS-only feature (gripper), it should be fine

well then you can ask Photoshop for panel list like this:

This is getter command so it should not have issue with executeAsModal

thanks for the lead! i’ll start working on it soon.

2 Likes

One more idea… uxp-panel element can have resize event. Window: resize event - Web APIs | MDN It is this simple.

This would be helpfull because mediaqueries work only if you have one panel in plugin.

1 Like

Another idea is to detect mouse over/out event for each panel. Because it is not so simple :smiley: I am making custom tooltips and when you mouse leaves panel too quickly it does not send mouseOut event fast enough and tooltip doesn’t get cleared. In browser you could use document.onmouseleave = e=>console.log(e) but this does not work for me in UXP.