diff --git a/docs/docs/languages/flutter.mdx b/docs/docs/languages/flutter.mdx index 8c0492d65..e3efa5fa2 100644 --- a/docs/docs/languages/flutter.mdx +++ b/docs/docs/languages/flutter.mdx @@ -72,6 +72,31 @@ class _CounterAppState extends State { } ``` +### Root Element + +Flutter draws its own UI, so it renders into an element in the [result page](../features/result.mdx). +Use `"livecodes-app"` as the element `id` to render into a specific element. Otherwise a `div` element is added to `document.body` and rendered into. + +The app is sized to that element's box, so the element must have a size. An empty `div` in normal flow has none, and `width`/`height` in percent resolve against an unsized body. + +Example: + +export const flutterRootDemo = { + activeEditor: 'script', + script: { + language: 'flutter', + content: `import 'package:flutter/material.dart';\n\nvoid main() {\n runApp(\n const Center(\n child: Text('Hello, World!', textDirection: TextDirection.ltr),\n ),\n );\n}`, + }, + markup: { + language: 'html', + content: `

Custom Root Element

+
+`, + }, +}; + + + ## Language Info ### Name diff --git a/src/livecodes/languages/dart/lang-dart-runtime.ts b/src/livecodes/languages/dart/lang-dart-runtime.ts index 7694d8dd6..234599e4b 100644 --- a/src/livecodes/languages/dart/lang-dart-runtime.ts +++ b/src/livecodes/languages/dart/lang-dart-runtime.ts @@ -9,10 +9,34 @@ export const setupDartRuntime = (engine: 'dart' | 'flutter') => { const postLoading = (payload: boolean) => parent.postMessage({ type: 'loading', payload }, '*'); // NOSONAR - the parent validates the source. - ns.ready ??= (async () => { - ns.runtime = await window.DartWasm.loadRuntime({ engine, baseUrl: dartWasmBaseUrl }); - return ns.runtime; - })(); + // Flutter draws its own UI, so the engine needs an element to draw into. LiveCodes' convention is + // a `#livecodes-app` element from the HTML tab when there is one, otherwise a `div` added to the + // page. The Dart engine runs in the page itself and needs no container. + const flutterContainer = () => { + const existing = document.querySelector('#livecodes-app'); + if (existing) return existing; + const container = document.createElement('div'); + // The engine sizes to the host element, so it has to fill the result pane the way the engine's + // own full-page host would have. + Object.assign(container.style, { + position: 'fixed', + inset: '0', + overflow: 'hidden', + }); + return document.body.appendChild(container); + }; + + // Created on the first run, not while this script is parsed: the result page's markup (and so + // `#livecodes-app`) does not exist yet when the head scripts run. + const ready = () => + (ns.ready ??= (async () => { + ns.runtime = await window.DartWasm.loadRuntime({ + engine, + baseUrl: dartWasmBaseUrl, + ...(engine === 'flutter' ? { container: flutterContainer() } : {}), + }); + return ns.runtime; + })()); ns.run ??= async (program: { libraryUri: string; @@ -20,7 +44,7 @@ export const setupDartRuntime = (engine: 'dart' | 'flutter') => { }) => { postLoading(true); try { - const runtime = await ns.ready; + const runtime = await ready(); await runtime.run(program); } catch (error) { // eslint-disable-next-line no-console diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 4958d9f7d..4d1c8285c 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -134,7 +134,7 @@ export const cytoscapeSvgUrl = /* @__PURE__ */ getUrl('cytoscape-svg@0.4.0/cytos export const cytoscapeUrl = /* @__PURE__ */ getUrl('cytoscape@3.25.0/dist/cytoscape.min.js'); -export const dartWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/dart-wasm@0.6.0/dist/'); +export const dartWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/dart-wasm@0.7.0/dist/'); export const ddietrCmThemesBaseUrl = /* @__PURE__ */ getUrl( '@ddietr/codemirror-themes@1.4.2/dist/theme/',