diff --git a/experimental/element_embedding_demo/.gitignore b/experimental/element_embedding_demo/.gitignore new file mode 100644 index 000000000..734c8638a --- /dev/null +++ b/experimental/element_embedding_demo/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# Keeping the repo +.metadata +pubspec.lock + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/experimental/element_embedding_demo/README.md b/experimental/element_embedding_demo/README.md new file mode 100644 index 000000000..4e0fcbd2e --- /dev/null +++ b/experimental/element_embedding_demo/README.md @@ -0,0 +1,25 @@ +# element_embedding_demo + +This package contains the application used to demonstrate the +upcoming Flutter web feature: "Element Embedding". + +This was first shown on the Flutter Forward event in Nairobi (Kenya), by Tim +Sneath. [See the replay here](https://www.youtube.com/watch?v=zKQYGKAe5W8&t=5799s). + +## Running the demo + +The demo is a Flutter web app, so it can be run as: + +```terminal +$ flutter run -d chrome +``` + +## Points of Interest + +* Check the new JS Interop: + * Look at `lib/main.dart`, find the `@js.JSExport()` annotation. + * Find the JS code that interacts with Dart in `web/js/demo-js-interop.js`. +* See how the Flutter web application is embedded into the page now: + * Find `hostElement` in `web/index.html`. + +_(Built by @ditman, @kevmoo and @malloc-error)_ diff --git a/experimental/element_embedding_demo/assets/dash.png b/experimental/element_embedding_demo/assets/dash.png new file mode 100644 index 000000000..b0710449e Binary files /dev/null and b/experimental/element_embedding_demo/assets/dash.png differ diff --git a/experimental/element_embedding_demo/lib/main.dart b/experimental/element_embedding_demo/lib/main.dart new file mode 100644 index 000000000..159af27ed --- /dev/null +++ b/experimental/element_embedding_demo/lib/main.dart @@ -0,0 +1,335 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:js/js.dart' as js; +import 'package:js/js_util.dart' as js_util; + +void main() { + runApp(const MyApp()); +} + +enum DemoScreen { counter, textField, custom } + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +@js.JSExport() +class _MyAppState extends State { + final _streamController = StreamController.broadcast(); + DemoScreen _currentDemoScreen = DemoScreen.counter; + int _counterScreenCount = 0; + + @override + void initState() { + super.initState(); + final export = js_util.createDartExport(this); + js_util.setProperty(js_util.globalThis, '_appState', export); + js_util.callMethod(js_util.globalThis, '_stateSet', []); + } + + @override + void dispose() { + _streamController.close(); + super.dispose(); + } + + @js.JSExport() + void increment() { + if (_currentDemoScreen == DemoScreen.counter) { + setState(() { + _counterScreenCount++; + _streamController.add(null); + }); + } + } + + @js.JSExport() + void addHandler(void Function() handler) { + _streamController.stream.listen((event) { + handler(); + }); + } + + @js.JSExport() + int get count => _counterScreenCount; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Element embedding', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + debugShowCheckedModeBanner: false, + home: demoScreenRouter(_currentDemoScreen), + ); + } + + Widget demoScreenRouter(DemoScreen which) { + switch (which) { + case DemoScreen.counter: + return CounterDemo( + title: 'Counter', + numToDisplay: _counterScreenCount, + incrementHandler: increment, + ); + case DemoScreen.textField: + return const TextFieldDemo(title: 'Note to Self'); + case DemoScreen.custom: + return const CustomDemo(title: 'Character Counter'); + } + } + + @js.JSExport() + void changeDemoScreenTo(String screenString) { + setState(() { + switch (screenString) { + case 'counter': + _currentDemoScreen = DemoScreen.counter; + break; + case 'textField': + _currentDemoScreen = DemoScreen.textField; + break; + case 'custom': + _currentDemoScreen = DemoScreen.custom; + break; + default: + _currentDemoScreen = DemoScreen.counter; + break; + } + }); + } +} + +class CounterDemo extends StatefulWidget { + final String title; + final int numToDisplay; + final VoidCallback incrementHandler; + + const CounterDemo({ + super.key, + required this.title, + required this.numToDisplay, + required this.incrementHandler, + }); + + @override + State createState() => _CounterDemoState(); +} + +class _CounterDemoState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '${widget.numToDisplay}', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: widget.incrementHandler, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} + +class TextFieldDemo extends StatelessWidget { + const TextFieldDemo({super.key, required this.title}); + final String title; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: const Center( + child: Padding( + padding: EdgeInsets.all(14.0), + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + // hintText: 'Text goes here!', + ), + ), + ), + ), + ); + } +} + +class CustomDemo extends StatefulWidget { + final String title; + + const CustomDemo({super.key, required this.title}); + + @override + State createState() => _CustomDemoState(); +} + +class _CustomDemoState extends State { + final double textFieldHeight = 80; + final Color colorPrimary = const Color(0xff027dfd); + // const Color(0xffd43324); + // const Color(0xff6200ee); + // const Color.fromARGB(255, 255, 82, 44); + final TextEditingController _textController = TextEditingController(); + late FocusNode textFocusNode; + + int totalCharCount = 0; + + @override + void initState() { + super.initState(); + textFocusNode = FocusNode(); + textFocusNode.requestFocus(); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: MediaQuery.of(context).size.height - textFieldHeight, + flexibleSpace: Container( + color: colorPrimary, + height: MediaQuery.of(context).size.height - textFieldHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'COUNT WITH DASH!', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + const SizedBox( + height: 26, + ), + Container( + width: 98, + height: 98, + decoration: BoxDecoration( + border: Border.all(width: 2, color: Colors.white), + shape: BoxShape.circle, + ), + child: Center( + child: Container( + width: 90, + height: 90, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/dash.png'), + fit: BoxFit.cover, + ), + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + ), + const SizedBox(height: 20), + Text( + totalCharCount.toString(), + style: const TextStyle(color: Colors.white, fontSize: 52), + ), + // const Text( + // 'characters typed', + // style: TextStyle(color: Colors.white, fontSize: 14), + // ), + ], + ), + ), + ), + body: Column( + children: [ + SizedBox( + height: textFieldHeight, + child: Center( + child: Padding( + padding: const EdgeInsets.only(left: 18, right: 18), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + focusNode: textFocusNode, + onSubmitted: (value) { + textFocusNode.requestFocus(); + }, + onChanged: (value) { + handleChange(); + }, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox( + width: 12, + ), + Center( + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: colorPrimary, + shape: BoxShape.circle, + ), + child: IconButton( + icon: const Icon(Icons.refresh), + color: Colors.white, + onPressed: () { + handleClear(); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + void handleChange() { + setState(() { + totalCharCount = _textController.value.text.toString().length; + }); + } + + void handleClear() { + setState(() { + _textController.clear(); + totalCharCount = 0; + }); + textFocusNode.requestFocus(); + } +} diff --git a/experimental/element_embedding_demo/pubspec.yaml b/experimental/element_embedding_demo/pubspec.yaml new file mode 100644 index 000000000..659daedba --- /dev/null +++ b/experimental/element_embedding_demo/pubspec.yaml @@ -0,0 +1,23 @@ +name: element_embedding_demo +description: A small app to be embedded into a HTML element (see web/index.html) +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0-0 <4.0.0' + +dependencies: + cupertino_icons: ^1.0.2 + flutter: + sdk: flutter + js: ^0.6.6 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true + assets: + - assets/dash.png diff --git a/experimental/element_embedding_demo/web/css/style.css b/experimental/element_embedding_demo/web/css/style.css new file mode 100644 index 000000000..7c654bc7f --- /dev/null +++ b/experimental/element_embedding_demo/web/css/style.css @@ -0,0 +1,260 @@ +@font-face { + font-family: "DM Sans"; + src: url(../fonts/DMSans-Regular.ttf); + font-weight: normal; +} + +@font-face { + font-family: "DM Sans"; + src: url(../fonts/DMSans-Bold.ttf); + font-weight: 700; +} + +/** Reset */ +* { + box-sizing: border-box; + font-family: "DM Sans", sans-serif; +} +html, body { + margin: 0; + padding: 0; + min-height: 100vh; +} + +body { + background-color: #fff; + background-image: radial-gradient( + ellipse at bottom, + #fafafa 5%, + transparent 60% + ), + linear-gradient(136deg, transparent, #eee 290%), + linear-gradient(115deg, #fafafa, transparent 40%), + linear-gradient(180deg, transparent 0, #ddd 70%), + radial-gradient(ellipse at -70% -180%, transparent 80%, #eee 0), + radial-gradient(ellipse at bottom, #71c7ee 40%, transparent 80%), + radial-gradient(ellipse at 5% 340%, transparent 80%, #ddd 0); + background-repeat: no-repeat; + color: #555; +} + +/** Layout **/ +body { display: flex; flex-direction: column; } +section.contents { + flex: 1 1 auto; + flex-direction: row; + display: flex; +} +section.contents aside { + flex: 0; + display: flex; + flex-direction: column; + order: -1; +} +section.contents aside fieldset { + display: flex; + flex-flow: wrap; + justify-content: space-between; + align-items: flex-end; +} +section.contents aside .align-top { + align-self: flex-start; +} +section.contents article { + flex: 1; + margin-top: 50px; + display: flex; + justify-content: center; +} + +/** Title */ +h1 { + font-weight: 700; + font-size: 48px; + padding: 0; + line-height: .9em; + letter-spacing: -2px; + margin: 0 0 30px 0; +} + +/** Controls for the demo (left column) */ +#demo_controls { + background: linear-gradient(90deg, rgba(255,255,255,1) 10%, rgba(255,255,255,0) 100%); + padding: 40px 20px 0px 20px; + z-index: 10; +} +#demo_controls fieldset { + padding: 0; + border: none; + width: 210px; +} +#demo_controls legend { + text-align: center; + font-size: 20px; + line-height: 40px; + margin-bottom: 3px; +} +#demo_controls select.screen { + display: block; + width: 120px; + padding: 4px; + text-align: center; + margin-bottom: 10px; +} +#demo_controls input { + display: block; + width: 100px; + margin: 0 0 10px 0; + text-align: center; +} +/** Keep controls that */ +#demo_controls .tight input { + margin: 0px; +} +#demo_controls input[type="button"] { + line-height: 10px; + font-size: 14px; + border-radius: 15px; + border: 1px solid #aaa; + border-style: outset; + background-color: #fff; + height: 30px; + color: #555; + transition: all 100ms ease-in-out; + cursor: pointer; +} +#demo_controls input[type="button"]:hover { + /* .active:hover background-color: #96B6E3;*/ + border-color: #1c68d4; + background-color: #1c68d4; + color: white; +} +#demo_controls input[type="button"].active { + border-color: #1c68d4; + background-color: #1c68d4; + color: white; +} +#demo_controls input#value { + font-size: 32px; + line-height: 1em; + min-height: 30px; + color: #888; +} +#demo_controls input#increment { + /* Center vertically next to taller input#value */ + position: relative; + top: -6px; +} +#demo_controls .disabled { + pointer-events: none; + opacity: .5; +} + +/** The style for the DIV where flutter will be rendered, and the CSS fx */ +#flutter_target { + border: 1px solid #aaa; + width: 320px; + height: 480px; + border-radius: 0px; + transition: all 150ms ease-in; +} +#flutter_target.resize { + width: 480px; + height: 320px; +} +#flutter_target.spin { animation: spin 6400ms ease-in-out infinite; } +#flutter_target.shadow { position: relative; } +#flutter_target.shadow::before { + content: ""; + position: absolute; + display: block; + width: 100%; + top: calc(100% - 1px); + left: 0; + height: 1px; + background-color: black; + border-radius: 50%; + z-index: -1; + transform: rotateX(80deg); + box-shadow: 0px 0px 60px 38px rgb(0 0 0 / 25%); +} +#flutter_target.mirror { + -webkit-box-reflect: below 0px linear-gradient(to bottom, rgba(0,0,0,0.0), rgba(0,0,0,0.4)); +} + +@keyframes spin { + 0% { + transform: perspective(1000px) rotateY(0deg); + animation-timing-function: ease-in; + } + 15% { + transform: perspective(1000px) rotateY(165deg); + animation-timing-function: linear; + } + 75% { + transform: perspective(1000px) rotateY(195deg); + animation-timing-function: linear; + } + 90% { + transform: perspective(1000px) rotateY(359deg); + animation-timing-function: ease-out; + } + 100% { + transform: perspective(1000px) rotateY(359deg); + animation-timing-function: linear; + } +} + +/** "Handheld"/Device mode container */ +#handheld::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: url(../icons/unsplash-x9WGMWwp1NM.png) no-repeat; + background-size: 1000px; + background-position: top right; + opacity: 1; + transition: opacity 200ms ease-out; +} + +#handheld::after { + content: ""; + position: absolute; + display: block; + width: 77px; + height: 67px; + top: 534px; + right: 573px; + background: url(../icons/nail.png) no-repeat; + background-size: 77px; + opacity: 1; + transition: opacity 200ms ease-out; +} + +#handheld.hidden::before, +#handheld.hidden::after { + opacity: 0; +} + +#flutter_target.handheld { + position: absolute; + right: 0px; + transform-origin: 0px 0px 0px; + transform: rotate(-14.1deg) scale(0.80) translate(-539px, -45px); + width: 316px; + height: 678px; + border-radius: 34px; + border: 1px solid #000; + overflow: hidden; +} + +.imageAttribution { + position: absolute; + bottom: 6px; + right: 6px; + font-size: 10px; +} +.imageAttribution, .imageAttribution a { color: #fff; } \ No newline at end of file diff --git a/experimental/element_embedding_demo/web/favicon.ico b/experimental/element_embedding_demo/web/favicon.ico new file mode 100644 index 000000000..f8a147bf2 Binary files /dev/null and b/experimental/element_embedding_demo/web/favicon.ico differ diff --git a/experimental/element_embedding_demo/web/fonts/DMSans-Bold.ttf b/experimental/element_embedding_demo/web/fonts/DMSans-Bold.ttf new file mode 100644 index 000000000..e70172a06 Binary files /dev/null and b/experimental/element_embedding_demo/web/fonts/DMSans-Bold.ttf differ diff --git a/experimental/element_embedding_demo/web/fonts/DMSans-Regular.ttf b/experimental/element_embedding_demo/web/fonts/DMSans-Regular.ttf new file mode 100644 index 000000000..cad73f073 Binary files /dev/null and b/experimental/element_embedding_demo/web/fonts/DMSans-Regular.ttf differ diff --git a/experimental/element_embedding_demo/web/fonts/OFL.txt b/experimental/element_embedding_demo/web/fonts/OFL.txt new file mode 100644 index 000000000..9de490697 --- /dev/null +++ b/experimental/element_embedding_demo/web/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2017 Indian Type Foundry (info@indiantypefoundry.com). Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/experimental/element_embedding_demo/web/icons/Icon-192.png b/experimental/element_embedding_demo/web/icons/Icon-192.png new file mode 100644 index 000000000..210951d01 Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/Icon-192.png differ diff --git a/experimental/element_embedding_demo/web/icons/Icon-512.png b/experimental/element_embedding_demo/web/icons/Icon-512.png new file mode 100644 index 000000000..25618cb57 Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/Icon-512.png differ diff --git a/experimental/element_embedding_demo/web/icons/Icon-maskable-192.png b/experimental/element_embedding_demo/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..210951d01 Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/Icon-maskable-192.png differ diff --git a/experimental/element_embedding_demo/web/icons/Icon-maskable-512.png b/experimental/element_embedding_demo/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..25618cb57 Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/Icon-maskable-512.png differ diff --git a/experimental/element_embedding_demo/web/icons/favicon.png b/experimental/element_embedding_demo/web/icons/favicon.png new file mode 100644 index 000000000..289117d66 Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/favicon.png differ diff --git a/experimental/element_embedding_demo/web/icons/nail.png b/experimental/element_embedding_demo/web/icons/nail.png new file mode 100644 index 000000000..0a4450eeb Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/nail.png differ diff --git a/experimental/element_embedding_demo/web/icons/unsplash-x9WGMWwp1NM.png b/experimental/element_embedding_demo/web/icons/unsplash-x9WGMWwp1NM.png new file mode 100644 index 000000000..46f973e2f Binary files /dev/null and b/experimental/element_embedding_demo/web/icons/unsplash-x9WGMWwp1NM.png differ diff --git a/experimental/element_embedding_demo/web/index.html b/experimental/element_embedding_demo/web/index.html new file mode 100644 index 000000000..32f4f86ba --- /dev/null +++ b/experimental/element_embedding_demo/web/index.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + Element embedding + + + + + + + + +
+
+
+
+ + +
+ + + + + + diff --git a/experimental/element_embedding_demo/web/js/demo-css-fx.js b/experimental/element_embedding_demo/web/js/demo-css-fx.js new file mode 100644 index 000000000..b857cecf1 --- /dev/null +++ b/experimental/element_embedding_demo/web/js/demo-css-fx.js @@ -0,0 +1,82 @@ +// Manages toggling the VFX of the buttons +(function () { + "use strict"; + + let handheld; + + let fxButtons = document.querySelector("#fx"); + let flutterTarget = document.querySelector("#flutter_target"); + + let attribution = document.createElement("span"); + attribution.className = "imageAttribution"; + attribution.innerHTML = "Photo by Nathana Rebouças on Unsplash"; + + // (Re)moves the flutterTarget inside a div#handheld. + function handleHandHeld(fx) { + resetRotation(); + if (!handheld) { + handheld = document.createElement("div"); + handheld.id = "handheld"; + handheld.classList.add("hidden"); + // Inject before the flutterTarget + flutterTarget.parentNode.insertBefore(handheld, flutterTarget); + handheld.append(flutterTarget); + handheld.append(attribution); + window.setTimeout(function () { + handheld.classList.remove("hidden"); + }, 100); + // Disable all effects on the flutter container + flutterTarget.className = ""; + setOtherFxEnabled(false); + } else { + handheld.classList.add("hidden"); + window.setTimeout(function () { + handheld.parentNode.insertBefore(flutterTarget, handheld); + handheld.remove(); + handheld = null; + }, 210); + setOtherFxEnabled(true); + } + window.requestAnimationFrame(function () { + // Let the browser flush the DOM... + flutterTarget.classList.toggle(fx); + }); + } + + // Sets a rotation style on the flutterTarget (in degrees). + function handleRotation(degrees) { + flutterTarget.style.transform = `perspective(1000px) rotateY(${degrees}deg)`; + } + + // Removes the inline style from the flutterTarget. + function resetRotation() { + flutterTarget.style = null; + } + + // Enables/disables the buttons that are not compatible with the "handheld" mode. + function setOtherFxEnabled(enabled) { + fxButtons.querySelectorAll('input').forEach((btn) => { + if (btn.dataset.fx != 'handheld') { + btn.classList.toggle('disabled', !enabled); + } + }); + } + + // Handles clicks on the buttons inside #fx. + fxButtons.addEventListener("click", (event) => { + let fx = event.target.dataset.fx; + if (fx === "handheld") { + handleHandHeld(fx); + return; + } + flutterTarget.classList.toggle(fx); + }); + + fxButtons.addEventListener("input", (event) => { + if (event.target.id === "rotation") { + flutterTarget.classList.toggle("spin", false); + handleRotation(event.target.value); + } + }) + +})(); diff --git a/experimental/element_embedding_demo/web/js/demo-js-interop.js b/experimental/element_embedding_demo/web/js/demo-js-interop.js new file mode 100644 index 000000000..e35164e68 --- /dev/null +++ b/experimental/element_embedding_demo/web/js/demo-js-interop.js @@ -0,0 +1,43 @@ +// Sets up a channel to JS-interop with Flutter +(function() { + "use strict"; + // This function will be called from Flutter when it prepares the JS-interop. + window._stateSet = function () { + window._stateSet = function () { + console.log("Call _stateSet only once!"); + }; + + // The state of the flutter app, see `class _MyAppState` in lib/main.dart. + let appState = window._appState; + + let valueField = document.querySelector("#value"); + let updateState = function () { + valueField.value = appState.count; + }; + + // Register a callback to update the HTML field from Flutter. + appState.addHandler(updateState); + + // Render the first value (0). + updateState(); + + let incrementButton = document.querySelector("#increment"); + incrementButton.addEventListener("click", (event) => { + appState.increment(); + }); + + let screenSelector = document.querySelector("#screen-selector"); + screenSelector.addEventListener("change", (event) => { + appState.changeDemoScreenTo(event.target.value); + setJsInteropControlsEnabled(event.target.value === 'counter'); + }); + + // Enables/disables the Value/Increment controls. + function setJsInteropControlsEnabled(enabled) { + let elements = document.querySelectorAll("#increment, label[for='value']"); + elements.forEach((el) => { + el.classList.toggle('disabled', !enabled); + }) + } + }; +}()); diff --git a/experimental/element_embedding_demo/web/manifest.json b/experimental/element_embedding_demo/web/manifest.json new file mode 100644 index 000000000..c2e2f0891 --- /dev/null +++ b/experimental/element_embedding_demo/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "element_embedding_demo", + "short_name": "element_embedding", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "An example of how to embed a Flutter Web app into any HTML Element of a page.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index 0632b987d..328d317b5 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -22,6 +22,7 @@ declare -ar PROJECT_NAMES=( "desktop_photo_search/fluent_ui" "desktop_photo_search/material" "experimental/context_menus" + "experimental/element_embedding_demo" "experimental/federated_plugin/federated_plugin" "experimental/federated_plugin/federated_plugin/example" "experimental/federated_plugin/federated_plugin_macos"