From 461d3a88ef32a19e3257404cfbf8aaab40ad0098 Mon Sep 17 00:00:00 2001 From: Aran-Fey Date: Mon, 25 Nov 2024 21:26:55 +0100 Subject: [PATCH] implement Webview --- frontend/code/componentManagement.ts | 6 +- frontend/code/components/html.ts | 87 -------------- frontend/code/components/website.ts | 31 ----- frontend/code/components/webview.ts | 172 +++++++++++++++++++++++++++ frontend/css/style.scss | 155 +++++++++--------------- rio/components/html.py | 14 ++- rio/components/switch.py | 2 +- rio/components/website.py | 18 ++- rio/components/webview.py | 69 +++++++++++ 9 files changed, 317 insertions(+), 237 deletions(-) delete mode 100644 frontend/code/components/html.ts delete mode 100644 frontend/code/components/website.ts create mode 100644 frontend/code/components/webview.ts create mode 100644 rio/components/webview.py diff --git a/frontend/code/componentManagement.ts b/frontend/code/componentManagement.ts index 176fb13f..fd52d4c7 100644 --- a/frontend/code/componentManagement.ts +++ b/frontend/code/componentManagement.ts @@ -25,7 +25,6 @@ import { FundamentalRootComponent } from "./components/fundamentalRootComponent" import { GridComponent } from "./components/grid"; import { HeadingListItemComponent } from "./components/headingListItem"; import { HighLevelComponent as HighLevelComponent } from "./components/highLevelComponent"; -import { HtmlComponent } from "./components/html"; import { IconComponent } from "./components/icon"; import { ImageComponent } from "./components/image"; import { KeyEventListenerComponent } from "./components/keyEventListener"; @@ -62,7 +61,7 @@ import { TextComponent } from "./components/text"; import { TextInputComponent } from "./components/textInput"; import { ThemeContextSwitcherComponent } from "./components/themeContextSwitcher"; import { TooltipComponent } from "./components/tooltip"; -import { WebsiteComponent } from "./components/website"; +import { WebviewComponent } from "./components/webview"; import { GraphEditorComponent } from "./components/graphEditor/graphEditor"; const COMPONENT_CLASSES = { @@ -90,7 +89,6 @@ const COMPONENT_CLASSES = { "Grid-builtin": GridComponent, "HeadingListItem-builtin": HeadingListItemComponent, "HighLevelComponent-builtin": HighLevelComponent, - "Html-builtin": HtmlComponent, "Icon-builtin": IconComponent, "IconButton-builtin": IconButtonComponent, "Image-builtin": ImageComponent, @@ -128,7 +126,7 @@ const COMPONENT_CLASSES = { "TextInput-builtin": TextInputComponent, "ThemeContextSwitcher-builtin": ThemeContextSwitcherComponent, "Tooltip-builtin": TooltipComponent, - "Website-builtin": WebsiteComponent, + "Webview-builtin": WebviewComponent, }; globalThis.COMPONENT_CLASSES = COMPONENT_CLASSES; diff --git a/frontend/code/components/html.ts b/frontend/code/components/html.ts deleted file mode 100644 index 2c2fd3c9..00000000 --- a/frontend/code/components/html.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ComponentBase, ComponentState } from "./componentBase"; - -export type HtmlState = ComponentState & { - _type_: "Html-builtin"; - html?: string; - enable_pointer_events?: boolean; -}; - -export class HtmlComponent extends ComponentBase { - declare state: Required; - - private isInitialized = false; - - createElement(): HTMLElement { - let element = document.createElement("div"); - element.classList.add("rio-html"); - return element; - } - - runScriptsInElement(): void { - for (let oldScriptElement of this.element.querySelectorAll("script")) { - // Create a new script element - const newScriptElement = document.createElement("script"); - - // Copy over all attributes - for (let i = 0; i < oldScriptElement.attributes.length; i++) { - const attr = oldScriptElement.attributes[i]; - newScriptElement.setAttribute(attr.name, attr.value); - } - - // And the source itself - newScriptElement.appendChild( - document.createTextNode(oldScriptElement.innerHTML) - ); - - // Finally replace the old script element with the new one so - // the browser executes it - oldScriptElement.parentNode!.replaceChild( - newScriptElement, - oldScriptElement - ); - } - } - - updateElement( - deltaState: HtmlState, - latentComponents: Set - ): void { - super.updateElement(deltaState, latentComponents); - - if (deltaState.html !== undefined) { - // If the HTML hasn't actually changed from last time, don't do - // anything. This is important so scripts don't get re-executed each - // time the component is updated. - if (deltaState.html === this.state.html && this.isInitialized) { - return; - } - - if (requiresIframe(deltaState.html)) { - this.element.innerHTML = ""; - - let iframe = document.createElement("iframe"); - iframe.srcdoc = deltaState.html; - - this.element.appendChild(iframe); - } else { - // Load the HTML - this.element.innerHTML = deltaState.html; - - // Just setting the innerHTML doesn't run scripts. Do that manually. - this.runScriptsInElement(); - } - - this.isInitialized = true; - } - - if (deltaState.enable_pointer_events !== undefined) { - this.element.style.pointerEvents = deltaState.enable_pointer_events - ? "auto" - : "none"; - } - } -} - -function requiresIframe(html: string): boolean { - return html.match(/^\s*(])/i) !== null; -} diff --git a/frontend/code/components/website.ts b/frontend/code/components/website.ts deleted file mode 100644 index ee0f3d20..00000000 --- a/frontend/code/components/website.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ComponentBase, ComponentState } from "./componentBase"; - -export type WebsiteState = ComponentState & { - _type_: "Website-builtin"; - url?: string; -}; - -export class WebsiteComponent extends ComponentBase { - declare state: Required; - element: HTMLIFrameElement; - - createElement(): HTMLElement { - let element = document.createElement("iframe"); - element.classList.add("rio-website"); - return element; - } - - updateElement( - deltaState: WebsiteState, - latentComponents: Set - ): void { - super.updateElement(deltaState, latentComponents); - - if ( - deltaState.url !== undefined && - deltaState.url !== this.element.src - ) { - this.element.src = deltaState.url; - } - } -} diff --git a/frontend/code/components/webview.ts b/frontend/code/components/webview.ts new file mode 100644 index 00000000..1d3a4a52 --- /dev/null +++ b/frontend/code/components/webview.ts @@ -0,0 +1,172 @@ +import { ComponentBase, ComponentState } from "./componentBase"; + +export type WebviewState = ComponentState & { + _type_: "Webview-builtin"; + content?: string; // Url or Html code + enable_pointer_events?: boolean; + resize_to_fit_content?: boolean; +}; + +export class WebviewComponent extends ComponentBase { + declare state: Required; + + private iframe: HTMLIFrameElement | null = null; + private resizeObserver: ResizeObserver | null = null; + private isInitialized = false; + + createElement(): HTMLElement { + let element = document.createElement("div"); + element.classList.add("rio-webview"); + return element; + } + + updateElement( + deltaState: WebviewState, + latentComponents: Set + ): void { + super.updateElement(deltaState, latentComponents); + + if (deltaState.content !== undefined) { + // If the URL/HTML hasn't actually changed from last time, don't do + // anything. This is important so scripts don't get re-executed each + // time the component is updated. + if ( + deltaState.content !== this.state.content || + !this.isInitialized + ) { + if (isUrl(deltaState.content)) { + this.element.innerHTML = ""; + + this.iframe = this.createIframe(); + this.iframe.src = deltaState.content; + + this.element.appendChild(this.iframe); + } else if (requiresIframe(deltaState.content)) { + this.element.innerHTML = ""; + + this.iframe = this.createIframe(); + this.iframe.srcdoc = deltaState.content; + + this.element.appendChild(this.iframe); + } else { + // Clean up stuff we no longer need + this.iframe = null; + this.resizeObserver = null; + + // Load the HTML + this.element.innerHTML = deltaState.content; + + // Just setting the innerHTML doesn't run scripts. Do that manually. + this.runScriptsInElement(); + } + + this.isInitialized = true; + } + } + + if (deltaState.enable_pointer_events !== undefined) { + this.element.style.pointerEvents = deltaState.enable_pointer_events + ? "auto" + : "none"; + } + + if ( + deltaState.resize_to_fit_content !== undefined && + this.iframe !== null + ) { + if (deltaState.resize_to_fit_content) { + if (this.resizeObserver === null) { + this.resizeObserver = tryCreateIframeResizeObserver( + this.iframe + ); + } + } else { + if (this.resizeObserver !== null) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + + this.iframe.style.removeProperty("width"); + this.iframe.style.removeProperty("height"); + } + } + } + } + + createIframe(): HTMLIFrameElement { + let iframe = document.createElement("iframe"); + + let self = this; + iframe.addEventListener("load", function () { + // Careful, this code runs with a delay! If this iframe has + // already been replaced by other content, do nothing. + if ( + self.iframe !== iframe || + self.resizeObserver !== null || + !self.state.resize_to_fit_content + ) { + return; + } + + self.resizeObserver = tryCreateIframeResizeObserver(iframe); + }); + + return iframe; + } + + runScriptsInElement(): void { + for (let oldScriptElement of this.element.querySelectorAll("script")) { + // Create a new script element + const newScriptElement = document.createElement("script"); + + // Copy over all attributes + for (let i = 0; i < oldScriptElement.attributes.length; i++) { + const attr = oldScriptElement.attributes[i]; + newScriptElement.setAttribute(attr.name, attr.value); + } + + // And the source itself + newScriptElement.appendChild( + document.createTextNode(oldScriptElement.innerHTML) + ); + + // Finally replace the old script element with the new one so + // the browser executes it + oldScriptElement.parentNode!.replaceChild( + newScriptElement, + oldScriptElement + ); + } + } +} + +function isUrl(urlOrHtml: string): boolean { + try { + new URL(urlOrHtml); + return true; + } catch (error) { + return false; + } +} + +function requiresIframe(html: string): boolean { + return html.match(/^\s*(])/i) !== null; +} + +function tryCreateIframeResizeObserver( + iframe: HTMLIFrameElement +): ResizeObserver | null { + let contentDoc = iframe.contentDocument; + if (contentDoc === null) { + return null; + } + + let docElement = contentDoc.documentElement; + + let resizeObserver = new ResizeObserver(function () { + iframe.style.width = `${docElement.scrollWidth}px`; + iframe.style.height = `${docElement.scrollHeight}px`; + }); + resizeObserver.observe(docElement); + + return resizeObserver; +} diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 57ec6c6d..0cd8c6c0 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -167,6 +167,14 @@ $monospace-fonts: var(--rio-global-monospace-font), monospace; flex-grow: 1; } +// Fallback dialog for requestFileUpload function +.request-file-upload-fallback-dialog { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + // General a { color: var(--rio-local-level-2-bg); @@ -196,7 +204,17 @@ body { font-family: var(--rio-global-font, sans-serif); - @include single-container(); + // It's pretty common for random elements to be added to the by + // browser extensions or JS libraries, so we can't simply use + // `@single-container()`. + // + // I'm not really sure what purpose(s) those elements serve, but it seems + // unwise to simply hide them. For now, we'll turn the body into a Stack. + display: inline-grid; + & > * { + grid-row: 1; + grid-column: 1; + } } // Force input elements to use the font-family we specified. For some reason @@ -355,9 +373,7 @@ select { background-color: transparent; opacity: 0; - transition: - opacity 0.3s ease-in-out, - background-color 1s ease-in-out; + transition: opacity 0.3s ease-in-out, background-color 1s ease-in-out; & > * { transform: translateY(-5rem); @@ -843,9 +859,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; outline: 0.15rem solid var(--rio-global-disabled-bg-variant); - transition: - all 0.3s ease-in-out, - outline 0.15s linear; + transition: all 0.3s ease-in-out, outline 0.15s linear; } svg { @@ -969,9 +983,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; } .rio-dropdown-popup { - transition: - max-height 0.2s ease-in-out, - box-shadow 0.2s ease-in-out; + transition: max-height 0.2s ease-in-out, box-shadow 0.2s ease-in-out; } .rio-dropdown-popup:not(.rio-popup-manager-open) { @@ -1215,9 +1227,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; // styles depend on them. --outer-text-color: var(--rio-local-text-color); --outer-bg-active-color: var(--rio-local-bg-active); - transition: - color 0.1s ease-in-out, - border-color 0.1s ease-in-out; + transition: color 0.1s ease-in-out, border-color 0.1s ease-in-out; // Create a stacking context. This is needed so the `colored-text` and // `plain-text` styles can reliably create an ::after element behind the @@ -1236,9 +1246,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; background-color: var(--rio-local-bg); box-shadow: 0 0 0 transparent; - transition: - background-color 0.1s ease-in-out, - box-shadow 0.2s ease-in-out; + transition: background-color 0.1s ease-in-out, box-shadow 0.2s ease-in-out; } .rio-buttonstyle-major:hover { @@ -1407,9 +1415,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; opacity: 0; transform: translateY(-50%); - transition: - opacity 0.45s ease-in-out, - transform 0.35s ease; + transition: opacity 0.45s ease-in-out, transform 0.35s ease; } .rio-revealer-open > * > .rio-revealer-content-inner { @@ -1519,10 +1525,8 @@ $rio-input-box-small-label-spacing-top: 0.5rem; background-color: var(--rio-local-level-2-bg); opacity: 0%; - transition: - left var(--rio-slider-position-transition-time) ease-in-out, - width 0.15s ease-in-out, - height 0.15s ease-in-out, + transition: left var(--rio-slider-position-transition-time) ease-in-out, + width 0.15s ease-in-out, height 0.15s ease-in-out, opacity 0.15s ease-in-out; } @@ -1547,8 +1551,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; box-shadow: 0 0.1rem 0.2rem var(--rio-global-shadow-color); - transition: - left var(--rio-slider-position-transition-time) ease-in-out, + transition: left var(--rio-slider-position-transition-time) ease-in-out, background-color 0.1s ease-in-out; } @@ -1699,9 +1702,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; border-radius: 50%; transform: translate(-50%, -50%); - transition: - width 0.2s ease-in-out, - height 0.2s ease-in-out; + transition: width 0.2s ease-in-out, height 0.2s ease-in-out; } .rio-media-player-timeline:hover .rio-media-player-timeline-knob { @@ -1897,7 +1898,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; // directions. That's not what we want - we want to increase the parent's // width instead. &[data-scroll-x="never"][data-scroll-y="auto"] > * { - scrollbar-gutter: stable; + scrollbar-gutter: stable !important; } } @@ -2377,9 +2378,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; padding: 0.3rem; border-radius: 0.5rem; - transition: - opacity 0.1s ease-in-out, - color 0.1s ease-in-out, + transition: opacity 0.1s ease-in-out, color 0.1s ease-in-out, background-color 0.1s ease-in-out; } @@ -2405,9 +2404,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; linear-gradient(45deg, var(--checker-color) 25%, transparent 25%); background-size: var(--checker-size) var(--checker-size); - background-position: - 0 0, - 0 0, + background-position: 0 0, 0 0, calc(var(--checker-size) * -0.5) calc(var(--checker-size) * -0.5), calc(var(--checker-size) * 0.5) calc(var(--checker-size) * 0.5); } @@ -2611,9 +2608,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; background-color: var(--rio-local-bg); box-shadow: 0 0 0 var(--rio-global-shadow-color); - transition: - box-shadow 0.15s ease-out, - background-color 0.1s ease-out; + transition: box-shadow 0.15s ease-out, background-color 0.1s ease-out; } .rio-card-elevate-on-hover:hover { @@ -2667,9 +2662,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem; color: var(--rio-local-text-color); - transition: - background-color 0.1s ease-out, - color 0.1s ease-out; + transition: background-color 0.1s ease-out, color 0.1s ease-out; } .rio-switcher-bar-option > .rio-switcher-bar-icon { @@ -3136,11 +3129,8 @@ $rio-input-box-small-label-spacing-top: 0.5rem; position: fixed; z-index: $z-index-dev-tools-highlighter; - transition: - left 0.3s ease-in-out, - top 0.3s ease-in-out, - width 0.3s ease-in-out, - height 0.3s ease-in-out; + transition: left 0.3s ease-in-out, top 0.3s ease-in-out, + width 0.3s ease-in-out, height 0.3s ease-in-out; } @keyframes pulse { @@ -3279,8 +3269,7 @@ html.picking-component * { } .rio-switcher-resizer { - transition: - min-width var(--rio-switcher-transition-time) ease-in-out, + transition: min-width var(--rio-switcher-transition-time) ease-in-out, min-height var(--rio-switcher-transition-time) ease-in-out; } @@ -3427,12 +3416,8 @@ html.picking-component * { opacity: 0; - transition: - opacity 0.3s ease-in-out, - left 0.3s ease-in-out, - top 0.3s ease-in-out, - width 0.3s ease-in-out, - height 0.3s ease-in-out; + transition: opacity 0.3s ease-in-out, left 0.3s ease-in-out, + top 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out; } .rio-code-explorer-highlighter::after { @@ -3506,11 +3491,8 @@ html.picking-component * { transform: translate(-50%, -50%); - transition: - top var(--rio-ripple-duration), - left var(--rio-ripple-duration), - width var(--rio-ripple-duration), - height var(--rio-ripple-duration), + transition: top var(--rio-ripple-duration), left var(--rio-ripple-duration), + width var(--rio-ripple-duration), height var(--rio-ripple-duration), opacity var(--rio-ripple-duration); } @@ -3527,17 +3509,14 @@ html.picking-component * { transform: scale(0); opacity: 0; - transition: - transform 0.2s linear, - opacity 0.1s ease-in-out; + transition: transform 0.2s linear, opacity 0.1s ease-in-out; } .rio-popup-animation-scale.rio-popup-manager-open { transform: scale(1); opacity: 1; - transition: - transform 0.2s $transition-timing-overshoot, + transition: transform 0.2s $transition-timing-overshoot, opacity 0.1s ease-in-out; } @@ -3696,9 +3675,7 @@ html.picking-component * { -moz-user-select: none; -ms-user-select: none; - transition: - background-color 0.1s ease-out, - box-shadow 0.15s ease-out; + transition: background-color 0.1s ease-out, box-shadow 0.15s ease-out; } .rio-layout-display-child:not(.rio-layout-display-target) { @@ -3757,11 +3734,8 @@ html.picking-component * { width: 1.4rem; height: 1.4rem; - transition: - opacity 0.2s ease-in-out, - border-width 0.2s ease-in-out, - border-color 0.2s ease-in-out, - background-color 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out, border-width 0.2s ease-in-out, + border-color 0.2s ease-in-out, background-color 0.2s ease-in-out; } .rio-checkbox.is-on .rio-checkbox-border { @@ -3787,10 +3761,9 @@ html.picking-component * { transform: scale(1); } -// Html -.rio-html { - // Disable pointer events, the user can always turn them back on if desired - pointer-events: none; +// Webview +.rio-webview { + // `pointer-events` is controlled via JS. @include single-container(); // FIXME: Should we do this? } @@ -3810,9 +3783,7 @@ html.picking-component * { opacity: 0; // Theses durations are also referenced in code! - transition: - opacity 0.2s ease-in-out, - background-color 0.5s ease-in-out; + transition: opacity 0.2s ease-in-out, background-color 0.5s ease-in-out; & > * { transform: translateY(-2rem); @@ -3831,12 +3802,7 @@ html.picking-component * { } } -// Website -.rio-website { - pointer-events: auto; -} - -// File Picker Area +// Upload Area .rio-file-picker-area { pointer-events: auto; @@ -3865,9 +3831,7 @@ html.picking-component * { color: var(--rio-local-fg); - transition: - background-color 0.1s ease-in-out, - color 0.1s ease-in-out; + transition: background-color 0.1s ease-in-out, color 0.1s ease-in-out; &::after { // Using the full foreground color for the outline is brutal. The ::after @@ -4059,9 +4023,7 @@ html.picking-component * { opacity: 0; transform: scale(0.5); - transition: - background-color 0.1s ease-in-out, - opacity 0.1s ease-in-out, + transition: background-color 0.1s ease-in-out, opacity 0.1s ease-in-out, transform 0.1s ease-in-out; } @@ -4166,9 +4128,7 @@ $graph-editor-port-size: 1.4rem; outline: 0 solid var(--rio-global-primary-bg); - transition: - background-color 0.1s ease-in-out, - box-shadow 0.1s ease-in-out, + transition: background-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out, outline 0.1s $transition-timing-overshoot; &:has(.rio-graph-editor-node-header:hover) { @@ -4258,11 +4218,8 @@ $graph-editor-port-size: 1.4rem; border-radius: 50%; background-color: var(--port-color); - transition: - left 0.1s ease-in-out, - top 0.1s ease-in-out, - right 0.1s ease-in-out, - bottom 0.1s ease-in-out; + transition: left 0.1s ease-in-out, top 0.1s ease-in-out, + right 0.1s ease-in-out, bottom 0.1s ease-in-out; } .rio-graph-editor-port-circle:hover::after { diff --git a/rio/components/html.py b/rio/components/html.py index ddd529bb..6615b4d6 100644 --- a/rio/components/html.py +++ b/rio/components/html.py @@ -1,13 +1,16 @@ import dataclasses import typing as t -from .fundamental_component import FundamentalComponent +from ..deprecations import deprecated +from .component import Component +from .webview import Webview __all__ = ["Html"] @t.final -class Html(FundamentalComponent): +@deprecated(since="0.11", replacement=Webview) +class Html(Component): """ Displays raw HTML. @@ -42,5 +45,8 @@ class Html(FundamentalComponent): _: dataclasses.KW_ONLY enable_pointer_events: bool = True - -Html._unique_id_ = "Html-builtin" + def build(self): + return Webview( + self.html, + enable_pointer_events=self.enable_pointer_events, + ) diff --git a/rio/components/switch.py b/rio/components/switch.py index 7eb5740d..39d20b0c 100644 --- a/rio/components/switch.py +++ b/rio/components/switch.py @@ -113,7 +113,7 @@ class Switch(FundamentalComponent): if "is_on" in delta_state and not self.is_sensitive: raise AssertionError( - f"Frontend tried to set `Switch.is_on` even though `is_sensitive` is `False`" + "Frontend tried to set `Switch.is_on` even though `is_sensitive` is `False`" ) async def _call_event_handlers_for_delta_state( diff --git a/rio/components/website.py b/rio/components/website.py index e1f033f5..d945e21b 100644 --- a/rio/components/website.py +++ b/rio/components/website.py @@ -1,9 +1,9 @@ import typing as t -from uniserde import JsonDoc - +from ..deprecations import deprecated from ..utils import URL -from .fundamental_component import FundamentalComponent +from .component import Component +from .webview import Webview __all__ = [ "Website", @@ -11,7 +11,8 @@ __all__ = [ @t.final -class Website(FundamentalComponent): +@deprecated(since="0.11", replacement=Webview) +class Website(Component): """ Displays a website. @@ -37,10 +38,5 @@ class Website(FundamentalComponent): url: URL - def _custom_serialize_(self) -> JsonDoc: - return { - "url": str(self.url), - } - - -Website._unique_id_ = "Website-builtin" + def build(self): + return Webview(self.url) diff --git a/rio/components/webview.py b/rio/components/webview.py new file mode 100644 index 00000000..5aeaa149 --- /dev/null +++ b/rio/components/webview.py @@ -0,0 +1,69 @@ +import dataclasses +import typing as t + +from uniserde import JsonDoc + +from ..utils import URL +from .fundamental_component import FundamentalComponent + +__all__ = ["Webview"] + + +@t.final +class Webview(FundamentalComponent): + """ + Displays a website or renders HTML. + + `Webview` takes a URL or HTML markup as input and displays the website + or the rendered HTML in your app. + + + ## Attributes + + `content`: The URL of the website you want to display, or the HTML + you want to render. + + `enable_pointer_events`: Whether the `Webview` component (and its contents) + are clickable. + + `resize_to_fit_content`: Whether the `Webview` component should automatically + update its size to match the size of its content. Note that this won't + work if the displayed website's domain doesn't match your own domain. + + + ## Examples + + This will display a website based on its URL: + + ```python + rio.Webview( + rio.URL("https://www.example.com"), + ) + ``` + + While this will render the given HTML markup: + + ```python + rio.Webview('Hello World') + ``` + + The HTML doesn't necessarily have to be an entire website; something + like this will also work just fine: + + ```python + rio.Webview('

Hello World

') + ``` + """ + + content: URL | str + _: dataclasses.KW_ONLY + enable_pointer_events: bool = True + resize_to_fit_content: bool = True + + def _custom_serialize_(self) -> JsonDoc: + return { + "content": str(self.content), + } + + +Webview._unique_id_ = "Webview-builtin"