Skip to content

media: file-image, file-preview (QuickLook), and link elements - #71

Merged
RCmerci merged 3 commits into
mainfrom
devin/1790579060-media-file
Sep 29, 2026
Merged

RCmerci merged 3 commits into
mainfrom
devin/1790579060-media-file

Conversation

@RCmerci

@RCmerci RCmerci commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three general-purpose element kinds so apps no longer need private extensions for file-backed media and external links (motivating case: replacing JournalMedia.swift-style components in logseq_journal).

New node kinds (schema/components.json → regenerated all wire schemas):

  • file-image — leaf. Required prop path (absolute path or file:// URL); optional max-pixel-size (int > 0) plus the standard sizing (width/height/max-width/max-height/corner-radius) and press-enabled + Press. Apple backend decodes off the main actor via CGImageSourceCreateThumbnailAtIndex with an NSCache keyed by "<maxPixelSize>|<path>" (~32MB cost cap, cost = bytesPerRow × height, default maxPixelSize 1024), shows a ProgressView while loading and a fallback icon on failure.
  • file-preview — non-rendering leaf that presents the file via QuickLook. Design choice: a separate node kind rather than a preview-path prop on file-image, matching the sheet/dialog "mount to present" pattern: presence in the tree means presented; dropping the node dismisses; interactive close emits Dismiss. It deliberately does not join isModalSurface — that invariant would force a text title and sheet chrome — instead it has its own presentation lane (LUIFilePreviewStore + .quickLookPreview) synced by the same syncModalPresentation tree walk, with the same pending-dismissal suppression as sheets. file-preview is a restrictive kind: only path + accessibility-identifier.
  • link — container. Required url (string); renders SwiftUI Link(destination:). Children form the label, or text/icon/icon-placement props when childless — so it composes inside list rows and overlays.

New properties: path, url, max-pixel-size. New wire kinds: "file-image", "file-preview", "link" (container: true). Events: Press on file-image, Dismiss on file-preview — no new event shapes.

Lui_elements.column [
  Lui_elements.link ~url:"https://example.com" ~text:"Docs" [];
  Lui_elements.file_image ~path:"/tmp/pic.png" ~max_pixel_size:512
    ~on_press:(fun () -> send OpenPreview) [];
  Lui_elements.file_preview ~path:"/tmp/doc.pdf" ~on_dismiss:(fun () -> send PreviewClosed) [];
]

Every journal-media item kind maps cleanly: file → file-image + file-preview, external → link, placeholder → existing skeleton/icon/spinner, menus → existing dropdown-menu/menu-item.

Other backends: flutter/qt/winui/web schema mirrors updated (support checks, child rules, property validation); Qt gains stub QML views registered in qt_add_qml_module; WinUI's generated RestrictiveMatrix covers file-preview automatically. No real rendering on non-Apple platforms per scope.

Tests: OCaml test_lui.ml gains property/event matrix + mount-op assertions; Swift gains mapsLink, mapsFileImage, mapsFilePreview (store sync, dismiss suppression, restrictive-kind rejection). dune build, dune runtest -j 4 (31 tests), make test-schema, swift test (136 tests) all green locally; WinUI dotnet test runs on CI.

Link to Devin session: https://app.devin.ai/sessions/1867341ba00d443e9cb294081562ccde
Open in Devin Desktop: https://app.devin.ai/desktop/session/1867341ba00d443e9cb294081562ccde?variant=devin
Requested by: @RCmerci

Three general-purpose element kinds sufficient to replace journal-media
style per-app extensions:

- file-image: loads a local path / file:// URL off the main actor via
  CGImageSource thumbnail decoding (NSCache keyed by path+pixel size,
  ~32MB cap), with loading spinner, failure icon fallback, and the
  standard press-enabled/Press path.
- file-preview: a non-rendering node that presents QuickLook via
  .quickLookPreview. Presence in the tree means presented, matching the
  sheet/dialog 'mount to present' pattern but on its own presentation
  lane (not isModalSurface, which forces a text title and sheet chrome).
  Interactive close emits Dismiss and is suppressed from re-asserting
  until the wire drops the node, same as modal dismissal.
- link: a container rendering SwiftUI Link(destination:) from a url
  prop; children (or text/icon props) form the label, so it composes
  inside list rows and overlays.

New wire properties: path, url, max-pixel-size. file-preview is a
restrictive kind (path + accessibility-identifier only). Flutter/Qt/
WinUI/web schema mirrors updated; Qt gains stub QML views; WinUI's
restrictive matrix covers file-preview automatically.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T07:15:18.051188Z bbf1db5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bbf1db5220

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +89 to +91
| Link -> "lui-link"
| FileImage -> "lui-file-image"
| FilePreview -> "lui-file-preview"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle the new properties in the web renderer

On the web backend, mounting any of these new kinds emits the required UrlValue or PathValue property, and may emit MaxPixelSize, but Lui_web_props.apply_secondary_property handles none of them and falls through to invalid_arg. Consequently, otherwise-valid link, file-image, and file-preview patches fail when their properties are applied instead of producing even a stub node; add apply/remove handlers for the new properties before exposing these kinds.

Useful? React with 👍 / 👎.

Comment on lines +28 to +32
TapHandler {
enabled: wrapper.props["press-enabled"] === true
onTapped: {
if (wrapper.node && wrapper.node.backend)
wrapper.node.backend.performPress(wrapper.node.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow Qt file images through performPress

When a Qt file-image has press-enabled, this tap handler calls LuiQmlBackend::performPress, but that method's pressable-kind whitelist does not include NodeKind::FileImage. Every such tap therefore returns false and emits no Press event despite the schema and OCaml on_press API advertising support; add the kind with the same press-enabled guard used here.

Useful? React with 👍 / 👎.

Comment on lines +192 to +195
private var filePreviewBinding: Binding<URL?> {
Binding(
get: { backend.filePreviewPresentation.item?.url },
set: { url in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope Quick Look presentation to the traversal root

In a backend hosting multiple LUISwiftUIRoots—the scenario this modifier explicitly supports—every root's binding returns the same global preview URL because it ignores LUIFilePreviewPresentation.rootID. A preview mounted under one traversal root can therefore be presented by the wrong host or by several hosts, and a dismissal from any of them clears the shared store; filter the item by rootID, as sheetBinding already does, in both preview bindings.

Useful? React with 👍 / 👎.

Comment on lines +3082 to +3087
.task(id: "\(maxPixelSize)|\(path)") {
if let decoded = await LUIFileImageLoader.thumbnail(
path: path,
maxPixelSize: maxPixelSize
) {
image = decoded

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore obsolete thumbnail task results

When path or maxPixelSize changes while a previous decode is running, SwiftUI cancels the old .task, but thumbnail launches an unstructured detached task and this closure neither checks cancellation nor verifies the captured key after awaiting it. The older decode can finish after the newer one and overwrite image with pixels from the previous path; guard cancellation or compare the task key before updating state.

Useful? React with 👍 / 👎.

| Slider -> "input"
| Divider -> "hr"
| Tooltip -> "span"
| Link -> "a"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Build a DOM label structure for link icons

Once the required URL property is handled, a web link using the newly supported icon prop still fails: links are created as empty simple <a> elements, while apply_inline_icon_name treats every non-list/non-bottom-tab kind as a button and calls Util.button_icon_node, which requires an element child at index 0. The normal OCaml constructor emits the icon before mounting label children, so this raises an out-of-bounds error; create the expected icon/label children for links or add link-specific icon handling.

Useful? React with 👍 / 👎.

case LUINodeKind.Text:
case LUINodeKind.TableCell:
case LUINodeKind.TimelineItem:
case LUINodeKind.FileImage:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Admit WinUI file images through PerformAction

When a WinUI adapter or test invokes the public core action gate for a file-image with press-enabled, LUISchema.EventSupported now advertises the event but LUIBackend.PerformAction still omits LUINodeKind.FileImage from its pressable whitelist. The call therefore throws instead of emitting LUIEvent.Press; add the same PressEnabled-guarded case used by the Apple and Flutter backends.

Useful? React with 👍 / 👎.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

E2E test results — media elements on native macOS

Tested end-to-end in the examples/split macOS demo app (scratch test view driving the real OCaml → SwiftUI wire path; reverted after testing). All assertions passed.

Devin session

  • link: both variants render; clicking opens the URL in the default browser — example.com and devin.ai both loaded in Safari.
  • file-image: decodes and renders a real PNG via absolute path and file:// URL; a missing path shows the fallback photo icon; Press events reach the OCaml reducer (counter incremented 0→2).
  • file-preview: mounting the node presents a QuickLook panel; interactive close emits Dismiss (dismissal counter 0→2 across two open/close cycles); no re-open loop, and a remounted node re-presents correctly.
Initial render QuickLook presented
media elements render QuickLook panel
More evidence
Safari: example.com via link Counter after presses + dismiss
example.com dismissals:1

…vent gates

- web: link renders as a structured <a> (icon + content spans) so
  inline icon, text, and children compose; url maps to href
  (+target=_blank, rel=noopener), path/max-pixel-size become
  data-* attributes with matching remove handlers; file-image clicks
  emit Press when press-enabled.
- apple: both filePreviewBindings filter on the presentation's
  rootID and only dismiss previews owned by that root — multiple
  LUISwiftUIRoots no longer share/dismiss one global preview.
- qt: performPress admits FileImage with the same press-enabled guard;
  performDismiss admits FilePreview (Dismiss is advertised for it).
- winui: PerformAction admits FileImage gated on PressEnabled;
  PerformDismiss admits FilePreview.
…a-file

# Conflicts:
#	platform/apple/Sources/LUIAppleBackend/LUIWireSchema.swift
#	platform/flutter/lib/lui_flutter_backend.dart
#	platform/qt/lib/CMakeLists.txt
#	platform/qt/lib/lui_wire_schema.h
#	platform/winui/LUI.Core/LUISchema.cs
#	platform/winui/LUI.Core/LUIWireSchema.g.cs
#	schema/components.json
#	src/lui_protocol.ml
#	src/lui_protocol.mli
#	src/lui_wire_schema.ml
#	test/test_lui.ml
@RCmerci
RCmerci merged commit 8ca8b42 into main Sep 29, 2026
4 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.

1 participant