diff --git a/.github/workflows/developer-guide-docs.yml b/.github/workflows/developer-guide-docs.yml index 8cefcec3378..fa7493a7fe5 100644 --- a/.github/workflows/developer-guide-docs.yml +++ b/.github/workflows/developer-guide-docs.yml @@ -149,7 +149,10 @@ jobs: if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' run: | set -euo pipefail - xvfb-run -a mvn -B -ntp -f maven/pom.xml \ + # Through retry.sh, transient failures only, like the demo install below: + # this is the step that resolves the most from Central, and a single 502 + # on a plugin dependency (maven-shared-utils) failed the whole guide build. + RETRY_ONLY_MATCHING=transient xvfb-run -a bash scripts/ci/retry.sh mvn -B -ntp -f maven/pom.xml \ -pl core,javase,android,css-compiler,codenameone-maven-plugin,backend \ -am install \ -Plocal-dev-javase \ diff --git a/.github/workflows/flutter-bench.yml b/.github/workflows/flutter-bench.yml new file mode 100644 index 00000000000..65be01d61f8 --- /dev/null +++ b/.github/workflows/flutter-bench.yml @@ -0,0 +1,505 @@ +name: Flutter benchmark + +# Measures ONE application built two ways -- Flutter's own release build and +# the identical Dart source transpiled by Codename One -- and publishes the +# comparison to the pull request and to port status. +# +# Neither application is vendored. `scripts/flutter-bench/app/prepare.sh` takes +# the gallery from the Flutter SDK this workflow installs +# ($FLUTTER_ROOT/dev/integration_tests/new_gallery) and copies the SAME 159 +# files into both sides, so the comparison cannot drift: there is no second +# copy for an edit to land on. The Codename One side is generated from the +# SHIPPING archetype, which means a change that breaks the documented Flutter +# wiring breaks this workflow too instead of passing unnoticed. + +on: + pull_request: + paths: + - 'maven/flutter-runtime/**' + - 'maven/dart-transpiler/**' + - 'maven/dart-runtime/**' + - 'maven/codenameone-maven-plugin/**' + # The builders the plugin runs live here since the build engine split out. + - 'maven/build-engine/**' + - 'maven/cn1app-archetype/**' + - 'scripts/flutter-bench/**' + # The regression gate's rules and CPU detection are the ParparVM gate's own + # (flutter_baseline.py runs vm/selfhost/perf_baseline.py with its own layout). + - 'vm/selfhost/perf_baseline.py' + - 'vm/selfhost/perf-gate.py' + - '.github/workflows/flutter-bench.yml' + schedule: + # Nightly, so port status carries a current number and the PR runs have a + # trend to sit against rather than a single point. + - cron: '0 3 * * *' + workflow_dispatch: + inputs: + platforms: + description: 'Comma separated platform ids, or "all"' + default: 'all' + +concurrency: + group: flutter-bench-${{ github.ref }} + cancel-in-progress: true + +env: + # The Flutter release this benchmark measures against, PINNED rather than + # tracking stable. + # + # A benchmark whose reference moves on its own is not one: the gallery, its + # dependency resolution and Flutter's own code generation all change between + # releases, so a number from last week and a number from today would differ + # for reasons that have nothing to do with this repository. Tracking stable + # also broke outright -- 3.47.5's tree resolved google_fonts to a version + # without robotoCondensed and three studies stopped compiling. + # + # Bumping this is a deliberate act that re-baselines the comparison: expect + # every start-up and memory ratio to move, and rebaseline them in the same + # change's own overlay (scripts/flutter-bench/flutter_baseline.py calibrate + # --pr N --reason "Flutter " over every platform's baseline-*.json). + FLUTTER_REF: '3.35.4' + +permissions: + contents: read + pull-requests: write + # The framework job runs in the pr-ci-container image on ghcr, and pulling it + # needs this. Without it the job dies in "Initialize containers" with a bare + # "Error response from daemon: denied", which reads like the image is missing + # rather than like the token cannot see it. + packages: read + +jobs: + # The framework is built ONCE, here, and the platform jobs consume the + # artifacts. Two reasons it cannot be built per platform: + # + # * It needs JDK 8. codenameone-javase imports javafx.*, which no JDK after + # 8 carries, so a JDK 17 build of the reactor fails compiling the JavaSE + # port. An earlier version of this workflow set up JDK 8 and then JDK 17, + # and since the last setup-java wins it silently built everything on 17. + # * macOS arm64 runners have no Temurin 8 at all -- setup-java reports + # "Could not find satisfied version for SemVer 8", the oldest offered + # being 11. + # + # The container is the same one pr.yml uses, which bakes JDK 8 and a + # cn1-binaries snapshot. + framework: + name: framework + runs-on: ubuntu-latest + container: ghcr.io/codenameone/codenameone/pr-ci-container:latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + + - name: Link cn1-binaries + run: ln -s /opt/cn1-binaries ../cn1-binaries + + # The container supplies JAVA_HOME_8 and JAVA_HOME_17; this is how pr.yml + # selects between them. Not setup-java, which would download a second JDK + # into an image that already has the right ones, and not a hard-coded + # path -- an earlier version of this guessed + # /usr/lib/jvm/java-8-openjdk-amd64 and the build died on "The JAVA_HOME + # environment variable is not defined correctly", which says nothing + # about which JDK was wanted or what was there. + - name: Check the container's JDKs + run: | + for var in JAVA_HOME_8 JAVA_HOME_17; do + eval "path=\${$var:-}" + if [ -z "$path" ] || [ ! -x "$path/bin/javac" ]; then + echo "$var is unset or has no javac: '${path:-}'" >&2 + exit 1 + fi + echo "$var=$path" + done + + # JDK 8 for everything that targets it. The flutter modules are NOT in + # this reactor: maven/pom.xml puts them behind a profile that activates + # on JDK 17 and newer, precisely because they cannot compile here. + - name: Build the framework on JDK 8 + run: | + cd maven + # -Plocal-dev-javase is required to build the JavaSE port, which + # -am pulls in. Without it the build dies in javase's + # cn1-generate-build-hint-data execution, which is a long way from + # anything this workflow is about. + # Through retry.sh, like every other Maven step in this repository: Central + # throttles runners with 403/429, and a bare mvn failed this job twice in + # one day at resolving org.junit:junit-bom before compiling anything. + JAVA_HOME="$JAVA_HOME_8" bash ../scripts/ci/retry.sh mvn -B -q install -DskipTests \ + -Dmaven.javadoc.skip=true -Dmaven.source.skip=true \ + -Plocal-dev-javase \ + -Dmaven.repo.local="$GITHUB_WORKSPACE/m2" \ + -pl core,dart-transpiler,codenameone-maven-plugin,cn1app-archetype -am + + # Without -am: everything these depend on was installed by the pass + # above, and re-resolving it here would rebuild core on the wrong JDK. + - name: Build the Flutter runtime on JDK 17 + run: | + cd maven + JAVA_HOME="$JAVA_HOME_17" bash ../scripts/ci/retry.sh mvn -B -q install -DskipTests \ + -Dmaven.javadoc.skip=true -Dmaven.source.skip=true \ + -Plocal-dev-javase \ + -Dmaven.repo.local="$GITHUB_WORKSPACE/m2" \ + -pl dart-runtime,flutter-runtime + + - name: Pack the repository + run: tar -C "$GITHUB_WORKSPACE" -czf framework-m2.tgz m2 + + - name: Upload the repository + uses: actions/upload-artifact@v4 + with: + name: flutter-bench-framework + path: framework-m2.tgz + retention-days: 1 + + # The platforms to measure. Every one on a pull request or the nightly run; on a + # manual dispatch, the ones its `platforms` input names. The input used to be + # read by nothing, so asking to re-run one platform launched all six -- several + # of them two-hour native builds. + plan: + name: plan + runs-on: ubuntu-latest + outputs: + include: ${{ steps.pick.outputs.include }} + steps: + - id: pick + env: + REQUESTED: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'all' }} + shell: python3 {0} + run: | + import json, os, sys + ALL = [ + {"platform": "macos", "runner": "macos-latest"}, + {"platform": "ios", "runner": "macos-latest"}, + {"platform": "android", "runner": "ubuntu-latest"}, + {"platform": "linux", "runner": "ubuntu-latest"}, + # PINNED, like FLUTTER_REF and for the same reason. windows-latest + # is Server 2025 and its Visual Studio has moved to 2026 (major + # 18), which Flutter 3.35.4 predates: it detects that install and + # reports it healthy, then maps it to no generator it knows and + # emits "Visual Studio 16 2019" instead. Pinning Flutter without + # pinning the toolchain it has to drive only moved the moving part. + {"platform": "windows", "runner": "windows-2022"}, + {"platform": "javascript", "runner": "ubuntu-latest"}, + ] + requested = os.environ.get("REQUESTED", "").strip().lower() + if requested in ("", "all"): + chosen = ALL + else: + names = [n.strip() for n in requested.split(",") if n.strip()] + known = {p["platform"] for p in ALL} + unknown = [n for n in names if n not in known] + if unknown or not names: + # Loud, not an empty matrix: a typo must not "succeed" by measuring nothing. + sys.exit("unknown platform(s) %s; choose from %s, or all" + % (", ".join(unknown) or "", ", ".join(sorted(known)))) + chosen = [p for p in ALL if p["platform"] in names] + print("measuring: " + ", ".join(p["platform"] for p in chosen)) + with open(os.environ["GITHUB_OUTPUT"], "a") as out: + out.write("include=" + json.dumps(chosen) + "\n") + + measure: + name: ${{ matrix.platform }} + needs: [framework, plan] + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + env: + # Which pull request's overlay (scripts/flutter-bench/baseline/pr/.json) is + # this run's own: a stale own overlay is judged around, not refused, and the + # printed calibrate command names it. A dispatched run asks gh for the branch's. + CN1_PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + strategy: + # One platform failing must not hide the others: a benchmark that + # publishes nothing because a single runner broke is worse than one that + # publishes five platforms and says the sixth did not run. + fail-fast: false + matrix: + include: ${{ fromJson(needs.plan.outputs.include) }} + + steps: + - uses: actions/checkout@v4 + + # JDK 17 only. Nothing here builds the framework, so nothing here needs + # the JDK 8 that is unavailable on an arm64 macOS runner anyway. + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Download the framework + uses: actions/download-artifact@v4 + with: + name: flutter-bench-framework + + - name: Unpack the framework + shell: bash + run: tar -xzf framework-m2.tgz + + # A full SDK CHECKOUT, not a release archive. The benchmark's application + # is dev/integration_tests/new_gallery inside the SDK, and the published + # release archives do not carry dev/ at all -- so the usual setup actions, + # which unpack a release, leave prepare.sh with nothing to copy. Cloning + # the SDK is also what pins the reference: the revision is recorded in + # every result, so a number can be traced to the Flutter that produced it. + - name: Check out the Flutter SDK + shell: bash + run: | + git clone --depth 1 --branch "$FLUTTER_REF" \ + https://github.com/flutter/flutter.git "$RUNNER_TEMP/flutter" + echo "$RUNNER_TEMP/flutter/bin" >> "$GITHUB_PATH" + echo "FLUTTER_ROOT=$RUNNER_TEMP/flutter" >> "$GITHUB_ENV" + + - name: Report the toolchain, and prove the gallery is present + shell: bash + run: | + flutter --version + flutter precache + test -d "$FLUTTER_ROOT/dev/integration_tests/new_gallery/lib" || { + echo "this Flutter checkout has no gallery; prepare.sh cannot run" >&2 + exit 1; } + echo "gallery Dart files: $(find "$FLUTTER_ROOT/dev/integration_tests/new_gallery/lib" -name '*.dart' | wc -l)" + test -f "$FLUTTER_ROOT/pubspec.lock" || { + echo "the SDK checkout has no root pubspec.lock; prepare.sh needs it" >&2 + echo "to hold the gallery's unpinned dependencies at tested versions" >&2 + exit 1; } + + # A benchmark records the toolchain it measured with; on Windows it also + # has to CHECK it. Flutter picks the CMake generator from the Visual + # Studio it detects and falls back to "Visual Studio 16 2019" when it + # detects NONE -- so an undetected Visual Studio does not say so. It fails + # minutes later inside CMake, claiming VS 2019 could not be found, which + # is true and is not the problem. vswhere's own view is printed beside + # Flutter's so the two can be compared: a Visual Studio that vswhere lists + # and Flutter does not is a different fault from one that is simply absent. + - name: Report the Windows toolchain, and prove Flutter can see it + if: ${{ runner.os == 'Windows' }} + shell: bash + run: | + VS_VERSION="" + VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + if [ -x "$VSWHERE" ]; then + echo "== vswhere sees:" + VS_VERSION="$("$VSWHERE" -products '*' -prerelease -format value \ + -property installationVersion 2>/dev/null | head -1)" + echo "${VS_VERSION:-}" + "$VSWHERE" -products '*' -prerelease -format value \ + -property installationPath || true + else + echo "== vswhere is not installed at the usual path" + fi + echo "== flutter doctor:" + flutter doctor -v 2>&1 | tee /tmp/doctor.txt || true + # Checking that Flutter can MAP the install, not that it can see one. + # Presence is the wrong question: doctor reported + # "[OK] Visual Studio - develop Windows apps (Visual Studio Enterprise + # 2026 18.9.2)" on the very run this replaces, and the build then died + # in CMake. flutter_tools/lib/src/windows/visual_studio.dart maps major + # 17 and nothing else; every other major, detected or not, silently + # becomes the "Visual Studio 16 2019" generator. Update SUPPORTED_VS + # when FLUTTER_REF moves, and note that it is the CMake generator + # mapping that decides, not what doctor is willing to tick. + SUPPORTED_VS=17 + MAJOR="$(printf '%s' "$VS_VERSION" | cut -d. -f1)" + if [ -z "$MAJOR" ] || [ "$MAJOR" != "$SUPPORTED_VS" ]; then + echo "This runner's Visual Studio is major '${MAJOR:-none}', and Flutter" >&2 + echo "$FLUTTER_REF only maps major $SUPPORTED_VS to a CMake generator." >&2 + echo "It would fall back to 'Visual Studio 16 2019' and fail inside" >&2 + echo "CMake rather than here -- naming a version nobody asked for." >&2 + echo "Pin the runner image to one carrying Visual Studio" >&2 + echo "$SUPPORTED_VS, or move FLUTTER_REF and re-record baselines." >&2 + exit 1 + fi + + # Flutter's own Linux build and Codename One's native Linux port both + # BOTH sides need a native toolchain here, and they need different + # things. Flutter's desktop build wants clang and a C++ standard library; + # the Codename One side compiles the native Linux port, whose CMakeLists + # resolves the whole GTK/GStreamer/WebKit stack through pkg_check_modules + # and stops at the first one missing. The shared script is that stack -- + # an ad-hoc subset here configured far enough to fail on libcurl, several + # minutes into a build that had already transpiled and compiled. + - name: Install the Linux desktop toolchain + if: ${{ matrix.platform == 'linux' }} + run: bash scripts/ci/install-linux-native-deps.sh clang libstdc++-12-dev + + # The Codename One build opens an AWT window (its CSS compiler), so a + # headless runner fails inside `cn1:css` after the transpile has already + # succeeded. build_apps.sh wraps Maven in xvfb-run on Linux and refuses + # to run without it rather than failing later and less legibly. The + # helper short-circuits when the image already ships xvfb, which these + # usually do. + - name: Install xvfb + if: ${{ runner.os == 'Linux' }} + run: bash scripts/ci/apt-get-install.sh xvfb + + - name: Prepare both applications from one source + shell: bash + run: | + scripts/flutter-bench/app/prepare.sh \ + --work "${RUNNER_TEMP}/fbench" \ + --maven-repo "${GITHUB_WORKSPACE}/m2" + + - name: Build both applications + id: build + shell: bash + run: | + scripts/flutter-bench/app/build_apps.sh \ + --work "${RUNNER_TEMP}/fbench" \ + --platform "${{ matrix.platform }}" \ + --maven-repo "${GITHUB_WORKSPACE}/m2" | tee "${RUNNER_TEMP}/paths.txt" + grep '^cn1=' "${RUNNER_TEMP}/paths.txt" >> "$GITHUB_OUTPUT" + grep '^flutter=' "${RUNNER_TEMP}/paths.txt" >> "$GITHUB_OUTPUT" + + # Every leg runs the same measure.sh; the legs differ only in what wraps it. + # + # Linux runs under Xvfb because both applications are GTK programs that + # need a display. build_apps.sh's xvfb-run does not help here: it lives + # exactly as long as the Maven build it wraps, so the Measure step started + # with no display at all and the adapter reported the platform + # unavailable after both applications had built. + - name: Measure + if: ${{ matrix.platform != 'android' }} + shell: bash + run: | + prefix="" + if [ "$RUNNER_OS" = "Linux" ]; then + prefix="xvfb-run -a" + fi + $prefix scripts/flutter-bench/app/measure.sh "${{ matrix.platform }}" \ + "${{ steps.build.outputs.cn1 }}" "${{ steps.build.outputs.flutter }}" \ + "${RUNNER_TEMP}/bench-out" + + # Android needs a device. A hosted runner has none, so the adapter found no + # emulator and recorded the whole platform as unavailable after building + # both APKs. Same emulator, API level and image as the Android port's own + # instrumentation leg (scripts-android.yml). + - name: Enable KVM for the Android emulator + if: ${{ matrix.platform == 'android' }} + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Measure on an Android emulator + if: ${{ matrix.platform == 'android' }} + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 36 + arch: x86_64 + target: google_apis + disk-size: 4096M + # One line: the action runs each line of `script` in its own shell. + script: scripts/flutter-bench/app/measure.sh android "${{ steps.build.outputs.cn1 }}" "${{ steps.build.outputs.flutter }}" "${{ runner.temp }}/bench-out" + + # The result and the baseline candidate measured from it. The candidate is + # what `flutter_baseline.py calibrate --pr N` reads to write the pull + # request's overlay when a row moved, so it travels with the result rather + # than having to be re-derived. + - name: Upload the result + if: always() + uses: actions/upload-artifact@v4 + with: + name: flutter-bench-${{ matrix.platform }} + path: | + ${{ runner.temp }}/bench-out/result-${{ matrix.platform }}.json + ${{ runner.temp }}/bench-out/baseline-${{ matrix.platform }}.json + if-no-files-found: warn + + publish: + name: Publish + needs: measure + # always(), so a platform that regressed still gets its numbers in front of + # a reviewer. The `measure` jobs already carry the red status; hiding the + # comment as well would leave the failure with no explanation attached. + if: always() + runs-on: ubuntu-latest + # Read-only contents: on a pull request this job runs the pull request's own + # files (run_bench.py, publish-quality-comment.js with an authenticated + # client), so it must never hold a token that can write the repository. The + # master-only publication that needs contents: write is its own job, + # publish-status, which a pull_request event never reaches. Job permissions + # replace the workflow's, so the comment's permission is restated. + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Collect every platform's result + uses: actions/download-artifact@v4 + with: + pattern: flutter-bench-* + path: results + merge-multiple: true + + - name: Render the comment + run: | + python3 scripts/flutter-bench/run_bench.py \ + --render 'results/result-*.json' \ + --markdown flutter-benchmark.md + cat flutter-benchmark.md + + - name: Publish the benchmark comment + if: ${{ github.event_name == 'pull_request' && hashFiles('flutter-benchmark.md') != '' }} + uses: actions/github-script@v9 + with: + retries: 3 + script: | + const { publishQualityComment } = require('./.github/scripts/publish-quality-comment.js'); + await publishQualityComment({ + github, context, core, + marker: '', + reportPath: 'flutter-benchmark.md', + }); + + - name: Upload the rendered report + if: always() + uses: actions/upload-artifact@v4 + with: + name: flutter-benchmark-report + path: flutter-benchmark.md + + # The only job holding contents: write. It runs for a scheduled or dispatched + # run of master alone -- never for a pull_request event -- so the code it + # executes is master's, not a contributor's. + publish-status: + name: Publish port status + needs: measure + # Only a run of every platform: port status shows all of them, and a dispatch + # for a subset would publish a table missing the rest. + if: >- + always() && + github.event_name != 'pull_request' && + github.ref == 'refs/heads/master' && + (github.event_name == 'schedule' || + (github.event_name == 'workflow_dispatch' && (inputs.platforms == 'all' || inputs.platforms == ''))) + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Collect every platform's result + uses: actions/download-artifact@v4 + with: + pattern: flutter-bench-* + path: results + merge-multiple: true + + # Published to the data branch the website build reads, the way every + # port's own status is. Writing it into docs/website in this job's + # checkout, as this step used to, put it where nothing would ever see it: + # the checkout is discarded when the job ends and nothing committed it. + # Master-only, like the port reports, so a branch's numbers never reach + # the public page. + - name: Publish the benchmark to the port status data + env: + GH_TOKEN: ${{ github.token }} + run: | + python3 scripts/flutter-bench/port_status.py \ + --results 'results/result-*.json' \ + --out "${RUNNER_TEMP}/port-status" + python3 scripts/flutter-bench/publish_benchmark.py \ + "${RUNNER_TEMP}/port-status/flutter_benchmark.json" "${{ github.repository }}" + diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml index ac2264228eb..3f4d6c170ce 100644 --- a/.github/workflows/input-validation.yml +++ b/.github/workflows/input-validation.yml @@ -36,6 +36,16 @@ on: - 'scripts/build-ios-app.sh' workflow_dispatch: {} +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + concurrency: # Only the latest revision needs validation. PR numbers keep forks isolated; # event names keep a manual/scheduled run from cancelling a push or PR run. diff --git a/.github/workflows/ios-packaging.yml b/.github/workflows/ios-packaging.yml index 8cf6839b651..b192a845fb7 100644 --- a/.github/workflows/ios-packaging.yml +++ b/.github/workflows/ios-packaging.yml @@ -15,6 +15,16 @@ concurrency: group: packaging-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + jobs: build-port: if: ${{ !inputs.port_prepared }} diff --git a/.github/workflows/linux-build-run.yml b/.github/workflows/linux-build-run.yml index 27259c7e8c9..067ff02e172 100644 --- a/.github/workflows/linux-build-run.yml +++ b/.github/workflows/linux-build-run.yml @@ -87,6 +87,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + jobs: # Build the codenameone maven plugin (it needs CEF, which is x64-only on Linux) # and the hellocodenameone screenshot suite, then publish the suite + ads-mock @@ -173,34 +183,14 @@ jobs: - name: Check out repository uses: actions/checkout@v6 - # The native Linux port's full dependency stack: GTK3 + Cairo + Pango + - # GdkPixbuf + GLib/GIO (render/widgets), FontConfig/FreeType (bundled-font - # registration), libcurl (HTTP), GStreamer (media/camera/audio), WebKitGTK - # (browser), libsecret (secure storage), libnotify (notifications), GeoClue - # (location), libepoxy + EGL/GLES + the Mesa software driver (the offscreen - # 3D backend). Plus CMake/Ninja, Xvfb and a font for Pango to lay out. - # - # The GStreamer plugin set matters as much as the library: base + good carry - # appsrc/appsink, videoconvert and mp4mux but NO codec. VideoIO's encoder is - # x264enc/x265enc (plugins-ugly) parsed by h264parse/h265parse (plugins-bad) - # with avenc_aac for audio and avdec_* for playback (libav), so without those - # three packages the port has no encoder and no H.264 decoder at all. + # The port's full dependency stack, shared with the Flutter benchmark, + # which builds the same local-linux-device target. See the script for what + # each group is for and why the GStreamer plugin set is part of it. - name: Install the GTK build stack + toolchain - run: | - set -e - # Through the shared helper: it refreshes the index once when a .deb - # 404s, which is what a point release landing mid-job looks like -- - # three openssl packages took this job out on 2026-08-25. - bash scripts/ci/apt-get-install.sh \ - cmake ninja-build pkg-config unzip xvfb fonts-dejavu-core \ - libgtk-3-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libglib2.0-dev \ - libfontconfig1-dev libfreetype-dev \ - libcurl4-openssl-dev libssl-dev \ - libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav \ - libwebkit2gtk-4.1-dev libsecret-1-dev libnotify-dev libgeoclue-2-dev \ - libepoxy-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri \ - clang time + run: bash scripts/ci/install-linux-native-deps.sh + + - name: Install clang and time + run: bash scripts/ci/apt-get-install.sh clang time - name: Set up JDK 8 uses: actions/setup-java@v5 diff --git a/.github/workflows/parparvm-parallel-mark.yml b/.github/workflows/parparvm-parallel-mark.yml index a974516ef4e..36789497844 100644 --- a/.github/workflows/parparvm-parallel-mark.yml +++ b/.github/workflows/parparvm-parallel-mark.yml @@ -135,15 +135,18 @@ jobs: # nothing for the tests module to resolve against, which is how the first # revision of this workflow failed on all three arms including the control. # This mirrors parparvm-tests.yml. + # Through retry.sh, transient failures only: a TLS handshake that did not + # complete fetching maven-clean-plugin from Central failed this step once, + # before anything under test had been built. - name: Build JavaAPI and the translator - run: mvn -B clean package -pl JavaAPI -am -DskipTests + run: RETRY_ONLY_MATCHING=transient bash ../scripts/ci/retry.sh mvn -B clean package -pl JavaAPI -am -DskipTests working-directory: vm - name: GC suite env: CN1_TEST_EXTRA_CFLAGS: ${{ matrix.cflags }} run: | - mvn -B test -pl tests -am \ + RETRY_ONLY_MATCHING=transient bash ../scripts/ci/retry.sh mvn -B test -pl tests -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest='GcHeapIntegrityIntegrationTest,GcOverflowSpiralIntegrationTest,GcUncooperativeThreadIntegrationTest,LargeArrayGcIntegrationTest,LowMemoryThrottleIntegrationTest,BibopPageFloorIntegrationTest' working-directory: vm diff --git a/.github/workflows/parparvm-selfhost.yml b/.github/workflows/parparvm-selfhost.yml index 322b8b0ee70..0192b6f661d 100644 --- a/.github/workflows/parparvm-selfhost.yml +++ b/.github/workflows/parparvm-selfhost.yml @@ -44,6 +44,14 @@ concurrency: env: CN1_NATIVE_VERIFY: strict + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' jobs: selfhost: diff --git a/.github/workflows/parparvm-tests-windows.yml b/.github/workflows/parparvm-tests-windows.yml index 669fd8a6cdc..787f04b945f 100644 --- a/.github/workflows/parparvm-tests-windows.yml +++ b/.github/workflows/parparvm-tests-windows.yml @@ -81,6 +81,14 @@ env: # the translator so a customer build that succeeds today keeps succeeding; see # NativeSignatureVerifier. CN1_NATIVE_VERIFY: strict + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' jobs: vm-tests-windows: @@ -359,11 +367,26 @@ jobs: with: distribution: 'temurin' java-version: '8' - cache: 'maven' - name: Save JDK 8 path shell: pwsh run: echo "JDK_8_HOME=$env:JAVA_HOME" >> $env:GITHUB_ENV + # The Maven repository, restored from the NEAREST earlier run rather than an + # exact match. setup-java's cache: 'maven' keys on the hash of every pom.xml + # and restores nothing on a miss, so any change to a pom started this job + # from an empty repository -- and Central rate-limits Windows runners hard: + # one such run got 429 Too Many Requests on junit-bom through all five + # attempts of the retry loop below, over ten minutes, before building + # anything. A near match already holds almost every artifact, so the few new + # ones are all that has to come from Central. + - name: Maven repository cache + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: maven-${{ runner.os }}-screenshot-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven-${{ runner.os }}-screenshot- + # Install the locally-built artifacts the hellocodenameone Maven build needs: # codename1-core + the Windows port (translator inputs), the codenameone # maven plugin (runs transcode-svg / css / annotation processing during the diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 6a3af1f9e61..c2a7b176d1e 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -92,6 +92,14 @@ env: # the translator so a customer build that succeeds today keeps succeeding; see # NativeSignatureVerifier. CN1_NATIVE_VERIFY: strict + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' jobs: # EVERY DOCUMENTED RUNTIME CONFIGURATION MUST STILL COMPILE. diff --git a/.github/workflows/perf-baseline.yml b/.github/workflows/perf-baseline.yml index f8a7a92c440..6867ea0106b 100644 --- a/.github/workflows/perf-baseline.yml +++ b/.github/workflows/perf-baseline.yml @@ -1,5 +1,10 @@ name: Performance baselines +# Two gates keep their baselines this way, with one set of rules +# (vm/selfhost/perf_baseline.py): the ParparVM performance gate in +# vm/selfhost/perf-baseline/, and the Flutter benchmark's regression gate in +# scripts/flutter-bench/baseline/ (scripts/flutter-bench/flutter_baseline.py). +# # The ParparVM performance gate's baselines live in vm/selfhost/perf-baseline/: # base/ holds the consolidated rows, and each pull request that changes a baseline # writes only its own pr/.json, so baseline changes stay in the pull @@ -49,16 +54,25 @@ jobs: working-directory: vm/selfhost run: python3 -m unittest test_perf_gate + - name: The Flutter benchmark gate's unit tests + working-directory: scripts/flutter-bench + run: python3 -m unittest test_flutter_baseline + - name: Check the baselines env: BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | + # Both, even when the first fails, so one comment shows every problem. + status=0 if [ -n "${BASE_SHA}" ]; then - python3 vm/selfhost/perf_baseline.py check --base "${BASE_SHA}" --pr "${PR_NUMBER}" + python3 vm/selfhost/perf_baseline.py check --base "${BASE_SHA}" --pr "${PR_NUMBER}" || status=1 + python3 scripts/flutter-bench/flutter_baseline.py check --base "${BASE_SHA}" --pr "${PR_NUMBER}" || status=1 else - python3 vm/selfhost/perf_baseline.py check + python3 vm/selfhost/perf_baseline.py check || status=1 + python3 scripts/flutter-bench/flutter_baseline.py check || status=1 fi + exit $status fold: if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' @@ -87,14 +101,15 @@ jobs: git fetch --quiet origin master git reset --quiet --hard origin/master python3 vm/selfhost/perf_baseline.py fold - git add -A vm/selfhost/perf-baseline + python3 scripts/flutter-bench/flutter_baseline.py fold + git add -A vm/selfhost/perf-baseline scripts/flutter-bench/baseline if git diff --staged --quiet; then echo "No overlays to fold." echo "folded=false" >> "$GITHUB_OUTPUT" exit 0 fi git diff --staged --stat - git commit --quiet -m "ci: fold merged performance baselines into perf-baseline/base" + git commit --quiet -m "ci: fold merged performance and Flutter benchmark baselines into base/" if git push origin HEAD:master; then echo "folded=true" >> "$GITHUB_OUTPUT" exit 0 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 980910452e0..0d7f252752f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -153,6 +153,14 @@ env: # outcome of customer builds that succeed today, so it is ours to enforce, not # theirs to trip over. See NativeSignatureVerifier. CN1_NATIVE_VERIFY: strict + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' concurrency: # Only the latest revision needs validation. PR numbers keep forks isolated; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 896118dd33e..b7ac3cf0c0e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -359,6 +359,46 @@ jobs: [ "$code" = "200" ] || { echo "MISSING on R2: codenameone-guibuilder (HTTP $code)" >&2; exit 1; } echo "ok: codenameone-guibuilder ${GITHUB_REF_NAME} on R2" + # --- Flutter runtime and Dart runtime ----------------------------------- + # codenameone-dart-runtime and codenameone-flutter-runtime compile at + # release 17, so the maven/ reactor only includes them on JDK 17 (the + # flutter-modules profile) and the JDK 8 core pass above never builds them. + # Without this pass a project that declares codenameone-flutter-runtime at a + # released version -- as the archetype documentation says to -- cannot + # resolve it. The transpiler itself builds on JDK 8 and ships with the core. + - name: Build and stage the Flutter runtimes + id: deploy_flutter + continue-on-error: true + if: >- + always() && (steps.deploy.outcome == 'success' || + steps.r2_core.outcome == 'success') + run: | + export GPG_TTY=$(tty) + cd maven + # -pl without -am: the parent and core were released above and are in + # the local repository at this version; re-staging them here would + # produce new signatures for artifacts that are already immutable on R2. + # With -pl the plugin stages under the FIRST reactor module's target -- + # maven/dart-runtime/target/central-staging, holding both runtimes -- + # not under maven/target, so the core tree is left untouched. + xvfb-run -a mvn deploy -Psign-artifacts -Dgpg.passphrase=$MAVEN_GPG_PASSPHRASE \ + -pl dart-runtime,flutter-runtime -DskipTests -DskipPublishing=true + env: + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Publish Flutter runtimes to R2 + id: r2_flutter + continue-on-error: true + if: always() && steps.deploy_flutter.conclusion != 'skipped' + run: | + bash maven/scripts/r2/publish-staging-to-r2.sh maven/dart-runtime/target/central-staging + for artifact in codenameone-dart-runtime codenameone-flutter-runtime; do + url="${R2_BASE_URL}/com/codenameone/${artifact}/${GITHUB_REF_NAME}/${artifact}-${GITHUB_REF_NAME}.pom?cb=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + code=$(curl -s -o /dev/null -w "%{http_code}" "$url") + [ "$code" = "200" ] || { echo "MISSING on R2: $artifact (HTTP $code)" >&2; exit 1; } + echo "ok: $artifact ${GITHUB_REF_NAME} on R2" + done + - name: Mark the R2 release complete id: r2_mark_complete continue-on-error: true @@ -387,12 +427,14 @@ jobs: steps.deploy_certificatewizard.outcome == 'success' && steps.deploy_settings.outcome == 'success' && steps.deploy_guibuilder.outcome == 'success' && + steps.deploy_flutter.outcome == 'success' && steps.r2_core.outcome == 'success' && steps.r2_core_confirm.outcome == 'success' && steps.r2_gamebuilder.outcome == 'success' && steps.r2_certificatewizard.outcome == 'success' && steps.r2_settings.outcome == 'success' && - steps.r2_guibuilder.outcome == 'success' + steps.r2_guibuilder.outcome == 'success' && + steps.r2_flutter.outcome == 'success' run: bash maven/scripts/r2/mark-release-complete.sh "${GITHUB_REF_NAME}" - name: Regenerate R2 metadata and archetype catalog @@ -445,12 +487,14 @@ jobs: bad "${{ steps.deploy_certificatewizard.outcome }}" && fail "Signing Wizard build" bad "${{ steps.deploy_settings.outcome }}" && fail "Settings build" bad "${{ steps.deploy_guibuilder.outcome }}" && fail "GUI Builder build" + bad "${{ steps.deploy_flutter.outcome }}" && fail "Flutter runtimes build" bad "${{ steps.r2_core.outcome }}" && fail "core reactor -> R2" bad "${{ steps.r2_core_confirm.outcome }}" && fail "core artifacts missing on R2" bad "${{ steps.r2_gamebuilder.outcome }}" && fail "Game Builder -> R2" bad "${{ steps.r2_certificatewizard.outcome }}" && fail "Signing Wizard -> R2" bad "${{ steps.r2_settings.outcome }}" && fail "Settings -> R2" bad "${{ steps.r2_guibuilder.outcome }}" && fail "GUI Builder -> R2" + bad "${{ steps.r2_flutter.outcome }}" && fail "Flutter runtimes -> R2" bad "${{ steps.r2_mark_complete.outcome }}" && fail "marking the R2 release complete" bad "${{ steps.r2_metadata.outcome }}" && fail "R2 metadata regeneration" # A skipped marker is not a legitimate skip: it is how a release ends up diff --git a/.github/workflows/scripts-ios-native.yml b/.github/workflows/scripts-ios-native.yml index aa38ee6ed66..8bde486014f 100644 --- a/.github/workflows/scripts-ios-native.yml +++ b/.github/workflows/scripts-ios-native.yml @@ -15,6 +15,16 @@ concurrency: group: native-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + jobs: build-port: if: ${{ !inputs.port_prepared }} diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index c1ab94c9f5f..b278eb04436 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -127,6 +127,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + jobs: select: runs-on: ubuntu-24.04 diff --git a/.github/workflows/scripts-mac-catalyst.yml b/.github/workflows/scripts-mac-catalyst.yml index bec32802ab8..76893b51cf4 100644 --- a/.github/workflows/scripts-mac-catalyst.yml +++ b/.github/workflows/scripts-mac-catalyst.yml @@ -23,6 +23,16 @@ on: type: boolean default: false +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + concurrency: # Only the latest revision needs validation. PR numbers keep forks isolated; # event names keep a manual/scheduled run from cancelling a push or PR run. diff --git a/.github/workflows/scripts-macos.yml b/.github/workflows/scripts-macos.yml index b0d3cb8f7f1..ef74c29c143 100644 --- a/.github/workflows/scripts-macos.yml +++ b/.github/workflows/scripts-macos.yml @@ -114,6 +114,16 @@ on: # the ORM, the database layer or the HTTP parser cannot reach this # transport. - 'vm/backend/demo/cn1ss/**' +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + concurrency: # Only the latest revision needs validation. PR numbers keep forks isolated; # event names keep a manual/scheduled run from cancelling a push or PR run. diff --git a/.github/workflows/windows-cross-build-run.yml b/.github/workflows/windows-cross-build-run.yml index 368b4b483ec..e4f9923b97d 100644 --- a/.github/workflows/windows-cross-build-run.yml +++ b/.github/workflows/windows-cross-build-run.yml @@ -58,6 +58,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + # A method the translator culled as unreachable is still emitted as a stub, + # because dispatch tables name it. With this on, the stub aborts with the + # method's name instead of returning 0, so a wrong cull fails the suite that + # reached it rather than shipping a silent null. Ours to enforce, like + # CN1_NATIVE_VERIFY: customer builds keep the plain stub. See + # BytecodeMethod.appendMethodC. Not in the size or perf benchmarks, whose + # binaries should be the ones customers get. + CN1_CULL_TRAP: '1' + jobs: cross-build: name: cross-build suite exe (Linux, x64 + WebView2) diff --git a/.gitignore b/.gitignore index 2e669e77b79..edf21be70d5 100644 --- a/.gitignore +++ b/.gitignore @@ -197,5 +197,10 @@ cn1-build-hints.json # every run; the parts under native-themes//*.css are the source. native-themes/*/target/ +# Resolved from the port-status-data branch during the website build +# (scripts/website/sync_port_status_reports.sh); never committed, because +# there is no checked-in fallback for it. +docs/website/data/port_status_flutter_benchmark.json + # Rendered from vm/selfhost/perf-baseline by scripts/website/build.sh; never committed. docs/website/data/port_status_jdk25.json diff --git a/CodenameOne/src/com/codename1/annotations/buildhints/Android.java b/CodenameOne/src/com/codename1/annotations/buildhints/Android.java index 086ae2dc24b..11bf532a421 100644 --- a/CodenameOne/src/com/codename1/annotations/buildhints/Android.java +++ b/CodenameOne/src/com/codename1/annotations/buildhints/Android.java @@ -132,14 +132,15 @@ int targetSDKVersion() default 0; /// `auto`, `modern` / `material`, `hololight` (default for existing apps), - /// `legacy`. `auto` and `modern` / `material` opt in to the CSS-generated + /// `legacy`, `custom`. `auto` and `modern` / `material` opt in to the CSS-generated /// Android Material 3 theme from `native-themes/android-material/theme.css`. /// `hololight` is Android Holo Light (what the framework shipped on API 14+ /// before this refactor). `legacy` loads the pre-Holo Android theme. The /// legacy alias `cn1.androidTheme` is still accepted, and `and.hololight=true` /// still maps to `hololight`. The default stays on `hololight` for existing - /// apps until you flip in a future release. - @Hint(name = "and.themeMode", valuePattern = "auto|modern|hololight|legacy") + /// apps until you flip in a future release. `custom` packages and installs no + /// Android theme at all, for an application whose own theme is the only one. + @Hint(name = "and.themeMode", valuePattern = "auto|modern|hololight|legacy|custom") ThemeMode themeMode() default ThemeMode.DEFAULT; /// Statements added to the top-level Gradle build file rather than the app diff --git a/CodenameOne/src/com/codename1/annotations/buildhints/Build.java b/CodenameOne/src/com/codename1/annotations/buildhints/Build.java index 88eaa0c470b..09a893c780c 100644 --- a/CodenameOne/src/com/codename1/annotations/buildhints/Build.java +++ b/CodenameOne/src/com/codename1/annotations/buildhints/Build.java @@ -53,8 +53,11 @@ /// override that sets `ios.themeMode` and `and.themeMode` together when those /// aren't set explicitly. `modern` = liquid glass + Material 3, `legacy` = /// iOS 7 flat + Holo Light, `custom` disables the framework native theme - /// entirely. The legacy alias `cn1.nativeTheme` is still accepted. + /// entirely: no platform theme is packaged or installed on any port, desktop + /// included, for an application whose own theme sets `includeNativeBool: false`. + /// The legacy alias `cn1.nativeTheme` is still accepted. /// + /// `native` and `custom` are the two values that reach the desktop too. /// `native` is `modern` plus the desktop: it additionally selects the host's /// own desktop theme -- Fluent, Aqua or Adwaita -- the way /// `desktop.themeMode = auto` does. `modern` stops short of the desktop on diff --git a/CodenameOne/src/com/codename1/annotations/buildhints/Ios.java b/CodenameOne/src/com/codename1/annotations/buildhints/Ios.java index acb104c5c67..7d960c2eb2f 100644 --- a/CodenameOne/src/com/codename1/annotations/buildhints/Ios.java +++ b/CodenameOne/src/com/codename1/annotations/buildhints/Ios.java @@ -173,14 +173,16 @@ @Hint(valuePattern = "26|27") IosThemeGeneration themeGeneration() default IosThemeGeneration.DEFAULT; - /// `auto` (default), `modern`, `ios7`, `legacy`. `auto` (unset) keeps the + /// `auto` (default), `modern`, `ios7`, `legacy`, `custom`. `auto` (unset) keeps the /// existing iOS 7 flat theme so pre-refactor screenshot goldens and apps see /// no behavior change. `modern` / `liquid` opts in to the CSS-generated iOS /// Modern (liquid-glass) theme shipped from /// `native-themes/ios-modern/theme.css`. `ios7` / `flat` is the same as `auto` /// - pre-liquid iOS 7 flat theme; `legacy` / `iphone` loads the pre-iOS 7 /// iPhone theme. The `auto` -> modern flip is planned for a future release. - @Hint(valuePattern = "auto|modern|ios7|legacy") + /// `custom` packages and installs no iOS theme at all, for an application + /// whose own theme is the only one. + @Hint(valuePattern = "auto|modern|ios7|legacy|custom") ThemeMode themeMode() default ThemeMode.DEFAULT; /// Allows intercepting a URL call using the syntax `urlPrefix` diff --git a/CodenameOne/src/com/codename1/annotations/buildhints/Mac.java b/CodenameOne/src/com/codename1/annotations/buildhints/Mac.java index 1693829cae9..c1df6478747 100644 --- a/CodenameOne/src/com/codename1/annotations/buildhints/Mac.java +++ b/CodenameOne/src/com/codename1/annotations/buildhints/Mac.java @@ -201,7 +201,8 @@ /// is no such history here, and the legacy theme defines no dark styles at /// all, so an application on it renders light however it asks for dark. The /// cross-platform `nativeTheme` hint is honoured when this is unset, with - /// `legacy` mapping to `ios7`. + /// `legacy` mapping to `ios7`. `custom` packages and installs no native theme + /// at all, for an application whose own theme is the only one. String themeMode() default ""; /// macOS builds. Raw XML members added to the generated `Info.plist`, the diff --git a/CodenameOne/src/com/codename1/annotations/buildhints/ThemeMode.java b/CodenameOne/src/com/codename1/annotations/buildhints/ThemeMode.java index d8e0560207e..42a1b0e0777 100644 --- a/CodenameOne/src/com/codename1/annotations/buildhints/ThemeMode.java +++ b/CodenameOne/src/com/codename1/annotations/buildhints/ThemeMode.java @@ -73,6 +73,14 @@ public enum ThemeMode { LEGACY, /// The theme the application ships, rather than a platform one. + /// + /// No native theme is packaged and none is installed, on every port: the + /// application's own theme is the only one it loads. Pair it with + /// `includeNativeBool: false` in the theme's CSS `#Constants`, which stops the + /// theme from asking for a native base in the first place. The hint is what + /// keeps the platform theme files out of the application, and what stops the + /// Linux and Windows ports from installing theirs at start-up whatever the + /// theme says. @HintValue("custom") CUSTOM; } diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index ffec2d464df..e41b06fe921 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -1174,6 +1174,57 @@ public void drawImageRounded(Object graphics, Object img, int x, int y, int w, i drawImage(graphics, img, x, y, w, h); } + /// Draws the region (u0, v0, du, dv) of an image -- normalised, 0,0 being its + /// top left and 1,1 its bottom right -- scaled into the destination, with the + /// DESTINATION's corners rounded. Only called when + /// `isRoundedImageDrawSupported()` answers true. + /// + /// The default draws the region square: the whole image placed so the region + /// lands on the destination, clipped to it. A port that rounds should override + /// it; one that does not never reaches it. + /// + /// #### Parameters + /// + /// - `graphics`: the graphics context + /// + /// - `img`: the image + /// + /// - `x`: destination x + /// + /// - `y`: destination y + /// + /// - `w`: destination width + /// + /// - `h`: destination height + /// + /// - `cornerRadius`: radius in destination pixels + /// + /// - `u0`: left of the region, normalised + /// + /// - `v0`: top of the region, normalised + /// + /// - `du`: width of the region, normalised + /// + /// - `dv`: height of the region, normalised + public void drawImageRegionRounded(Object graphics, Object img, int x, int y, int w, int h, + float cornerRadius, float u0, float v0, float du, float dv) { + if (du <= 0 || dv <= 0) { + return; + } + int fw = (int) Math.round(w / (double) du); + int fh = (int) Math.round(h / (double) dv); + int fx = x - (int) Math.round(u0 * (double) fw); + int fy = y - (int) Math.round(v0 * (double) fh); + int cx = getClipX(graphics); + int cy = getClipY(graphics); + int cw = getClipWidth(graphics); + int ch = getClipHeight(graphics); + clipRect(graphics, x, y, w, h); + drawImage(graphics, img, fx, fy, fw, fh); + setClip(graphics, cx, cy, cw, ch); + } + + /// Returns the width of a native image /// /// #### Parameters @@ -5409,6 +5460,19 @@ public boolean downloadBytesAsFile(String fileName, byte[] bytes) { /// /// #### Returns /// + /// The platform's own logical-pixel scale factor: device pixels per logical pixel, + /// the number iOS calls `UIScreen.scale` and Android calls `density`. + /// + /// This is NOT the same question as [#getDeviceDensity], even though the two are + /// easily confused. Density is a coarse DPI bucket used to pick artwork and to size + /// things in physical units. The scale factor is what the platform itself uses to + /// convert its own layout units into pixels, and on iOS it is only ever 1, 2 or 3 -- + /// never the 3.5 that a 560-dpi bucket would imply. Anything laying out in + /// platform-logical units (a Flutter-style `dp`) has to ask this question, not the + /// density one, or it renders every dimension off by the ratio between them. + /// + /// #### Returns + /// /// pixels per logical pixel, or 0 when the platform does not report one -- callers /// should then fall back to deriving it from the density bucket public float getDevicePixelRatio() { diff --git a/CodenameOne/src/com/codename1/l10n/SimpleDateFormat.java b/CodenameOne/src/com/codename1/l10n/SimpleDateFormat.java index 4ccc96e8958..4834b909f26 100644 --- a/CodenameOne/src/com/codename1/l10n/SimpleDateFormat.java +++ b/CodenameOne/src/com/codename1/l10n/SimpleDateFormat.java @@ -117,6 +117,8 @@ public class SimpleDateFormat extends DateFormat { private String pattern; /// The parsed pattern private List patternTokens; + /// The zone fields are formatted in; null means the device's default zone. + private TimeZone timeZone; /// Construct a SimpleDateFormat with no pattern. public SimpleDateFormat() { @@ -240,9 +242,33 @@ public int hashCode() { public Object clone() { SimpleDateFormat sdf = new SimpleDateFormat(pattern); sdf.setDateFormatSymbols(dateFormatSymbols); + sdf.timeZone = timeZone; return sdf; } + /// Sets the time zone the date's fields are formatted in, as `java.text.DateFormat` + /// does. Without one the device's default zone is used. + /// + /// A formatter fixed to the device zone cannot print an instant's fields in any other: + /// a UTC time had to be moved onto a local one first, and inside a daylight-saving gap + /// that local time does not exist, so its hour came out shifted. + /// + /// #### Parameters + /// + /// - `zone`: the zone, or null for the device's default + public void setTimeZone(TimeZone zone) { + this.timeZone = zone; + } + + /// The zone set with `setTimeZone`, or the device's default when none was. + /// + /// #### Returns + /// + /// the zone fields are formatted in + public TimeZone getTimeZone() { + return timeZone != null ? timeZone : TimeZone.getDefault(); + } + /* * (non-Javadoc) * @@ -272,8 +298,8 @@ String format(Date source, StringBuilder toAppendTo) { if (pattern == null) { return super.format(source, toAppendTo); } - // format based on local timezone - Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); + // format in the configured zone, the device's by default + Calendar calendar = Calendar.getInstance(getTimeZone()); calendar.setTime(source); List pattern = getPatternTokens(); for (String token : pattern) { @@ -561,7 +587,9 @@ public Date parse(String source) throws ParseException { } } - TimeZone localTimezone = Calendar.getInstance().getTimeZone(); + // Text without a zone of its own is read in the formatter's zone, the one + // setTimeZone configured, so parse round-trips what format wrote. + TimeZone localTimezone = getTimeZone(); calendar.getTime(); // this seems to be necessary to calculate the time before changing the timzezone calendar.setTimeZone(localTimezone); if (pmMinutes != 0) { @@ -669,7 +697,7 @@ int parseNumber(String source, int ofs, String name, int min, int max) throws Pa /// /// - `source` int getLocalDSTOffset(Calendar source) { - TimeZone localTimezone = Calendar.getInstance().getTimeZone(); + TimeZone localTimezone = getTimeZone(); int rawOffset = localTimezone.getRawOffset() / MILLIS_TO_MINUTES; return getOffsetInMinutes(source, localTimezone) - rawOffset; } diff --git a/CodenameOne/src/com/codename1/location/GeofenceManager.java b/CodenameOne/src/com/codename1/location/GeofenceManager.java index fb881b666d8..9ea9913840e 100644 --- a/CodenameOne/src/com/codename1/location/GeofenceManager.java +++ b/CodenameOne/src/com/codename1/location/GeofenceManager.java @@ -259,6 +259,8 @@ public synchronized Class getListenerClass() { String className = (String) Storage.getInstance().readObject(LISTENER_CLASS_KEY); if (className != null) { try { + // ParparVM's ReachabilityCull (FOR_NAME_SITES) keeps only + // GeofenceListener implementations for this forName. listenerClass = (Class) Class.forName(className); } catch (Throwable t) { Log.e(t); diff --git a/CodenameOne/src/com/codename1/system/NativeLookup.java b/CodenameOne/src/com/codename1/system/NativeLookup.java index a7fe39eeed8..4fc9eb24acb 100644 --- a/CodenameOne/src/com/codename1/system/NativeLookup.java +++ b/CodenameOne/src/com/codename1/system/NativeLookup.java @@ -80,7 +80,9 @@ public static T create(Class c) { if (cls != null) { return (T) cls.newInstance(); } - // special case for JavaSE native interfaces + // special case for JavaSE native interfaces. ParparVM's ReachabilityCull + // (FOR_NAME_SITES) keeps only NativeInterface implementations for this + // forName; a name of any other type here must be added there. return (T) Class.forName(c.getName() + "Impl").newInstance(); } catch (Throwable ex) { if (verbose) { diff --git a/CodenameOne/src/com/codename1/testing/DeviceRunner.java b/CodenameOne/src/com/codename1/testing/DeviceRunner.java index 6a126fbe7e1..6eca3fa1f0b 100644 --- a/CodenameOne/src/com/codename1/testing/DeviceRunner.java +++ b/CodenameOne/src/com/codename1/testing/DeviceRunner.java @@ -107,6 +107,8 @@ public void runTests() { @SuppressWarnings("BanClassForName") public void runTest(String testClassName) { try { + // ParparVM's ReachabilityCull (FOR_NAME_SITES) keeps only UnitTest + // implementations for this forName. final UnitTest t = (UnitTest) Class.forName(testClassName).newInstance(); try { TestReporting.getInstance().startingTestCase(t.getClass().getName()); diff --git a/CodenameOne/src/com/codename1/ui/Dialog.java b/CodenameOne/src/com/codename1/ui/Dialog.java index ff8de3029ee..9fc864d2410 100644 --- a/CodenameOne/src/com/codename1/ui/Dialog.java +++ b/CodenameOne/src/com/codename1/ui/Dialog.java @@ -3898,6 +3898,16 @@ public boolean animate() { } private boolean isTimedOut() { + // Disposing is EDT work, and this is reached from off it: invokeAndBlock's + // waiting thread polls isDisposed() while a modal show blocks, and it used to + // dispose the dialog and deregister its animation from that thread, while the + // EDT was walking the same animation list -- an ArrayIndexOutOfBoundsException + // out of ArrayList.remove. Off the EDT the deadline is left alone; the EDT's own + // poll (animate) and the timeout clock, which calls back onto the EDT, dispose + // it, and the waiting thread sees it disposed on its next check. + if (!Display.getInstance().isEdt()) { + return false; + } if (time != 0 && System.currentTimeMillis() >= time) { time = 0; cancelTimeoutClock(); diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index de239447359..b408c69a955 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -1363,6 +1363,31 @@ public boolean isInTransition() { return false; } + /// The form transition currently being painted, or null when there is none. + /// + /// The companion to [isInTransition()][#isInTransition()], for code that needs the + /// transition itself rather than the fact of one. A transition paints the frame + /// BETWEEN two forms, so neither form can be asked what is on screen while one is + /// running - painting the destination gives the finished state and painting the + /// source gives the state before it began. Handing back the transition lets a caller + /// paint the frame that is actually being shown, which is what capturing a transition + /// for comparison needs, and pairs with + /// [AnimationTime][com.codename1.ui.animations.AnimationTime] to step one frame by + /// frame. + /// + /// #### Returns + /// + /// the running transition, or null + public Transition getRunningTransition() { + if (animationQueue != null && !animationQueue.isEmpty()) { + Animation a = animationQueue.get(0); + if (a instanceof Transition) { + return (Transition) a; + } + } + return null; + } + // Seems to be a false positive on this rule @SuppressWarnings({"PMD.SimplifyConditional", "PMD.AvoidBranchingStatementAsLastInLoop"}) private void paintTransitionAnimation() { diff --git a/CodenameOne/src/com/codename1/ui/Graphics.java b/CodenameOne/src/com/codename1/ui/Graphics.java index cd018150a10..40af8af3b6c 100644 --- a/CodenameOne/src/com/codename1/ui/Graphics.java +++ b/CodenameOne/src/com/codename1/ui/Graphics.java @@ -1030,6 +1030,57 @@ public void drawImageRounded(Image img, int x, int y, int w, int h, float corner x + xTranslate, y + yTranslate, w, h, cornerRadius); } + /// Draws part of an image, scaled into a destination rectangle, with the + /// destination's corners rounded -- without building a cropped or rounded copy + /// of the image, on platforms that support it. + /// + /// This is `drawImageRounded` for a picture scaled to overflow the box it + /// sits in, a "cover" fit: drawing the whole picture rounded would put the + /// corners outside the box, where a clip cuts them off and the box shows + /// square. Where rounding is not supported, the region is drawn square, + /// clipped to the destination. + /// + /// #### Parameters + /// + /// - `img`: the image to draw + /// - `srcX`: left of the region, in the image's pixels + /// - `srcY`: top of the region, in the image's pixels + /// - `srcW`: width of the region, in the image's pixels + /// - `srcH`: height of the region, in the image's pixels + /// - `x`: destination x + /// - `y`: destination y + /// - `w`: destination width + /// - `h`: destination height + /// - `cornerRadius`: radius in pixels, clamped to half the smaller side of + /// the destination + public void drawImageRegionRounded(Image img, int srcX, int srcY, int srcW, int srcH, + int x, int y, int w, int h, float cornerRadius) { + int iw = img.getWidth(); + int ih = img.getHeight(); + if (iw <= 0 || ih <= 0 || srcW <= 0 || srcH <= 0 || w <= 0 || h <= 0) { + return; + } + float u0 = srcX / (float) iw; + float v0 = srcY / (float) ih; + float du = srcW / (float) iw; + float dv = srcH / (float) ih; + if (isRoundedImageSupported(img)) { + impl.drawImageRegionRounded(nativeGraphics, img.roundedDrawPeer(), + x + xTranslate, y + yTranslate, w, h, cornerRadius, u0, v0, du, dv); + return; + } + // Square: the whole image placed so the region lands on the destination, + // clipped to the destination. + int fw = (int) Math.round(w / (double) du); + int fh = (int) Math.round(h / (double) dv); + int fx = x - (int) Math.round(u0 * (double) fw); + int fy = y - (int) Math.round(v0 * (double) fh); + int[] clip = getClip(); + clipRect(x, y, w, h); + drawImage(img, fx, fy, fw, fh); + setClip(clip[0], clip[1], clip[2], clip[3]); + } + public void drawImage(Image img, int x, int y, int w, int h) { if (impl.isScaledImageDrawingSupported()) { img.drawImage(this, nativeGraphics, x, y, w, h); diff --git a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java index a5deb4110bd..20506b74d7a 100644 --- a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java +++ b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java @@ -1439,6 +1439,15 @@ public Transition copy(boolean reverse) { break; } retVal.linearMotion = linearMotion; + // A motion the caller supplied has to survive the copy. Display copies a + // transition before running it, so everything set through setMotion() -- which is + // public API, documented as the way to give a transition "a more appropriate + // physical feel" -- was dropped on the way to the screen and the default ease ran + // instead. Silently: the transition still played, just not the one that was asked + // for. + retVal.motionSetManually = motionSetManually; + retVal.motion = motion; + retVal.lazyMotion = lazyMotion; return retVal; } diff --git a/CodenameOne/src/com/codename1/ui/animations/ContainerTransformTransition.java b/CodenameOne/src/com/codename1/ui/animations/ContainerTransformTransition.java new file mode 100644 index 00000000000..f8ca04c3d7a --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/animations/ContainerTransformTransition.java @@ -0,0 +1,561 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.animations; + +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; +import com.codename1.ui.geom.GeneralPath; + +/// A transition in which one component GROWS into the whole of the next form, the way +/// Material's container transform does: a card or a button becomes the page it opens. +/// +/// The difference from [MorphTransition][MorphTransition] is what is being animated. +/// A morph moves a component from where it is in one form to where the same component is +/// in the other, so it needs a counterpart on both sides and it animates a COMPONENT. +/// This animates a SURFACE: a rounded rectangle travels from the tapped component's +/// bounds out to the full form, its corners straightening as it goes, and the two +/// contents cross-fade inside it -- the thing that was tapped fading out while the page +/// fades in. Nothing needs to exist on both sides, which is the usual case: a button does +/// not reappear on the page it opened. +/// +/// Geometry follows a fast-out-slow-in curve and the cross-fade is deliberately not +/// symmetric: the outgoing content is gone by the time the incoming content begins, +/// so the two are never both half visible, which reads as a dissolve rather than a +/// transformation. +/// +/// Use it where a tap on something becomes a screen: +/// +/// ```java +/// tappedCard.setName("card"); +/// nextForm.setTransitionInAnimator( +/// ContainerTransformTransition.create("card", 300)); +/// nextForm.show(); +/// ``` +public final class ContainerTransformTransition extends Transition { + + /// Material's container transform curve: fast out, slow in. + private static final float CP0 = 0.4f; + private static final float CP1 = 0.0f; + private static final float CP2 = 0.2f; + private static final float CP3 = 1.0f; + + + /// Material states this transform's colour and opacity changes in fifths of the run. + private static final float FIFTH = 0.2f; + + /// Material's scrim over the page being left: black at 54% opacity. + private static final int SCRIM_ALPHA = 138; + + private static final int SCALE = 1000; + + private final String componentName; + private final int duration; + + private Motion motion; + private int progress; + private Image sourceBuffer; + private Image destBuffer; + /// The tapped component on its own, so it can fade out inside the growing surface. + private Image originBuffer; + private int startX; + private int startY; + private int startW; + private int startH; + private int startRadius; + private int surfaceColor; + private int openColor; + private GeneralPath path; + + /// Whether this instance is the CLOSE half, which runs on the mirrored curve. + private boolean closing; + + /// Whether the thing being grown out of was round, which changes the SHAPE of the + /// travelling surface for the whole run -- see the squash in paint. + private boolean originRound; + + private ContainerTransformTransition(String componentName, int duration) { + this.componentName = componentName; + this.duration = duration; + } + + /// Creates a transition that grows the named component into the next form. + /// + /// #### Parameters + /// + /// - `componentName`: the [Component#setName(String)][Component#setName(String)] of the + /// component in the OUTGOING form that the next form grows out of. When no component + /// carries that name the transition still runs, growing from the centre of the screen. + /// + /// - `duration`: the duration in milliseconds + /// + /// #### Returns + /// + /// the transition + public static ContainerTransformTransition create(String componentName, int duration) { + return new ContainerTransformTransition(componentName, duration); + } + + private static Component findByName(Container root, String name) { + int count = root.getComponentCount(); + for (int iter = 0; iter < count; iter++) { + Component c = root.getComponentAt(iter); + String n = c.getName(); + if (n != null && n.equals(name)) { + return c; + } + if (c instanceof Container) { + Component child = findByName((Container) c, name); + if (child != null) { + return child; + } + } + } + return null; + } + + @Override + public void initTransition() { + Component source = getSource(); + Component destination = getDestination(); + // Cleared first: a transition object can be reused, and an early return must not + // leave the previous run's motion to be replayed. + motion = null; + // The Transition contract allows no source -- the first Form shown has nothing to + // transition from -- and dereferencing it here made that first show throw. With + // either side missing there is nothing to transform between, so no animation. + if (source == null || destination == null) { + return; + } + int w = destination.getWidth(); + int h = destination.getHeight(); + if (w <= 0 || h <= 0) { + return; + } + // The SAME curve both ways. Closing is the open progress run backwards (see + // paint), and 1 - curve(elapsed) is already the mirrored easing -- selecting a + // mirrored curve here as well would mirror it twice and give + // curve(1 - elapsed), which is a different motion. + motion = Motion.createCubicBezierMotion(0, SCALE, duration, CP0, CP1, CP2, CP3); + motion.start(); + progress = 0; + + sourceBuffer = Image.createImage(source.getWidth(), source.getHeight()); + source.paintComponent(sourceBuffer.getGraphics(), true); + destBuffer = Image.createImage(w, h); + destination.paintComponent(destBuffer.getGraphics(), true); + + // The thing the surface grows out of lives on whichever page is NOT the one + // travelling. Opening, that is the page being left; CLOSING, it is the page being + // returned to -- so looking on the source form either way found nothing on the + // way back, and the transform fell through to its "no origin" guess and played + // the opening animation out of the middle of the screen. Going back looked + // nothing like the way in. + Component anchorOn = closing ? destination : source; + Form anchorForm = anchorOn.getComponentForm(); + Component origin = anchorForm == null || componentName == null + ? null : findByName(anchorForm, componentName); + if (origin == null) { + // Nothing to grow from. The middle of the screen is a poor guess but it is a + // transition rather than nothing at all, and the caller still gets the fade. + startW = Math.max(1, w / 8); + startH = startW; + startX = (w - startW) / 2; + startY = (h - startH) / 2; + startRadius = startW / 2; + surfaceColor = openPage().getStyle().getBgColor(); + openColor = surfaceColor; + } else { + startX = origin.getAbsoluteX(); + startY = origin.getAbsoluteY(); + startW = Math.max(1, origin.getWidth()); + startH = Math.max(1, origin.getHeight()); + // A round thing stays round while it grows; anything else keeps its corners. + startRadius = Math.min(startW, startH) / 2; + // Square to within a pixel or two IS the test for round: a circular button is + // the only thing that can have been one, and the squash below would be wrong + // for a card. + originRound = Math.abs(startW - startH) <= Math.max(2, startW / 16); + surfaceColor = origin.getStyle().getBgColor(); + openColor = openPage().getStyle().getBgColor(); + // Cut out of the PAGE's snapshot, not painted from the component. + // + // The named component is the tapped surface itself, and what is drawn on top + // of it -- a glyph, a label, a whole row -- can be a separate component beside + // it rather than a child of it. Painting the component alone therefore gave a + // bare capsule: the compose button's pencil was simply absent from the card + // for the whole transform, where the reference carries it the entire way. + // The page has already been photographed a few lines above, and in that + // photograph the button is whole. + Image anchorShot = closing ? destBuffer : sourceBuffer; + originBuffer = Image.createImage(startW, startH, 0); + originBuffer.getGraphics().drawImage(anchorShot, -startX, -startY); + // The commonest colour in it, not the middle pixel: the middle of a button is + // usually its glyph, and taking that made the growing surface the colour of + // the icon instead of the colour of the button. + surfaceColor = dominantColor(originBuffer, origin.getStyle().getBgColor()); + // ...and then keep only what was drawn ON the button. + // + // A cut-out of the page brings the page with it: the compose button sits in + // the notch of the bottom bar, so its rectangle is mostly dark bar, and scaled + // four times into the card that read as a black frame around the glyph. What + // belongs to the button is what lies inside its outline and is not its own + // colour -- which is exactly its content, and the card is already painting the + // colour underneath it. + originBuffer = maskToContent(originBuffer, startW, startH, surfaceColor, originRound); + } + } + + /// The page that TRAVELS: the one growing out of the origin, or shrinking back into + /// it. Opening it is the destination; closing it is the source. + private Component openPage() { + return closing ? getSource() : getDestination(); + } + + /// The snapshot of the page that travels. + private Image openBuffer() { + return closing ? sourceBuffer : destBuffer; + } + + /// The snapshot of the page that stays put underneath. + private Image staticBuffer() { + return closing ? destBuffer : sourceBuffer; + } + + @Override + public boolean animate() { + if (motion == null) { + return false; + } + progress = motion.getValue(); + return !motion.isFinished(); + } + + @Override + public void paint(Graphics g) { + if (motion == null || openBuffer() == null) { + return; + } + // Geometry follows the curve; everything else does not. Material drives the + // rectangle off a fast-out-slow-in animation and the colours and opacities off + // the RAW one, in fifths: the page behind dims over the first fifth, then the + // surface colour and the incoming content cross over during the second, and the + // rest of the run is the page settling into place. + // + // Both are OPEN progress -- 0 is folded into the origin, 1 is the full page -- + // and closing runs them backwards. Everything below is written once, for the way + // in, and the way out is the same transform played in reverse: the rectangle + // shrinks back into what was tapped, the scrim lifts, and the contents cross + // over the other way. Without this the close ran the OPENING animation, so a + // page folded away by growing out of its button a second time. + float t = ((float) progress) / SCALE; + float linear = motion.getDuration() <= 0 ? 1f + : Math.min(1f, ((float) motion.getCurrentMotionTime()) / motion.getDuration()); + // Only the GEOMETRY reverses. The fifths that govern the colours and the two + // contents are measured from the start of whichever run is playing, so closing + // crosses them over at the same point in its own run rather than at the mirrored + // point -- it just crosses them the other way round, which is the swap below. + if (closing) { + t = 1f - t; + } + float cross = crossover(linear); + Component dest = getDestination(); + int fullW = dest.getWidth(); + int fullH = dest.getHeight(); + + // What we came from, unchanged and underneath: the page being left does not move + // in a container transform, it is covered. + if (staticBuffer() != null) { + g.drawImage(staticBuffer(), 0, 0); + } + // ...and dimmed. Without the scrim the whole background stays at full brightness + // through the transition, which is most of the screen disagreeing with the + // reference for most of the run -- far more pixels than the surface itself. + // Off the CURVED progress, not the raw clock -- unlike the opacities and the + // surface colour below, which Material does drive off the raw one. Getting this + // one wrong is not a subtle shading difference: the scrim covers the whole + // screen, so while it is ramping, every pixel is at the wrong brightness. It + // cost a single frame 83% wrong pixels against the reference, between two + // neighbours at 5% and 13%, because the raw clock reaches full dim more than + // twice as fast as the curve does. + // + // Measured at the 50ms frame of a 300ms run, mean luma over the screen: + // raw predicts 116.6 and we rendered 117.3; the curve predicts 163.4 and the + // reference rendered 163.2. + // Opening, the scrim arrives over the first fifth and then stands. Closing, it + // does NOT mirror that: it lifts smoothly across the whole run, in proportion to + // how much of the transform is left. Mirroring the fifths instead held it at + // full black over the middle of the run and then dropped it in one step -- the + // page behind stayed dark almost until the surface had gone, where the reference + // has it brightening the whole way. + int scrim = closing + ? (int) (SCRIM_ALPHA * t) + : (int) (SCRIM_ALPHA * Math.min(1f, t / FIFTH)); + if (scrim > 0) { + int old = g.getAlpha(); + g.setAlpha(scrim); + g.setColor(0); + g.fillRect(0, 0, fullW, fullH); + g.setAlpha(old); + } + + int x = lerp(startX, 0, t); + int y = lerp(startY, 0, t); + int w = lerp(startW, fullW, t); + int h = lerp(startH, fullH, t); + + // The SHAPE is not the rectangle. Material lerps the tapped thing's outline into + // the page's, and a circle squashes the rectangle toward a square about its centre + // as it goes -- that is what keeps a round button looking round instead of + // stretching into a lozenge the instant it starts to grow. + // + // It is most of the geometry, not a rounding detail. Measured against the + // reference at the middle of the close, the rectangle is 685 x 1392 and the + // painted surface 685 x 1066: the same box and the same centre, 326 pixels + // shorter. Painting the rectangle itself put a third of the card's height in the + // wrong place for the whole run. + float circularity = originRound ? 1f - t : 0f; + int px = x; + int py = y; + int pw = w; + int ph = h; + if (circularity > 0) { + if (w < h) { + int d = (int) (circularity * (h - w) / 2f); + py += d; + ph -= 2 * d; + } else { + int d = (int) (circularity * (w - h) / 2f); + px += d; + pw -= 2 * d; + } + } + // Off the UNADJUSTED box, as Flutter's _adjustBorderRadius is: the radius that + // makes the squashed box a circle is half the short side of the box it came from. + int radius = originRound + ? (int) (circularity * Math.min(w, h) / 2f) + : lerp(startRadius, 0, t); + + int[] clip = g.getClip(); + if (radius > 0 && g.isShapeClipSupported()) { + g.setClip(roundRect(px, py, pw, ph, radius)); + } else { + g.setClip(px, py, pw, ph); + } + + // The surface holds the tapped thing's colour for the first fifth, crosses to the + // page's over the second, and is the page's thereafter. + // Opening runs the tapped thing's colour to the page's; closing runs it back. + g.setColor(closing ? blend(openColor, surfaceColor, cross) + : blend(surfaceColor, openColor, cross)); + g.fillRect(px, py, pw, ph); + + // Both contents are drawn at their OWN size scaled to the box's WIDTH, anchored + // at its top-left corner. + // + // This is the part that makes it a transform rather than a window. Drawn at 1:1 + // and clipped, the page inside a half-sized box is the page's top-left QUARTER, + // so folding the compose page away showed a crop of its header sliding about + // while the reference shows the whole page shrinking into the button. Width, not + // height: the aspect ratios of a button and a page have nothing to do with each + // other, and fitting the width is what keeps the text at the size the box implies. + // + // The tapped thing stays FULLY OPAQUE the whole way and is simply covered as the + // page arrives over it. That is what the fade variant of the transform does -- its + // closed content has a constant opacity of 1 and only the page's opacity moves -- + // and it is the visible difference between a button that becomes the page and two + // pictures dissolving into each other. Measured on the reference at the middle of + // the close, the pencil inside the shrinking card is pure black, not a tint. + if (originBuffer != null) { + drawFittedToWidth(g, originBuffer, x, y, w); + } + + float open = closing ? 1f - cross : cross; + if (open > 0) { + int old = g.getAlpha(); + g.setAlpha((int) (255 * open)); + drawFittedToWidth(g, openBuffer(), x, y, w); + g.setAlpha(old); + } + g.setClip(clip[0], clip[1], clip[2], clip[3]); + } + + /// Draws an image scaled so its WIDTH is {@code w}, anchored at {@code x, y}, with + /// its aspect ratio kept. The caller's clip decides how much of it is seen. + private static void drawFittedToWidth(Graphics g, Image img, int x, int y, int w) { + if (img == null || img.getWidth() <= 0) { + return; + } + int h = Math.max(1, (int) ((long) img.getHeight() * w / img.getWidth())); + g.drawImage(img, x, y, Math.max(1, w), h); + } + + /// 0 before the second fifth, 1 after it, and the crossing in between. + private static float crossover(float linear) { + if (linear <= FIFTH) { + return 0f; + } + if (linear >= FIFTH * 2) { + return 1f; + } + return (linear - FIFTH) / FIFTH; + } + + private GeneralPath roundRect(int x, int y, int w, int h, int r) { + if (path == null) { + path = new GeneralPath(); + } + path.reset(); + int rad = Math.min(r, Math.min(w, h) / 2); + // CLOCKWISE, explicitly. GeneralPath.arcTo defaults to counter-clockwise, and the + // rectangle below is walked clockwise, so every corner took the long way round -- + // a 270 degree sweep that bulges back into the box instead of a 90 degree one. + // With a small radius that reads as a slightly soft corner; with a large one the + // shape is unrecognisable, and this transition's corners reach half the short side + // at the start of the run. + path.moveTo(x + rad, y); + path.lineTo(x + w - rad, y); + path.arcTo(x + w - rad, y + rad, x + w, y + rad, true); + path.lineTo(x + w, y + h - rad); + path.arcTo(x + w - rad, y + h - rad, x + w - rad, y + h, true); + path.lineTo(x + rad, y + h); + path.arcTo(x + rad, y + h - rad, x, y + h - rad, true); + path.lineTo(x, y + rad); + path.arcTo(x + rad, y + rad, x + rad, y, true); + path.closePath(); + return path; + } + + private static int lerp(int from, int to, float t) { + return from + (int) ((to - from) * t); + } + + /// Clears everything outside the tapped thing's outline, and everything inside it that + /// is the thing's own colour, leaving its content on transparency. + private static Image maskToContent(Image img, int w, int h, int surface, boolean round) { + try { + int[] px = img.getRGB(); + int cx = w / 2; + int cy = h / 2; + // Inside the outline by a few pixels. The edge of a round button is + // anti-aliased against whatever is behind it, so the outermost ring is neither + // the button's colour nor its content -- and magnified four times it drew a + // pale arc across the card that belongs to nothing. + int rad = Math.min(w, h) / 2; + rad -= Math.max(2, rad / 12); + int radSq = rad * rad; + int sr = (surface >> 16) & 0xff; + int sg = (surface >> 8) & 0xff; + int sb = surface & 0xff; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int i = y * w + x; + if (round) { + int dx = x - cx; + int dy = y - cy; + if (dx * dx + dy * dy > radSq) { + px[i] = 0; + continue; + } + } + int p = px[i]; + if (Math.abs(((p >> 16) & 0xff) - sr) <= TOLERANCE + && Math.abs(((p >> 8) & 0xff) - sg) <= TOLERANCE + && Math.abs((p & 0xff) - sb) <= TOLERANCE) { + px[i] = 0; + } + } + } + return Image.createImage(px, w, h); + } catch (Throwable t) { + return img; + } + } + + /// How close to the surface colour counts as the surface rather than its content. + private static final int TOLERANCE = 24; + + /// The commonest opaque colour in a snapshot, which is the surface colour of whatever + /// was tapped however it came to be painted. Falls back to {@code fallback} when the + /// snapshot is empty or unreadable. + private static int dominantColor(Image img, int fallback) { + try { + int[] rgb = img.getRGB(); + java.util.HashMap counts = new java.util.HashMap(); + int best = fallback; + int bestN = 0; + for (int pixel : rgb) { + if (((pixel >>> 24) & 0xff) < 128) { + continue; + } + Integer key = Integer.valueOf(pixel & 0xffffff); + Object prev = counts.get(key); + // instanceof rather than a bare cast inside this try: ParparVM's + // CHECKCAST is unchecked, so a failed cast does not throw on iOS + // and the catch below would never see it -- the wrong object + // would simply be read as an Integer. + int n = prev instanceof Integer ? ((Integer) prev).intValue() + 1 : 1; + counts.put(key, Integer.valueOf(n)); + if (n > bestN) { + bestN = n; + best = key.intValue(); + } + } + return bestN == 0 ? fallback : best; + } catch (Throwable t) { + return fallback; + } + } + + /// Mixes two packed RGB colours, channel by channel. + private static int blend(int from, int to, float t) { + int r = lerp((from >> 16) & 0xff, (to >> 16) & 0xff, t); + int g = lerp((from >> 8) & 0xff, (to >> 8) & 0xff, t); + int b = lerp(from & 0xff, to & 0xff, t); + return (r << 16) | (g << 8) | b; + } + + @Override + public void cleanup() { + // The base releases source and destination -- both forms and their whole + // component trees, which a caller holding this transition (from + // Display.getRunningTransition) otherwise kept reachable after it ended. + super.cleanup(); + sourceBuffer = null; + destBuffer = null; + originBuffer = null; + motion = null; + path = null; + } + + @Override + public Transition copy(boolean reverse) { + ContainerTransformTransition t = + new ContainerTransformTransition(componentName, duration); + t.closing = reverse; + return t; + } +} diff --git a/CodenameOne/src/com/codename1/ui/animations/CupertinoPageTransition.java b/CodenameOne/src/com/codename1/ui/animations/CupertinoPageTransition.java new file mode 100644 index 00000000000..f37e46e97d4 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/animations/CupertinoPageTransition.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codename1.ui.animations; + +import com.codename1.ui.Component; +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; + +/// The iOS page push: two pages moving at different speeds, over different distances. +/// +/// A plain slide holds the two pages a fixed screen apart and moves the pair, which makes +/// them one rigid object. This transition is the reason iOS depth reads the way it does: +/// the arriving page crosses the WHOLE screen while the page it covers drifts only a +/// THIRD of it, and each rides its own curve. The gap between them closes as they travel, +/// which is what says one is in front of the other. +/// +/// Sliding both the full distance is not a subtle difference. A third of the way through +/// a push the strip of the old page still showing is not dimmer or shifted -- it is a +/// different PART of that page: measured against the reference at 150ms of a 500ms push +/// on a 1125px screen, the old page belongs 270px to the left and ours had it 855px to +/// the left, so the visible strip showed its far edge where the reference shows its +/// middle. +public final class CupertinoPageTransition extends Transition { + + /// The arriving page's curve: fast ease in to slow ease out, a three-point cubic. + private static final float[] ARRIVING = { + 0.056f, 0.024f, 0.108f, 0.3085f, + 0.198f, 0.541f, + 0.3655f, 1.0f, 0.5465f, 0.989f, + }; + + /// The departing page's curve: linear to ease out. A DIFFERENT curve, which is half + /// of why the two do not move as one piece. + private static final float[] DEPARTING = {0.35f, 0.91f, 0.33f, 0.97f}; + + /// How far the covered page travels, as a fraction of the screen. The other half of + /// why they do not move as one piece. + private static final int PARALLAX_DENOMINATOR = 3; + + private static final int SCALE = 1000; + + private final int duration; + private boolean back; + + private Motion arriving; + private Motion departing; + private Image sourceBuffer; + private Image destBuffer; + + private CupertinoPageTransition(int duration) { + this.duration = duration; + } + + /// Creates the transition. + /// + /// #### Parameters + /// + /// - `duration`: the push duration in milliseconds + /// + /// #### Returns + /// + /// the transition + public static CupertinoPageTransition create(int duration) { + return new CupertinoPageTransition(duration); + } + + /// The push duration in milliseconds. + /// + /// #### Returns + /// + /// the duration this transition was created with + public int getDuration() { + return duration; + } + + /// Whether this instance plays the way BACK, with the roles of the two pages swapped. + /// + /// #### Returns + /// + /// true if this is the pop half of the transition + public boolean isBack() { + return back; + } + + @Override + public void initTransition() { + Component source = getSource(); + Component destination = getDestination(); + if (source == null || destination == null) { + return; + } + int w = destination.getWidth(); + int h = destination.getHeight(); + if (w <= 0 || h <= 0) { + return; + } + arriving = Motion.createThreePointCubicMotion(0, SCALE, duration, + ARRIVING[0], ARRIVING[1], ARRIVING[2], ARRIVING[3], ARRIVING[4], + ARRIVING[5], ARRIVING[6], ARRIVING[7], ARRIVING[8], ARRIVING[9]); + departing = Motion.createCubicBezierMotion(0, SCALE, duration, + DEPARTING[0], DEPARTING[1], DEPARTING[2], DEPARTING[3]); + arriving.start(); + departing.start(); + + sourceBuffer = Image.createImage(source.getWidth(), source.getHeight()); + source.paintComponent(sourceBuffer.getGraphics(), true); + destBuffer = Image.createImage(w, h); + destination.paintComponent(destBuffer.getGraphics(), true); + } + + @Override + public boolean animate() { + // Both, though they are set and cleared together: a reader (and a static analyser) + // should not have to know that to see this is safe. + if (arriving == null || departing == null) { + return false; + } + departing.getValue(); + return !arriving.isFinished(); + } + + @Override + public void paint(Graphics g) { + Component destination = getDestination(); + if (arriving == null || departing == null || destination == null) { + return; + } + int w = destination.getWidth(); + float front = arriving.getValue() / (float) SCALE; + float behind = departing.getValue() / (float) SCALE; + int parallax = w / PARALLAX_DENOMINATOR; + + int sourceX; + int destX; + if (back) { + // Going back: the page on top leaves across the whole screen, and the one + // underneath comes home from the third of the way out it was left at. + sourceX = Math.round(w * front); + destX = -Math.round(parallax * (1 - behind)); + } else { + sourceX = -Math.round(parallax * behind); + destX = Math.round(w * (1 - front)); + } + + // The covered page first: the arriving one is opaque and passes over it. + if (sourceBuffer != null) { + g.drawImage(sourceBuffer, sourceX, 0); + } + if (destBuffer != null) { + g.drawImage(destBuffer, destX, 0); + } + } + + @Override + public void cleanup() { + super.cleanup(); + sourceBuffer = null; + destBuffer = null; + arriving = null; + departing = null; + } + + @Override + public Transition copy(boolean reverse) { + CupertinoPageTransition t = new CupertinoPageTransition(duration); + t.back = reverse; + return t; + } +} diff --git a/CodenameOne/src/com/codename1/ui/animations/Motion.java b/CodenameOne/src/com/codename1/ui/animations/Motion.java index d05fb54eaab..a4b6c3cc828 100644 --- a/CodenameOne/src/com/codename1/ui/animations/Motion.java +++ b/CodenameOne/src/com/codename1/ui/animations/Motion.java @@ -46,6 +46,7 @@ public class Motion { private static final int COLOR_LINEAR = 5; private static final int EXPONENTIAL_DECAY = 6; private static final int CRITICAL_DAMPED_SPRING = 7; + private static final int THREE_POINT_CUBIC = 8; private static boolean slowMotion; private final int[] previousLastReturnedValue = new int[3]; private final long[] previousLastReturnedValueTime = new long[3]; @@ -60,6 +61,14 @@ public class Motion { private int lastReturnedValue; private long currentMotionTime = -1; private long previousCurrentMotionTime = -1; + /// The joint and the second segment's control points of a three-point cubic. + private float midX; + private float midY; + private float q0; + private float q1; + private float q2; + private float q3; + private float p0; private float p1; private float p2; @@ -163,6 +172,53 @@ public static Motion createCubicBezierMotion(int sourceValue, int destinationVal return m; } + /// A curve made of TWO cubic beziers joined at a point, which a single cubic cannot + /// express. + /// + /// A plain `cubic-bezier` is monotonic in a way some motion is not: it cannot + /// accelerate hard, ease, and then ease out again, because it has only two control + /// points to spend. Curves that do this are specified as a pair of beziers meeting at + /// a midpoint, each with its own controls, and the joint is where the character of + /// the motion changes. + /// + /// The segments are evaluated in their own normalized space and rescaled, so each + /// half is an ordinary CSS cubic-bezier and the two meet exactly at the midpoint. + /// + /// #### Parameters + /// + /// - `sourceValue`: the initial value + /// + /// - `destinationValue`: the value at the end of the motion + /// + /// - `duration`: the motion duration in milliseconds + /// + /// - `a1X`, `a1Y`, `b1X`, `b1Y`: control points of the first segment + /// + /// - `midX`, `midY`: the point the two segments meet at + /// + /// - `a2X`, `a2Y`, `b2X`, `b2Y`: control points of the second segment + /// + /// #### Returns + /// + /// Motion instance + public static Motion createThreePointCubicMotion(int sourceValue, int destinationValue, + int duration, float a1X, float a1Y, float b1X, float b1Y, + float midX, float midY, float a2X, float a2Y, float b2X, float b2Y) { + Motion m = new Motion(sourceValue, destinationValue, duration); + m.motionType = THREE_POINT_CUBIC; + m.p0 = a1X; + m.p1 = a1Y; + m.p2 = b1X; + m.p3 = b1Y; + m.midX = midX; + m.midY = midY; + m.q0 = a2X; + m.q1 = a2Y; + m.q2 = b2X; + m.q3 = b2Y; + return m; + } + /// Equivalent to createCubicBezierMotion with 0, 0.42, 0.58, 1.0 as arguments. /// /// #### Parameters @@ -483,6 +539,73 @@ private int getSplineValue() { return x; } + private int getThreePointCubicValue() { + if (isFinished()) { + return destinationValue; + } + float totalTime = duration; + float currentTime = Math.min((int) getCurrentMotionTime(), (int) totalTime); + if (currentTime < 0f) { + currentTime = 0f; + } + float t = currentTime / totalTime; + + // Each segment is solved in its OWN normalized space: the controls are expressed + // relative to the segment's start and divided by its extent, so the solver below + // sees an ordinary cubic-bezier from (0,0) to (1,1). The result is then scaled + // back, which is what makes the two halves meet exactly at the midpoint instead + // of stepping there. + boolean first = t < midX; + float scaleX = first ? midX : 1f - midX; + float scaleY = first ? midY : 1f - midY; + float value; + if (scaleX <= 0f) { + // A segment with no duration is only ever reached at its end. + value = first ? midY : 1f; + } else if (scaleY <= 0f) { + // No vertical extent -- the midpoint lies on the top or bottom edge -- so the + // Y axis cannot be normalized. Substituting linear progress here made the + // value run toward t and then jump back at the join. X still normalizes, so + // solve for the curve parameter as usual and evaluate this segment's Y cubic + // from its real endpoints and controls. + float scaledT = (t - (first ? 0f : midX)) / scaleX; + float x1 = first ? p0 / scaleX : (q0 - midX) / scaleX; + float x2 = first ? p2 / scaleX : (q2 - midX) / scaleX; + float u = solveBezierForT(scaledT, x1, x2); + float y0 = first ? 0f : midY; + float y3 = first ? midY : 1f; + float c1 = first ? p1 : q1; + float c2 = first ? p3 : q3; + float inv = 1f - u; + value = inv * inv * inv * y0 + 3f * inv * inv * u * c1 + 3f * inv * u * u * c2 + u * u * u * y3; + } else { + float scaledT = (t - (first ? 0f : midX)) / scaleX; + float x1; + float y1; + float x2; + float y2; + if (first) { + x1 = p0 / scaleX; + y1 = p1 / scaleY; + x2 = p2 / scaleX; + y2 = p3 / scaleY; + } else { + x1 = (q0 - midX) / scaleX; + y1 = (q1 - midY) / scaleY; + x2 = (q2 - midX) / scaleX; + y2 = (q3 - midY) / scaleY; + } + float u = solveBezierForT(scaledT, x1, x2); + value = bezierAxis(u, y1, y2) * scaleY + (first ? 0f : midY); + } + + float dis = Math.abs(destinationValue - sourceValue); + if (destinationValue > sourceValue) { + return sourceValue + (int) (value * dis); + } + return sourceValue - (int) (value * dis); + } + private int getCubicValue() { //make sure we reach the destination value. if (isFinished()) { @@ -629,6 +752,9 @@ public int getValue() { case CUBIC: lastReturnedValue = getCubicValue(); break; + case THREE_POINT_CUBIC: + lastReturnedValue = getThreePointCubicValue(); + break; case FRICTION: lastReturnedValue = getFriction(); break; diff --git a/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java b/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java index ee90cc09155..692d71efa74 100644 --- a/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java +++ b/CodenameOne/src/com/codename1/ui/geom/GeneralPath.java @@ -533,11 +533,27 @@ public boolean equals(Shape shape, Transform t) { return true; } if (shape instanceof Rectangle) { + // A path equals a rectangle only when it IS that rectangle. Comparing bounds + // alone made EVERY non-rectangular path equal to its own bounding box, and + // the clip bookkeeping asks exactly this question before deciding that a + // setClip changes nothing and can be skipped. + // + // That is a silent, total failure of shaped clipping wherever the shape fills + // its own component, which is the usual case: a component's clip has just been + // narrowed to its bounds, so a circle or rounded rectangle inscribed in it + // arrives with bounds equal to the current clip, compares EQUAL, and is + // DISCARDED. The subtree then paints square with nothing reported. Only ports + // whose clip state goes through this comparison are affected, which is why it + // could be reproduced on a device and never in the desktop simulator. + // + // isRectangle() walks the path, so it is asked only once the bounds have + // already matched -- the rare case. A clip that genuinely changed is rejected + // on the bounds alone, as before. Rectangle r = (Rectangle) shape; Rectangle tmpRect = createRectFromPool(); try { getBounds(tmpRect); - return r.equals(tmpRect); + return r.equals(tmpRect) && isRectangle(); } finally { recycle(tmpRect); } diff --git a/CodenameOne/src/com/codename1/ui/plaf/DefaultLookAndFeel.java b/CodenameOne/src/com/codename1/ui/plaf/DefaultLookAndFeel.java index c1e9bc7ea3c..bf1a8b4236d 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/DefaultLookAndFeel.java +++ b/CodenameOne/src/com/codename1/ui/plaf/DefaultLookAndFeel.java @@ -72,6 +72,16 @@ public class DefaultLookAndFeel extends LookAndFeel implements FocusListener { private Image[] rButtonImages = null; private Image[] chkBoxImagesFocus = null; private Image[] rButtonImagesFocus = null; + + // The five groups above are built on first use rather than in refreshTheme; see + // refreshTheme. Each flag says its group still has to be built from the current + // theme, and a setter clears its own group's, so an image an application set + // itself is never overwritten by a late build. + private boolean comboImagePending; + private boolean chkBoxImagesPending; + private boolean chkBoxImagesFocusPending; + private boolean rButtonImagesPending; + private boolean rButtonImagesFocusPending; private boolean tickWhenFocused = true; private char passwordChar = '\u25CF'; //used for the pull to refresh feature @@ -193,6 +203,7 @@ public void setCheckBoxImages(Image checkedX, Image uncheckedX) { /// /// - `disabledUnchecked`: same as unchecked for the disabled state public void setCheckBoxImages(Image checkedX, Image uncheckedX, Image disabledChecked, Image disabledUnchecked) { + chkBoxImagesPending = false; if (checkedX == null || uncheckedX == null) { chkBoxImages = null; } else { @@ -218,6 +229,7 @@ public void setCheckBoxImages(Image checkedX, Image uncheckedX, Image disabledCh /// /// - `disabledUnchecked`: same as unchecked for the disabled state public void setCheckBoxFocusImages(Image checkedX, Image uncheckedX, Image disabledChecked, Image disabledUnchecked) { + chkBoxImagesFocusPending = false; if (checkedX == null || uncheckedX == null) { chkBoxImagesFocus = null; } else { @@ -237,6 +249,7 @@ public void setCheckBoxFocusImages(Image checkedX, Image uncheckedX, Image disab /// /// - `picker`: picker image public void setComboBoxImage(Image picker) { + comboImagePending = false; comboImage = picker; } @@ -248,6 +261,7 @@ public void setComboBoxImage(Image picker) { /// /// - `unselected`: the image to draw in order to represent an unselected radio button public void setRadioButtonImages(Image selected, Image unselected) { + rButtonImagesPending = false; if (selected == null || unselected == null) { rButtonImages = null; } else { @@ -267,6 +281,7 @@ public void setRadioButtonImages(Image selected, Image unselected) { /// /// - `disabledUnselected`: same as unselected for the disabled state public void setRadioButtonImages(Image selected, Image unselected, Image disabledSelected, Image disabledUnselected) { + rButtonImagesPending = false; if (selected == null || unselected == null) { rButtonImages = null; } else { @@ -292,6 +307,7 @@ public void setRadioButtonImages(Image selected, Image unselected, Image disable /// /// - `disabledUnselected`: same as unselected for the disabled state public void setRadioButtonFocusImages(Image selected, Image unselected, Image disabledSelected, Image disabledUnselected) { + rButtonImagesFocusPending = false; if (selected == null || unselected == null) { rButtonImagesFocus = null; } else { @@ -320,6 +336,7 @@ public void setPasswordChar(char c) { /// /// images representing the radio button or null for using the default drawing public Image[] getRadioButtonImages() { + ensureRadioButtonImages(); return rButtonImages; } @@ -329,6 +346,7 @@ public Image[] getRadioButtonImages() { /// /// images representing the radio button or null for using the default drawing public Image[] getRadioButtonFocusImages() { + ensureRadioButtonFocusImages(); return rButtonImagesFocus; } @@ -338,6 +356,7 @@ public Image[] getRadioButtonFocusImages() { /// /// images representing the check box or null for using the default drawing public Image[] getCheckBoxImages() { + ensureCheckBoxImages(); return chkBoxImages; } @@ -347,6 +366,7 @@ public Image[] getCheckBoxImages() { /// /// images representing the check box or null for using the default drawing public Image[] getCheckBoxFocusImages() { + ensureCheckBoxFocusImages(); return chkBoxImagesFocus; } @@ -363,6 +383,8 @@ public void drawButton(Graphics g, Button b) { /// {@inheritDoc} @Override public void drawCheckBox(Graphics g, Button cb) { + ensureCheckBoxImages(); + ensureCheckBoxFocusImages(); if (chkBoxImages != null) { Image x; if (chkBoxImagesFocus != null && chkBoxImagesFocus[0] != null && cb.hasFocus() && Display.getInstance().shouldRenderSelection(cb)) { @@ -631,6 +653,8 @@ public Span calculateLabelSpan(TextSelection sel, Label l) { /// {@inheritDoc} @Override public void drawRadioButton(Graphics g, Button rb) { + ensureRadioButtonImages(); + ensureRadioButtonFocusImages(); if (rButtonImages != null) { Image x; if (rButtonImagesFocus != null && rButtonImagesFocus[0] != null && rb.hasFocus() && Display.getInstance().shouldRenderSelection(rb)) { @@ -681,6 +705,7 @@ public void drawRadioButton(Graphics g, Button rb) { /// {@inheritDoc} @Override public void drawComboBox(Graphics g, List cb) { + ensureComboBoxImage(); int border = 2; Style style = cb.getStyle(); int leftPadding = style.getPaddingLeft(cb.isRTL()); @@ -984,6 +1009,7 @@ public Dimension getButtonPreferredSize(Button b) { /// {@inheritDoc} @Override public Dimension getCheckBoxPreferredSize(Button cb) { + ensureCheckBoxImages(); if (cb.isToggle()) { return getButtonPreferredSize(cb); } @@ -1201,6 +1227,7 @@ private Dimension getListPreferredSizeImpl(List l) { /// {@inheritDoc} @Override public Dimension getRadioButtonPreferredSize(Button rb) { + ensureRadioButtonImages(); if (rb.isToggle()) { return getButtonPreferredSize(rb); } @@ -1810,6 +1837,7 @@ private boolean fastCharWidthCheck(String s, int length, int width, int charWidt /// {@inheritDoc} @Override public Dimension getComboBoxPreferredSize(List cb) { + ensureComboBoxImage(); Dimension d = getListPreferredSize(cb); Image comboBoxImage = ((ComboBox) cb).getComboBoxImage() != null ? ((ComboBox) cb).getComboBoxImage() : comboImage; if (comboBoxImage != null) { @@ -2622,8 +2650,25 @@ public void refreshTheme(boolean b) { chkBoxImagesFocus = null; rButtonImagesFocus = null; super.refreshTheme(b); - UIManager m = getUIManager(); - Image combo = m.getThemeImageConstant("comboImage"); + // Built on first use, not here. Every theme install ends in this method, and + // building these meant loading the Material icon font and rendering eight + // glyph images for check boxes, radio buttons and the combo box arrow -- on + // the transpiled Flutter gallery about a third of its whole theme install, + // for controls most screens never show. What is built is unchanged: the same + // code, reading the same theme, the first time anything asks for it. + comboImagePending = true; + chkBoxImagesPending = true; + chkBoxImagesFocusPending = true; + rButtonImagesPending = true; + rButtonImagesFocusPending = true; + } + + private void ensureComboBoxImage() { + if (!comboImagePending) { + return; + } + comboImagePending = false; + Image combo = getUIManager().getThemeImageConstant("comboImage"); if (combo != null) { setComboBoxImage(combo); } else { @@ -2633,10 +2678,34 @@ public void refreshTheme(boolean b) { setComboBoxImage(combo); } } - updateCheckBoxConstants(m, false, ""); - updateCheckBoxConstants(m, true, "Focus"); - updateRadioButtonConstants(m, false, ""); - updateRadioButtonConstants(m, true, "Focus"); + } + + private void ensureCheckBoxImages() { + if (chkBoxImagesPending) { + chkBoxImagesPending = false; + updateCheckBoxConstants(getUIManager(), false, ""); + } + } + + private void ensureCheckBoxFocusImages() { + if (chkBoxImagesFocusPending) { + chkBoxImagesFocusPending = false; + updateCheckBoxConstants(getUIManager(), true, "Focus"); + } + } + + private void ensureRadioButtonImages() { + if (rButtonImagesPending) { + rButtonImagesPending = false; + updateRadioButtonConstants(getUIManager(), false, ""); + } + } + + private void ensureRadioButtonFocusImages() { + if (rButtonImagesFocusPending) { + rButtonImagesFocusPending = false; + updateRadioButtonConstants(getUIManager(), true, "Focus"); + } } /// Builds a check-box / radio glyph. When sizeMM is non-null it sizes the box diff --git a/CodenameOne/src/com/codename1/ui/plaf/RoundBorder.java b/CodenameOne/src/com/codename1/ui/plaf/RoundBorder.java index 7f01c3ea526..1578c2474f5 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/RoundBorder.java +++ b/CodenameOne/src/com/codename1/ui/plaf/RoundBorder.java @@ -152,6 +152,9 @@ public final class RoundBorder extends Border { /// square. This is ignored when the rectangle mode is false private boolean onlyRightRounded; private boolean uiid; + /// True when the shape is drawn straight onto the Graphics rather than + /// through a cached offscreen image. Off by default; see `directPaint`. + private boolean directPaint; /// This is useful for showing an Uber like stroke effect progress bar private int strokeAngle = 360; @@ -523,6 +526,44 @@ public boolean isOnlyRightRounded() { return onlyRightRounded; } + /// Draws the shape straight onto the Graphics instead of through a cached + /// offscreen image. + /// + /// OFF by default, and that default is a compatibility decision rather + /// than a preference. Drawing directly anti-aliases the outline against + /// whatever is really behind it; the image path anti-aliases against + /// transparency and then composites the result, so alpha is quantised + /// twice. The two agree to within a fraction of a pixel along the edge -- + /// invisible to a person, and still a difference a screenshot test + /// measures. Turning it on everywhere would move pixels in the UI of every + /// application that already uses this border. + /// + /// Turn it on for a shape whose SIZE ANIMATES, which is where the image + /// path is not merely slower but unusable: the cache is keyed by size, so + /// a growing circle misses it on every frame and allocates, fills and + /// discards a surface as large as the component each time. + /// + /// #### Parameters + /// + /// - `directPaint`: true to draw without an offscreen image + /// + /// #### Returns + /// + /// border instance so these calls can be chained + public RoundBorder directPaint(boolean directPaint) { + this.directPaint = directPaint; + return this; + } + + /// Whether this border draws without an offscreen image. + /// + /// #### Returns + /// + /// True when direct painting is enabled. + public boolean isDirectPaint() { + return directPaint; + } + private Image createTargetImage(Component c, int w, int h, boolean fast) { Image target = ImageFactory.createImage(c, w, h, 0); @@ -616,12 +657,67 @@ private Image createTargetImage(Component c, int w, int h, boolean fast) { return target; } + /// Whether this border can be drawn straight onto the Graphics instead of + /// through an offscreen image. + /// + /// Two configurations genuinely need the image and are excluded. A shadow is + /// built by overdrawing the shape once per spread step and then blurring the + /// result, which is a read-back of what has been drawn so far and therefore + /// has to own its surface. The `uiid` mode paints the COMPONENT's background + /// painter through a shape clip, swapping the component's border out while it + /// does so, and that re-entry is only safe against a surface of its own. + /// + /// Everything else is a fill and an optional stroke of a circle or a pill in + /// this border's own colours -- `fillShape` never reads the destination -- so + /// drawing it directly produces the same pixels without allocating anything. + private boolean canPaintDirectly(Graphics g) { + return directPaint && shadowOpacity <= 0 && !uiid && g.isAntiAliasingSupported(); + } + + /// Draws the shape onto `g` with the same geometry `createTargetImage` would + /// have drawn into an image of the component's size. + /// + /// The clip reproduces the one the image gave for free: a stroke is centred + /// on the outline, so half of it falls outside the box and used to be cropped + /// by the image's own bounds. + private void paintDirectly(Graphics g, int x, int y, int w, int h) { + int priorColor = g.getColor(); + int priorAlpha = g.getAlpha(); + boolean priorAntiAliased = g.isAntiAliased(); + g.pushClip(); + try { + g.clipRect(x, y, w, h); + g.translate(x, y); + try { + g.setAntiAliased(true); + fillShape(g, color, opacity, w, h, true); + } finally { + g.translate(-x, -y); + } + } finally { + g.popClip(); + g.setAntiAliased(priorAntiAliased); + g.setColor(priorColor); + g.setAlpha(priorAlpha); + } + } + @Override public void paintBorderBackground(Graphics g, final Component c) { final int w = c.getWidth(); final int h = c.getHeight(); int x = c.getX(); int y = c.getY(); + if (w > 0 && h > 0 && canPaintDirectly(g)) { + // NO IMAGE, when the caller has asked for it. The cached path + // allocates a mutable image the size of the component and rebuilds + // it on every size change, which is ruinous for a shape that + // ANIMATES its size: a coach-mark circle growing to 1618x1618 over + // half a second throws away a 10MB surface per frame and queues a + // refinement pass behind each one. A flat fill needs none of that. + paintDirectly(g, x, y, w, h); + return; + } if (w > 0 && h > 0) { Object k = c.getClientProperty(CACHE_KEY + instanceVal); if (k instanceof CacheValue) { diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index bd4f443c314..0261010aa30 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -411,6 +411,10 @@ private void rememberNativeValue(String key, Object value) { private Hashtable resourceBundle; private Map bundle; private boolean wasThemeInstalled; + + /// A base theme a port has asked to install, not yet installed; see + /// [#setDeferredBaseTheme(Runnable)]. + private Runnable deferredBaseTheme; /// This EventDispatcher holds all listeners who would like to register to /// Theme refreshed event private EventDispatcher themelisteners; @@ -630,13 +634,47 @@ public void setUseLargerTextScale(boolean useLargerTextScale) { this.useLargerTextScale = useLargerTextScale; } + /// Registers a base theme to install the first time anything reads the theme, + /// instead of immediately. + /// + /// For a port that supplies a default look to applications with no theme of + /// their own. Installing it eagerly while the display initializes is wasted + /// whenever the application then installs a theme with [#setThemeProps(Hashtable)]: + /// that call resets the property table, so the base theme and every style built + /// from it are discarded before anything is drawn. A Flutter application always + /// does this, and so does any theme without `@includeNativeBool`. Deferred, + /// the base theme is installed only if something reads the theme first -- a + /// style, a theme constant or the look and feel -- or when a theme is layered + /// on it with [#addThemeProps(Hashtable)]. A theme installed with + /// `setThemeProps` supersedes it, and one that asks for the native theme + /// through `@includeNativeBool` installs it at that point as it always has. + /// + /// #### Parameters + /// + /// - `install`: installs the base theme, typically `Display.installNativeTheme()`; + /// run at most once, on the thread that first reads the theme + public void setDeferredBaseTheme(Runnable install) { + deferredBaseTheme = install; + } + + /// Installs a deferred base theme, if one is still pending. + /// + /// Cleared before it runs, because installing a theme reads the theme itself. + private void ensureBaseTheme() { + Runnable install = deferredBaseTheme; + if (install != null) { + deferredBaseTheme = null; + install.run(); + } + } + /// Indicates if a theme was previously installed since the last reset /// /// #### Returns /// /// true if setThemeProps was invoked public boolean wasThemeInstalled() { - return wasThemeInstalled; + return wasThemeInstalled || deferredBaseTheme != null; } /// Returns the currently installed look and feel @@ -645,6 +683,7 @@ public boolean wasThemeInstalled() { /// /// the currently installed look and feel public LookAndFeel getLookAndFeel() { + ensureBaseTheme(); return current; } @@ -766,6 +805,7 @@ public void setComponentSelectedStyle(String id, Style style) { /// /// the appropriate style (this method never returns null) public final Style getComponentStyle(String id) { + ensureBaseTheme(); return getComponentStyleImpl(id, false, ""); } @@ -781,6 +821,7 @@ public final Style getComponentStyle(String id) { /// The IconUIID corresponding to the given ID - or null if none is defined in the theme. /// public String getIconUIIDFor(String id) { + ensureBaseTheme(); if (id == null || id.length() == 0) { return null; } @@ -838,6 +879,7 @@ public Style parseComponentStyle(Resources theme, String baseStyle, String id, S /// /// the appropriate style (this method never returns null) public Style getComponentSelectedStyle(String id) { + ensureBaseTheme(); return getComponentStyleImpl(id, true, "sel#"); } @@ -882,6 +924,7 @@ public Style parseComponentSelectedStyle(Resources theme, String baseStyle, Stri /// /// the appropriate style (this method never returns null) public Style getComponentCustomStyle(String id, String type) { + ensureBaseTheme(); return getComponentStyleImpl(id, false, type + "#"); } @@ -916,6 +959,7 @@ public Style getComponentCustomStyle(String id, String type) { /// /// true when that custom style is installed or declared for this UIID public boolean hasComponentCustomStyle(String id, String type) { + ensureBaseTheme(); if (type == null || type.length() == 0) { return false; } @@ -1034,6 +1078,8 @@ private Style getComponentStyleImpl(String id, boolean selected, String prefix) /// /// the name of the current theme for theme switching UI's public String getThemeName() { + // The name of a deferred base theme is only known once it is installed. + ensureBaseTheme(); if (themeProps != null) { return (String) themeProps.get("name"); } @@ -1051,6 +1097,9 @@ HashMap getThemeProps() { /// /// - `themeProps`: the properties of the given theme public void setThemeProps(Hashtable themeProps) { + // Replacing the whole theme supersedes a deferred base theme: it would be + // reset away here before anything saw it. See setDeferredBaseTheme. + deferredBaseTheme = null; if (accessible) { setThemePropsImpl(themeProps); } @@ -1999,6 +2048,8 @@ private void resetThemeProps(Hashtable installedTheme) { /// /// - `themeProps`: the properties of the given theme public void addThemeProps(Hashtable themeProps) { + // Layered ON the base theme, so a deferred one has to be installed first. + ensureBaseTheme(); if (accessible) { restoreLargerTextFonts(); restoreNativeThemeValues(); @@ -2081,6 +2132,9 @@ public void zoomFonts(float factor) { if (factor == 1f) { return; } + // Scale the base theme itself; a deferred one installed after this call + // would otherwise come in at its original size. + ensureBaseTheme(); // Zoom the bundled originals, then reapply OS and accessibility settings. // Otherwise zoom replaces the instances tracked by the restoration maps. restoreLargerTextFonts(); @@ -2196,6 +2250,7 @@ public void refreshTheme() { /// /// the value of the constant or the default if the constant isn't in the theme public int getThemeConstant(String constantName, int def) { + ensureBaseTheme(); String v = (String) themeConstants.get(constantName); if (v != null) { try { @@ -2219,6 +2274,7 @@ public int getThemeConstant(String constantName, int def) { /// /// the value of the constant or the default if the constant isn't in the theme public final String getThemeConstant(String constantName, String def) { + ensureBaseTheme(); String v = (String) themeConstants.get(constantName); if (v != null) { return v; @@ -2238,6 +2294,7 @@ public final String getThemeConstant(String constantName, String def) { /// /// the value of the constant or the default if the constant isn't in the theme public final boolean isThemeConstant(String constantName, boolean def) { + ensureBaseTheme(); String c = getThemeConstant(constantName, null); if (c == null) { return def; @@ -2255,6 +2312,7 @@ public final boolean isThemeConstant(String constantName, boolean def) { /// /// the value of the constant or null if the constant isn't in the theme public Boolean isThemeConstant(String constantName) { + ensureBaseTheme(); String c = getThemeConstant(constantName, null); if (c == null) { return null; @@ -2275,6 +2333,7 @@ public Boolean isThemeConstant(String constantName) { /// /// the image if defined public Image getThemeImageConstant(String constantName) { + ensureBaseTheme(); return (Image) themeConstants.get(constantName); } @@ -2288,6 +2347,7 @@ public Image getThemeImageConstant(String constantName) { /// /// the mask if defined public Object getThemeMaskConstant(String constantName) { + ensureBaseTheme(); Object o = themeConstants.get(constantName + "Mask"); if (o != null) { return o; diff --git a/CodenameOne/src/com/codename1/ui/spinner/DateSpinner3D.java b/CodenameOne/src/com/codename1/ui/spinner/DateSpinner3D.java index 289f265e716..db966497e65 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/DateSpinner3D.java +++ b/CodenameOne/src/com/codename1/ui/spinner/DateSpinner3D.java @@ -37,10 +37,20 @@ import static com.codename1.ui.CN.convertToPixels; -/// A date spinner allows selecting a date value within the given date range +/// Three wheels -- day, month and year -- that together select one date inside +/// a configured range. /// -/// This is used by the Picker when in lightweight mode. -class DateSpinner3D extends Container implements InternalPickerWidget { +/// The body of a lightweight [Picker][com.codename1.ui.spinner.Picker] of type +/// date, and usable on its own as an ordinary `Container` when a form wants +/// the wheels inline rather than in a dialog. The range is set with +/// `#setStartYear(int)` and `#setEndYear(int)`; a value outside it is clamped. +/// +/// [getValue()][#getValue()] and [setValue(Object)][#setValue(Object)] carry a +/// `java.util.Date`. The wheels select only the date, but the time of day of +/// whatever was last passed to `setValue` is remembered and handed back by +/// `getValue`, so a round trip through this widget does not silently move a +/// timestamp to midnight. +public class DateSpinner3D extends Container implements InternalPickerWidget { private final SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM"); private final Container wrapper = new Container(BoxLayout.x()); private final Calendar tmpCal = Calendar.getInstance(); diff --git a/CodenameOne/src/com/codename1/ui/spinner/DateTimeSpinner3D.java b/CodenameOne/src/com/codename1/ui/spinner/DateTimeSpinner3D.java index 2f3be6cf6b0..30adc79da47 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/DateTimeSpinner3D.java +++ b/CodenameOne/src/com/codename1/ui/spinner/DateTimeSpinner3D.java @@ -36,10 +36,17 @@ import static com.codename1.ui.CN.convertToPixels; -/// The date and time spinner extends the time spinner by allowing to pick a specific day as well +/// A date and a time of day on one row of wheels: a day wheel beside the hour +/// and minute wheels of [TimeSpinner3D][com.codename1.ui.spinner.TimeSpinner3D]. /// -/// Used by Picker in lightweight mode. -class DateTimeSpinner3D extends Container implements InternalPickerWidget { +/// The body of a lightweight [Picker][com.codename1.ui.spinner.Picker] of type +/// date and time, and usable on its own as an ordinary `Container`. The day +/// wheel spans the configured range and shows each day by name rather than by +/// number, which is what the iOS picker it follows does. +/// +/// [getValue()][#getValue()] and [setValue(Object)][#setValue(Object)] carry a +/// `java.util.Date` in which both the date and the time components are used. +public class DateTimeSpinner3D extends Container implements InternalPickerWidget { private final Date today = new Date(); private final int off; private final Container wrapper = new Container(BoxLayout.x()); diff --git a/CodenameOne/src/com/codename1/ui/spinner/DurationSpinner3D.java b/CodenameOne/src/com/codename1/ui/spinner/DurationSpinner3D.java index 27088a61a99..a54adb539dc 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/DurationSpinner3D.java +++ b/CodenameOne/src/com/codename1/ui/spinner/DurationSpinner3D.java @@ -31,8 +31,18 @@ import com.codename1.ui.plaf.Style; import com.codename1.ui.plaf.UIManager; -/// A duration Spinner widget Used by the Picker in lightweight mode. -class DurationSpinner3D extends Container implements InternalPickerWidget { +/// Wheels that select a LENGTH of time rather than a point in it -- days, +/// hours and minutes, according to how it is configured. +/// +/// The body of a lightweight [Picker][com.codename1.ui.spinner.Picker] of type +/// duration, and usable on its own as an ordinary `Container`. Which wheels +/// appear is decided at construction; a duration picker that only needs hours +/// and minutes does not show a day wheel. +/// +/// [getValue()][#getValue()] and [setValue(Object)][#setValue(Object)] carry +/// the duration in MILLISECONDS, as a `Long`, so that it can be added to a +/// timestamp without unit conversion at the call site. +public class DurationSpinner3D extends Container implements InternalPickerWidget { public static final int FIELD_YEAR = 0; public static final int FIELD_MONTH = 1; public static final int FIELD_DAY = 2; diff --git a/CodenameOne/src/com/codename1/ui/spinner/InternalPickerWidget.java b/CodenameOne/src/com/codename1/ui/spinner/InternalPickerWidget.java index 58a7e864a4e..cb0b29190a0 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/InternalPickerWidget.java +++ b/CodenameOne/src/com/codename1/ui/spinner/InternalPickerWidget.java @@ -22,10 +22,44 @@ */ package com.codename1.ui.spinner; -/// Interface for 3D spinners that allow selected values to set and retrieved -/// by the Picker. -interface InternalPickerWidget { +/// The value contract every lightweight picker spinner implements, so +/// [Picker][com.codename1.ui.spinner.Picker] can drive any of them without +/// knowing which one it holds. +/// +/// A `Picker` in lightweight mode owns one of the `*Spinner3D` containers +/// according to its type -- date, time, date and time, duration or a plain +/// string list -- and moves a value in and out of it through this interface +/// alone. Implementing it is what makes a container usable as the body of a +/// picker. +/// +/// The value's runtime type is the implementation's own: `Date` for +/// [DateSpinner3D][com.codename1.ui.spinner.DateSpinner3D], an `int[]` of +/// hours and minutes for +/// [DurationSpinner3D][com.codename1.ui.spinner.DurationSpinner3D], and so on. +/// Each implementation documents what it expects; passing something else is a +/// programming error rather than a recoverable condition. +/// +/// This is public because the lightweight spinners are, and those are usable +/// on their own -- embedded in a form rather than shown in a picker dialog. +/// The name is retained for source compatibility with the releases in which it +/// was package private. +public interface InternalPickerWidget { + /// The currently selected value. + /// + /// #### Returns + /// + /// the selection, in whatever type this implementation documents; never + /// null once the widget has been laid out Object getValue(); + /// Moves the selection to `value`. + /// + /// The widget scrolls to the new selection rather than jumping, when it is + /// already on screen. + /// + /// #### Parameters + /// + /// - `value`: the new selection, in the type this implementation + /// documents. A value outside the widget's range is clamped into it. void setValue(Object value); } diff --git a/CodenameOne/src/com/codename1/ui/spinner/Spinner3D.java b/CodenameOne/src/com/codename1/ui/spinner/Spinner3D.java index 1ad4b070d60..3443daff769 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/Spinner3D.java +++ b/CodenameOne/src/com/codename1/ui/spinner/Spinner3D.java @@ -20,6 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ + package com.codename1.ui.spinner; import com.codename1.l10n.DateFormat; @@ -44,10 +45,26 @@ import static com.codename1.ui.ComponentSelector.$; -/// A spinner widget that tries to look and feel like the iOS picker. +/// A scrolling wheel of values styled after the iOS picker, and the body of a +/// lightweight [Picker][com.codename1.ui.spinner.Picker]. +/// +/// Rows come from a [ListModel][com.codename1.ui.list.ListModel], so the +/// contents can be anything a model can produce; the selection is whatever +/// that model holds at the selected index. `Spinner3D` is the plain-list +/// member of the family, beside +/// [DateSpinner3D][com.codename1.ui.spinner.DateSpinner3D], +/// [TimeSpinner3D][com.codename1.ui.spinner.TimeSpinner3D], +/// [DateTimeSpinner3D][com.codename1.ui.spinner.DateTimeSpinner3D] and +/// [DurationSpinner3D][com.codename1.ui.spinner.DurationSpinner3D]. +/// +/// It is an ordinary `Container`, so it can be added to a form directly rather +/// than shown through a picker dialog -- which is why it is public. Use +/// `Picker` instead when you want the platform's native picker where one +/// exists, because a `Picker` falls back to this only in lightweight mode. /// -/// This is used by the Picker widget when in lightweight mode. -class Spinner3D extends Container implements InternalPickerWidget { +/// [getValue()][#getValue()] and [setValue(Object)][#setValue(Object)] carry +/// the selected model element. +public class Spinner3D extends Container implements InternalPickerWidget { private final SpinnerNode root; private final ScrollingContainer scroller; diff --git a/CodenameOne/src/com/codename1/ui/spinner/TimeSpinner3D.java b/CodenameOne/src/com/codename1/ui/spinner/TimeSpinner3D.java index d95ff479c4d..9d69675853f 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/TimeSpinner3D.java +++ b/CodenameOne/src/com/codename1/ui/spinner/TimeSpinner3D.java @@ -36,11 +36,22 @@ import static com.codename1.ui.CN.convertToPixels; -/// Allows selecting a time of day either in 24 hour batches or AM/PM format. +/// Hour and minute wheels selecting a time of day, in either 24 hour or AM/PM +/// form according to `#setShowMeridiem(boolean)`. /// -/// If `#setDurationMode(boolean)` is true then this will allow -/// users to set a duration in hours and minutes. -class TimeSpinner3D extends Container implements InternalPickerWidget { +/// The body of a lightweight [Picker][com.codename1.ui.spinner.Picker] of type +/// time, and usable on its own as an ordinary `Container`. The minute wheel +/// can step by more than one minute, which is what a picker offering quarter +/// hours uses. +/// +/// With `#setDurationMode(boolean)` set it selects a LENGTH of time instead, +/// and the meridiem is not shown; prefer +/// [DurationSpinner3D][com.codename1.ui.spinner.DurationSpinner3D] for that, +/// which also offers a day wheel. +/// +/// [getValue()][#getValue()] and [setValue(Object)][#setValue(Object)] carry +/// the time as minutes since midnight, as an `Integer`. +public class TimeSpinner3D extends Container implements InternalPickerWidget { static final int DEFAULT_MINUTE_STEP = 5; private Spinner3D hour; diff --git a/CodenameOne/src/com/codename1/ui/util/Resources.java b/CodenameOne/src/com/codename1/ui/util/Resources.java index bb1abd0f84a..33c28d2c531 100644 --- a/CodenameOne/src/com/codename1/ui/util/Resources.java +++ b/CodenameOne/src/com/codename1/ui/util/Resources.java @@ -481,7 +481,14 @@ void openFile(InputStream input) throws IOException { } private void openFileImpl(InputStream input) throws IOException { - this.input = new DataInputStream(input); + // Parsing is thousands of two- and four-byte reads, and the stream a port + // hands back for a bundled resource is usually com.codename1.io's + // BufferedInputStream, whose every read is synchronized and stamps the + // clock three times for its network idle timeout. A 3463-key theme took + // 5-6ms to open on macOS, much of it in gettimeofday. A plain private + // buffer in front of it makes each small read an array index. + this.input = new DataInputStream(input instanceof ByteArrayInputStream + ? input : new ParseBuffer(input)); int resourceCount = this.input.readShort(); if (resourceCount < 0) { throw new IOException("Invalid resource file!"); @@ -1411,16 +1418,41 @@ Font loadBitmapFont(DataInputStream input, String id, Font font) throws IOExcept return font; } + /// System fonts this resource file has already asked for, by face, style and size. + private HashMap systemFonts; + + /// One system font per face/style/size for the whole file. + /// + /// Every font entry in a theme starts from a system font -- the fallback, and the + /// style a TrueType font is derived with -- and Font.createSystemFont builds a new + /// native font on each call. A theme has a font entry for every UIID and state: + /// the Flutter runtime's Material theme has 330, nearly all the same system font, + /// and on Linux each one was a Pango font with its metrics measured -- 319 of the + /// 360 font creations in a launch were the same Sans 18px. A font is immutable, so + /// the entries can share one. + private Font systemFont(int face, int style, int size) { + if (systemFonts == null) { + systemFonts = new HashMap(); + } + Integer key = Integer.valueOf(((face & 0xff) << 16) | ((style & 0xff) << 8) | (size & 0xff)); + Font f = systemFonts.get(key); + if (f == null) { + f = Font.createSystemFont(face, style, size); + systemFonts.put(key, f); + } + return f; + } + Font createTrueTypeFont(Font f, String fontName, String fileName, float fontSize, int sizeSetting) { switch (sizeSetting) { case 0: // small - fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL).getHeight(); + fontSize = systemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL).getHeight(); break; case 1: // medium - fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM).getHeight(); + fontSize = systemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM).getHeight(); break; case 2: // large - fontSize = Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE).getHeight(); + fontSize = systemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE).getHeight(); break; case 3: // millimetres fontSize = Display.getInstance().convertToPixels((int) (fontSize * 10), true) / 10.0f; @@ -1733,7 +1765,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException { continue; } } else { - f = Font.createSystemFont(input.readByte(), input.readByte(), input.readByte()); + f = systemFont(input.readByte(), input.readByte(), input.readByte()); if (minorVersion > 4) { boolean hasTTF = input.readBoolean(); if (hasTTF) { @@ -2224,4 +2256,87 @@ static class MediaRule { String rawKey; String translatedKey; } + + /// An unsynchronized read buffer for parsing; see openFileImpl. + private static final class ParseBuffer extends InputStream { + private final InputStream in; + private final byte[] buf = new byte[8192]; + private int pos; + private int count; + + ParseBuffer(InputStream in) { + this.in = in; + } + + private boolean fill() throws IOException { + pos = 0; + count = in.read(buf, 0, buf.length); + if (count < 0) { + count = 0; + return false; + } + return true; + } + + @Override + public int read() throws IOException { + while (pos >= count) { + if (!fill()) { + return -1; + } + } + return buf[pos++] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int n = 0; + int avail = count - pos; + if (avail > 0) { + n = Math.min(avail, len); + System.arraycopy(buf, pos, b, off, n); + pos += n; + if (n == len) { + return n; + } + } + if (len - n >= buf.length) { + // A large read (an image) goes straight to the source. + int r = in.read(b, off + n, len - n); + if (r < 0) { + return n == 0 ? -1 : n; + } + return n + r; + } + if (!fill()) { + return n == 0 ? -1 : n; + } + int m = Math.min(count - pos, len - n); + System.arraycopy(buf, pos, b, off + n, m); + pos += m; + return n + m; + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } + int avail = count - pos; + if (avail > 0) { + int k = (int) Math.min(avail, n); + pos += k; + return k; + } + return in.skip(n); + } + + @Override + public int available() throws IOException { + return (count - pos) + in.available(); + } + } } diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 5f2fa550db5..6f9579ab073 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -5987,6 +5987,12 @@ public boolean dispatchKeyEvent(KeyEvent event) { private boolean nativeThemeAvailable; public boolean hasNativeTheme() { + if ("custom".equals(nativeThemeMode())) { + // The application ships the only theme it uses, and the builder packaged + // no native one for it. Asked every time rather than cached: the mode is a + // Display property the application may set before its theme loads. + return false; + } if (!testedNativeTheme) { testedNativeTheme = true; try { @@ -6007,50 +6013,58 @@ public boolean hasNativeTheme() { return nativeThemeAvailable; } + /// The theme flavor the build hints ask for. and.themeMode is the per-platform + /// hint (auto | modern | material | hololight | legacy | custom); the legacy + /// name cn1.androidTheme is still honored for back-compat. The cross-platform + /// shortcut nativeTheme=modern/legacy/custom (deprecated alias: cn1.nativeTheme) + /// feeds in when no platform-specific hint is set. Default stays on + /// android_holo_light - what master shipped and what existing screenshot goldens + /// are anchored against. The ancient pre-Holo androidTheme.res is only reached + /// via explicit and.hololight=true (historical back-compat) or + /// and.themeMode=legacy. + /// + /// Mirrored by the builders' NativeThemes.androidThemesFor, which decides which + /// of the themes are packaged at all. + private static String nativeThemeMode() { + Display d = Display.getInstance(); + String mode = d.getProperty("and.themeMode", + d.getProperty("cn1.androidTheme", null)); + if (mode != null) { + return asciiLower(mode.trim()); + } + String shared = d.getProperty("nativeTheme", + d.getProperty("cn1.nativeTheme", null)); + // "native" is "modern plus the desktop": the desktop half belongs to + // the JavaSE port, and Android's own answer to "the platform's own + // look" is Material either way. Without it the value fell through to + // the hololight default below, so asking for the native look got the + // legacy one. + if ("modern".equalsIgnoreCase(shared) || "native".equalsIgnoreCase(shared)) { + return "material"; + } + if ("custom".equalsIgnoreCase(shared)) { + return "custom"; + } + if ("legacy".equalsIgnoreCase(shared)) { + return "hololight"; + } + if ("true".equalsIgnoreCase(d.getProperty("and.hololight", "false"))) { + return "legacy"; + } + return "hololight"; + } + /** * Installs the native theme, this is only applicable if hasNativeTheme() * returned true. Notice that this method might replace the * DefaultLookAndFeel instance and the default transitions. */ public void installNativeTheme() { - hasNativeTheme(); - if (!nativeThemeAvailable) { + if (!hasNativeTheme()) { return; } try { - // Resolve desired theme flavor. and.themeMode is the per-platform - // hint (auto | modern | material | hololight | legacy); the legacy - // name cn1.androidTheme is still honored for back-compat. The - // cross-platform shortcut nativeTheme=modern/legacy (deprecated - // alias: cn1.nativeTheme) feeds in when no platform-specific hint - // is set. Default stays on android_holo_light - what master - // shipped and what existing screenshot goldens are anchored - // against. The ancient pre-Holo androidTheme.res is only reached - // via explicit and.hololight=true (historical back-compat) or - // and.themeMode=legacy. - Display d = Display.getInstance(); - String mode = d.getProperty("and.themeMode", - d.getProperty("cn1.androidTheme", null)); - if (mode == null) { - String shared = d.getProperty("nativeTheme", - d.getProperty("cn1.nativeTheme", null)); - // "native" is "modern plus the desktop": the desktop half belongs to - // the JavaSE port, and Android's own answer to "the platform's own - // look" is Material either way. Without it the value fell through to - // the hololight default below, so asking for the native look got the - // legacy one. - if ("modern".equalsIgnoreCase(shared) || "native".equalsIgnoreCase(shared)) { - mode = "material"; - } else if ("legacy".equalsIgnoreCase(shared)) { - mode = "hololight"; - } else if ("true".equalsIgnoreCase(d.getProperty("and.hololight", "false"))) { - mode = "legacy"; - } else { - mode = "hololight"; - } - } else { - mode = mode.toLowerCase(); - } + String mode = nativeThemeMode(); String resPath; if ("material".equals(mode) || "modern".equals(mode) || "auto".equals(mode)) { diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index edcf111ccc4..7179de02591 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -3406,6 +3406,10 @@ private static String resolveAutoNativeTheme(String platformName) { if ("legacy".equalsIgnoreCase(iosMode) || "iphone".equalsIgnoreCase(iosMode)) { return "iPhoneTheme"; } + if ("custom".equalsIgnoreCase(iosMode)) { + // The device installs none; the modern default below would not match it. + return null; + } } String shared = sharedNativeThemeHint(); if ("legacy".equalsIgnoreCase(shared)) { @@ -3432,6 +3436,10 @@ private static String resolveAutoNativeTheme(String platformName) { if ("legacy".equalsIgnoreCase(andMode)) { return "androidTheme"; } + if ("custom".equalsIgnoreCase(andMode)) { + // The device installs none; Material 3 below would not match it. + return null; + } } String shared = sharedNativeThemeHint(); if ("legacy".equalsIgnoreCase(shared)) { @@ -4230,6 +4238,8 @@ public void blit() { if(menuDisplayed){ return; } + long blitStartNanos = BLIT_TRACE ? System.nanoTime() : 0; + long bufferDoneNanos = 0; // We keep a blitCounter that gets reset in paintComponent() // If blit is called a number of times with no call to paintComponet @@ -4289,6 +4299,9 @@ public void blit() { } + if (BLIT_TRACE) { + bufferDoneNanos = System.nanoTime(); + } try { Runnable r = new Runnable() { public void run() { @@ -4346,6 +4359,10 @@ public void run() { } catch(Exception err) { err.printStackTrace(); } + if (BLIT_TRACE) { + recordBlit(bufferDoneNanos - blitStartNanos, + System.nanoTime() - bufferDoneNanos, bufferSafeMode); + } } public void blit(int x, int y, int w, int h) { @@ -13011,6 +13028,60 @@ private void checkLastFrame() { /** * @inheritDoc */ + /** + * Diagnostic for simulator frame pacing, enabled with -Dcn1.blit.trace=true. + * + *

The simulator presents a frame by handing the buffer to AWT through + * {@code SwingUtilities.invokeAndWait}, which BLOCKS the Codename One EDT until the + * AWT event thread has run it. That makes the simulator's frame rate a property of + * AWT's scheduling rather than of how long the app takes to paint, and it is + * invisible to any measurement taken inside Codename One - which is exactly why an + * app can paint a frame in 5ms and still advance only a few times a second.

+ * + *

The two phases are reported separately because they have different causes: the + * buffer copy is work the simulator does (and in {@code bufferSafeMode} it copies + * the whole screen under a lock, every frame), while the present time is pure + * waiting on AWT.

+ */ + private static final boolean BLIT_TRACE = "true".equals(System.getProperty("cn1.blit.trace")); + private static int blitTraceCount; + private static long blitTraceBufferNanos; + private static long blitTracePresentNanos; + private static long blitTraceWorstPresentNanos; + private static long blitTraceLastReport; + private static int blitTraceSafeModeFrames; + + private static synchronized void recordBlit(long bufferNanos, long presentNanos, + boolean safeMode) { + blitTraceCount++; + blitTraceBufferNanos += bufferNanos; + blitTracePresentNanos += presentNanos; + blitTraceWorstPresentNanos = Math.max(blitTraceWorstPresentNanos, presentNanos); + if (safeMode) { + blitTraceSafeModeFrames++; + } + long now = System.currentTimeMillis(); + if (blitTraceLastReport == 0) { + blitTraceLastReport = now; + return; + } + if (now - blitTraceLastReport < 1000) { + return; + } + System.out.println("BLITTRACE frames=" + blitTraceCount + + " fps=" + (blitTraceCount * 1000L / Math.max(1, now - blitTraceLastReport)) + + " bufferMs=" + (blitTraceBufferNanos / 1000000.0 / blitTraceCount) + + " presentMs=" + (blitTracePresentNanos / 1000000.0 / blitTraceCount) + + " worstPresentMs=" + (blitTraceWorstPresentNanos / 1000000.0) + + " safeModeFrames=" + blitTraceSafeModeFrames); + blitTraceCount = 0; + blitTraceBufferNanos = 0; + blitTracePresentNanos = 0; + blitTraceWorstPresentNanos = 0; + blitTraceSafeModeFrames = 0; + blitTraceLastReport = now; + } + public void flushGraphics(int x, int y, int width, int height) { if (isShowEDTWarnings()) { checkEDT(); @@ -14540,7 +14611,7 @@ private java.awt.Font desktopNativeFont(String alias) { Float weight; if ("Thin".equals(weightName)) weight = TextAttribute.WEIGHT_EXTRA_LIGHT; else if ("Light".equals(weightName)) weight = TextAttribute.WEIGHT_LIGHT; - else if ("Regular".equals(weightName)) weight = TextAttribute.WEIGHT_REGULAR; + else if ("Regular".equals(weightName) || "Normal".equals(weightName)) weight = TextAttribute.WEIGHT_REGULAR; else if ("Bold".equals(weightName)) weight = TextAttribute.WEIGHT_BOLD; else if ("Black".equals(weightName)) weight = TextAttribute.WEIGHT_HEAVY; else throw new IllegalArgumentException("Unsupported native font type: " + alias); @@ -14585,6 +14656,15 @@ public Object loadTrueTypeFont(String fontName, String fileName) { res = "Medium"; break; + // The system face at its TRUE regular weight. native:MainRegular + // has meant Roboto-Medium since the alias was introduced and the + // iOS port maps it one step heavy too, so the honest regular gets + // a name of its own rather than a change that would restyle every + // existing application. + case "native:MainNormal": + res = "Regular"; + break; + case "native:MainBold": res = "Bold"; break; diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/ParparVMBootstrap.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/ParparVMBootstrap.java index 6fcbcb1ce24..fbf1d983012 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/ParparVMBootstrap.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/ParparVMBootstrap.java @@ -48,8 +48,8 @@ public static void bootstrap(Lifecycle lifecycle) { * As {@link #bootstrap(Lifecycle)}, but runs {@code afterInit} once {@code Display} is * initialized and before the lifecycle's {@code init}/{@code start} callbacks. The generated * launcher uses this to stamp the app-hardening metadata (so {@code Hardening.isHardened()} and - * any crash raised during {@code init}/{@code start} already see the mapping id and level), - * which a post-bootstrap stamp would miss because {@code run()} invokes the lifecycle inline. + * any crash raised during {@code init}/{@code start} already see the mapping id and level). + * The lifecycle itself is then queued to the EDT, so {@code afterInit} always runs first. * * @param lifecycle the application lifecycle * @param afterInit code to run after {@code Display.init} and before the lifecycle starts; may be null @@ -61,7 +61,12 @@ public static void bootstrap(Lifecycle lifecycle, Runnable afterInit) { if (afterInit != null) { afterInit.run(); } - bootstrap.run(); + // The lifecycle runs on the EDT, as it does on every other port (iOS hands its stub + // to Display.init, which runs it there). This used to call run() inline, on the + // worker's main thread, so init() and start() ran off the EDT: anything that checks + // -- a transpiled Flutter app's runApp asserts it -- threw before the first frame, + // and the transpiled Flutter gallery never got past its loading screen. + Display.getInstance().callSerially(bootstrap); } // ``window.cn1Initialized = true`` lands on the worker's global diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js index c25ad12abea..6b0ece58965 100644 --- a/Ports/JavaScriptPort/src/main/webapp/port.js +++ b/Ports/JavaScriptPort/src/main/webapp/port.js @@ -3505,92 +3505,12 @@ bindCiFallback("HTML5Implementation.determineFontHeightCoerce", [ return 16.0; }); -const hashMapComputeHashCodeImplMethodId = "cn1_java_util_HashMap_computeHashCode_java_lang_Object_R_int__impl"; -const hashMapComputeHashCodeMethodId = "cn1_java_util_HashMap_computeHashCode_java_lang_Object_R_int"; - -bindCiFallback("HashMap.computeHashCodeNullKey", [ - hashMapComputeHashCodeImplMethodId, - hashMapComputeHashCodeMethodId -], function*(key) { - if (key == null) { - emitDiagLine("PARPAR:DIAG:FALLBACK:hashMapComputeHashCode:nullKey=1"); - return 0; - } - // Resolve the translator-generated original lazily — port.js evaluates - // before translated_app.js, so a snapshot taken at load time would be - // null and force every non-null lookup down the resolveVirtual fallback. - // jvm.translatedMethods is populated by bindNative when registering - // the native overrides; checking it last preserves any port-specific - // override of the same method. - let original = null; - if (jvm && jvm.translatedMethods) { - original = jvm.translatedMethods[hashMapComputeHashCodeImplMethodId] - || jvm.translatedMethods[hashMapComputeHashCodeMethodId] - || null; - } - if (typeof original !== "function") { - if (typeof global[hashMapComputeHashCodeImplMethodId] === "function" - && !global[hashMapComputeHashCodeImplMethodId].__cn1CiFallbackSymbol) { - original = global[hashMapComputeHashCodeImplMethodId]; - } else if (typeof global[hashMapComputeHashCodeMethodId] === "function" - && !global[hashMapComputeHashCodeMethodId].__cn1CiFallbackSymbol) { - original = global[hashMapComputeHashCodeMethodId]; - } - } - if (typeof original === "function") { - return yield* cn1_ivAdapt(original(key)); - } - // Last-ditch path when the translated original genuinely isn't - // available. ``computeHashCode(key)`` is just ``key.hashCode()`` — - // dispatch via the SHARED dispatch id (``cn1_s_hashCode_R_int``), not - // the legacy class-specific name. Every translated class registers its - // ``hashCode`` slot under the shared key after the dispatch-id - // refactor; resolving against ``cn1_java_lang_Object_hashCode_R_int`` - // skips that slot and silently returns the inherited Object.hashCode - // (identity hash), which made every String key in CSSBorder.STYLE_MAP - // store under its identity hash and every subsequent ``get("solid")`` - // miss the entry. - var hashCodeMethod = jvm.resolveVirtual(key.__class || "java_lang_Object", - "cn1_s_hashCode_R_int"); - if (typeof hashCodeMethod === "function") { - return yield* cn1_ivAdapt(hashCodeMethod(key)); - } - return 0; -}); -if (typeof global[hashMapComputeHashCodeImplMethodId] === "function") { - const originalHashMapComputeHashCodeImpl = global[hashMapComputeHashCodeImplMethodId]; - global[hashMapComputeHashCodeImplMethodId] = function*(key) { - if (key == null) { - emitDiagLine("PARPAR:DIAG:FALLBACK:hashMapComputeHashCodeDirect:nullKey=1"); - return 0; - } - return yield* cn1_ivAdapt(originalHashMapComputeHashCodeImpl(key)); - }; - emitDiagLine("PARPAR:DIAG:INIT:shim=hashMapComputeHashCodeImplNullKey"); -} -if (typeof global[hashMapComputeHashCodeMethodId] === "function") { - const originalHashMapComputeHashCode = global[hashMapComputeHashCodeMethodId]; - global[hashMapComputeHashCodeMethodId] = function*(key) { - if (key == null) { - emitDiagLine("PARPAR:DIAG:FALLBACK:hashMapComputeHashCodeDirect:nullKey=1"); - return 0; - } - return yield* cn1_ivAdapt(originalHashMapComputeHashCode(key)); - }; - emitDiagLine("PARPAR:DIAG:INIT:shim=hashMapComputeHashCodeNullKey"); -} -const hashMapClassDef = jvm.classes && jvm.classes["java_util_HashMap"]; -if (hashMapClassDef && hashMapClassDef.methods && typeof hashMapClassDef.methods[hashMapComputeHashCodeMethodId] === "function") { - const originalClassHashMapComputeHashCode = hashMapClassDef.methods[hashMapComputeHashCodeMethodId]; - hashMapClassDef.methods[hashMapComputeHashCodeMethodId] = function*(__cn1ThisObject, key) { - if (key == null) { - emitDiagLine("PARPAR:DIAG:FALLBACK:hashMapComputeHashCodeClass:nullKey=1"); - return 0; - } - return yield* cn1_ivAdapt(originalClassHashMapComputeHashCode(__cn1ThisObject, key)); - }; - emitDiagLine("PARPAR:DIAG:INIT:shim=hashMapComputeHashCodeClassNullKey"); -} +// HashMap.computeHashCode used to carry a null-key fallback here, three ways (a +// bindCiFallback and two direct replacements). The compact HashMap never passes it +// a null key -- its one caller, cn1Marker, answers null itself -- so the fallback +// could not fire. It was not free: a replaced method is suspending by definition, +// and this one sat under every HashMap get/put/containsKey, so every lookup in +// every app was a generator. const styleSetPaddingUnitMethodId = "cn1_com_codename1_ui_plaf_Style_setPaddingUnit_byte_1ARRAY"; const styleSetMarginUnitMethodId = "cn1_com_codename1_ui_plaf_Style_setMarginUnit_byte_1ARRAY"; @@ -5751,8 +5671,6 @@ const cn1ssWs = { queue: [], // {test, bytes} buffered while the socket is still connecting pending: 0 // sent-but-unacked frames, for an optional flush at suite end }; -cn1RefreshAlias(hashMapComputeHashCodeMethodId, global[hashMapComputeHashCodeMethodId]); -cn1RefreshAlias(hashMapComputeHashCodeImplMethodId, global[hashMapComputeHashCodeImplMethodId]); function cn1ssWsHost() { try { diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_text.c b/Ports/LinuxPort/nativeSources/cn1_linux_text.c index 1817e317cd9..a86f492f681 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_text.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_text.c @@ -66,8 +66,33 @@ PangoContext* cn1LinuxPangoContext(void) { return cn1Pango; } +/* Metrics already computed, keyed by font description (Pango's own hash/equality, + * so family, weight, style and size all take part). pango_context_get_metrics + * shapes a sample string to estimate character widths this port never reads, and + * a theme creates the same few descriptions over and over: on the transpiled + * Flutter gallery it was 7-8% of the launch's CPU. The cache returns exactly what + * Pango answered the first time, so no layout moves. Each entry owns a copy of its + * key; nothing is ever evicted, since a program uses a bounded set of fonts. The + * value packs ascent and height, both small non-negative pixel counts. + * Process-wide, like cn1Pango beside it: a thread-local copy would survive the + * reset in loadTrueTypeFont on every thread but the one that registered the font. */ +static GHashTable* cn1MetricsCache = NULL; + /* Snapshots ascent/height for the description into the CN1Font. */ static void cn1FontMetrics(CN1Font* f) { + gpointer hit; + if (cn1MetricsCache == NULL) { + cn1MetricsCache = g_hash_table_new_full( + (GHashFunc) pango_font_description_hash, + (GEqualFunc) pango_font_description_equal, + (GDestroyNotify) pango_font_description_free, NULL); + } + if (g_hash_table_lookup_extended(cn1MetricsCache, f->desc, NULL, &hit)) { + guint64 packed = (guint64) (guintptr) hit; + f->ascent = (int) (packed >> 32); + f->height = (int) (packed & 0xffffffffu); + return; + } PangoFontMetrics* m = pango_context_get_metrics(cn1LinuxPangoContext(), f->desc, 0); if (m != 0) { f->ascent = pango_font_metrics_get_ascent(m) / PANGO_SCALE; @@ -77,6 +102,11 @@ static void cn1FontMetrics(CN1Font* f) { f->ascent = f->pixelSize; f->height = f->pixelSize + f->pixelSize / 4; } + if (sizeof(gpointer) >= 8 && f->ascent >= 0 && f->height >= 0) { + guint64 packed = ((guint64) (guint32) f->ascent << 32) | (guint32) f->height; + g_hash_table_insert(cn1MetricsCache, pango_font_description_copy(f->desc), + (gpointer) (guintptr) packed); + } } static CN1Font* cn1MakeFont(const char* family, int weight, int italic, int pixelSize) { @@ -158,7 +188,53 @@ static void cn1LayoutSetText(PangoLayout* layout, const char* s, int len) { g_free(valid); } -static int cn1MeasureUtf8(CN1Font* f, const char* utf8, int byteLen) { +/* Widths already measured, keyed by (font, exact UTF-8 bytes). Measuring shapes + * the string through Pango, and a UI measures the same labels in the same fonts + * over and over -- every layout pass, every preferred-size query: ~5.6% of the + * transpiled Flutter gallery's launch. The cache returns exactly what Pango + * answered, so nothing moves. The font's address is a sound key because a + * CN1Font is never freed. Bounded by dropping everything once it holds + * CN1_WIDTH_CACHE_MAX entries -- a UI's working set is far smaller, and a full + * clear keeps the bound without bookkeeping on the hot path. Cleared with the + * metrics cache when a TrueType font is registered. Process-wide, like cn1Pango. */ +#define CN1_WIDTH_CACHE_MAX 8192 +#define CN1_WIDTH_CACHE_TEXT 256 +typedef struct { + CN1Font* font; + int len; + guint hash; + char bytes[1]; +} CN1WidthKey; + +static GHashTable* cn1WidthCache = NULL; + +static guint cn1WidthKeyHash(gconstpointer p) { + return ((const CN1WidthKey*) p)->hash; +} + +static gboolean cn1WidthKeyEqual(gconstpointer a, gconstpointer b) { + const CN1WidthKey* x = (const CN1WidthKey*) a; + const CN1WidthKey* y = (const CN1WidthKey*) b; + return x->font == y->font && x->len == y->len && memcmp(x->bytes, y->bytes, (size_t) x->len) == 0; +} + +static guint cn1WidthHash(CN1Font* f, const char* s, int len) { + /* FNV-1a over the bytes, seeded with the font's address. */ + guint h = 2166136261u ^ (guint) (guintptr) f; + int i; + for (i = 0; i < len; i++) { + h = (h ^ (unsigned char) s[i]) * 16777619u; + } + return h; +} + +static void cn1WidthCacheClear(void) { + if (cn1WidthCache != NULL) { + g_hash_table_remove_all(cn1WidthCache); + } +} + +static int cn1MeasureUtf8Uncached(CN1Font* f, const char* utf8, int byteLen) { PangoLayout* layout = pango_layout_new(cn1LinuxPangoContext()); int w = 0, h = 0; pango_layout_set_font_description(layout, f->desc); @@ -168,6 +244,47 @@ static int cn1MeasureUtf8(CN1Font* f, const char* utf8, int byteLen) { return w; } +static int cn1MeasureUtf8(CN1Font* f, const char* utf8, int byteLen) { + union { + CN1WidthKey key; + char room[sizeof(CN1WidthKey) + CN1_WIDTH_CACHE_TEXT]; + } probe; + CN1WidthKey* key; + gpointer hit; + int len; + int w; + if (utf8 == NULL) { + return cn1MeasureUtf8Uncached(f, utf8, byteLen); + } + len = byteLen < 0 ? (int) strlen(utf8) : byteLen; + if (len > CN1_WIDTH_CACHE_TEXT) { + /* Long text is measured once per wrap and rarely repeats; not worth a copy. */ + return cn1MeasureUtf8Uncached(f, utf8, byteLen); + } + if (cn1WidthCache == NULL) { + cn1WidthCache = g_hash_table_new_full(cn1WidthKeyHash, cn1WidthKeyEqual, free, NULL); + } + /* Looked up with a key on the stack, so a hit allocates nothing. */ + probe.key.font = f; + probe.key.len = len; + probe.key.hash = cn1WidthHash(f, utf8, len); + memcpy(probe.key.bytes, utf8, (size_t) len); + if (g_hash_table_lookup_extended(cn1WidthCache, &probe.key, NULL, &hit)) { + /* Stored +1 so a zero width is distinguishable from a missing entry. */ + return (int) (gintptr) hit - 1; + } + w = cn1MeasureUtf8Uncached(f, utf8, len); + key = (CN1WidthKey*) malloc(sizeof(CN1WidthKey) + (size_t) len); + if (key != NULL) { + memcpy(key, &probe.key, sizeof(CN1WidthKey) + (size_t) len); + if (g_hash_table_size(cn1WidthCache) >= CN1_WIDTH_CACHE_MAX) { + g_hash_table_remove_all(cn1WidthCache); + } + g_hash_table_insert(cn1WidthCache, key, (gpointer) (gintptr) (w + 1)); + } + return w; +} + JAVA_INT com_codename1_impl_linux_LinuxNative_stringWidth___long_java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG font, JAVA_OBJECT str) { const char* s; if (str == JAVA_NULL) { @@ -276,6 +393,12 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFontFromMemory___java * layout sees the freshly registered family. */ pango_cairo_font_map_set_default(0); cn1Pango = 0; + /* A family that fell back to a system face until now may resolve to + * the new one, so every cached answer is suspect. */ + if (cn1MetricsCache != NULL) { + g_hash_table_remove_all(cn1MetricsCache); + } + cn1WidthCacheClear(); } else { close(fd); } diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_window.c b/Ports/LinuxPort/nativeSources/cn1_linux_window.c index efccf73b015..f18b4f42f87 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_window.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_window.c @@ -1058,6 +1058,15 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_initDisplay___java_lang_String_in } cn1GtkThread = pthread_self(); + /* GDK's own OpenGL, off unless the user asked for something else. Opening the + * display otherwise probes GLX to pick GL-capable visuals -- epoxy_glx_version, + * glXQueryServerString and a dlopen of the Mesa driver -- which was the larger + * part of gtk_init and about a tenth of the whole launch on the CI runner. This + * port never uses GDK's GL: it draws with cairo, and the 3D backend creates its + * own EGL context. WebKitGTK (the BrowserComponent peer) still renders without + * it, verified with WebKitGTK 2.50 under Xvfb. Not overwritten when set, so + * GDK_GL=... from the environment keeps working for anyone who needs it. */ + g_setenv("GDK_GL", "disable", FALSE); gtk_init(0, 0); cn1Window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(cn1Window), t); @@ -1543,8 +1552,41 @@ static void cn1A11yBeginMain(void* ignored) { } } +/* One tree publication is queued on the calling thread and applied in a single + * hop to the GTK thread by accessibilityEnd. Each node and action used to make + * its own cn1LinuxRunOnMainAndWait, so publishing the gallery's tree was hundreds + * of synchronous round trips between the event dispatch thread and the GTK loop, + * each one waiting for the loop to come round. Begin, node, action and end are + * always called in that order from the one thread that publishes the tree, and + * only that thread touches this queue: accessibilityEnd hands the whole array to + * the GTK thread as a batch and starts a fresh one, so the queue needs no lock. */ +typedef struct { + int action; /* 0 = node, 1 = action */ + void* data; +} CN1A11yOp; +static CN1A11yOp* cn1A11yOps = 0; +static int cn1A11yOpCount = 0; +static int cn1A11yOpCapacity = 0; + +static void cn1A11yQueue(int action, void* data) { + if (cn1A11yOpCount == cn1A11yOpCapacity) { + int capacity = cn1A11yOpCapacity == 0 ? 256 : cn1A11yOpCapacity * 2; + CN1A11yOp* grown = (CN1A11yOp*) realloc(cn1A11yOps, (size_t) capacity * sizeof(CN1A11yOp)); + if (grown == 0) { + return; + } + cn1A11yOps = grown; + cn1A11yOpCapacity = capacity; + } + cn1A11yOps[cn1A11yOpCount].action = action; + cn1A11yOps[cn1A11yOpCount].data = data; + cn1A11yOpCount++; +} + JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityBegin__(CODENAME_ONE_THREAD_STATE) { - cn1LinuxRunOnMainAndWait(cn1A11yBeginMain, 0); + /* Nothing reaches GTK yet; see accessibilityEnd. A publication that never + * reached its end leaves ops behind, and they are applied with this one. */ + (void) threadStateData; } typedef struct { @@ -1614,7 +1656,7 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityNode___long_long_jav call->label = label == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, label)); call->description = description == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, description)); call->value = value == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, value)); - cn1LinuxRunOnMainAndWait(cn1A11yNodeMain, call); + cn1A11yQueue(0, call); } static void cn1A11yActionActivated(GtkWidget* widget, gpointer pointer) { @@ -1662,17 +1704,104 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityAction___long_java_l action->nodeId = nodeId; action->actionHash = actionHash; action->actionId = actionId == JAVA_NULL ? strdup("") : strdup(stringToUTF8(threadStateData, actionId)); action->label = label == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, label)); - cn1LinuxRunOnMainAndWait(cn1A11yActionMain, action); + cn1A11yQueue(1, action); +} + +/* A complete publication, handed from the publishing thread to the GTK thread. */ +typedef struct { + CN1A11yOp* ops; + int count; +} CN1A11yBatch; + +/* The newest publication not yet applied, and whether an idle is already queued + * to apply it. Guarded by cn1A11yPendingLock: the publishing thread stores, the + * GTK thread takes. */ +static GMutex cn1A11yPendingLock; +static CN1A11yBatch* cn1A11yPending = 0; +static int cn1A11yApplyQueued = 0; + +static void cn1A11yFreeBatch(CN1A11yBatch* batch) { + int i; + if (batch == 0) { + return; + } + for (i = 0; i < batch->count; i++) { + if (batch->ops[i].action) { + cn1A11yActionFree(batch->ops[i].data, 0); + } else { + CN1A11yNodeCall* call = (CN1A11yNodeCall*) batch->ops[i].data; + free(call->role); free(call->label); free(call->description); free(call->value); free(call); + } + } + free(batch->ops); + free(batch); } -static void cn1A11yEndMain(void* pointer) { - (void) pointer; +static gboolean cn1A11yApplyIdle(gpointer ignored) { + int i; + CN1A11yBatch* batch; + (void) ignored; + g_mutex_lock(&cn1A11yPendingLock); + batch = cn1A11yPending; + cn1A11yPending = 0; + cn1A11yApplyQueued = 0; + g_mutex_unlock(&cn1A11yPendingLock); + if (batch == 0) { + return G_SOURCE_REMOVE; + } + cn1A11yBeginMain(0); + for (i = 0; i < batch->count; i++) { + if (batch->ops[i].action) { + cn1A11yActionMain(batch->ops[i].data); + } else { + cn1A11yNodeMain(batch->ops[i].data); + } + } + /* The node and action payloads now belong to the widgets and signal + * closures they were applied to; only the array is ours. */ + free(batch->ops); + free(batch); if (cn1AccessibilityFixed) gtk_widget_show_all(cn1AccessibilityFixed); + return G_SOURCE_REMOVE; } +/* Applied on the GTK thread LATER, at low priority, instead of while this thread + * waits. Building the tree creates a real GTK widget per node -- toggle buttons, + * entries, scales -- which on the first form of the transpiled Flutter gallery + * was about 5% of the launch's CPU, all of it before the first frame, with the + * event dispatch thread blocked until it finished. Nothing reads the tree back + * synchronously, and assistive technology cannot tell a tree built a few + * milliseconds after the frame from one built before it. + * + * Every publication replaces the whole tree (cn1A11yBeginMain destroys what the + * previous one built), so a publication that is still waiting when a newer one + * arrives is simply superseded and freed: a burst of form changes costs one + * rebuild, not one per change. */ JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) { + CN1A11yBatch* batch; + CN1A11yBatch* superseded; + int queue; (void) changeType; - cn1LinuxRunOnMainAndWait(cn1A11yEndMain, 0); + (void) threadStateData; + batch = (CN1A11yBatch*) calloc(1, sizeof(CN1A11yBatch)); + if (batch == 0) { + return; + } + batch->ops = cn1A11yOps; + batch->count = cn1A11yOpCount; + cn1A11yOps = 0; + cn1A11yOpCount = 0; + cn1A11yOpCapacity = 0; + g_mutex_lock(&cn1A11yPendingLock); + superseded = cn1A11yPending; + cn1A11yPending = batch; + queue = !cn1A11yApplyQueued; + cn1A11yApplyQueued = 1; + g_mutex_unlock(&cn1A11yPendingLock); + cn1A11yFreeBatch(superseded); + if (queue) { + gdk_threads_add_idle_full(G_PRIORITY_LOW, cn1A11yApplyIdle, 0, 0); + } } JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pumpMessages___R_boolean(CODENAME_ONE_THREAD_STATE) { diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 6847c31ffe0..0872f5f9d09 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -219,9 +219,29 @@ private static byte[] readFully(InputStream i) throws IOException { */ private static LinuxImplementation mainLoopInstance; + /// The size the main window opens at, before the user or the window manager + /// changes it. Set by the generated bootstrap stub from the `desktop.width` and + /// `desktop.height` build hints, BEFORE Display.init: init creates the window, + /// and a resize afterwards would lay the first form out twice. + private static int defaultWindowWidth = 800; + private static int defaultWindowHeight = 600; + + /// Called by the generated bootstrap stub only; see defaultWindowWidth. + /// + /// @param width the window's initial content width in pixels; ignored unless positive + /// @param height the window's initial content height in pixels; ignored unless positive + public static void setDefaultWindowSize(int width, int height) { + if (width > 0) { + defaultWindowWidth = width; + } + if (height > 0) { + defaultWindowHeight = height; + } + } + @Override public void init(Object m) { - LinuxNative.initDisplay("Codename One", 800, 600); + LinuxNative.initDisplay("Codename One", defaultWindowWidth, defaultWindowHeight); windowGraphicsPeer = LinuxNative.getWindowGraphics(); windowGraphics = Long.valueOf(windowGraphicsPeer); defaultFont = Long.valueOf(LinuxNative.getDefaultFont()); @@ -230,7 +250,19 @@ public void init(Object m) { screenDpi = 96; } mainLoopInstance = this; - installNativeTheme(); + // Deferred, not installed here: an application that installs its own theme + // with setThemeProps -- every Flutter application, and any theme without + // @includeNativeBool -- resets the table and discards this one before + // anything reads it, and parsing and applying it was about 26ms of every + // launch of the transpiled Flutter gallery, all of it thrown away. It is + // still installed for an application with no theme of its own, the moment + // anything first reads the theme. See UIManager.setDeferredBaseTheme. + com.codename1.ui.plaf.UIManager.getInstance().setDeferredBaseTheme(new Runnable() { + @Override + public void run() { + installNativeTheme(); + } + }); } /** @@ -300,7 +332,8 @@ public boolean hasNativeTheme() { * iOSModernTheme.res. An app that loads its own theme layers over this. * Silently does nothing when the theme resource is absent. This is the * framework hook (UIManager calls it for {@code @includeNativeBool} themes); - * init() also calls it so an app with no theme of its own still gets it. + * init() defers it (UIManager.setDeferredBaseTheme) so an app with no + * theme of its own still gets it, without paying for it when the app installs one. */ private boolean installingNativeTheme; @@ -1931,6 +1964,25 @@ public void drawImage(Object graphics, Object img, int x, int y) { LinuxNative.drawImage(peer(graphics), peer(img), x, y); } + /// Cairo scales while it paints (drawImageScaled: the same cairo_scale and + /// default filter a pre-scaled copy would have been made with), so this port + /// can draw an image at any size directly. It used to answer the inherited + /// false, and Graphics.drawImage(img, x, y, w, h) then scaled the picture + /// first -- which for an EncodedImage means EncodedImage.scaled: decode, + /// scale, RE-ENCODE to PNG (or lossy JPEG for an opaque picture), decode + /// again. On every paint, since nothing kept the result: the transpiled + /// Flutter gallery ran 13 PNG encodes before its first frame, all from + /// scaled draws of its cards, icons and logo. + @Override + public boolean isScaledImageDrawingSupported() { + return true; + } + + @Override + public void drawImage(Object graphics, Object img, int x, int y, int w, int h) { + LinuxNative.drawImageScaled(peer(graphics), peer(img), x, y, w, h); + } + @Override public void drawRGB(Object graphics, int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { LinuxNative.drawRGB(peer(graphics), rgbData, offset, x, y, w, h, processAlpha); diff --git a/Ports/MacPort/nativeSources/CN1MacAppDelegate.m b/Ports/MacPort/nativeSources/CN1MacAppDelegate.m index 745c5dd708a..8cfb5080048 100644 --- a/Ports/MacPort/nativeSources/CN1MacAppDelegate.m +++ b/Ports/MacPort/nativeSources/CN1MacAppDelegate.m @@ -660,6 +660,10 @@ @implementation CN1MacAppDelegate // paint path reads, and it has to be right from the first frame. - (void)applicationDidFinishLaunching:(NSNotification *)notification { + // Normally a no-op: CN1MacBuildMainWindowBeforeRun released it before + // [NSApp run]. This is the backstop for a generated main that never calls + // that, so held menu commands cannot stay held for good. + CN1MacReleaseStartupMenu(); // Installed unconditionally, and this early. A scheduled notification is // delivered through this delegate whether it fires while the application is // frontmost or the user opens it from Notification Center, so without it a @@ -965,8 +969,80 @@ - (void)application:(NSApplication *)application @end -/// Installs the delegate and the menu bar. Called from the generated main, -/// before [NSApp run]. +/// Lets the event dispatch thread put its first frame on screen BEFORE [NSApp run]. +/// +/// [NSApp run] starts with finishLaunching, which is AppKit's own launch work -- +/// will/didFinishLaunching, state restoration, activation, the menu bar -- and it +/// runs synchronously on the main thread before the run loop first turns. Every +/// hop the event dispatch thread makes onto the main queue waits behind all of it, +/// and the first frame is such a hop (the flush in CN1MacViewController). Measured +/// on the GitHub macOS runner, per launch: the Java side had shown its first Form +/// at ~175 ms and the window existed at ~197 ms, but the main thread then spent +/// ~60-110 ms in finishLaunching and another ~50-80 ms in activation before it +/// served the flush, so the frame went out at ~330-560 ms. The EDT sat blocked in +/// dispatch_sync for that whole stretch (100-130 ms on a single geometry getter). +/// +/// So the run loop is turned here, in the default mode, until the first frame +/// has been presented. That services exactly what the EDT is waiting for -- the +/// main dispatch queue -- and nothing that needs the launch to be finished: input +/// events are not dequeued until NSApp does it, and the window was already +/// ordered front by the build above, so the frame lands in a visible window. +/// Anything the launch delivers later (activation, open-URL, notification +/// responses) still goes through the delegate, which already copes with either +/// order relative to the Java side being ready (see cn1_mac_runtime_markJavaReady). +/// +/// Bounded, because an application is free to show nothing for a long time: past +/// the deadline the launch simply proceeds as it did before, and the frame is +/// served by [NSApp run] like any other hop. The bound is short enough that a +/// slow first screen does not hold the menu bar and activation hostage, and long +/// enough to cover a normal first frame on a slow machine. +static void CN1MacServeFirstFrameBeforeRun(void) { + extern BOOL cn1MacFirstFramePresented; + CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + 0.75; + while (!cn1MacFirstFramePresented) { + CFTimeInterval left = deadline - CFAbsoluteTimeGetCurrent(); + if (left <= 0) { + break; + } + // returnAfterSourceHandled: come back after every serviced block, so the + // loop notices the frame as soon as the flush that presented it returns. + // A pool per turn, because nothing else provides one here: this runs from + // the generated main before [NSApp run], whose event loop is what normally + // drains the main thread's autoreleased objects -- and a frame makes many. + SInt32 result; + @autoreleasepool { + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, left, true); + } + if (result == kCFRunLoopRunFinished) { + // Nothing in the mode to wait on; never the case on the main run loop, + // which always services the main queue, but a spin is worse than leaving. + break; + } + } + cn1StartupPhase("firstFrameServedBeforeRun"); +} + +/// Builds the main window on the main thread before [NSApp run], called from the +/// generated main right after it has dispatched the VM boot. The queued build in +/// CN1MacInstallAppDelegate then finds the window already there, and stays only as +/// the fallback for a generated main that predates this function. +/// +/// Measured on the GitHub macOS runner (flutter-bench, transpiled gallery): the +/// queued build could not start until [NSApp run] had finished launching, ~264 ms +/// into the process, and the first frame waited on it. Built here it starts at +/// ~130 ms, overlapping the VM boot, and the Java side's first paint no longer +/// waits for a busy main thread (125 ms -> 7-32 ms). +void CN1MacBuildMainWindowBeforeRun(void) { + cn1StartupPhase("buildWindowBeforeRun"); + (void)[CN1MacHost sharedHost].renderingView; + CN1MacServeFirstFrameBeforeRun(); + // The menu bar and any commands held back while the frame was served. + CN1MacReleaseStartupMenu(); +} + +/// Installs the delegate. Called from the generated main, before [NSApp run]; the +/// menu bar is built later, once the first frame is out (see the generated main +/// and CN1MacReleaseStartupMenu). void CN1MacInstallAppDelegate(void) { static CN1MacAppDelegate *delegate = nil; if (delegate == nil) { @@ -986,6 +1062,10 @@ void CN1MacInstallAppDelegate(void) { // thread skip a dispatch_sync onto a main queue that is still busy. extern void CN1MacPublishPrimaryScale(void); CN1MacPublishPrimaryScale(); + // And the size the window will really open at, for the same reason: the first + // layout happens before the window exists. See CN1MacHost.m. + extern void CN1MacPublishExpectedContentSize(void); + CN1MacPublishExpectedContentSize(); // Build the window HERE, once, on this thread. // // It has to exist before the event dispatch thread first asks anything about diff --git a/Ports/MacPort/nativeSources/CN1MacChrome.m b/Ports/MacPort/nativeSources/CN1MacChrome.m index 63ad663b940..93e9288b450 100644 --- a/Ports/MacPort/nativeSources/CN1MacChrome.m +++ b/Ports/MacPort/nativeSources/CN1MacChrome.m @@ -204,7 +204,50 @@ static void cn1PruneEmptyGeneratedMenus(NSMenu *mainMenu) { #endif } +/// Commands published before the first frame, and whether that hold is over. +/// Main thread only, like everything that touches the menu. +static NSArray *cn1PendingStartupRows = nil; +static BOOL cn1StartupMenuReleased = NO; + +static void cn1ApplyMenuCommands(NSArray *rows); + void CN1MacHostSetMenuCommands(NSArray *rows) { + // Held until the first frame is on screen. A Form's commands are published + // as it is shown, which at launch is while the main thread is still serving + // the first frame before [NSApp run] -- and applying them builds the whole + // menu bar first (CN1MacInstallMainMenu), 10-20ms of main thread in front of + // the frame the user is waiting for on the benchmark runner. Nobody can open + // a menu before [NSApp run] takes events, so only the latest set is kept and + // CN1MacReleaseStartupMenu applies it once the frame is out. + if (!cn1StartupMenuReleased) { +#ifndef CN1_USE_ARC + [cn1PendingStartupRows release]; + cn1PendingStartupRows = [rows retain]; +#else + cn1PendingStartupRows = rows; +#endif + return; + } + cn1ApplyMenuCommands(rows); +} + +void CN1MacReleaseStartupMenu(void) { + if (cn1StartupMenuReleased) { + return; + } + cn1StartupMenuReleased = YES; + CN1MacInstallMainMenu(); + if (cn1PendingStartupRows != nil) { + NSArray *rows = cn1PendingStartupRows; + cn1PendingStartupRows = nil; + cn1ApplyMenuCommands(rows); +#ifndef CN1_USE_ARC + [rows release]; +#endif + } +} + +static void cn1ApplyMenuCommands(NSArray *rows) { #ifndef CN1_USE_ARC [cn1MenuRows release]; cn1MenuRows = [rows retain]; diff --git a/Ports/MacPort/nativeSources/CN1MacHost.m b/Ports/MacPort/nativeSources/CN1MacHost.m index 931e94b8a81..050539ea722 100644 --- a/Ports/MacPort/nativeSources/CN1MacHost.m +++ b/Ports/MacPort/nativeSources/CN1MacHost.m @@ -111,6 +111,87 @@ void CN1MacRefreshScaleValue(void) { } } +/// The content size the main window will open at, in points, published once on +/// the main thread before the application's main runs. 0 until then. +/// +/// The window is created at the default 1024x685, but AppKit fits a window to the +/// screen's visible frame when it is shown -- and on a small display, the menu bar +/// and the Dock leave less than that. The GitHub macOS runner opens it at 1024x642. +/// The size query answered 685 until the window existed and screenSizeChanged then +/// said 642, so every launch there built and laid the tree out twice and settled +/// 14MB heavier. Fitting the size up front, and building the window at it, makes +/// the first answer the final one. +static int cn1MacExpectedContentWidth = 0; +static int cn1MacExpectedContentHeight = 0; + +static NSWindowStyleMask cn1MacMainWindowStyle(void) { + return NSWindowStyleMaskTitled | NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; +} + +/// Main thread only; see cn1MacExpectedContentWidth. +void CN1MacPublishExpectedContentSize(void) { + if (![NSThread isMainThread]) { + return; + } + CGFloat w = CN1_MAC_DEFAULT_WIDTH; + CGFloat h = CN1_MAC_DEFAULT_HEIGHT; + // The size the window will be given, not the size it is built with. Two things + // override the default after construction: macos.fixedWindowSize (the + // CN1FixedWindow* keys, applied by setFixedContentSize) and the frame AppKit + // restores from setFrameAutosaveName -- the size the user, or the previous + // launch, left it at. The benchmark runner hit the second one: the first launch + // fitted correctly, and every later launch restored the saved frame and laid the + // tree out twice again. + NSNumber *fixedW = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CN1FixedWindowWidth"]; + NSNumber *fixedH = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CN1FixedWindowHeight"]; + if (fixedW != nil && fixedH != nil && fixedW.doubleValue > 0 && fixedH.doubleValue > 0) { + w = fixedW.doubleValue; + h = fixedH.doubleValue; + } else { + NSString *saved = [[NSUserDefaults standardUserDefaults] + stringForKey:@"NSWindow Frame CN1MainWindow"]; + NSArray *parts = [saved componentsSeparatedByCharactersInSet: + [NSCharacterSet whitespaceCharacterSet]]; + if (parts.count >= 4) { + NSRect savedFrame = NSMakeRect(parts[0].doubleValue, parts[1].doubleValue, + parts[2].doubleValue, parts[3].doubleValue); + NSRect savedContent = [NSWindow contentRectForFrameRect:savedFrame + styleMask:cn1MacMainWindowStyle()]; + if (savedContent.size.width > 0 && savedContent.size.height > 0) { + w = savedContent.size.width; + h = savedContent.size.height; + } + } + } + NSArray *screens = [NSScreen screens]; + if (screens.count > 0) { + NSRect visible = [[screens objectAtIndex:0] visibleFrame]; + NSRect content = NSMakeRect(0, 0, w, h); + NSRect frame = [NSWindow frameRectForContentRect:content styleMask:cn1MacMainWindowStyle()]; + CGFloat chromeW = frame.size.width - content.size.width; + CGFloat chromeH = frame.size.height - content.size.height; + if (visible.size.width - chromeW < w) { + w = floor(visible.size.width - chromeW); + } + if (visible.size.height - chromeH < h) { + h = floor(visible.size.height - chromeH); + } + } + __atomic_store_n(&cn1MacExpectedContentWidth, (int)w, __ATOMIC_RELEASE); + __atomic_store_n(&cn1MacExpectedContentHeight, (int)h, __ATOMIC_RELEASE); +} + +static CGFloat cn1MacContentWidthPoints(void) { + int w = __atomic_load_n(&cn1MacExpectedContentWidth, __ATOMIC_ACQUIRE); + return w > 0 ? w : CN1_MAC_DEFAULT_WIDTH; +} + +static CGFloat cn1MacContentHeightPoints(void) { + int h = __atomic_load_n(&cn1MacExpectedContentHeight, __ATOMIC_ACQUIRE); + return h > 0 ? h : CN1_MAC_DEFAULT_HEIGHT; +} + @implementation CN1MacHost { NSWindow *_window; METALView *_renderingView; @@ -170,17 +251,27 @@ - (void)buildWindow { return; } - NSRect frame = NSMakeRect(0, 0, CN1_MAC_DEFAULT_WIDTH, CN1_MAC_DEFAULT_HEIGHT); - NSWindowStyleMask style = NSWindowStyleMaskTitled - | NSWindowStyleMaskClosable - | NSWindowStyleMaskMiniaturizable - | NSWindowStyleMaskResizable; + cn1StartupPhase("buildWindow.enter"); + NSRect frame = NSMakeRect(0, 0, cn1MacContentWidthPoints(), cn1MacContentHeightPoints()); + NSWindowStyleMask style = cn1MacMainWindowStyle(); // CN1MacWindow, not NSWindow: a modal dialog usually blocks THIS window, and // blocking it has to include refusing key focus. See CN1AppKitWindows.h. _window = [[CN1MacWindow alloc] initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:NO]; + // Published HERE, the instant builtWindow stops being nil, rather than from + // the block CN1MacInstallAppDelegate queues for later. In between, + // macMonitorForMainWindow found a built window with nothing published and + // marshalled onto this thread to ask it -- which, at start-up, is busy + // finishing this very build and then launching the application. Measured on + // the benchmark runner: the event dispatch thread sat 60-70ms in that hop, + // inside UIManager's constructor, on the launches where its first + // convertToPixels landed after the window existed. The window has no screen + // yet, so this publishes the primary one -- the same answer the hop gave -- + // and the republish below corrects it once the window is placed. + extern void CN1MacPublishMainWindowScreen(void); + CN1MacPublishMainWindowScreen(); ((CN1MacWindow *)_window).cn1AcceptsKey = YES; // Remembers where the user left it between launches. One line, and its // absence is the kind of thing that makes an app feel unfinished. @@ -260,6 +351,27 @@ - (void)buildWindow { // this process in front. Without this the app launches, runs and draws -- // behind whatever the user was already looking at. [NSApp activateIgnoringOtherApps:YES]; + // On screen now, so the real screen rather than the primary stand-in. + CN1MacPublishMainWindowScreen(); + cn1StartupPhase("buildWindow.exit"); +} + +/// The scale to answer with before the window exists. +/// +/// The default size is in POINTS -- it is the content rectangle the window is +/// built with -- and Codename One lays out in device pixels. Answering it +/// unscaled told a Retina launch its screen was 1024x685 until the window +/// arrived and screenSizeChanged said 2048x1370, so the first layout of every +/// launch was done at half size and then thrown away: the whole tree was built, +/// laid out and rebuilt inside the first frame. The primary screen's scale is +/// already published before the application's main runs (see +/// CN1MacPublishPrimaryScale), so the answer can be in pixels without touching +/// AppKit off the main thread. 0 from the reader means "not safe to guess" -- a +/// mixed-scale desktop -- and then this stays at 1, as it always did. +static CGFloat cn1MacDefaultScale(void) { + extern int cn1MacPublishedScaleTimes100(void); + int s100 = cn1MacPublishedScaleTimes100(); + return s100 > 0 ? s100 / 100.0 : 1.0; } /// Answering a size query must not WAIT for the window either. @@ -275,7 +387,7 @@ - (void)buildWindow { - (int)displayWidth { NSView *v = _renderingView; if (v == nil) { - return (int)CN1_MAC_DEFAULT_WIDTH; + return (int)(cn1MacContentWidthPoints() * cn1MacDefaultScale()); } // Device pixels, not points: Codename One lays out in pixels and a Retina // display has two of them per point. @@ -285,7 +397,7 @@ - (int)displayWidth { - (int)displayHeight { NSView *v = _renderingView; if (v == nil) { - return (int)CN1_MAC_DEFAULT_HEIGHT; + return (int)(cn1MacContentHeightPoints() * cn1MacDefaultScale()); } return (int)(v.bounds.size.height * CN1AppKitBackingScale(v)); } diff --git a/Ports/MacPort/nativeSources/CN1MacMenu.h b/Ports/MacPort/nativeSources/CN1MacMenu.h index 63c1dc15d4b..269a784bd61 100644 --- a/Ports/MacPort/nativeSources/CN1MacMenu.h +++ b/Ports/MacPort/nativeSources/CN1MacMenu.h @@ -31,7 +31,8 @@ extern "C" { /// Builds the application's menu bar. /// -/// Called from the generated `main` before the run loop starts, because an app +/// Called from the generated `main` before the run loop starts -- after the main +/// window and its first frame, which is all the user is waiting for -- because an app /// with no menu bar has no Quit item, and an app with no Quit item does not pass /// App Store review and cannot be closed from the keyboard. /// @@ -41,8 +42,13 @@ extern "C" { /// XIBs. void CN1MacInstallMainMenu(void); -/// Installs the NSApplicationDelegate. Called from the generated main alongside -/// the menu bar, before [NSApp run]. +/// Lets the menu bar be built, and applies any commands the application +/// published before it could be. Main thread only; idempotent. See +/// CN1MacHostSetMenuCommands for why commands are held back until this runs. +void CN1MacReleaseStartupMenu(void); + +/// Installs the NSApplicationDelegate. Called from the generated main before +/// [NSApp run], ahead of the window and the menu bar. void CN1MacInstallAppDelegate(void); #ifdef __cplusplus diff --git a/Ports/MacPort/nativeSources/CN1MacMenu.m b/Ports/MacPort/nativeSources/CN1MacMenu.m index ca9c28b077d..fb4a2ced794 100644 --- a/Ports/MacPort/nativeSources/CN1MacMenu.m +++ b/Ports/MacPort/nativeSources/CN1MacMenu.m @@ -34,7 +34,21 @@ return name != nil ? name : @"Application"; } +/// Set once the menu bar exists; see CN1MacInstallMainMenu. Main thread only. +static BOOL cn1MacMainMenuInstalled = NO; + void CN1MacInstallMainMenu(void) { + // Idempotent, because it now has two callers that can run in either order. + // The generated main installs the menu only after the first frame (see the + // comment there), and by then the application may already have published + // its own commands -- CN1MacHostSetMenuCommands installs the menu itself + // when it finds none and appends to it. Building a second menu over that one + // would silently drop every command the application had added. + if (cn1MacMainMenuInstalled) { + return; + } + cn1MacMainMenuInstalled = YES; + cn1StartupPhase("installMainMenu.enter"); NSString *appName = CN1MacAppName(); NSMenu *mainMenu = [[NSMenu alloc] initWithTitle:@""]; @@ -112,6 +126,16 @@ void CN1MacInstallMainMenu(void) { [NSApp setHelpMenu:helpMenu]; [NSApp setMainMenu:mainMenu]; + + // The menu can be installed after the main window is already on screen, and + // a Window menu handed over late does not necessarily list the windows that + // existed before it. changeWindowsItem adds the entry when there is none and + // only retitles one that is already there, so this is safe either way. + for (NSWindow *w in [NSApp windows]) { + if (w.isVisible && !w.isExcludedFromWindowsMenu) { + [NSApp changeWindowsItem:w title:(w.title != nil ? w.title : @"") filename:NO]; + } + } } #endif diff --git a/Ports/MacPort/nativeSources/CN1MacViewController.m b/Ports/MacPort/nativeSources/CN1MacViewController.m index a1500771b8e..e9c23bed706 100644 --- a/Ports/MacPort/nativeSources/CN1MacViewController.m +++ b/Ports/MacPort/nativeSources/CN1MacViewController.m @@ -52,6 +52,9 @@ /// Defined here because on the UIKit ports it lives in the app delegate, which /// this port replaces. BOOL isAppSuspended = NO; +/// Set on the main thread once the first frame has been handed to the GPU. Read +/// only by CN1MacBuildMainWindowBeforeRun, on the main thread as well. +BOOL cn1MacFirstFramePresented = NO; int mallocWhileSuspended = 0; static CodenameOne_GLViewController *singletonInstance = nil; @@ -199,6 +202,8 @@ - (void)drawFrame:(CGRect)rect { } - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive { + static int firstDraw = 1; + if (firstDraw) { firstDraw = 0; cn1StartupPhase("firstDrawFrame"); } METALView *v = (METALView *)[CN1MacHost sharedHost].activeRenderingView; if (v == nil) { return; @@ -281,6 +286,7 @@ - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive { [DrawStringTextureCache flushDeleted]; [v presentFramebuffer]; painted = YES; + cn1MacFirstFramePresented = YES; #ifndef CN1_USE_ARC [ops release]; #endif diff --git a/Ports/MacPort/nativeSources/METALView.m b/Ports/MacPort/nativeSources/METALView.m index c2f85f0ac7e..fc919b57904 100644 --- a/Ports/MacPort/nativeSources/METALView.m +++ b/Ports/MacPort/nativeSources/METALView.m @@ -29,6 +29,7 @@ #import "CN1Metalcompat.h" #include #import +#include /// Three, not two. The window server is not asked when it has finished reading a /// surface -- there is no such callback -- so the only safe surface to draw into @@ -199,6 +200,50 @@ @implementation METALView { /// surface under the layer is never a legal target no matter who owns it, /// and it stops being current the moment a later frame replaces it. _Atomic int cn1PresentCurrent; + + /// Whether each surface is currently marked purgeable (volatile). + /// + /// Memory at rest. Three full-window surfaces exist so a frame always has one + /// the window server is not reading, but once the application is idle only + /// the surface under the layer is ever looked at again. The other two stay + /// allocated, and each is a whole window of BGRA -- two thirds of what this + /// path holds, counted in the process footprint for as long as the window is + /// open. A volatile surface is not counted, and the kernel may reclaim its + /// pages under pressure. Nothing is lost if it does: every frame blits the + /// entire framebuffer into the surface it picks, so a surface's previous + /// contents are never read. A surface is made non-volatile again before that + /// blit, under cn1PresentLock, so it cannot be purged while it is written. + BOOL cn1PresentVolatile[CN1_PRESENT_SURFACE_COUNT]; + + /// Guards cn1PresentVolatile and the choice of a surface against the idle + /// sweep that marks surfaces volatile (cn1RetireIdleSurfaces, on the main + /// queue) and against the rebuild that releases them. Without it the sweep + /// could mark a surface volatile between the render thread choosing it and + /// the blit, or touch a surface the rebuild has just released. Native-side + /// state only; held for a handful of flag reads and at most three + /// IOSurfaceSetPurgeable calls. + os_unfair_lock cn1PresentLock; + + /// Consecutive sweeps that found a released surface still held by the window + /// server. Bounds the retries, so a surface that stays in use (it should not) + /// cannot keep a timer alive forever. + int cn1PresentSweepRetries; + + /// Whether screenTexture is currently marked purgeable, and the frame counter + /// that decides when it may be. Both guarded by cn1PresentLock. + /// + /// At rest the persistent target duplicates the surface under the layer: every + /// presented frame blits all of it there. So once a frame's GPU work completes + /// and no newer frame has started, the texture is marked volatile and stops + /// counting in the footprint. The next user of it -- a frame, or a readback -- + /// makes it non-volatile first (cn1ScreenTextureResidentFor:), and if the kernel + /// did reclaim the pages, copies the surface under the layer back into it + /// before anything reads it. The counter is what keeps a late completion from + /// marking the texture volatile under a frame that has already begun. + BOOL cn1ScreenVolatile; + unsigned long long cn1ScreenFrameSeq; + /// The counter value of the frame currently being encoded; render thread only. + unsigned long long cn1OpenFrameSeq; } @synthesize commandQueue; @@ -521,7 +566,12 @@ - (void)applyPendingFrameBufferSize { self.layer.contentsScale = CN1AppKitBackingScale(self); // The surface the window server reads. Replaced rather than resized: an - // IOSurface is fixed at its creation size. + // IOSurface is fixed at its creation size. Under cn1PresentLock, so the idle + // sweep never touches a surface while it is being released or replaced. + os_unfair_lock_lock(&cn1PresentLock); + for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { + cn1PresentVolatile[i] = NO; + } for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { if (cn1PresentTextures[i] != nil) { [cn1PresentTextures[i] release]; @@ -578,6 +628,7 @@ - (void)applyPendingFrameBufferSize { [CATransaction commit]; atomic_store_explicit(&cn1PresentCurrent, 0, memory_order_release); } + os_unfair_lock_unlock(&cn1PresentLock); // The persistent target. Codename One only queues the operations that // changed since the previous frame, so it has to survive between them; a @@ -588,7 +639,10 @@ - (void)applyPendingFrameBufferSize { desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; desc.storageMode = MTLStorageModePrivate; id newScreen = [CN1MetalDevice() newTextureWithDescriptor:desc]; + os_unfair_lock_lock(&cn1PresentLock); self.screenTexture = newScreen; + cn1ScreenVolatile = NO; + os_unfair_lock_unlock(&cn1PresentLock); #ifndef CN1_USE_ARC [newScreen release]; #endif @@ -614,6 +668,15 @@ - (void)applyPendingFrameBufferSize { width:pw height:ph mipmapped:NO]; stencilDesc.usage = MTLTextureUsageRenderTarget; stencilDesc.storageMode = MTLStorageModePrivate; + // Memoryless where the GPU can: the stencil is cleared at the start of every + // pass and never stored (createRenderPassDescriptor), so on an Apple GPU it can + // live in tile memory alone and needs no allocation -- a full window of + // Stencil8 less at rest. Private elsewhere, where memoryless does not exist. + if (@available(macOS 11.0, *)) { + if ([CN1MetalDevice() supportsFamily:MTLGPUFamilyApple1]) { + stencilDesc.storageMode = MTLStorageModeMemoryless; + } + } id newStencil = [CN1MetalDevice() newTextureWithDescriptor:stencilDesc]; self.stencilTexture = newStencil; #ifndef CN1_USE_ARC @@ -713,6 +776,57 @@ - (void)createRenderPassDescriptor { } } +/// Makes screenTexture safe to read and draw into for work encoded on {@code cb}, +/// restoring its contents if the kernel reclaimed them while it was volatile +/// (see cn1ScreenVolatile). Render thread, or readback, before anything touches the +/// texture on {@code cb}. Returns the frame counter value this use owns. +- (unsigned long long)cn1ScreenTextureResidentFor:(id)cb { + BOOL lost = NO; + id source = nil; + id target = nil; + os_unfair_lock_lock(&cn1PresentLock); + unsigned long long seq = ++cn1ScreenFrameSeq; + if (cn1ScreenVolatile && self.screenTexture != nil) { + MTLPurgeableState old = [self.screenTexture setPurgeableState:MTLPurgeableStateNonVolatile]; + cn1ScreenVolatile = NO; + if (old == MTLPurgeableStateEmpty) { + lost = YES; + target = self.screenTexture; + // The surface under the layer holds exactly what the texture held: the + // last presented frame copied all of it there. It is never volatile. + int current = atomic_load_explicit(&cn1PresentCurrent, memory_order_acquire); + if (current >= 0 && current < CN1_PRESENT_SURFACE_COUNT) { + source = cn1PresentTextures[current]; + } +#ifndef CN1_USE_ARC + [target retain]; + [source retain]; +#endif + } + } + os_unfair_lock_unlock(&cn1PresentLock); + if (lost) { + if (source != nil && cb != nil + && source.width == target.width && source.height == target.height) { + id restore = [cb blitCommandEncoder]; + [restore copyFromTexture:source sourceSlice:0 sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake(target.width, target.height, 1) + toTexture:target destinationSlice:0 destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [restore endEncoding]; + } else { + // Nothing to copy from: clear, and let the next full repaint fill it. + [self invalidateRetainedFramebuffer]; + } +#ifndef CN1_USE_ARC + [target release]; + [source release]; +#endif + } + return seq; +} + - (void)setFramebuffer { // Tolerates being called more than once per frame, as the UIKit backend // does: creating a second encoder would discard everything queued against @@ -724,6 +838,8 @@ - (void)setFramebuffer { // no encoder exists yet, so nothing can be encoding into what is replaced. [self applyPendingFrameBufferSize]; self.commandBuffer = [self.commandQueue commandBuffer]; + // Before the render pass loads it; see cn1ScreenVolatile. + cn1OpenFrameSeq = [self cn1ScreenTextureResidentFor:self.commandBuffer]; [self createRenderPassDescriptor]; if (self.renderPassDescriptor == nil) { self.renderCommandEncoder = nil; @@ -740,6 +856,39 @@ - (void)setFramebuffer { framebufferWidth, framebufferHeight); } +/// Marks every surface nobody is reading volatile, so an idle window counts one +/// surface in the footprint rather than three (see cn1PresentVolatile). +/// +/// Main queue only. A surface is left alone while it is under the layer, while a +/// blit into it is in flight, or while the window server still holds it; the +/// last of those clears on its own a composite later, so the sweep retries a +/// bounded number of times instead of leaving that surface resident. +- (void)cn1RetireIdleSurfaces { + BOOL held = NO; + os_unfair_lock_lock(&cn1PresentLock); + int current = atomic_load_explicit(&cn1PresentCurrent, memory_order_acquire); + for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { + if (i == current || cn1PresentSurfaces[i] == NULL || cn1PresentVolatile[i] + || atomic_load_explicit(&cn1PresentInFlight[i], memory_order_acquire)) { + continue; + } + if (IOSurfaceIsInUse(cn1PresentSurfaces[i])) { + held = YES; + continue; + } + IOSurfaceSetPurgeable(cn1PresentSurfaces[i], kIOSurfacePurgeableVolatile, NULL); + cn1PresentVolatile[i] = YES; + } + os_unfair_lock_unlock(&cn1PresentLock); + if (held && cn1PresentSweepRetries++ < 40) { + METALView *sweepView = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 20)), + dispatch_get_main_queue(), ^{ + [sweepView cn1RetireIdleSurfaces]; + }); + } +} + /// Asks the framework for another frame, if one is owed. /// /// Clears the owed flag first and only acts if it was set, so the two callers -- @@ -832,6 +981,9 @@ - (BOOL)presentFramebuffer { // The search starts at the slot after the one used last, so a surface is not // immediately reused while its own completion handler is still pending. int presentIdx = -1; + // Choosing, reserving and making the surface non-volatile happen together + // under the lock: see cn1PresentVolatile. + os_unfair_lock_lock(&cn1PresentLock); for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { int candidate = (cn1PresentIndex + i) % CN1_PRESENT_SURFACE_COUNT; if (cn1PresentSurfaces[candidate] != NULL @@ -847,6 +999,19 @@ - (BOOL)presentFramebuffer { break; } } + if (presentIdx >= 0) { + // Reserved here rather than after the blit is encoded, so the sweep cannot + // see it as idle in between. + atomic_store_explicit(&cn1PresentInFlight[presentIdx], 1, memory_order_release); + if (cn1PresentVolatile[presentIdx]) { + // Whatever the kernel did with the old pages does not matter: the blit + // below overwrites the whole surface. + IOSurfaceSetPurgeable(cn1PresentSurfaces[presentIdx], + kIOSurfacePurgeableNonVolatile, NULL); + cn1PresentVolatile[presentIdx] = NO; + } + } + os_unfair_lock_unlock(&cn1PresentLock); if (presentIdx < 0) { // Every surface is still held. Overwriting one anyway would undo the // check just made, so drop the frame instead: the command buffer is @@ -869,6 +1034,7 @@ - (BOOL)presentFramebuffer { // Selection excludes these now, so this is belt and braces -- but it // gives up the same way everything else does rather than losing the // frame, which is what it used to do. + atomic_store_explicit(&cn1PresentInFlight[presentIdx], 0, memory_order_release); return [self cn1DeferFrameAndCommit]; } id blit = [self.commandBuffer blitCommandEncoder]; @@ -909,6 +1075,7 @@ - (BOOL)presentFramebuffer { int frameGeneration = atomic_load_explicit(&cn1PresentGeneration, memory_order_acquire); METALView *presentView = self; const int completedIdx = presentIdx; + const unsigned long long frameSeq = cn1OpenFrameSeq; [self.commandBuffer addCompletedHandler:^(id cb) { // One load for both decisions below, so they cannot disagree. int currentGeneration = atomic_load_explicit(&presentView->cn1PresentGeneration, @@ -949,6 +1116,16 @@ - (BOOL)presentFramebuffer { if (currentGeneration == frameGeneration) { atomic_store_explicit(&presentView->cn1PresentInFlight[completedIdx], 0, memory_order_release); + // The persistent target duplicates what this frame just put under the + // layer. Unless a newer frame (or a readback) has started using it since, + // it may stop counting in the footprint; see cn1ScreenVolatile. + os_unfair_lock_lock(&presentView->cn1PresentLock); + if (presentView->cn1ScreenFrameSeq == frameSeq && !presentView->cn1ScreenVolatile + && presentView.screenTexture != nil) { + [presentView.screenTexture setPurgeableState:MTLPurgeableStateVolatile]; + presentView->cn1ScreenVolatile = YES; + } + os_unfair_lock_unlock(&presentView->cn1PresentLock); // A surface just became free. If a frame was dropped because none // was, ask the framework to paint again -- the content of that frame // is still in screenTexture, but only a frame started on the thread @@ -959,6 +1136,14 @@ - (BOOL)presentFramebuffer { dispatch_async(dispatch_get_main_queue(), ^{ [presentView cn1PayDeferredPresent]; }); + // The surface this replaced is idle now, or will be once the window + // server lets go of it. Shortly after, not immediately: the server + // still holds it until its next composite. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 20)), + dispatch_get_main_queue(), ^{ + presentView->cn1PresentSweepRetries = 0; + [presentView cn1RetireIdleSurfaces]; + }); } CFRelease(presented); }]; @@ -1003,8 +1188,15 @@ - (void)dealloc { #endif - (void)deleteFramebuffer { + os_unfair_lock_lock(&cn1PresentLock); self.screenTexture = nil; + cn1ScreenVolatile = NO; + os_unfair_lock_unlock(&cn1PresentLock); self.stencilTexture = nil; + os_unfair_lock_lock(&cn1PresentLock); + for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { + cn1PresentVolatile[i] = NO; + } for (int i = 0; i < CN1_PRESENT_SURFACE_COUNT; i++) { if (cn1PresentTextures[i] != nil) { [cn1PresentTextures[i] release]; @@ -1026,6 +1218,7 @@ - (void)deleteFramebuffer { atomic_store_explicit(&cn1PresentDeferred, 0, memory_order_release); // Nothing is under the layer any more either; the surface it named is gone. atomic_store_explicit(&cn1PresentCurrent, -1, memory_order_release); + os_unfair_lock_unlock(&cn1PresentLock); framebufferWidth = 0; framebufferHeight = 0; } @@ -1084,6 +1277,8 @@ - (BOOL)readbackInto:(unsigned int *)argb width:(int)w height:(int)h { return NO; } id cb = [self.commandQueue commandBuffer]; + // The texture may be volatile between frames; see cn1ScreenVolatile. + [self cn1ScreenTextureResidentFor:cb]; id blit = [cb blitCommandEncoder]; [blit copyFromTexture:self.screenTexture sourceSlice:0 sourceLevel:0 diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index bbfde989e0e..c58721544c5 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -221,9 +221,29 @@ private static byte[] readFully(InputStream i) throws IOException { */ private static WindowsImplementation mainLoopInstance; + /// The size the main window opens at, before the user or the window manager + /// changes it. Set by the generated bootstrap stub from the `desktop.width` and + /// `desktop.height` build hints, BEFORE Display.init: init creates the window, + /// and a resize afterwards would lay the first form out twice. + private static int defaultWindowWidth = 800; + private static int defaultWindowHeight = 600; + + /// Called by the generated bootstrap stub only; see defaultWindowWidth. + /// + /// @param width the window's initial content width in pixels; ignored unless positive + /// @param height the window's initial content height in pixels; ignored unless positive + public static void setDefaultWindowSize(int width, int height) { + if (width > 0) { + defaultWindowWidth = width; + } + if (height > 0) { + defaultWindowHeight = height; + } + } + @Override public void init(Object m) { - WindowsNative.initDisplay("Codename One", 800, 600); + WindowsNative.initDisplay("Codename One", defaultWindowWidth, defaultWindowHeight); windowGraphicsPeer = WindowsNative.getWindowGraphics(); windowGraphics = Long.valueOf(windowGraphicsPeer); defaultFont = Long.valueOf(WindowsNative.getDefaultFont()); @@ -232,7 +252,19 @@ public void init(Object m) { screenDpi = 96; } mainLoopInstance = this; - installNativeTheme(); + // Deferred, not installed here: an application that installs its own theme + // with setThemeProps -- every Flutter application, and any theme without + // @includeNativeBool -- resets the table and discards this one before + // anything reads it, and parsing and applying it was about 26ms of every + // launch of the transpiled Flutter gallery, all of it thrown away. It is + // still installed for an application with no theme of its own, the moment + // anything first reads the theme. See UIManager.setDeferredBaseTheme. + com.codename1.ui.plaf.UIManager.getInstance().setDeferredBaseTheme(new Runnable() { + @Override + public void run() { + installNativeTheme(); + } + }); } /** @@ -302,7 +334,8 @@ public boolean hasNativeTheme() { * iOSModernTheme.res. An app that loads its own theme layers over this. * Silently does nothing when the theme resource is absent. This is the * framework hook (UIManager calls it for {@code @includeNativeBool} themes); - * init() also calls it so an app with no theme of its own still gets it. + * init() defers it (UIManager.setDeferredBaseTheme) so an app with no + * theme of its own still gets it, without paying for it when the app installs one. */ private boolean installingNativeTheme; @@ -1930,6 +1963,29 @@ public void drawImage(Object graphics, Object img, int x, int y) { WindowsNative.drawImage(peer(graphics), peer(img), x, y); } + /// Direct2D scales while it draws (drawImageScaled: the same bitmap, clip and + /// linear interpolation as the unscaled draw, into a w x h rectangle), so this + /// port can draw an image at any size directly. It used to answer the + /// inherited false, and Graphics.drawImage(img, x, y, w, h) then scaled the + /// picture first -- which for an EncodedImage means EncodedImage.scaled: + /// decode, scale, RE-ENCODE to PNG (or lossy JPEG for an opaque picture), + /// decode again, on every paint. The Linux port had the identical gap; the + /// transpiled Flutter gallery ran 13 PNG encodes before its first frame there. + @Override + public boolean isScaledImageDrawingSupported() { + return true; + } + + @Override + public void drawImage(Object graphics, Object img, int x, int y, int w, int h) { + // The native builds its destination as x..x+w, y..y+h and has no guard of + // its own; a degenerate or inverted rectangle has nothing to draw. + if (w <= 0 || h <= 0) { + return; + } + WindowsNative.drawImageScaled(peer(graphics), peer(img), x, y, w, h); + } + @Override public void drawRGB(Object graphics, int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { WindowsNative.drawRGB(peer(graphics), rgbData, offset, x, y, w, h, processAlpha); diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/de.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/de.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/de.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/de.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/en.lproj/InfoPlist.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/en.lproj/InfoPlist.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/en.lproj/InfoPlist.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/en.lproj/InfoPlist.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/en.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/en.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/en.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/en.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/es.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/es.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/es.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/es.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/fr.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/fr.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/fr.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/fr.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/he.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/he.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/he.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/he.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/hu.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/hu.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/hu.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/hu.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/it.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/it.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/it.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/it.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/pt.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/pt.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/pt.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/pt.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/zoozi18n.bundle/tr.lproj/Localizable.strings b/Ports/iOSPort/Zooz/zoozi18n.bundle/tr.lproj/Localizable.strings similarity index 100% rename from Ports/iOSPort/nativeSources/zoozi18n.bundle/tr.lproj/Localizable.strings rename to Ports/iOSPort/Zooz/zoozi18n.bundle/tr.lproj/Localizable.strings diff --git a/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m index f934a3e77f0..ded25c8f63e 100644 --- a/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m +++ b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m @@ -394,7 +394,27 @@ - (CN1MetalGlyphSlot *)slotForGlyph:(CGGlyph)glyph { CGPoint origin = CGPointMake((CGFloat)CN1_METAL_ATLAS_PADDING - bbox.origin.x, (CGFloat)CN1_METAL_ATLAS_PADDING - bbox.origin.y); - CTFontDrawGlyphs(_ctFont, &glyph, &origin, 1, ctx); + // Monochrome glyphs go through the CGFont path rather than CTFontDrawGlyphs. + // CTFontDrawGlyphs checks every draw for glyphs that overlap in a connected + // script, and the first time it does that for a font it loads the font's + // design-language metadata: 10-13ms on the main thread inside the first frame + // of a macOS launch, measured on every run, for text that has no connected + // script in it. CGContextShowGlyphsAtPositions rasterises through the same + // renderer without the check -- byte-identical coverage across 27 fonts and + // ~3.8M pixels (system weights and italics, Helvetica, Times, Menlo, the + // Material icon font, six Google fonts), and the first frame 12ms sooner. + // A font with its own matrix (a synthetic oblique) is the one case that did + // not match, because the matrix then also moves the glyph origin, so it + // keeps CTFontDrawGlyphs, as do colour glyphs. + if (!_isColor && CGAffineTransformIsIdentity(CTFontGetMatrix(_ctFont))) { + CGFontRef cgFont = CTFontCopyGraphicsFont(_ctFont, NULL); + CGContextSetFont(ctx, cgFont); + CGContextSetFontSize(ctx, CTFontGetSize(_ctFont)); + CGContextShowGlyphsAtPositions(ctx, &glyph, &origin, 1); + CGFontRelease(cgFont); + } else { + CTFontDrawGlyphs(_ctFont, &glyph, &origin, 1, ctx); + } CGContextRelease(ctx); [_texture replaceRegion:MTLRegionMake2D((NSUInteger)slotX, (NSUInteger)slotY, diff --git a/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m index 8ba41398bd0..982e578cad4 100644 --- a/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m +++ b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m @@ -30,6 +30,10 @@ @interface CN1MetalPipelineCache () { id _device; id _states[CN1MetalPipelineCount]; + /// The app's default.metallib, loaded once. newDefaultLibrary opens and + /// parses the library file on every call, and each pipeline the first frame + /// needed used to call it again -- and, without ARC, leaked the result. + id _library; } @end @@ -207,7 +211,10 @@ static void configureStencilWriteOnly(MTLRenderPipelineColorAttachmentDescriptor if (pipeline < 0 || pipeline >= CN1MetalPipelineCount) return nil; if (_states[pipeline] != nil) return _states[pipeline]; - id library = [_device newDefaultLibrary]; + if (_library == nil) { + _library = [_device newDefaultLibrary]; + } + id library = _library; if (library == nil) { NSLog(@"CN1MetalPipelineCache: device has no default.metallib — is CN1MetalShaders.metal in the Xcode project?"); return nil; diff --git a/Ports/iOSPort/nativeSources/CN1MetalShaders.metal b/Ports/iOSPort/nativeSources/CN1MetalShaders.metal index 6601346ca93..64f38a0b485 100644 --- a/Ports/iOSPort/nativeSources/CN1MetalShaders.metal +++ b/Ports/iOSPort/nativeSources/CN1MetalShaders.metal @@ -121,6 +121,11 @@ fragment float4 cn1_fs_textured( // the bitmap first. // // params.xy is the destination size in pixels and params.z the corner radius. +// region is the part of the texture the quad shows -- (u0, v0, du, dv) in the +// same normalised coordinates as texcoord -- so a picture scaled to OVERFLOW its +// box (BoxFit.cover) can be drawn as just the box, with the box's corners +// rounded. The corners are computed over the quad (texcoord), never over the +// region, which is what makes that work. (0, 0, 1, 1) is the whole texture. // Coverage comes from a rounded-rectangle signed distance field and is a smooth // ramp across the last pixel, so the edge is ANTIALIASED. That is the whole // point of doing it here: a stencil clip of the same shape has a hard edge, and @@ -131,6 +136,7 @@ fragment float4 cn1_fs_textured_rounded( VertexOutTextured in [[stage_in]], constant float4 &tint [[buffer(0)]], constant float4 ¶ms [[buffer(1)]], + constant float4 ®ion [[buffer(2)]], texture2d tex [[texture(0)]]) { constexpr sampler s(mag_filter::linear, min_filter::linear, address::clamp_to_edge); @@ -166,7 +172,7 @@ fragment float4 cn1_fs_textured_rounded( } // Coverage scales every channel: the texture pipeline works in // premultiplied terms, exactly as the tint modulator above it does. - return tex.sample(s, in.texcoord) * tint * coverage; + return tex.sample(s, region.xy + in.texcoord * region.zw) * tint * coverage; } // --------- AlphaMask pipeline (Phase 2/4) --------- diff --git a/Ports/iOSPort/nativeSources/CN1Metalcompat.h b/Ports/iOSPort/nativeSources/CN1Metalcompat.h index 57ba8db6ff7..9581af61cc9 100644 --- a/Ports/iOSPort/nativeSources/CN1Metalcompat.h +++ b/Ports/iOSPort/nativeSources/CN1Metalcompat.h @@ -349,6 +349,12 @@ void CN1MetalFillGradient(int kind, id CN1MetalTextureFromUIImage(CN1Image *image); void CN1MetalDrawImageRounded(id texture, int alpha, int x, int y, int width, int height, float cornerRadius); +// As above for the region (u0, v0, du, dv) of the picture, in its normalised +// coordinates (0,0 top left), with the DESTINATION's corners rounded. A du or dv +// of zero or less means the whole picture. +void CN1MetalDrawImageRegionRounded(id texture, int alpha, int x, int y, + int width, int height, float cornerRadius, + float u0, float v0, float du, float dv); // Global Metal device (from METALView's command queue); shared by anyone // who needs to allocate Metal resources. diff --git a/Ports/iOSPort/nativeSources/CN1Metalcompat.m b/Ports/iOSPort/nativeSources/CN1Metalcompat.m index 24bc35555ba..4893718a60d 100644 --- a/Ports/iOSPort/nativeSources/CN1Metalcompat.m +++ b/Ports/iOSPort/nativeSources/CN1Metalcompat.m @@ -877,8 +877,32 @@ void CN1MetalDrawImage(id texture, int alpha, int x, int y, int widt */ void CN1MetalDrawImageRounded(id texture, int alpha, int x, int y, int width, int height, float cornerRadius) { + CN1MetalDrawImageRegionRounded(texture, alpha, x, y, width, height, cornerRadius, + 0.0f, 0.0f, 1.0f, 1.0f); +} + +/** + * CN1MetalDrawImageRounded for PART of the texture: the region (u0, v0, du, dv), in + * the normalised coordinates of the drawn picture (0,0 top left, 1,1 bottom right), + * is stretched over the destination and the DESTINATION's corners are rounded. + * + *

This is what lets a picture scaled to overflow its box -- BoxFit.cover, the + * gallery's cards -- be drawn rounded without a copy. Drawing the whole picture + * rounded puts the corners outside the box, where the box's clip cuts them off and + * the card shows square; so the caller used to read the pixels back, cut the corners + * into them and upload a second image, holding every such picture twice in memory + * and twice on the GPU. Showing only the region inside the box, rounded, is the + * same picture with no second copy.

+ */ +void CN1MetalDrawImageRegionRounded(id texture, int alpha, int x, int y, + int width, int height, float cornerRadius, + float u0, float v0, float du, float dv) { if (texture == nil || width <= 0 || height <= 0) return; - if (cornerRadius <= 0.0f) { + if (du <= 0.0f || dv <= 0.0f) { + u0 = 0.0f; v0 = 0.0f; du = 1.0f; dv = 1.0f; + } + BOOL whole = u0 == 0.0f && v0 == 0.0f && du == 1.0f && dv == 1.0f; + if (cornerRadius <= 0.0f && whole) { CN1MetalDrawImage(texture, alpha, x, y, width, height); return; } @@ -905,8 +929,13 @@ void CN1MetalDrawImageRounded(id texture, int alpha, int x, int y, uploadMatricesIfChanged(1); [activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2]; [activeEncoder setFragmentBytes:&tint length:sizeof(tint) atIndex:0]; - simd_float4 params = (simd_float4){ (float)width, (float)height, cornerRadius, 0.0f }; + // A zero radius is valid here: the rounded pipeline at r = 0 is a plain + // textured draw of the region, which CN1MetalDrawImage cannot do. + simd_float4 params = (simd_float4){ (float)width, (float)height, + cornerRadius > 0.0f ? cornerRadius : 0.0f, 0.0f }; [activeEncoder setFragmentBytes:¶ms length:sizeof(params) atIndex:1]; + simd_float4 region = (simd_float4){ u0, v0, du, dv }; + [activeEncoder setFragmentBytes:®ion length:sizeof(region) atIndex:2]; [activeEncoder setFragmentTexture:texture atIndex:0]; [activeEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; } diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index ca437f31124..58cbb26bd55 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -565,6 +565,7 @@ - (BOOL)cn1OpenURL:(UIApplication *)application url:(NSURL *)url sourceApplicati - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + { extern void cn1StartupPhase(const char*); cn1StartupPhase("didFinishLaunching"); } #ifdef CN1_DETECT_JAILBREAK cn1DetectJailbreakBypassesAndExit(); #endif diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 9ad3e02314f..4b6ba684b19 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -21,6 +21,8 @@ * need additional information or have any questions. */ #import +#include +#include #import #import "CodenameOne_GLViewController.h" @@ -3119,7 +3121,8 @@ JAVA_BOOLEAN Java_com_codename1_impl_ios_IOSImplementation_isRoundedImageDrawSup } void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedGlobalImpl -(void* peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius) { +(void* peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, + float u0, float v0, float du, float dv) { #ifdef CN1_USE_METAL if(((BRIDGE_CAST void*)[CodenameOne_GLViewController instance].currentMutableImage) == peer) { Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl(); @@ -3127,6 +3130,7 @@ JAVA_BOOLEAN Java_com_codename1_impl_ios_IOSImplementation_isRoundedImageDrawSup DrawImage* f = [[DrawImage alloc] initWithArgs:alpha xpos:x ypos:y i:(BRIDGE_CAST GLUIImage*)peer w:width h:height]; [f setRenderingHints:renderingHints]; [f setCornerRadius:cornerRadius]; + [f setRegionX:u0 y:v0 w:du h:dv]; [CodenameOne_GLViewController upcoming:f]; #ifndef CN1_USE_ARC [f release]; @@ -3137,13 +3141,15 @@ JAVA_BOOLEAN Java_com_codename1_impl_ios_IOSImplementation_isRoundedImageDrawSup } void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedMutableImpl -(void* peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius) { +(void* peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, + float u0, float v0, float du, float dv) { #ifdef CN1_USE_METAL GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; if (target == nil) return; DrawImage *f = [[DrawImage alloc] initWithArgs:alpha xpos:x ypos:y i:(BRIDGE_CAST GLUIImage*)peer w:width h:height]; [f setRenderingHints:renderingHints]; [f setCornerRadius:cornerRadius]; + [f setRegionX:u0 y:v0 w:du h:dv]; [f setTarget:target]; [CodenameOne_GLViewController upcoming:f]; #ifndef CN1_USE_ARC @@ -3228,10 +3234,16 @@ int getResourceSize(const char* name, int nameLen, const char* type, int typeLen if(path == nil) { return -1; } - NSData* iData = [NSData dataWithContentsOfFile:path]; - int size = [iData length]; - //CN1Log(@"getResourceSize %i finished", size); - return size; + // The size from the file system, not by reading the file. This used to load + // the whole resource into an NSData just to take its length, and it runs in + // front of every getResourceAsStream -- so each bundled resource was read in + // full twice, and the gallery's images, theme and fonts paid for it inside + // the first frame of a launch. + struct stat st; + if (stat([path fileSystemRepresentation], &st) != 0) { + return -1; + } + return st.st_size > INT_MAX ? INT_MAX : (int)st.st_size; } diff --git a/Ports/iOSPort/nativeSources/DrawImage.h b/Ports/iOSPort/nativeSources/DrawImage.h index 831cb0d926b..b574018b0b9 100644 --- a/Ports/iOSPort/nativeSources/DrawImage.h +++ b/Ports/iOSPort/nativeSources/DrawImage.h @@ -36,11 +36,18 @@ // ordinary case; anything positive rounds the quad in the fragment shader // so nobody has to build a rounded copy of the bitmap. float cornerRadius; + // The part of the picture shown, in its normalised coordinates. regionW or + // regionH of zero -- the ivars' initial value -- is the whole picture. + float regionX; + float regionY; + float regionW; + float regionH; } -(id)initWithArgs:(int)a xpos:(int)xpos ypos:(int)ypos i:(GLUIImage*)i w:(int)w h:(int)h; -(void)execute; -(void)setRenderingHints:(JAVA_INT)hints; -(void)setCornerRadius:(float)r; +-(void)setRegionX:(float)u y:(float)v w:(float)du h:(float)dv; @end diff --git a/Ports/iOSPort/nativeSources/DrawImage.m b/Ports/iOSPort/nativeSources/DrawImage.m index 9816740634d..d7fc7a35429 100644 --- a/Ports/iOSPort/nativeSources/DrawImage.m +++ b/Ports/iOSPort/nativeSources/DrawImage.m @@ -48,6 +48,12 @@ -(id)initWithArgs:(int)a xpos:(int)xpos ypos:(int)ypos i:(GLUIImage*)i w:(int)w -(void)setCornerRadius:(float)r { cornerRadius = r; } +-(void)setRegionX:(float)u y:(float)v w:(float)du h:(float)dv { + regionX = u; + regionY = v; + regionW = du; + regionH = dv; +} #if TARGET_OS_WATCH -(void)execute { @@ -60,7 +66,10 @@ -(void)execute { #else -(void)execute { #ifdef CN1_USE_METAL - if (cornerRadius > 0.0f) { + if (regionW > 0.0f && regionH > 0.0f) { + CN1MetalDrawImageRegionRounded([img getMTLTexture], alpha, x, y, width, height, cornerRadius, + regionX, regionY, regionW, regionH); + } else if (cornerRadius > 0.0f) { CN1MetalDrawImageRounded([img getMTLTexture], alpha, x, y, width, height, cornerRadius); } else { CN1MetalDrawImage([img getMTLTexture], alpha, x, y, width, height); diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 4091342e0f5..eb0408d27a4 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -662,6 +662,7 @@ void com_codename1_impl_ios_IOSNative_initVM__(CN1_THREAD_STATE_MULTI_ARG JAVA_O #else #if !TARGET_OS_WATCH POOL_BEGIN(); + cn1StartupPhase("initVM->UIApplicationMain"); int retVal = UIApplicationMain(0, nil, nil, @"CodenameOne_GLAppDelegate"); POOL_END(); #else @@ -2727,19 +2728,21 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isRoundedImageDrawSupported___R_bo return Java_com_codename1_impl_ios_IOSImplementation_isRoundedImageDrawSupportedImpl(); } -void com_codename1_impl_ios_IOSNative_nativeDrawImageRoundedGlobal___long_int_int_int_int_int_int_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG n1, JAVA_INT alpha, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT n5, JAVA_INT renderingHints, JAVA_FLOAT cornerRadius) +// u0, v0, du, dv: the part of the picture shown, normalised; du or dv <= 0 means all of +// it. See CN1MetalDrawImageRegionRounded. +void com_codename1_impl_ios_IOSNative_nativeDrawImageRoundedGlobal___long_int_int_int_int_int_int_float_float_float_float_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG n1, JAVA_INT alpha, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT n5, JAVA_INT renderingHints, JAVA_FLOAT cornerRadius, JAVA_FLOAT u0, JAVA_FLOAT v0, JAVA_FLOAT du, JAVA_FLOAT dv) { POOL_BEGIN(); - extern void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedGlobalImpl(void*, int, int, int, int, int, int, float); - Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedGlobalImpl((void *)n1, alpha, n2, n3, n4, n5, renderingHints, cornerRadius); + extern void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedGlobalImpl(void*, int, int, int, int, int, int, float, float, float, float, float); + Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedGlobalImpl((void *)n1, alpha, n2, n3, n4, n5, renderingHints, cornerRadius, u0, v0, du, dv); POOL_END(); } -void com_codename1_impl_ios_IOSNative_nativeDrawImageRoundedMutable___long_int_int_int_int_int_int_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG n1, JAVA_INT alpha, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT n5, JAVA_INT renderingHints, JAVA_FLOAT cornerRadius) +void com_codename1_impl_ios_IOSNative_nativeDrawImageRoundedMutable___long_int_int_int_int_int_int_float_float_float_float_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG n1, JAVA_INT alpha, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT n5, JAVA_INT renderingHints, JAVA_FLOAT cornerRadius, JAVA_FLOAT u0, JAVA_FLOAT v0, JAVA_FLOAT du, JAVA_FLOAT dv) { POOL_BEGIN(); - extern void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedMutableImpl(void*, int, int, int, int, int, int, float); - Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedMutableImpl((void *)n1, alpha, n2, n3, n4, n5, renderingHints, cornerRadius); + extern void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedMutableImpl(void*, int, int, int, int, int, int, float, float, float, float, float); + Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageRoundedMutableImpl((void *)n1, alpha, n2, n3, n4, n5, renderingHints, cornerRadius, u0, v0, du, dv); POOL_END(); } @@ -5613,7 +5616,6 @@ void com_codename1_impl_ios_IOSNative_clearRadialGradientPaintMutable__(CN1_THRE [PaintOp setCurrentMutable:NULL]; } - void com_codename1_impl_ios_IOSNative_releasePeer___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { #ifndef CN1_USE_ARC dispatch_async(dispatch_get_main_queue(), ^{ @@ -13909,7 +13911,7 @@ void com_codename1_impl_ios_IOSNative_registerBundledFont___java_lang_String(CN1 weight = NSFontWeightThin; } else if ([weightName isEqualToString:@"Light"]) { weight = NSFontWeightLight; - } else if ([weightName isEqualToString:@"Regular"]) { + } else if ([weightName isEqualToString:@"Regular"] || [weightName isEqualToString:@"Normal"]) { weight = NSFontWeightRegular; } else if ([weightName isEqualToString:@"Bold"]) { weight = NSFontWeightBold; @@ -13937,6 +13939,13 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createTruetypeFont___java_lang_String // Explicit font names continue through the existing shared loader. fnt = cn1MacSystemFontForAlias(str, pSize); #endif + // The system font at its true regular weight. IOSImplementation maps + // native:MainNormal to this sentinel because no HelveticaNeue alias reaches + // UIFontWeightRegular -- native:MainRegular is deliberately Medium -- and a + // text style asking for weight 400 was therefore rendering one step heavy. + if(fnt == nil && [str isEqualToString:@"CN1SystemRegular"]) { + fnt = [CN1Font systemFontOfSize:pSize]; + } if(fnt == nil && isIOS8_2() && [str hasPrefix:@"HelveticaNeue"]) { if([str isEqualToString:@"HelveticaNeue-UltraLight"]) { fnt = [CN1Font systemFontOfSize:pSize weight:UIFontWeightUltraLight]; diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index f4aa49985b2..c8af37f80d7 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -2953,8 +2953,11 @@ public Object createImage(String path) throws IOException { return n; } + /// False for the `custom` theme mode, which says the application ships the only + /// theme it uses: the builder packages no native theme for it, so there is none to + /// install. The macOS port inherits this. public boolean hasNativeTheme() { - return true; + return !"custom".equals(nativeThemeMode()); } private static String iosMode = "auto"; @@ -3019,9 +3022,16 @@ protected String nativeThemeResourceName(String mode) { } public void installNativeTheme() { + String mode = nativeThemeMode(); + if ("custom".equals(mode)) { + // The application ships its own theme and the builder shipped no native one, + // so there is nothing to open. This used to fall through to the pre-flat + // iPhoneTheme.res at the bottom of the chain, so a custom application + // shipped and installed the legacy iPhone theme under its own. + return; + } try { Resources r; - String mode = nativeThemeMode(); // A subclass may own a theme this class knows nothing about. The macOS port // extends this one and ships Aqua, which is not in the list below; without the // hook it inherited the iOS chain and installed an iPhone theme on a Mac. @@ -5087,12 +5097,12 @@ long getShapeID(Shape shape, Stroke stroke){ private void nativeDrawImageMutable(long peer, int alpha, int x, int y, int width, int height, int renderingHints) { nativeInstance.nativeDrawImageMutable(peer, alpha, x, y, width, height, renderingHints); } - private void nativeDrawImageRoundedMutable(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius) { - nativeInstance.nativeDrawImageRoundedMutable(peer, alpha, x, y, width, height, renderingHints, cornerRadius); + private void nativeDrawImageRoundedMutable(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, float u0, float v0, float du, float dv) { + nativeInstance.nativeDrawImageRoundedMutable(peer, alpha, x, y, width, height, renderingHints, cornerRadius, u0, v0, du, dv); } - private void nativeDrawImageRoundedGlobal(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius) { - nativeInstance.nativeDrawImageRoundedGlobal(peer, alpha, x, y, width, height, renderingHints, cornerRadius); + private void nativeDrawImageRoundedGlobal(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, float u0, float v0, float du, float dv) { + nativeInstance.nativeDrawImageRoundedGlobal(peer, alpha, x, y, width, height, renderingHints, cornerRadius, u0, v0, du, dv); } private void nativeDrawImageGlobal(long peer, int alpha, int x, int y, int width, int height, int renderingHints) { @@ -5397,6 +5407,8 @@ public Class getBackgroundLocationListener() { if (superVal == null && !"".equals(Preferences.get(PREFS_BACKGROUND_LOCATION_LISTENER_CLASS, ""))) { String backgroundLocationListenerClassName = Preferences.get(PREFS_BACKGROUND_LOCATION_LISTENER_CLASS, ""); try { + // ReachabilityCull (FOR_NAME_SITES) keeps only LocationListener + // implementations for this forName. Class backgroundLocationListenerClass = (Class)Class.forName(backgroundLocationListenerClassName); super.setBackgroundLocationListener(backgroundLocationListenerClass); } catch (Throwable t) {} @@ -5522,7 +5534,9 @@ GeofenceListener getGeofenceListener(String id) { if (geofenceListeners().containsKey(id)) { Class cls = null; try { - cls = Class.forName(geofenceListeners.get(id)); + // ReachabilityCull (FOR_NAME_SITES) keeps only GeofenceListener + // implementations for this forName. + cls = Class.forName(geofenceListeners.get(id)); if (cls == null) { return null; } @@ -8097,8 +8111,8 @@ void nativeDrawImage(long peer, int alpha, int x, int y, int width, int height) nativeDrawImageMutable(peer, alpha, x, y, width, height, renderingHints); } - void nativeDrawImageRounded(long peer, int alpha, int x, int y, int width, int height, float cornerRadius) { - nativeDrawImageRoundedMutable(peer, alpha, x, y, width, height, renderingHints, cornerRadius); + void nativeDrawImageRounded(long peer, int alpha, int x, int y, int width, int height, float cornerRadius, float u0, float v0, float du, float dv) { + nativeDrawImageRoundedMutable(peer, alpha, x, y, width, height, renderingHints, cornerRadius, u0, v0, du, dv); } @@ -8782,8 +8796,8 @@ void nativeDrawImage(long peer, int alpha, int x, int y, int width, int height) } @Override - void nativeDrawImageRounded(long peer, int alpha, int x, int y, int width, int height, float cornerRadius) { - nativeDrawImageRoundedGlobal(peer, alpha, x, y, width, height, renderingHints, cornerRadius); + void nativeDrawImageRounded(long peer, int alpha, int x, int y, int width, int height, float cornerRadius, float u0, float v0, float du, float dv) { + nativeDrawImageRoundedGlobal(peer, alpha, x, y, width, height, renderingHints, cornerRadius, u0, v0, du, dv); } @Override @@ -9256,6 +9270,7 @@ public float getDevicePixelRatio() { return scale > 0 ? scale : super.getDevicePixelRatio(); } + @Override public int getDeviceDensity() { // IMPORTANT: If you modify this method, you MUST make the equivalent changes @@ -10588,7 +10603,26 @@ public void drawImageRounded(Object graphics, Object img, int x, int y, int w, i ng.applyTransform(); ng.applyClip(); NativeImage nm = (NativeImage)img; - ng.nativeDrawImageRounded(nm.peer, ng.alpha, x, y, w, h, cornerRadius); + ng.nativeDrawImageRounded(nm.peer, ng.alpha, x, y, w, h, cornerRadius, 0, 0, 0, 0); + } + + @Override + public void drawImageRegionRounded(Object graphics, Object img, int x, int y, int w, int h, + float cornerRadius, float u0, float v0, float du, float dv) { + if (img == null) return; + if (!isRoundedImageDrawSupported()) { + super.drawImageRegionRounded(graphics, img, x, y, w, h, cornerRadius, u0, v0, du, dv); + return; + } + NativeGraphics ng = (NativeGraphics)graphics; + ng.checkControl(); + ng.applyTransform(); + ng.applyClip(); + NativeImage nm = (NativeImage)img; + // The corner radius may be zero here: the Metal pipeline then draws the + // region square, which is still the region and not the whole picture. + ng.nativeDrawImageRounded(nm.peer, ng.alpha, x, y, w, h, cornerRadius > 0 ? cornerRadius : 0, + u0, v0, du, dv); } @Override @@ -11358,6 +11392,20 @@ protected String nativeFontName(String fontName) { if("native:MainRegular".equals(fontName)) { return "HelveticaNeue-Medium"; } + // native:MainNormal is the system font at its TRUE regular weight. + // + // native:MainRegular has meant HelveticaNeue-Medium since the alias + // was introduced, and on iOS 8.2+ that resolves to the system font + // at UIFontWeightMedium -- one step heavier than the platform's own + // body weight. Changing it would restyle every existing app, so the + // honest regular gets a name of its own. Note the macOS branch + // already maps native:MainRegular to NSFontWeightRegular, so the two + // platforms disagree about the same alias; this name means the same + // thing on both. + if("native:MainNormal".equals(fontName)) { + return "CN1SystemRegular"; + } + if("native:MainBold".equals(fontName)) { return "HelveticaNeue-Bold"; @@ -15447,6 +15495,8 @@ public static void runBackgroundProcessing(final String id) { return; } try { + // ReachabilityCull (FOR_NAME_SITES) keeps only BackgroundWorker + // implementations for this forName. Class cls = Class.forName(workerClass); BackgroundWorker worker = (BackgroundWorker) cls.newInstance(); java.util.Map input = new java.util.HashMap(); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 44e1946e02f..c877267e520 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -135,9 +135,10 @@ native void editStringAt(int x, int y, int w, int h, long peer, boolean singleLi /// the Metal renderer has the shader. native boolean isRoundedImageDrawSupported(); - native void nativeDrawImageRoundedGlobal(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius); + // u0/v0/du/dv: the normalised part of the picture shown; du or dv <= 0 is all of it. + native void nativeDrawImageRoundedGlobal(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, float u0, float v0, float du, float dv); - native void nativeDrawImageRoundedMutable(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius); + native void nativeDrawImageRoundedMutable(long peer, int alpha, int x, int y, int width, int height, int renderingHints, float cornerRadius, float u0, float v0, float du, float dv); native void nativeTileImageGlobal(long peer, int alpha, int x, int y, int width, int height); native int stringWidthNative(long peer, String str); native int charWidthNative(long peer, char ch); diff --git a/docs/demos/common/pom.xml b/docs/demos/common/pom.xml index 40104bae6ec..fb2175fc417 100644 --- a/docs/demos/common/pom.xml +++ b/docs/demos/common/pom.xml @@ -19,6 +19,20 @@ provided + + + com.codenameone + codenameone-flutter-runtime + ${cn1.version} + provided + + diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/FlutterInteropSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/FlutterInteropSnippets.java new file mode 100644 index 00000000000..c1ef0dc077b --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/flutter/FlutterInteropSnippets.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.flutter; + +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.widgets.Text; +import com.codename1.ui.Button; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BoxLayout; + +/** + * The developer guide's Flutter interop examples, compiled rather than taken + * on trust. + * + *

Both entry points are shown with a {@code Text} widget standing in for a + * real one, because the point is where the boundary sits, not what the widget + * draws.

+ */ +public final class FlutterInteropSnippets { + + private FlutterInteropSnippets() { + } + + /** One Dart screen inside an otherwise ordinary Codename One form. */ + public static void embedOneScreen() { + // tag::flutter-interop-java-001[] + Form dashboard = new Form("Dashboard", BoxLayout.y()); + dashboard.add(new Label("Written in Codename One")); + + // A Dart widget tree, compiled to Java at build time. wrap() returns + // an ordinary Container, so it is added like any other component. + dashboard.add(FlutterUI.wrap(new Text("Rendered from Dart"))); + + dashboard.add(new Button("Also Codename One")); + dashboard.show(); + // end::flutter-interop-java-001[] + } + + /** The widget tree IS the application. */ + public static void runTheWholeApplication() { + // tag::flutter-interop-java-002[] + // runApp mounts the tree in a Form of its own and shows it. Unlike + // wrap(), this installs the Material base theme, because the widget + // tree is expected to be the whole UI. + FlutterUI.runApp(new Text("The entire application")); + // end::flutter-interop-java-002[] + } +} diff --git a/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.sh b/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.sh new file mode 100644 index 00000000000..13728e3fed8 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.sh @@ -0,0 +1,10 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::flutter-interop-bash-001[] +python3 scripts/flutter-bench/run_bench.py --list + +python3 scripts/flutter-bench/run_bench.py --platform macos \ + --cn1-app /path/to/Bench.app \ + --flutter-app /path/to/gallery.app \ + --json out/macos.json --markdown out/macos.md +// end::flutter-interop-bash-001[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.xml b/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.xml new file mode 100644 index 00000000000..d1b7dadd538 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/flutter-interop.xml @@ -0,0 +1,9 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::flutter-interop-xml-001[] + + com.codenameone + codenameone-flutter-runtime + ${cn1.version} + +// end::flutter-interop-xml-001[] diff --git a/docs/developer-guide/Flutter-Interop.asciidoc b/docs/developer-guide/Flutter-Interop.asciidoc new file mode 100644 index 00000000000..66e1987105c --- /dev/null +++ b/docs/developer-guide/Flutter-Interop.asciidoc @@ -0,0 +1,203 @@ +[[flutter-interop]] +== Running Flutter widget code + +Codename One can compile Dart widget source into a Codename One UI at build +time. The Dart is translated to Java, the Java is compiled by the normal +toolchain, and the resulting widgets render through Codename One's own +pipeline on every port the framework supports. + +There is no Dart VM in the application, no embedded rendering engine, and no +platform view. A screen written as Dart widgets becomes ordinary Codename One +components, so it inherits the theme, the event thread, the accessibility +tree and the native build at no extra cost -- and it can sit in the same `Form` as +hand-written Codename One code. + +Two things follow from that, and they're the reason to reach for this at all: + +* An existing Dart UI can be reused without adopting a second runtime, a + second build system or a second set of platform plugins. +* An existing Codename One application can adopt a Dart screen without being + rewritten. + +=== How it fits together + +The pipeline has three stages, and each is an ordinary build step you can run +and inspect on its own. + +image::img/flutter-pipeline.svg[Dart source through the transpiler to the Codename One ports] + +Dart widget source in `src/main/flutter` is read by the `transcode-flutter` +goal during `generate-sources`. It emits Java into +`target/generated-sources/flutter`, which the normal compile phase picks up. +At runtime those generated classes call into the Flutter runtime library, +which implements the widget, element and render-object model on top of +Codename One containers. + +The key design point is where the boundary sits. The runtime reimplements +Flutter's *layout and composition* model -- constraints down, sizes up, +elements reconciled against widgets -- but it doesn't reimplement Flutter's +*rasterizer*. Painting is Codename One's, which is what makes the output a +real Codename One component tree rather than a texture. + +=== Enabling it in a project + +Add the runtime dependency to the `common` module. Projects generated from the +archetype already contain it, commented out: + +[source,xml] +---- +include::../demos/common/src/main/snippets/developer-guide/flutter-interop.xml[tag=flutter-interop-xml-001,indent=0] +---- + +Then put Dart files under `common/src/main/flutter`. The goal is already bound +in the generated `pom.xml` and is a silent no-op while that directory doesn't +exist, so nothing changes for projects that don't use it. + +No Dart SDK is required. The transpiler is Java and runs as part of the Maven +build; the Dart source is input, not something that gets executed. + +The generated package defaults to `com.codename1.generated.flutter` and the +paths are configurable: + +[cols="1,2", options="header"] +|=== +|Property |Meaning + +|`cn1.flutter.sourceDir` +|Where the Dart lives. Defaults to `src/main/flutter`. + +|`cn1.flutter.outputDir` +|Where the generated Java is written. Defaults to +`target/generated-sources/flutter`. + +|`cn1.flutter.package` +|The package the generated classes are emitted into. +|=== + +=== Use case: One screen inside an existing application + +This is the incremental path, and the one to start with. `FlutterUI.wrap` +inflates a widget into a plain `Container` that can be added anywhere an +ordinary component can: + +[source,java] +---- +include::../demos/common/src/main/java/com/codenameone/developerguide/flutter/FlutterInteropSnippets.java[tag=flutter-interop-java-001,indent=0] +---- + +The wrapped subtree is a component like any other: it participates in the +parent's layout, scrolls with it, and is styled by the same theme. An embedded +subtree does *not* install the Material base theme, so dropping a +widget into an existing screen won't restyle the rest of it. + +That makes this the right shape for reusing a piece of design work -- a chart, +a card, an onboarding pane -- without committing the rest of the application to +anything. + +=== Use case: Porting an application wholesale + +When the Dart source *is* the application, `FlutterUI.runApp` mounts the tree +as the root of a new `Form` and shows it, which is the direct analog of +Flutter's own `runApp`: + +[source,java] +---- +include::../demos/common/src/main/java/com/codenameone/developerguide/flutter/FlutterInteropSnippets.java[tag=flutter-interop-java-002,indent=0] +---- + +Unlike `wrap`, this path installs the Material base theme, because the widget +tree is expected to be the whole UI and to look the way its author intended. + +A wholesale port is mostly a question of how much of the Dart language and of +the Flutter libraries the application actually uses. The transpiler reports +what it can't handle rather than guessing: a construct outside the supported +subset fails the build with a milestone code identifying the feature, so the +gap is a list you can work through rather than a runtime surprise. + +At runtime the same principle applies. Widgets that aren't implemented are +recorded through the error inventory rather than drawing nothing, +so a screen that's missing something says so. + +=== What to expect from the result + +The transpiled UI is a Codename One application in every respect that matters +for shipping it: it builds for the same targets, it's signed and packaged the +same way, and it has no additional runtime to install on the device. + +That last point is what the size and start-up differences come from, and it +cuts both ways -- a Flutter application carries an engine that's good at +what it does, while a transpiled one carries Codename One's. The benchmark +described in <> measures both sides of the same application +on each platform and publishes the result rather than asserting a direction. + +image::img/flutter-parity-textfields.png[The same screen rendered by Flutter and by Codename One] + +The screen above is the gallery's text-field demo, rendered by Flutter on the +left and by the transpiled build on the right, from one Dart source file. + +[[flutter-benchmark]] +=== Measuring it against Flutter + +The project keeps a benchmark that builds *one* application both ways -- the +Flutter toolchain's own release build, and the identical Dart source +transpiled and built by Codename One -- and measures both on the same machine. +It lives in `scripts/flutter-bench` and runs in CI, publishing its numbers to +the pull request and to the port status page. + +What it reports, per platform: + +[cols="1,3", options="header"] +|=== +|Metric |What it means + +|Installed size +|The sum of the artifact's file lengths, which is what the user's device gives +up. Not `du`, which rounds every file up to a block. + +|Executable code +|Every compiled binary in the artifact, added together. Counting only the main +executable is wrong on iOS, where a Flutter application's own code isn't in +the executable at all -- it's in the frameworks beside it. + +|Download size +|The artifact compressed, because that's what a store ships. + +|Cold start +|Wall time from launching the process to the first frame being on screen, +measured from outside so neither runtime is trusted to time itself. + +|Memory at rest +|The platform's own accounting for a settled, idle application. +|=== + +Three rules keep the numbers honest, and each exists because the obvious +alternative produced a flattering result: + +* *Interleaved runs, best of N.* One run of each side, alternating. A machine + that gets busier halfway through then penalizes both sides equally instead + of whichever happened to run second, and the load average is recorded with + every result so a surprising ratio can be checked against it. + +* *Start-up is a bracket, not a point.* The two runtimes don't expose the + same event. Flutter's post-frame callback runs before that frame is + rasterized, while Codename One's marker fires once the form is on the + screen, so comparing them directly charges one runtime for rasterizing its + first screen and not the other. The benchmark therefore reports Flutter's + figure as a range and computes the ratio from the end least favorable to + Codename One. + +* *A platform that can't be measured says so.* iOS start-up and memory are + reported as not measured rather than taken from a simulator, because Dart + can't compile ahead-of-time for the simulator -- a simulator comparison + would time Flutter's debug build against a Codename One release build. + iOS sizes come from release device bundles, which need no signing to measure. + +To run it locally against builds you already have: + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/flutter-interop.sh[tag=flutter-interop-bash-001,indent=0] +---- + +`--list` also reports which platform adapters have been exercised end to end, +which isn't the same question as which ones exist. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 631206b4dd2..aae6086f8a4 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -99,6 +99,8 @@ include::Game-Builder.asciidoc[] include::Game-Assets.asciidoc[] +include::Flutter-Interop.asciidoc[] + = Data, media and networking include::io.asciidoc[] diff --git a/docs/developer-guide/img/flutter-parity-textfields.png b/docs/developer-guide/img/flutter-parity-textfields.png new file mode 100644 index 00000000000..bfe9d161152 Binary files /dev/null and b/docs/developer-guide/img/flutter-parity-textfields.png differ diff --git a/docs/developer-guide/img/flutter-pipeline.svg b/docs/developer-guide/img/flutter-pipeline.svg new file mode 100644 index 00000000000..65781d360f7 --- /dev/null +++ b/docs/developer-guide/img/flutter-pipeline.svg @@ -0,0 +1,79 @@ + + + From Dart widget source to a native Codename One build + + + + + + + + + + + + BUILD TIME + + + Dart widget + source + src/main/flutter + + + + + transcode- + flutter + Maven goal + + + + + Generated + Java + target/generated + + + + + javac + + app code + + + + + Native build + iOS, Android, + desktop, web + + + + RUN TIME + + + Flutter runtime library + widgets, elements, layout + + + + + Codename One components + theme, EDT, accessibility + + + + + Port rendering pipeline + no engine, no platform view + diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt index b55c63d062e..566b343df7b 100644 --- a/docs/developer-guide/languagetool-accept.txt +++ b/docs/developer-guide/languagetool-accept.txt @@ -404,6 +404,7 @@ toolchain toolchains toolset transferal +transpiled transpiler unix unlayout diff --git a/docs/website/layouts/_default/port-status.html b/docs/website/layouts/_default/port-status.html index c01640aeb8d..12649edc3a5 100644 --- a/docs/website/layouts/_default/port-status.html +++ b/docs/website/layouts/_default/port-status.html @@ -4,6 +4,7 @@ {{- $supplement := site.Data.port_status_supplement -}} {{- $support := site.Data.port_status_support -}} {{- $environment := site.Data.port_status_environment -}} +{{- $flutterBench := site.Data.port_status_flutter_benchmark -}} {{- $snapshotTime := now.UTC -}}
@@ -319,6 +320,58 @@

{{ $support.benchmark.title }}

+ {{- with $flutterBench }} +
+
+

Flutter compared

+

One Flutter application, built by Flutter and by Codename One

+

+ {{ .app }}. Lower is better for every metric, so a ratio above 1.00x means Codename One is ahead by that factor. + {{- with .generated_at }} Measured {{ . }}{{ end }}{{ with .commit }} from commit {{ substr . 0 10 }}{{ end }}. +

+
+ {{- range $id, $platform := .platforms }} +

{{ $id }}

+ {{- if eq $platform.status "measured" }} +
+ + + + + + + + + + + {{- range $key, $metric := $platform.metrics }} + {{- if $metric.label }} + + + {{- if eq $metric.unit "bytes" }} + + + {{- else }} + + + {{- end }} + + + {{- end }} + {{- end }} + +
MetricCodename OneFlutterRatio
{{ $metric.label }}{{ printf "%.1f MB" (div (float $metric.codenameone) 1048576.0) }}{{ printf "%.1f MB" (div (float $metric.flutter) 1048576.0) }}{{ printf "%.0f ms" (float $metric.codenameone) }}{{ printf "%.0f ms" (float $metric.flutter) }}{{ printf "%.2fx" (float $metric.ratio) }}
+
+ {{- else }} +

Not measured: {{ $platform.reason }}

+ {{- end }} + {{- end }} +

+ Benchmark harness and method +

+
+ {{- end }} + {{- /* Rendered by scripts/website/build.sh from vm/selfhost/perf-baseline: the ratios every platform build's performance gate holds ParparVM to, so this table and the gate cannot disagree. Absent from a bare `hugo` run, hence the guard. */ -}} diff --git a/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index c493544d905..75a6edb5f04 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -4448,6 +4448,31 @@ public void usesClassMethod(String cls, String method) { throw new BuildException("Failed to extract android port sources from "+androidPortSrcJar, ex); } + // Only the native themes installNativeTheme() can load are shipped. The port + // carries three -- the pre-Holo androidTheme alone is half a megabyte -- and + // the mode that picks one comes from the build hints the stub copies into + // Display properties. android_holo_light always stays: hasNativeTheme() + // probes for it. Placed after the port unzip, which overwrites any same-named + // file, so only the port's own copies can be removed. + try { + List droppedThemes = NativeThemes.removeUnusedAndroid(assetsDir, + NativeThemes.androidThemesFor(request.getArg("and.themeMode", null), + request.getArg("cn1.androidTheme", null), + request.getArg("nativeTheme", null), + request.getArg("cn1.nativeTheme", null), + // and.hololight is a run-time Display property only: no + // build hint carries it into the stub, so it is set by the + // application or not at all, and removeUnusedAndroid keeps + // every theme for an application that names it. + null), + dummyClassesDir); + if (!droppedThemes.isEmpty()) { + log("Native themes not used by this build, not shipped: " + droppedThemes); + } + } catch (IOException ex) { + throw new BuildException("Failed to remove the unused native themes", ex); + } + // Health background-listener bindings: generated rather than // resolved reflectively, so each listener is reached through a // direct constructor call that shrinking and obfuscation follow. diff --git a/maven/build-engine/src/main/java/com/codename1/builders/Executor.java b/maven/build-engine/src/main/java/com/codename1/builders/Executor.java index 649fd453d7f..f3c77da0cf1 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/Executor.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/Executor.java @@ -572,6 +572,43 @@ protected String databaseLegacyStubProperty(BuildRequest request, boolean usesDa return " Display.getInstance().setProperty(\"db.legacy\", \"true\");\n"; } + /// The `desktop.width` and `desktop.height` build hints, as a call the generated stub makes + /// before `Display.init` on the native Linux and Windows ports. + /// + /// Both hints were declared (`@DesktopBuild`) and honoured only by the JavaSE desktop + /// wrapper; the ParparVM desktop ports opened every window at a hard-coded 800x600. It has + /// to run before init because init is what creates the window, and resizing it afterwards + /// lays the first form out twice. + /// + /// @param request the build request carrying the hints + /// @param implementationClass the port's implementation class, fully qualified + /// @return the stub line, or an empty string when neither hint is set + protected String desktopWindowSizeStubCall(BuildRequest request, String implementationClass) { + int width = positiveIntArg(request, "desktop.width"); + int height = positiveIntArg(request, "desktop.height"); + if (width <= 0 && height <= 0) { + return ""; + } + return " " + implementationClass + ".setDefaultWindowSize(" + width + ", " + height + ");\n"; + } + + private int positiveIntArg(BuildRequest request, String name) { + String value = request.getArg(name, null); + if (value == null || value.trim().length() == 0) { + return 0; + } + try { + int v = Integer.parseInt(value.trim()); + if (v > 0) { + return v; + } + } catch (NumberFormatException ex) { + // reported below + } + log("Invalid " + name + " build hint: '" + value + "'. Ignoring it."); + return 0; + } + /// The `desktop.titleBar` build hint, surfaced to the generated stub as a Display property. /// /// Without this the hint is INERT on every ParparVM desktop port. `Display.getProperty` @@ -3514,6 +3551,13 @@ protected boolean isDllResource() { return false; } + /// True for an application-archive entry that exists only for the build and must + /// not reach the installed application. See unzip(). + static boolean isBuildTimeOnlyEntry(String entryName) { + String n = entryName.startsWith("/") ? entryName.substring(1) : entryName; + return n.startsWith("META-INF/dart/"); + } + public void unzip(InputStream source, File classesDir, File resDir, File sourceDir, File libsDir, File xmlDir) throws IOException { try { BufferedOutputStream dest = null; @@ -3599,6 +3643,16 @@ public void unzip(InputStream source, File classesDir, File resDir, File sourceD continue; } + // Build-time metadata, never shipped. META-INF/dart/*.dart are the + // Dart API declarations a runtime jar (flutter-runtime) carries for + // the transpiler, which reads them from the jar on its own classpath + // long before this point. The application jar merges every + // dependency, so without this they were copied into every bundle on + // every platform -- 22 files of Dart source in a Flutter app. + if (isBuildTimeOnlyEntry(entryName)) { + continue; + } + // Entries outside the supported virtual cn1lib namespaces // retain their original ZIP name, which must be relative and // remain inside the extraction root. diff --git a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java index a3aeccbba4c..180ae957b74 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -128,7 +128,6 @@ public class IPhoneBuilder extends Executor { private File resultDir; private boolean includePush; private File tmpFile; - private File icon57; private File icon512; // Bumped from 12.0 to 13.0 to enable NSURLSessionWebSocketTask // (iOS 13+) used by com.codename1.io.WebSocket's iOS implementation. @@ -2346,6 +2345,10 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException // to the FLAT iOS 7 theme -- the exact opposite of what was asked for, // and silently, because an unrecognised mode is not an error here. iosMode = "modern"; + } else if (NativeThemes.isCustom(sharedMode)) { + // The application's own theme is the only one: the stub tells the + // runtime to install none, and no native theme is packaged. + iosMode = "custom"; } else { iosMode = "auto"; } @@ -3514,6 +3517,22 @@ public void usesClassMethod(String cls, String method) { new File(buildinRes, "iOS7Theme.res").delete(); } + // Only the native theme the stub's mode will load is shipped. nativeios.jar + // carries all four iOS themes, over a megabyte, and installNativeTheme() + // installs exactly one: the mode is fixed here at build time (the stub calls + // setIosMode / setIosThemeGeneration) and nothing reads it at run time. + // btres holds only the port's files -- the application's own resources are + // in resDir -- so nothing the developer shipped is removed. + try { + List droppedThemes = NativeThemes.removeUnusedApple(buildinRes, + iosMode, iosThemeGeneration, false, classesDir); + if (!droppedThemes.isEmpty()) { + log("Native themes not used by this build, not shipped: " + droppedThemes); + } + } catch (IOException ex) { + throw new BuildException("Failed to remove the unused native themes", ex); + } + // Flip the crypto build toggles in CN1Crypto.h based on what the // user's bytecode references. Apps that don't touch // com.codename1.security.* get stub-only versions of the iOS @@ -17546,10 +17565,6 @@ private File getIconDirectory(BuildRequest request) { } - private void copyIcon(String name, File srcDir, File destDir) throws IOException { - copy(new File(srcDir, name), new File(destDir, name)); - } - private String buildLocalizedIconSelectorObjC() { StringBuilder mapping = new StringBuilder(); mapping.append(" @{ "); @@ -17637,82 +17652,59 @@ private String buildLocalizedIconSelectorObjC() { + " }\n"; } - private void copyIcons(File srcDir, File destDir, String... icons) throws IOException { - for (String icon : icons) { - copyIcon(icon, srcDir, destDir); - } - } - + /// The AppIcon asset-catalog images: file name and pixel size. Each is named by + /// vm/ByteCodeTranslator/src/Icons.json, the set's Contents.json, which + /// IPhoneBuilderIconsTest checks. + static final Object[][] APP_ICONS = { + {"iPhoneNotification@2x.png", 40}, {"iPhoneNotification@3x.png", 60}, + {"iPhoneSpotlight.png", 29}, {"iPhoneSpotlight@2x.png", 58}, {"iPhoneSpotlight@3x.png", 87}, + {"iPhone7Spotlight@2x.png", 80}, {"iPhone7Spotlight@3x.png", 120}, + {"iPhoneApp.png", 57}, {"iPhoneApp@2x.png", 114}, + {"iPhone7App@2x.png", 120}, {"iPhone7App@3x.png", 180}, + {"iPadNotifications.png", 20}, {"iPadNotification@2x.png", 40}, + {"iPadSettings.png", 29}, {"iPadSettings@2x.png", 58}, + {"iPadSpotlight7.png", 40}, {"iPadSpotlight7@2x.png", 80}, + {"iPadSpotlight.png", 50}, {"iPadSpotlight@2x.png", 100}, + {"iPadApp.png", 72}, {"iPadApp@2x.png", 144}, + {"iPadApp7.png", 76}, {"iPadApp7@2x.png", 152}, {"iPadPro@2x.png", 167}, + {"AppStore.png", 1024}, + }; + private boolean generateIcons(BuildRequest request) throws Exception { File iconDirectory = getIconDirectory(request); File resDir = getResDir(); - + BufferedImage iconImage = ImageIO.read(new ByteArrayInputStream(request.getIcon())); - // Legacy iOS icon files are still copied into the root resources, but should not - // be placed inside AppIcon.appiconset as they are not referenced by Contents.json. - icon512 = new File(resDir, "iTunesArtwork"); + // The icons live in the AppIcon asset catalog and nowhere else. They used to be + // copied into the bundle root as well -- iTunesArtwork, Icon.png and every size + // above, about 0.6MB -- for the top-level CFBundleIconFiles of iOS 6 and older. + // Nothing reads those now: every SDK this builder supports has a deployment + // floor of 12 or more, where the system takes the icon from CFBundleIcons / + // CFBundleIconName, which actool writes for the catalog. The Info.plist template + // no longer lists them either, so App Store validation finds no dangling name. + // + // The original image is still kept, outside the bundle, as the source of the + // macOS icon set. + File iconSource = new File(tmpFile, "icon-source"); + iconSource.mkdirs(); + icon512 = new File(iconSource, "iTunesArtwork"); createFile(icon512, request.getIcon()); - icon57 = new File(resDir, "Icon.png"); - createIconFile(icon57, iconImage, 57, 57); - createIconFile(new File(iconDirectory, "iPhoneNotification@2x.png"), iconImage, 40, 40); - createIconFile(new File(iconDirectory, "iPhoneNotification@3x.png"), iconImage, 60, 60); - createIconFile(new File(iconDirectory, "iPhoneSpotlight.png"), iconImage, 29, 29); - createIconFile(new File(iconDirectory, "iPhoneSpotlight@2x.png"), iconImage, 58, 58); - createIconFile(new File(iconDirectory, "iPhoneSpotlight@3x.png"), iconImage, 87, 87); - createIconFile(new File(iconDirectory, "iPhone7Spotlight@2x.png"), iconImage, 80, 80); - createIconFile(new File(iconDirectory, "iPhone7Spotlight@3x.png"), iconImage, 120, 120); - createIconFile(new File(iconDirectory, "iPhoneApp.png"), iconImage, 57, 57); - createIconFile(new File(iconDirectory, "iPhoneApp@2x.png"), iconImage, 114, 114); - createIconFile(new File(iconDirectory, "iPhone7App@2x.png"), iconImage, 120, 120); - createIconFile(new File(iconDirectory, "iPhone7App@3x.png"), iconImage, 180, 180); - createIconFile(new File(iconDirectory, "iPadNotifications.png"), iconImage, 20, 20); - createIconFile(new File(iconDirectory, "iPadNotification@2x.png"), iconImage, 40, 40); - createIconFile(new File(iconDirectory, "iPadSettings.png"), iconImage, 29, 29); - createIconFile(new File(iconDirectory, "iPadSettings@2x.png"), iconImage, 58, 58); - createIconFile(new File(iconDirectory, "iPadSpotlight7.png"), iconImage, 40, 40); - createIconFile(new File(iconDirectory, "iPadSpotlight7@2x.png"), iconImage, 80, 80); - createIconFile(new File(iconDirectory, "iPadSpotlight.png"), iconImage, 50, 50); - createIconFile(new File(iconDirectory, "iPadSpotlight@2x.png"), iconImage, 100, 100); - createIconFile(new File(iconDirectory, "iPadApp.png"), iconImage, 72, 72); - createIconFile(new File(iconDirectory, "iPadApp@2x.png"), iconImage, 144, 144); - createIconFile(new File(iconDirectory, "iPadApp7.png"), iconImage, 76, 76); - createIconFile(new File(iconDirectory, "iPadApp7@2x.png"), iconImage, 152, 152); - createIconFile(new File(iconDirectory, "iPadPro@2x.png"), iconImage, 167, 167); - createIconFile(new File(iconDirectory, "AppStore.png"), iconImage, 1024, 1024); - - copyIcons(iconDirectory, resDir, - "iPhoneNotification@2x.png", - "iPhoneNotification@3x.png", - "iPhoneSpotlight.png", - "iPhoneSpotlight@2x.png", - "iPhoneSpotlight@3x.png", - "iPhone7Spotlight@2x.png", - "iPhone7Spotlight@3x.png", - "iPhoneApp.png", - "iPhoneApp@2x.png", - "iPhone7App@2x.png", - "iPhone7App@3x.png", - "iPadNotifications.png", - "iPadNotification@2x.png", - "iPadSettings.png", - "iPadSettings@2x.png", - "iPadSpotlight7.png", - "iPadSpotlight7@2x.png", - "iPadSpotlight.png", - "iPadSpotlight@2x.png", - "iPadApp.png", - "iPadApp@2x.png", - "iPadApp7.png", - "iPadApp7@2x.png", - "iPadPro@2x.png", - "AppStore.png"); + writeAppIcons(iconImage, iconDirectory); processLocalizedIcons(resDir, request); return true; } + /// Writes every AppIcon asset-catalog image into `iconDirectory`, and nothing else. + void writeAppIcons(BufferedImage iconImage, File iconDirectory) throws IOException { + for (Object[] icon : APP_ICONS) { + int size = ((Integer) icon[1]).intValue(); + createIconFile(new File(iconDirectory, (String) icon[0]), iconImage, size, size); + } + } + /** * Scans the resources directory for files named cn1_icon_LANG[_COUNTRY].png * and registers them as iOS alternate app icons. Each detected locale is diff --git a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java index a575de0f1a8..233c2c6f914 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java @@ -252,6 +252,14 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException // sources), the same mechanism parparvm-compiler.jar uses. extractJarResource("/LinuxPort.jar", portClasses); extractJarResource("/nativelinux.jar", nativeSources); + // The port installs its bundled theme from init() whenever the file is there, + // before any application code runs, so the custom theme mode -- the + // application's own theme is the only one -- is honoured by not shipping it. + if (NativeThemes.desktopShipsNoTheme(request.getArg("desktop.themeMode", null), + request.getArg("nativeTheme", null), request.getArg("cn1.nativeTheme", null)) + && NativeThemes.removeDesktopTheme(portClasses, "linuxNativeTheme.res")) { + log("Native theme not used by this build, not shipped: linuxNativeTheme.res"); + } } catch (Exception ex) { throw new BuildException("Failed to stage the LinuxPort native layer. The codenameone " + "maven plugin must provide the codenameone-linux 'bundle' artifact " @@ -694,6 +702,9 @@ private void writeBootstrapStub(BuildRequest request, File classesDir, File stub // clients and @OpenTelemetry all compiled here and did nothing at run time. src.append(routeDispatcherInstallSource(classesDir, " ")); src.append(annotationFrameworksInstallSource(classesDir, " ")); + // desktop.width/height: before init, which creates the window. See + // Executor.desktopWindowSizeStubCall. + src.append(desktopWindowSizeStubCall(request, "com.codename1.impl.linux.LinuxImplementation")); src.append(" Display.init(null);\n"); // The application's identity, which nothing else gives this platform. The stub passes // null to Display.init, so the implementation never derives a package from an object, and diff --git a/maven/build-engine/src/main/java/com/codename1/builders/MacOSBuildHints.java b/maven/build-engine/src/main/java/com/codename1/builders/MacOSBuildHints.java index 1bbbef4eb78..efbdf938ae7 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/MacOSBuildHints.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/MacOSBuildHints.java @@ -773,6 +773,9 @@ public String getThemeMode() { // the Aqua theme could not be selected at all -- by a hint or by default. "aqua", "native", "modern", "liquid", "material", "ios7", "flat", "auto", + // custom installs no native theme. Rejected here it became "modern", so an + // application that asked for none got the iOS modern theme under its own. + "custom", }; /** diff --git a/maven/build-engine/src/main/java/com/codename1/builders/MacOSNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/MacOSNativeBuilder.java index 0a2772f5601..58b2176a261 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/MacOSNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/MacOSNativeBuilder.java @@ -266,6 +266,24 @@ public String get(String key, String defaultValue) { // dark palette on, so every dark-appearance screen rendered light // however carefully the application asked for dark. Nothing failed // to build and nothing was logged. + // + // Only the theme the stub's mode will load is shipped. nativemac.jar + // carries every native theme -- Aqua and four iOS ones, about a + // megabyte -- and the runtime installs exactly one of them. Removed + // from nativeSources, before staging, so neither the staged copy nor + // the translator (which copies every resource on its input path into + // the project) picks the others up; the application's own resources + // are never touched. + List droppedThemes = NativeThemes.removeUnusedApple(nativeSources, + hints.getThemeMode(), null, true, classesDir); + // MacPort.jar carries a copy too (iPhoneTheme.res, from the iOSPort + // sources it shares), and the port classes are translator input as well. + droppedThemes.addAll(NativeThemes.removeUnusedApple(portClasses, + hints.getThemeMode(), null, true, classesDir)); + if (!droppedThemes.isEmpty()) { + log("Native themes not used by this build, not shipped: " + + new java.util.LinkedHashSet(droppedThemes)); + } stageThemeResources(nativeSources, buildinRes); } catch (Exception ex) { throw new BuildException("Failed to stage the MacPort native layer. The codenameone " @@ -2352,6 +2370,7 @@ private boolean buildChannel(BuildRequest request, MacOSBuildHints hints, File d // nobody notices until an Intel user reports it. cmd.add("ARCHS=" + hints.getArch()); cmd.add("ONLY_ACTIVE_ARCH=NO"); + cmd.addAll(stripSettings(hints.getConfiguration())); cmd.add("MACOSX_DEPLOYMENT_TARGET=" + hints.getMinDeploymentTarget()); // The identifier signing and provisioning read. Checked against a // generated project rather than assumed: neither the macOS template nor @@ -2743,6 +2762,29 @@ static void stageThemeResources(File nativeSources, File buildinRes) } } + /// The xcodebuild settings that strip the shipped binary, empty for Debug. + /// + /// This builder runs `xcodebuild build`, never `archive`, and a plain build + /// leaves the symbol table in the product: a universal gallery binary shipped + /// 98,805 symbols, 8MB of __LINKEDIT per architecture, about a quarter of the + /// file. The iOS cloud build does not have the problem because `archive` sets + /// DEPLOYMENT_POSTPROCESSING itself; here it has to be asked for. Xcode strips + /// before it signs, so the signature covers the stripped binary -- stripping + /// afterwards would break it. dwarf-with-dsym keeps a dSYM beside the app so + /// crash reports from a stripped build can still be symbolicated. + // Package-visible so a test can assert it without running xcodebuild. + static List stripSettings(String configuration) { + List out = new ArrayList(); + if (configuration != null && configuration.trim().equalsIgnoreCase("Debug")) { + return out; + } + out.add("DEPLOYMENT_POSTPROCESSING=YES"); + out.add("STRIP_INSTALLED_PRODUCT=YES"); + out.add("STRIP_STYLE=all"); + out.add("DEBUG_INFORMATION_FORMAT=dwarf-with-dsym"); + return out; + } + /// Removes a tree, tolerating one that is not there. /// /// No symlink following: descending through a link would delete outside the diff --git a/maven/build-engine/src/main/java/com/codename1/builders/NativeThemes.java b/maven/build-engine/src/main/java/com/codename1/builders/NativeThemes.java new file mode 100644 index 00000000000..6cb807a4e41 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/builders/NativeThemes.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/// Which of the port's native theme resources an application actually ships. +/// +/// The iOS and macOS ports each carry every native theme -- iPhone, iOS 7, the +/// two modern generations and, on macOS, Aqua -- about a megabyte between them, +/// and the builders used to copy all of them into every application. The +/// runtime loads exactly one: the mode the builder writes into the generated +/// stub decides it (`IOSImplementation.setIosMode`), nothing reads a theme mode +/// at run time, so the one it will load is known when the application is built. +/// +/// The choice here mirrors `IOSImplementation.installNativeTheme()` and +/// `MacImplementation.nativeThemeResourceName()`. A theme the application's own +/// classes name -- `Resources.open("/iOS7Theme.res")` -- is kept as well, since +/// that is a use no build hint can see. +/// +/// The `custom` mode ships none of them. It says the application's own theme is +/// the only one it uses -- the theme pairs it with `includeNativeBool: false` -- +/// and every port's runtime answers it by installing nothing, so a platform theme +/// in the bundle would be dead weight. +final class NativeThemes { + + /// Every native theme a port jar carries, without the `.res`. + static final String[] ALL = { + "iPhoneTheme", "iOS7Theme", "iOSModernTheme", "iOSModern27Theme", "MacOSAquaTheme" + }; + + private NativeThemes() { + } + + /// The native theme the runtime will install for `mode`, without the `.res`, + /// or null for `custom`, which installs none. + /// + /// @param mode the theme mode written into the stub; null means the runtime default, "auto" + /// @param generation the iOS modern generation, "26" or "27"; null means 26 + /// @param mac true for the native macOS port, whose Aqua theme answers auto/aqua/native + static String themeFor(String mode, String generation, boolean mac) { + String m = mode == null ? "auto" : asciiLower(mode.trim()); + if (isCustom(m)) { + return null; + } + if (mac && ("auto".equals(m) || "aqua".equals(m) || "native".equals(m))) { + return "MacOSAquaTheme"; + } + if ("modern".equals(m) || "liquid".equals(m)) { + return "27".equals(generation) ? "iOSModern27Theme" : "iOSModernTheme"; + } + if ("ios7".equals(m) || "flat".equals(m) || "auto".equals(m)) { + return "iOS7Theme"; + } + // "legacy", "iphone", "material" and anything else fall to the pre-flat + // theme in installNativeTheme(), so that is the one they need. + return "iPhoneTheme"; + } + + /// Every native theme the Android port carries, without the `.res`. + static final String[] ANDROID_ALL = {"androidTheme", "AndroidMaterialTheme", "android_holo_light"}; + + /// The Display properties AndroidImplementation.installNativeTheme() reads the + /// mode from. The builder writes them into the stub from the build hints, but an + /// application can set them itself before the theme is installed, so one that + /// names any of them keeps every Android theme. + static final String[] ANDROID_MODE_PROPERTIES = { + "and.themeMode", "cn1.androidTheme", "nativeTheme", "cn1.nativeTheme", "and.hololight" + }; + + /// The Android themes a build needs: the one installNativeTheme() resolves the + /// hints to, and always android_holo_light, which hasNativeTheme() probes for on + /// every device from API 14 up and without which no native theme is installed. + /// None at all for `custom`, which installs no native theme and does not probe. + static Set androidThemesFor(String andThemeMode, String cn1AndroidTheme, + String nativeTheme, String cn1NativeTheme, String hololight) { + String mode = andThemeMode != null ? andThemeMode : cn1AndroidTheme; + if (mode == null) { + String shared = nativeTheme != null ? nativeTheme : cn1NativeTheme; + if ("modern".equalsIgnoreCase(shared) || "native".equalsIgnoreCase(shared)) { + mode = "material"; + } else if (isCustom(shared)) { + mode = "custom"; + } else if ("legacy".equalsIgnoreCase(shared)) { + mode = "hololight"; + } else if ("true".equalsIgnoreCase(hololight)) { + mode = "legacy"; + } else { + mode = "hololight"; + } + } else { + mode = asciiLower(mode.trim()); + } + Set keep = new LinkedHashSet(); + if (isCustom(mode)) { + return keep; + } + if ("material".equals(mode) || "modern".equals(mode) || "auto".equals(mode)) { + keep.add("AndroidMaterialTheme"); + } else if (!"hololight".equals(mode) && !"holo".equals(mode)) { + keep.add("androidTheme"); + } + keep.add("android_holo_light"); + return keep; + } + + /// Deletes the Android themes `keep` does not name from `dir`, unless the + /// application sets the theme mode itself, in which case the choice is made at + /// run time and every theme stays. + static List removeUnusedAndroid(File dir, Set keep, File appClasses) + throws IOException { + if (namesAny(appClasses, ANDROID_MODE_PROPERTIES)) { + return new ArrayList(); + } + return removeUnused(dir, keep, ANDROID_ALL, appClasses); + } + + /// The runtime property an application reads to learn which modern theme the + /// port carries, and then installs it itself -- the screenshot suite does, and + /// its dark captures all came out light when that theme had been filtered out. + /// IOSImplementation answers it with the configured generation's modern theme. + static final String NATIVE_THEME_RESOURCE_PROPERTY = "cn1.nativeThemeResource"; + + /// Removes the Apple native themes this build does not need from `dir`: every + /// one except the theme the stub's mode will install, a theme the application + /// names itself, and -- when the application asks the port for its modern + /// theme through {@link #NATIVE_THEME_RESOURCE_PROPERTY} -- that modern theme. + /// + /// @return the names removed, for the build log + static List removeUnusedApple(File dir, String mode, String generation, boolean mac, + File appClasses) throws IOException { + Set keep = new LinkedHashSet(); + String theme = themeFor(mode, generation, mac); + if (theme != null) { + keep.add(theme); + } + if (namesAny(appClasses, new String[] {NATIVE_THEME_RESOURCE_PROPERTY})) { + keep.add(themeFor("modern", generation, false)); + } + return removeUnused(dir, keep, ALL, appClasses); + } + + /// True for the `custom` theme mode: the application ships the only theme it + /// uses, so no platform theme is packaged and none is installed. + static boolean isCustom(String mode) { + return mode != null && "custom".equalsIgnoreCase(mode.trim()); + } + + /// The single native theme a native desktop port (Linux, Windows) bundles in its + /// port jar, and installs from `init()` whenever the file is present. + /// + /// Those ports have one theme and no mode to choose between, so the only question + /// is whether to ship it: not when `desktop.themeMode` is `custom`, or when it is + /// unset and the cross-platform `nativeTheme` (`cn1.nativeTheme`) is. The same + /// resolution the JavaSE desktop port runs. + /// + /// @return true when the port's theme file should be removed before translation + static boolean desktopShipsNoTheme(String desktopThemeMode, String nativeTheme, + String cn1NativeTheme) { + if (desktopThemeMode != null && desktopThemeMode.trim().length() > 0) { + return isCustom(desktopThemeMode); + } + return isCustom(nativeTheme != null ? nativeTheme : cn1NativeTheme); + } + + /// Removes `fileName` from `dir` when present, for a desktop port whose theme + /// the build does not ship. See [#desktopShipsNoTheme(String, String, String)]. + /// + /// @return true when the file was there and is now gone + static boolean removeDesktopTheme(File dir, String fileName) throws IOException { + File f = new File(dir, fileName); + if (!f.isFile()) { + return false; + } + if (!f.delete()) { + throw new IOException("Could not remove the unused theme " + f); + } + return true; + } + + /// Deletes from `dir` every native theme other than `keep` and the ones the + /// application names in `appClasses`. + /// + /// @return the names removed, for the build log + static List removeUnused(File dir, String keep, File appClasses) throws IOException { + Set k = new LinkedHashSet(); + k.add(keep); + return removeUnused(dir, k, ALL, appClasses); + } + + private static List removeUnused(File dir, Set keep, String[] candidates, + File appClasses) throws IOException { + List removed = new ArrayList(); + if (dir == null || !dir.isDirectory()) { + return removed; + } + Set referenced = new LinkedHashSet(); + for (String name : candidates) { + if (namesAny(appClasses, new String[] {name + ".res"})) { + referenced.add(name); + } + } + for (String name : candidates) { + if (keep.contains(name) || referenced.contains(name)) { + continue; + } + File f = new File(dir, name + ".res"); + if (f.isFile()) { + if (!f.delete()) { + throw new IOException("Could not remove the unused theme " + f); + } + removed.add(name); + } + } + return removed; + } + + /// True when a class under `classesDir` contains any of `needles` as a string. + /// + /// The ports' own `com.codename1.impl` packages are skipped, since they name + /// every theme and every mode property, and so is `com.codename1.annotations`, + /// whose build-hint declarations name the properties without setting them. + static boolean namesAny(File classesDir, String[] needles) throws IOException { + return classesDir != null && classesDir.isDirectory() + && scan(classesDir, classesDir, needles); + } + + private static boolean scan(File root, File f, String[] needles) throws IOException { + if (f.isDirectory()) { + String rel = root.toPath().relativize(f.toPath()).toString().replace('\\', '/'); + if ("com/codename1/impl".equals(rel) || "com/codename1/annotations".equals(rel)) { + return false; + } + File[] children = f.listFiles(); + if (children != null) { + for (File c : children) { + if (scan(root, c, needles)) { + return true; + } + } + } + return false; + } + if (!f.getName().endsWith(".class")) { + return false; + } + // The class file's constant pool holds a string literal as modified UTF-8, + // which for these ASCII names is the same bytes as the name itself. + String body = new String(Files.readAllBytes(f.toPath()), StandardCharsets.ISO_8859_1); + for (String needle : needles) { + if (body.contains(needle)) { + return true; + } + } + return false; + } + + private static String asciiLower(String s) { + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + b.append(c >= 'A' && c <= 'Z' ? (char) (c + ('a' - 'A')) : c); + } + return b.toString(); + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java index bc8e9c509b8..9050a837cdb 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java @@ -234,6 +234,14 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException // C/C++ sources), the same mechanism parparvm-compiler.jar uses. extractJarResource("/WindowsPort.jar", portClasses); extractJarResource("/nativewindows.jar", nativeSources); + // The port installs its bundled theme from init() whenever the file is there, + // before any application code runs, so the custom theme mode -- the + // application's own theme is the only one -- is honoured by not shipping it. + if (NativeThemes.desktopShipsNoTheme(request.getArg("desktop.themeMode", null), + request.getArg("nativeTheme", null), request.getArg("cn1.nativeTheme", null)) + && NativeThemes.removeDesktopTheme(portClasses, "windowsNativeTheme.res")) { + log("Native theme not used by this build, not shipped: windowsNativeTheme.res"); + } } catch (Exception ex) { throw new BuildException("Failed to stage the WindowsPort native layer. The codenameone " + "maven plugin must provide the codenameone-windows 'bundle' artifact " @@ -1250,6 +1258,9 @@ private void writeBootstrapStub(BuildRequest request, File classesDir, File stub // clients and @OpenTelemetry all compiled here and did nothing at run time. src.append(routeDispatcherInstallSource(classesDir, " ")); src.append(annotationFrameworksInstallSource(classesDir, " ")); + // desktop.width/height: before init, which creates the window. See + // Executor.desktopWindowSizeStubCall. + src.append(desktopWindowSizeStubCall(request, "com.codename1.impl.windows.WindowsImplementation")); src.append(" Display.init(null);\n"); // The application's identity, which nothing else gives this platform. The stub passes // null to Display.init, so the implementation never derives a package from an object, and diff --git a/maven/build-engine/src/test/java/com/codename1/builders/DesktopWindowSizeStubCallTest.java b/maven/build-engine/src/test/java/com/codename1/builders/DesktopWindowSizeStubCallTest.java new file mode 100644 index 00000000000..7814cf9212c --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/DesktopWindowSizeStubCallTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// desktop.width and desktop.height reach the native Linux and Windows stubs as a call made +/// BEFORE Display.init, since init is what creates the window. Both hints were declared and read +/// only by the JavaSE wrapper, so every ParparVM desktop window opened at a hard-coded 800x600. +class DesktopWindowSizeStubCallTest { + + /// Executor is abstract; none of these hooks are exercised here. + private static final class TestExecutor extends Executor { + @Override + protected String getDeviceIdCode() { + return ""; + } + + @Override + protected String generatePeerComponentCreationCode(String methodCallString) { + return ""; + } + + @Override + protected String convertPeerComponentToNative(String param) { + return ""; + } + + @Override + public boolean build(File sourceZip, BuildRequest request) { + return false; + } + } + + private static final String IMPL = "com.codename1.impl.linux.LinuxImplementation"; + + private final TestExecutor executor = new TestExecutor(); + + private static BuildRequest size(String width, String height) { + BuildRequest r = new BuildRequest(); + if (width != null) { + r.putArgument("desktop.width", width); + } + if (height != null) { + r.putArgument("desktop.height", height); + } + return r; + } + + @Test + void noHintKeepsThePortDefault() { + assertEquals("", executor.desktopWindowSizeStubCall(size(null, null), IMPL)); + } + + @Test + void bothDimensionsReachTheStub() { + assertEquals(" " + IMPL + ".setDefaultWindowSize(1280, 720);\n", + executor.desktopWindowSizeStubCall(size("1280", " 720 "), IMPL)); + } + + @Test + void oneDimensionAloneLeavesTheOtherToThePort() { + // Zero is "keep the port default" in setDefaultWindowSize. + assertEquals(" " + IMPL + ".setDefaultWindowSize(0, 900);\n", + executor.desktopWindowSizeStubCall(size(null, "900"), IMPL)); + } + + @Test + void anUnusableValueIsDroppedRatherThanCompiledIntoTheStub() { + assertEquals("", executor.desktopWindowSizeStubCall(size("wide", "-3"), IMPL)); + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/builders/ExecutorArchiveExtractionTest.java b/maven/build-engine/src/test/java/com/codename1/builders/ExecutorArchiveExtractionTest.java index 308c268cdf6..e6e0325d710 100644 --- a/maven/build-engine/src/test/java/com/codename1/builders/ExecutorArchiveExtractionTest.java +++ b/maven/build-engine/src/test/java/com/codename1/builders/ExecutorArchiveExtractionTest.java @@ -129,6 +129,26 @@ void preservesSupportedLeadingSlashVirtualEntries() throws Exception { "javase.lib.tar"); } + /** + * Dart API declarations a runtime jar carries for the transpiler are build-time + * metadata: the application jar merges every dependency, and they used to be + * copied into every installed bundle. + */ + @Test + void doesNotShipTheTranspilersDartDeclarations() throws Exception { + File root = temporaryDirectory.resolve("dart-metadata").toFile(); + unzipSpecialEntry(root, "META-INF/dart/flutter_material.dart"); + java.util.List written = new java.util.ArrayList<>(); + java.nio.file.Files.walk(root.toPath()).filter(java.nio.file.Files::isRegularFile) + .forEach(p -> written.add(root.toPath().relativize(p).toString())); + assertTrue(written.isEmpty(), "nothing should be extracted, got: " + written); + + File other = temporaryDirectory.resolve("dart-neighbour").toFile(); + File resources = unzipSpecialEntry(other, "gallery.res"); + assertTrue(new File(resources, "gallery.res").isFile(), + "an ordinary resource must still be extracted"); + } + private static byte[] zipEntry(String name, String contents) throws IOException { byte[] payload = contents.getBytes(StandardCharsets.UTF_8); diff --git a/maven/build-engine/src/test/java/com/codename1/builders/IPhoneBuilderIconsTest.java b/maven/build-engine/src/test/java/com/codename1/builders/IPhoneBuilderIconsTest.java new file mode 100644 index 00000000000..2d1603de619 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/IPhoneBuilderIconsTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/// The iOS app icon lives only in the AppIcon asset catalog. +/// +/// The builder used to copy every icon size, Icon.png and iTunesArtwork into the bundle +/// root as well, for the top-level CFBundleIconFiles of iOS 6 and older -- about 0.6MB +/// that nothing on a supported iOS reads. These pin the replacement: every catalog image +/// is written, into the catalog and nowhere else, each is one the catalog's Contents.json +/// names, and the Info.plist template no longer lists the loose files, which would +/// otherwise be a dangling reference App Store validation reports. +public class IPhoneBuilderIconsTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void everyCatalogIconIsWrittenAtItsSizeAndNothingElse() throws Exception { + File dir = tmp.newFolder("AppIcon.appiconset"); + BufferedImage source = new BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB); + new IPhoneBuilder().writeAppIcons(source, dir); + + Set expected = new HashSet(); + for (Object[] icon : IPhoneBuilder.APP_ICONS) { + String name = (String) icon[0]; + expected.add(name); + BufferedImage written = ImageIO.read(new File(dir, name)); + int size = ((Integer) icon[1]).intValue(); + assertEquals(name + " width", size, written.getWidth()); + assertEquals(name + " height", size, written.getHeight()); + } + assertEquals(new HashSet(Arrays.asList(dir.list())), expected); + } + + @Test + public void everyCatalogIconIsNamedByTheCatalog() throws Exception { + String contents = new String(Files.readAllBytes( + Paths.get("..", "..", "vm", "ByteCodeTranslator", "src", "Icons.json")), StandardCharsets.UTF_8); + Set named = new HashSet(); + Matcher m = Pattern.compile("\"filename\"\\s*:\\s*\"([^\"]+)\"").matcher(contents); + while (m.find()) { + named.add(m.group(1)); + } + for (Object[] icon : IPhoneBuilder.APP_ICONS) { + assertTrue(icon[0] + " is written but the asset catalog does not use it", + named.contains((String) icon[0])); + } + } + + @Test + public void theInfoPlistTemplateNamesNoLooseIcons() throws Exception { + String plist = new String(Files.readAllBytes(Paths.get("..", "..", "vm", "ByteCodeTranslator", "src", + "template", "template", "template-Info.plist")), StandardCharsets.UTF_8); + // CFBundleIcons / CFBundleIconName come from actool for the catalog; the + // top-level list is the one that named the loose files. + assertFalse("the top-level CFBundleIconFiles names files the bundle no longer carries", + plist.contains("CFBundleIconFiles")); + for (Object[] icon : IPhoneBuilder.APP_ICONS) { + assertFalse(icon[0] + " is still named by the Info.plist template", + plist.contains(">" + icon[0] + "<")); + } + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/builders/MacOSBuildHintsTest.java b/maven/build-engine/src/test/java/com/codename1/builders/MacOSBuildHintsTest.java index 0ebfe6b71ba..b31eeb9753f 100644 --- a/maven/build-engine/src/test/java/com/codename1/builders/MacOSBuildHintsTest.java +++ b/maven/build-engine/src/test/java/com/codename1/builders/MacOSBuildHintsTest.java @@ -626,6 +626,9 @@ public void theNativeThemeDefaultsToAquaAndIsConstrainedToKnownModes() { // whitelist above lets it through unchanged. assertEquals("native", parse(raw("nativeTheme", "native"), "p").getThemeMode()); assertEquals("native", parse(raw("cn1.nativeTheme", "native"), "p").getThemeMode()); + // custom installs no native theme; the whitelist used to turn it into "modern". + assertEquals("custom", parse(raw("nativeTheme", "custom"), "p").getThemeMode()); + assertEquals("custom", parse(raw("macos.themeMode", "custom"), "p").getThemeMode()); // The value is interpolated into generated Java source, so it is // constrained to what the runtime understands rather than passed diff --git a/maven/build-engine/src/test/java/com/codename1/builders/MacOSStripSettingsTest.java b/maven/build-engine/src/test/java/com/codename1/builders/MacOSStripSettingsTest.java new file mode 100644 index 00000000000..366ac13bc95 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/MacOSStripSettingsTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; + +/// A Release macOS build is stripped by xcodebuild, before it is signed. +public class MacOSStripSettingsTest { + + @Test + public void aReleaseBuildIsStripped() { + assertEquals(Arrays.asList("DEPLOYMENT_POSTPROCESSING=YES", "STRIP_INSTALLED_PRODUCT=YES", + "STRIP_STYLE=all", "DEBUG_INFORMATION_FORMAT=dwarf-with-dsym"), + MacOSNativeBuilder.stripSettings("Release")); + assertEquals(MacOSNativeBuilder.stripSettings("Release"), MacOSNativeBuilder.stripSettings(null)); + } + + @Test + public void aDebugBuildKeepsItsSymbols() { + assertEquals(Collections.emptyList(), MacOSNativeBuilder.stripSettings("Debug")); + assertEquals(Collections.emptyList(), MacOSNativeBuilder.stripSettings(" debug ")); + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/builders/NativeThemesTest.java b/maven/build-engine/src/test/java/com/codename1/builders/NativeThemesTest.java new file mode 100644 index 00000000000..cf4e14e5cdf --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/NativeThemesTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/// The builders ship only the native theme the runtime will install. +public class NativeThemesTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void theIosChainPicksWhatInstallNativeThemeLoads() { + assertEquals("iOS7Theme", NativeThemes.themeFor(null, null, false)); + assertEquals("iOS7Theme", NativeThemes.themeFor("auto", "26", false)); + assertEquals("iOS7Theme", NativeThemes.themeFor("FLAT", "26", false)); + assertEquals("iOSModernTheme", NativeThemes.themeFor("modern", "26", false)); + assertEquals("iOSModernTheme", NativeThemes.themeFor("liquid", null, false)); + assertEquals("iOSModern27Theme", NativeThemes.themeFor("modern", "27", false)); + assertEquals("iPhoneTheme", NativeThemes.themeFor("legacy", "26", false)); + assertEquals("iPhoneTheme", NativeThemes.themeFor("material", "26", false)); + } + + @Test + public void macGetsAquaUnlessItAsksForAnIosLook() { + assertEquals("MacOSAquaTheme", NativeThemes.themeFor("aqua", null, true)); + assertEquals("MacOSAquaTheme", NativeThemes.themeFor("auto", null, true)); + assertEquals("MacOSAquaTheme", NativeThemes.themeFor("native", null, true)); + // The macOS stub never sets a generation, so the modern theme there is 26. + assertEquals("iOSModernTheme", NativeThemes.themeFor("modern", null, true)); + assertEquals("iOS7Theme", NativeThemes.themeFor("ios7", null, true)); + } + + @Test + public void onlyTheLoadedThemeSurvivesAndOtherFilesAreUntouched() throws IOException { + File dir = portDir(); + List removed = NativeThemes.removeUnused(dir, "MacOSAquaTheme", tmp.newFolder("classes")); + assertEquals(Arrays.asList("iPhoneTheme", "iOS7Theme", "iOSModernTheme", "iOSModern27Theme"), removed); + assertTrue(new File(dir, "MacOSAquaTheme.res").isFile()); + assertTrue("not a native theme", new File(dir, "CN1Resource.res").isFile()); + assertTrue("not a theme at all", new File(dir, "cn1_globals.m").isFile()); + } + + @Test + public void aThemeTheApplicationOpensItselfIsKept() throws IOException { + File dir = portDir(); + File classes = tmp.newFolder("appclasses"); + File cls = new File(classes, "com/example/App.class"); + cls.getParentFile().mkdirs(); + Files.write(cls.toPath(), bytesWith("/iPhoneTheme.res")); + NativeThemes.removeUnused(dir, "iOS7Theme", classes); + assertTrue(new File(dir, "iPhoneTheme.res").isFile()); + assertTrue(new File(dir, "iOS7Theme.res").isFile()); + assertFalse(new File(dir, "iOSModernTheme.res").isFile()); + } + + @Test + public void thePortsOwnReferencesDoNotKeepAnything() throws IOException { + // IOSImplementation names every theme; that must not count as a use. + File dir = portDir(); + File classes = tmp.newFolder("merged"); + File impl = new File(classes, "com/codename1/impl/ios/IOSImplementation.class"); + impl.getParentFile().mkdirs(); + Files.write(impl.toPath(), bytesWith("/iPhoneTheme.res /iOSModern27Theme.res")); + NativeThemes.removeUnused(dir, "iOS7Theme", classes); + assertFalse(new File(dir, "iPhoneTheme.res").isFile()); + assertFalse(new File(dir, "iOSModern27Theme.res").isFile()); + } + + @Test + public void anAppThatAsksThePortForItsModernThemeKeepsIt() throws IOException { + // The screenshot suite installs the theme cn1.nativeThemeResource names, + // in an app built in the default auto mode. Filtering by the mode alone + // removed the modern theme, and every dark capture rendered light. + File dir = portDir(); + File classes = tmp.newFolder("suite"); + File cls = new File(classes, "com/example/tests/DualAppearanceBaseTest.class"); + cls.getParentFile().mkdirs(); + Files.write(cls.toPath(), bytesWith("cn1.nativeThemeResource")); + NativeThemes.removeUnusedApple(dir, "auto", "27", false, classes); + assertTrue(new File(dir, "iOS7Theme.res").isFile()); + assertTrue(new File(dir, "iOSModern27Theme.res").isFile()); + assertFalse(new File(dir, "iOSModernTheme.res").isFile()); + assertFalse(new File(dir, "iPhoneTheme.res").isFile()); + } + + @Test + public void anAppThatDoesNotAskKeepsOnlyTheModeTheme() throws IOException { + File dir = portDir(); + NativeThemes.removeUnusedApple(dir, "auto", "27", false, tmp.newFolder("plain")); + assertTrue(new File(dir, "iOS7Theme.res").isFile()); + assertFalse(new File(dir, "iOSModern27Theme.res").isFile()); + } + + @Test + public void androidKeepsTheResolvedThemeAndTheOneHasNativeThemeProbes() { + assertEquals(set("android_holo_light"), NativeThemes.androidThemesFor(null, null, null, null, null)); + assertEquals(set("AndroidMaterialTheme", "android_holo_light"), + NativeThemes.androidThemesFor(null, null, "modern", null, null)); + assertEquals(set("AndroidMaterialTheme", "android_holo_light"), + NativeThemes.androidThemesFor("Material", null, null, null, null)); + assertEquals(set("androidTheme", "android_holo_light"), + NativeThemes.androidThemesFor("legacy", null, null, null, null)); + assertEquals(set("androidTheme", "android_holo_light"), + NativeThemes.androidThemesFor(null, null, null, null, "true")); + // and.themeMode wins over the cross-platform hint, as it does at run time. + assertEquals(set("android_holo_light"), + NativeThemes.androidThemesFor("hololight", null, "modern", null, null)); + } + + @Test + public void androidRemovesOnlyThePortThemesItWillNotLoad() throws IOException { + File dir = tmp.newFolder("assets"); + for (String name : NativeThemes.ANDROID_ALL) { + Files.write(new File(dir, name + ".res").toPath(), new byte[] {1}); + } + Files.write(new File(dir, "theme.res").toPath(), new byte[] {1}); + List removed = NativeThemes.removeUnusedAndroid(dir, + NativeThemes.androidThemesFor(null, null, "modern", null, null), tmp.newFolder("c1")); + assertEquals(Arrays.asList("androidTheme"), removed); + assertTrue(new File(dir, "AndroidMaterialTheme.res").isFile()); + assertTrue(new File(dir, "android_holo_light.res").isFile()); + assertTrue("the application's own theme", new File(dir, "theme.res").isFile()); + } + + @Test + public void anAndroidAppThatSetsTheModeItselfKeepsEveryTheme() throws IOException { + File dir = tmp.newFolder("assets2"); + for (String name : NativeThemes.ANDROID_ALL) { + Files.write(new File(dir, name + ".res").toPath(), new byte[] {1}); + } + File classes = tmp.newFolder("c2"); + File cls = new File(classes, "com/example/App.class"); + cls.getParentFile().mkdirs(); + Files.write(cls.toPath(), bytesWith("and.themeMode")); + assertTrue(NativeThemes.removeUnusedAndroid(dir, + NativeThemes.androidThemesFor(null, null, null, null, null), classes).isEmpty()); + assertTrue(new File(dir, "androidTheme.res").isFile()); + } + + @Test + public void aBuildHintDeclarationIsNotAUse() throws IOException { + // com.codename1.annotations.buildhints names the properties to document them. + File classes = tmp.newFolder("c3"); + File cls = new File(classes, "com/codename1/annotations/buildhints/Android.class"); + cls.getParentFile().mkdirs(); + Files.write(cls.toPath(), bytesWith("and.themeMode nativeTheme")); + assertFalse(NativeThemes.namesAny(classes, NativeThemes.ANDROID_MODE_PROPERTIES)); + } + + @Test + public void customShipsNoAppleTheme() throws IOException { + assertNull(NativeThemes.themeFor("custom", "26", false)); + assertNull(NativeThemes.themeFor(" CUSTOM ", null, true)); + File dir = portDir(); + List removed = NativeThemes.removeUnusedApple(dir, "custom", "26", false, + tmp.newFolder("customClasses")); + assertEquals(Arrays.asList(NativeThemes.ALL), removed); + assertTrue("not a native theme", new File(dir, "CN1Resource.res").isFile()); + } + + @Test + public void customShipsNoAndroidTheme() throws IOException { + assertTrue(NativeThemes.androidThemesFor("custom", null, null, null, null).isEmpty()); + assertTrue(NativeThemes.androidThemesFor(null, "Custom", null, null, null).isEmpty()); + assertTrue(NativeThemes.androidThemesFor(null, null, "custom", null, null).isEmpty()); + assertTrue(NativeThemes.androidThemesFor(null, null, null, "custom", null).isEmpty()); + // The platform hint outranks the shared one, in both directions. + assertEquals(set("AndroidMaterialTheme", "android_holo_light"), + NativeThemes.androidThemesFor("modern", null, "custom", null, null)); + File dir = tmp.newFolder(); + for (String name : NativeThemes.ANDROID_ALL) { + Files.write(new File(dir, name + ".res").toPath(), new byte[] {1}); + } + List removed = NativeThemes.removeUnusedAndroid(dir, + NativeThemes.androidThemesFor(null, null, "custom", null, null), + tmp.newFolder("customAndroidClasses")); + assertEquals(Arrays.asList(NativeThemes.ANDROID_ALL), removed); + } + + @Test + public void desktopPortsDropTheirThemeOnlyForCustom() throws IOException { + assertTrue(NativeThemes.desktopShipsNoTheme("custom", null, null)); + assertTrue(NativeThemes.desktopShipsNoTheme(null, "custom", null)); + assertTrue(NativeThemes.desktopShipsNoTheme("", null, "custom")); + // desktop.themeMode decides when it is set. + assertFalse(NativeThemes.desktopShipsNoTheme("auto", "custom", null)); + assertFalse(NativeThemes.desktopShipsNoTheme(null, "modern", null)); + assertFalse(NativeThemes.desktopShipsNoTheme(null, null, null)); + File dir = tmp.newFolder(); + File theme = new File(dir, "linuxNativeTheme.res"); + Files.write(theme.toPath(), new byte[] {1}); + assertTrue(NativeThemes.removeDesktopTheme(dir, "linuxNativeTheme.res")); + assertFalse(theme.exists()); + assertFalse(NativeThemes.removeDesktopTheme(dir, "linuxNativeTheme.res")); + } + + private static java.util.Set set(String... names) { + return new java.util.LinkedHashSet(Arrays.asList(names)); + } + + private File portDir() throws IOException { + File dir = tmp.newFolder(); + for (String name : NativeThemes.ALL) { + Files.write(new File(dir, name + ".res").toPath(), new byte[] {1}); + } + Files.write(new File(dir, "CN1Resource.res").toPath(), new byte[] {1}); + Files.write(new File(dir, "cn1_globals.m").toPath(), new byte[] {1}); + return dir; + } + + private static byte[] bytesWith(String s) { + byte[] head = {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE, 0, 0, 0, 52}; + byte[] body = s.getBytes(StandardCharsets.US_ASCII); + byte[] out = new byte[head.length + body.length]; + System.arraycopy(head, 0, out, 0, head.length); + System.arraycopy(body, 0, out, head.length, body.length); + return out; + } +} diff --git a/maven/cn1app-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/maven/cn1app-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index 9fb192cc5a0..45f0be21ab7 100644 --- a/maven/cn1app-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/maven/cn1app-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -15,11 +15,12 @@ - auto + source/target. Java 17 is the default (matching start.codenameone.com); + it is also required for Flutter support (src/main/flutter), whose + transpiler emits Java 17 source. Pass 8 explicitly for legacy + projects, or "auto" to pick 17 on a JDK >= 17 and 8 otherwise via + archetype-post-generate.groovy. --> + 17 diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml index dcadd0d70f1..1233abb7455 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml @@ -19,6 +19,18 @@ provided + + + + transcode-flutter + generate-sources + + transcode-flutter + + generate-gui-sources process-sources diff --git a/maven/codenameone-maven-plugin/pom.xml b/maven/codenameone-maven-plugin/pom.xml index 2074a09cd73..8b87bfb2d95 100644 --- a/maven/codenameone-maven-plugin/pom.xml +++ b/maven/codenameone-maven-plugin/pom.xml @@ -130,6 +130,11 @@ codenameone-lottie-transcoder ${project.version} + + ${project.groupId} + codenameone-dart-transpiler + ${project.version} + org.apache.maven maven-plugin-api diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateAppProjectMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateAppProjectMojo.java index 57377dd29b3..e5c55492bc8 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateAppProjectMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateAppProjectMojo.java @@ -66,6 +66,14 @@ public class GenerateAppProjectMojo extends AbstractMojo { @Parameter(property="artifactId") private String artifactId; + /// The Java release the generated project should target: 8 or 17. + /// + /// Left unset the archetype chooses, which is what a user normally wants. + /// A build that will compile the generated project with a specific JDK has + /// to say so, because the two have to agree. + @Parameter(property = "javaVersion") + private String javaVersion; + @Parameter(property="groupId") private String groupId; @@ -125,6 +133,17 @@ private void generateProject() throws MojoExecutionException{ "package="+packageName() }; Properties props = new Properties(); + // The archetype's javaVersion, when the caller named one. + // + // This goal invokes archetype:generate as a SEPARATE Maven process, and + // that process does not inherit this one's -D properties -- only what is + // put in this list reaches it. So a caller passing -DjavaVersion=8 was + // silently ignored and got the archetype's default instead, which is 17: + // a generated project that a JDK 8 build cannot compile, failing with + // "invalid target release: 17" in a module the caller never wrote. + if (javaVersion != null && javaVersion.trim().length() > 0) { + props.setProperty("javaVersion", javaVersion.trim()); + } for (String prop : propsArr) { int eqpos = prop.indexOf("="); if (eqpos > 0) { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/SimulatorMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/SimulatorMojo.java index 0ec1fa2114a..9c94d13601b 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/SimulatorMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/SimulatorMojo.java @@ -215,6 +215,30 @@ protected void processBuffer(ByteArrayOutputStream buffer) { + /** + * Hands every {@code cn1.*} system property to the simulator. + * + *

The simulator runs in a JVM of its own, so {@code mvn cn1:run -Dcn1.something=x} + * sets the property on Maven and nowhere the application can see it -- which looks + * exactly like the property being ignored. Only {@code ffmpeg.dir} was forwarded, one + * key at a time, so anything else needed a code change to become settable at all.

+ * + *

Scoped to the {@code cn1.} prefix rather than passing the whole environment: + * Maven's own properties (its repository, its offline flag, the JDK's) mean something + * different inside a forked JVM, and a few of them would change how it behaves.

+ */ + private void forwardCn1Properties(Java java) { + Properties props = System.getProperties(); + for (String name : props.stringPropertyNames()) { + if (name.startsWith("cn1.")) { + Variable v = new Variable(); + v.setKey(name); + v.setValue(props.getProperty(name)); + java.addSysproperty(v); + } + } + } + private Path prepareClasspath(Java java) { Log log = getLog(); log.debug("Preparing classpath for Simulator"); @@ -225,6 +249,7 @@ private Path prepareClasspath(Java java) { v.setValue(System.getProperty("ffmpeg.dir")); java.addSysproperty(v); } + forwardCn1Properties(java); copyKotlinIncrementalCompileOutputToOutputDir(); for (Artifact artifact : project.getArtifacts()) { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java new file mode 100644 index 00000000000..2dffaba29ae --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/TranscodeFlutterMojo.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.dart.transpiler.api.DartTranspiler; +import com.codename1.dart.transpiler.api.Diagnostic; +import com.codename1.dart.transpiler.api.TranspileRequest; +import com.codename1.dart.transpiler.api.TranspileResult; + +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Transpiles Flutter/Dart sources under {@code src/main/flutter} into Java + * source targeting the Codename One Flutter runtime + * ({@code codenameone-flutter-runtime}), so Flutter UI code runs as plain + * Codename One components at native speed -- no Dart VM or Flutter engine. + * + *

Source layout

+ *
    + *
  • {@code src/main/flutter/**/*.dart} -- Dart sources (whole-program + * transpile; subdirectories allowed)
  • + *
  • {@code src/main/flutter/assets/**} and {@code src/main/flutter/packages/**} -- + * bundled assets, flattened into the + * build output (Codename One resources are flat on every port) so + * {@code Image.asset(...)} resolves
  • + *
+ * + *

When the directory does not exist the goal is a silent no-op. When it + * exists, generated sources land in {@code target/generated-sources/flutter} + * (registered as a compile source root). Generated code is Java 17 source, + * so the build must run on JDK 17+ -- checked here with a friendly error. + * The transpile is whole-program with a digest-based fast skip; unchanged + * outputs are not rewritten, keeping incremental javac warm.

+ */ +@Mojo(name = "transcode-flutter", defaultPhase = LifecyclePhase.GENERATE_SOURCES, + requiresDependencyResolution = ResolutionScope.COMPILE) +public class TranscodeFlutterMojo extends AbstractCN1Mojo { + + private static final String RUNTIME_ARTIFACT = "codenameone-flutter-runtime"; + + @Parameter(property = "cn1.flutter.sourceDir", defaultValue = "${project.basedir}/src/main/flutter") + private File flutterSourceDir; + + @Parameter(property = "cn1.flutter.outputDir", defaultValue = "${project.build.directory}/generated-sources/flutter") + private File flutterOutputDir; + + @Parameter(property = "cn1.flutter.package", defaultValue = "com.codename1.generated.flutter") + private String flutterPackage; + + @Parameter(property = "cn1.flutter.stateFile", defaultValue = "${project.build.directory}/flutter-transpiler/state.txt") + private File stateFile; + + @Override + protected void executeImpl() throws MojoExecutionException, MojoFailureException { + if (flutterSourceDir == null || !flutterSourceDir.isDirectory()) { + sweepStaleOutput(); + // Flutter was removed from the project: its assets go with it. + removeStaleAssets(new HashSet()); + return; + } + checkJdk(); + checkRuntimeDependency(); + + TranspileRequest req = new TranspileRequest() + .sourceRoot(flutterSourceDir) + .outputDir(flutterOutputDir) + .packageName(flutterPackage) + .stateFile(stateFile); + // runtime dependencies contribute API stubs via META-INF/dart/*.dart + for (Object o : project.getArtifacts()) { + org.apache.maven.artifact.Artifact a = (org.apache.maven.artifact.Artifact) o; + if (a.getFile() != null) { + req.stubClasspathEntry(a.getFile()); + } + } + TranspileResult result = new DartTranspiler().transpile(req); + for (Diagnostic d : result.getDiagnostics()) { + String msg = "src/main/flutter/" + d.file + ":[" + d.line + "," + d.col + "] " + + d.message + " (dart2java:" + d.code + ")"; + if (d.severity == Diagnostic.Severity.ERROR) { + getLog().error(msg); + } else if (d.severity == Diagnostic.Severity.WARNING) { + getLog().warn(msg); + } else { + getLog().info(msg); + } + } + if (result.hasErrors()) { + throw new MojoFailureException("Flutter transpilation failed with " + + result.errors().size() + " error(s); see log above. " + + "Confirm the Dart files pass `dart analyze` -- constructs outside the " + + "currently supported subset are reported with a milestone code."); + } + if (result.isUpToDate()) { + getLog().info("Flutter sources are up to date"); + } else { + getLog().info("Transpiled Flutter sources to " + flutterOutputDir); + } + copyAssets(); + registerSourceRoot(); + } + + private void checkJdk() throws MojoFailureException { + String spec = System.getProperty("java.specification.version", "1.8"); + int major; + try { + major = Integer.parseInt(spec.startsWith("1.") ? spec.substring(2) : spec); + } catch (NumberFormatException e) { + major = 8; + } + if (major < 17) { + throw new MojoFailureException("Flutter support generates Java 17 source, but this build " + + "is running on JDK " + spec + ". Run Maven with JDK 17 or newer " + + "(e.g. from https://adoptium.net) to use src/main/flutter."); + } + } + + private void checkRuntimeDependency() throws MojoFailureException { + for (Object o : project.getDependencies()) { + Dependency d = (Dependency) o; + if (RUNTIME_ARTIFACT.equals(d.getArtifactId())) { + return; + } + } + throw new MojoFailureException("src/main/flutter contains Dart sources but the project " + + "does not declare the Flutter runtime dependency. Add this to common/pom.xml:\n\n" + + " \n" + + " com.codenameone\n" + + " " + RUNTIME_ARTIFACT + "\n" + + " ${cn1.version}\n" + + " \n"); + } + + /** + * Copies {@code src/main/flutter/assets} into the build output, flattened. + * + *

Codename One resources are flat on every port -- {@code getResourceAsStream} + * rejects a name containing a {@code '/'} past the leading one -- so the asset + * tree cannot be mirrored. Each asset is written to the output root under the + * name produced by {@link #flatAssetName}, which the Flutter runtime's + * {@code FlutterAssets} recomputes when resolving {@code Image.asset(...)}.

+ */ + void copyAssets() throws MojoExecutionException { + File outDir = new File(project.getBuild().getOutputDirectory()); + Set written = new HashSet(); + List keys = new ArrayList(); + int count = 0; + try { + // Both roots a pubspec asset key can start with. "assets/" is an app's own + // bundle; "packages/" is one it pulls from a dependency, which is how the + // gallery ships its artwork (packages/flutter_gallery_assets/assets/...). + // Only "assets/" was copied before, so every packaged asset key resolved to + // nothing on every port - the images were simply absent. + for (String root : ASSET_ROOTS) { + File dir = new File(flutterSourceDir, root); + if (!dir.isDirectory()) { + continue; + } + Files.createDirectories(outDir.toPath()); + // the root stays in the Flutter asset key, matching pubspec paths + count += flattenInto(dir, root, outDir, written, keys); + } + if (count > 0) { + writeAssetManifest(outDir, keys, written); + getLog().info("Flattened " + count + " Flutter asset(s) into the build output"); + } + } catch (IOException e) { + throw new MojoExecutionException("Failed copying Flutter assets", e); + } + removeStaleAssets(written); + } + + /** + * Deletes the flattened assets an earlier build wrote that this one did not, + * then records what this build wrote. + * + *

Flattening only ever ADDS to {@code target/classes}. An asset renamed or + * deleted in {@code src/main/flutter} -- or the whole Flutter tree removed -- + * therefore kept being packaged by every incremental build until a clean one: + * a bigger application, and a stale {@code Image.asset} lookup that still + * succeeded against a file the project no longer has.

+ * + *

Only names from this plugin's own record are ever deleted. Sweeping every + * {@code cn1f_*} file would be simpler, but the output directory is shared with + * the application's own resources, and a name this plugin did not write is not + * one it should remove.

+ */ + void removeStaleAssets(Set written) throws MojoExecutionException { + File outDir = new File(project.getBuild().getOutputDirectory()); + File manifest = assetManifest(); + try { + if (manifest.isFile()) { + for (String name : Files.readAllLines(manifest.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { + name = name.trim(); + // Defensive: a record entry must name a flat asset in the output + // root, never a path, whatever ended up in the file. + if (name.startsWith("cn1f_") && name.indexOf('/') < 0 && name.indexOf('\\') < 0 + && !written.contains(name)) { + Files.deleteIfExists(new File(outDir, name).toPath()); + } + } + } + if (written.isEmpty()) { + Files.deleteIfExists(manifest.toPath()); + } else { + List sorted = new ArrayList(written); + java.util.Collections.sort(sorted); + Files.createDirectories(manifest.getParentFile().toPath()); + Files.write(manifest.toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new MojoExecutionException("Failed removing stale Flutter assets", e); + } + } + + /** + * The record of flattened asset names. Beside the output rather than inside + * the generated-sources directory, which {@link #sweepStaleOutput} deletes. + */ + private File assetManifest() { + return new File(project.getBuild().getDirectory(), "cn1-flutter-assets.lst"); + } + + /** The directory names under {@code src/main/flutter} that hold bundled assets. */ + private static final String[] ASSET_ROOTS = {"assets", "packages"}; + + /** + * The bundled list of asset keys, one per line, which the runtime's + * {@code FlutterAssets} reads to learn which resolution variants exist. Flutter + * builds the same thing (AssetManifest) for the same reason: a variant can sit + * in any positive scale directory -- {@code 2.5x/}, or {@code 2x/} as well as + * {@code 2.0x/} -- so no fixed probe list finds them all, and one outside it was + * bundled and never used. Its name cannot collide with an asset: every asset key + * starts with {@code assets/} or {@code packages/}, so every asset's flat name + * carries a {@code _s} after that root. + */ + static final String ASSET_MANIFEST = "cn1f_AssetManifest.txt"; + + private void writeAssetManifest(File outDir, List keys, Set written) throws IOException { + List sorted = new ArrayList(keys); + java.util.Collections.sort(sorted); + Files.write(new File(outDir, ASSET_MANIFEST).toPath(), sorted, java.nio.charset.StandardCharsets.UTF_8); + written.add(ASSET_MANIFEST); + } + + private int flattenInto(File dir, String assetPrefix, File outDir, Set written, + List keys) throws IOException { + File[] children = dir.listFiles(); + if (children == null) { + return 0; + } + int count = 0; + for (File child : children) { + String key = assetPrefix + "/" + child.getName(); + if (child.isDirectory()) { + count += flattenInto(child, key, outDir, written, keys); + } else { + String flat = flatAssetName(key); + Files.copy(child.toPath(), new File(outDir, flat).toPath(), + StandardCopyOption.REPLACE_EXISTING); + written.add(flat); + keys.add(key); + count++; + } + } + return count; + } + + /** + * The flat resource name for a Flutter asset key. {@code '_'} is an escape + * always followed by one character -- {@code "__"} for an underscore, + * {@code "_s"} for the separator -- which is prefix-free, so no two keys + * share a name. Doubling underscores and using a single one as the + * separator was not: {@code a_/b} and {@code a/_b} both became + * {@code a___b}, and one asset was silently written over the other. + * Extensions survive for native bundlers. + * + *

Keep in sync with {@code com.codename1.flutter.FlutterAssets} in + * the Flutter runtime -- deliberately duplicated rather than shared, because + * this plugin must not depend on the runtime it builds against.

+ */ + static String flatAssetName(String assetKey) { + String p = assetKey; + while (p.startsWith("/")) { + p = p.substring(1); + } + StringBuilder sb = new StringBuilder("cn1f_"); + for (int i = 0; i < p.length(); i++) { + char c = p.charAt(i); + if (c == '_') { + sb.append("__"); + } else if (c == '/') { + sb.append("_s"); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private void sweepStaleOutput() { + if (flutterOutputDir != null && flutterOutputDir.isDirectory()) { + deleteRecursive(flutterOutputDir); + getLog().debug("Removed stale Flutter generated sources"); + } + } + + private void deleteRecursive(File dir) { + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + if (child.isDirectory()) { + deleteRecursive(child); + } else { + child.delete(); + } + } + } + dir.delete(); + } + + private void registerSourceRoot() { + String path = flutterOutputDir.getAbsolutePath(); + if (!project.getCompileSourceRoots().contains(path)) { + project.addCompileSourceRoot(path); + getLog().debug("Added compile source root " + path); + } + } +} diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java new file mode 100644 index 00000000000..0f963a93f38 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/TranscodeFlutterAssetsTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.apache.maven.model.Build; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.HashSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Flattened Flutter assets follow the source tree across incremental builds: + * what disappears from src/main/flutter disappears from the output, and nothing + * the plugin did not write is ever removed. + */ +public class TranscodeFlutterAssetsTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private TranscodeFlutterMojo mojo; + private File flutter; + private File classes; + + @Before + public void setUp() throws Exception { + File target = tmp.newFolder("target"); + classes = new File(target, "classes"); + flutter = tmp.newFolder("flutter"); + MavenProject project = new MavenProject(); + Build build = new Build(); + build.setDirectory(target.getAbsolutePath()); + build.setOutputDirectory(classes.getAbsolutePath()); + project.setBuild(build); + mojo = new TranscodeFlutterMojo(); + set("project", project); + set("flutterSourceDir", flutter); + } + + @Test + public void aRenamedAssetLeavesNoStaleCopy() throws Exception { + File old = asset("assets/icons/old_name.png"); + asset("assets/kept.png"); + mojo.copyAssets(); + File oldFlat = flat("assets/icons/old_name.png"); + assertTrue(oldFlat.isFile()); + + assertTrue(old.delete()); + asset("assets/icons/new_name.png"); + mojo.copyAssets(); + + assertFalse("the renamed-away asset must not be packaged again", oldFlat.exists()); + assertTrue(flat("assets/icons/new_name.png").isFile()); + assertTrue(flat("assets/kept.png").isFile()); + } + + @Test + public void theManifestListsEveryBundledAssetKey() throws Exception { + asset("assets/a.png"); + asset("assets/2.5x/a.png"); + asset("packages/lib/assets/3x/b.png"); + mojo.copyAssets(); + File manifest = new File(classes, TranscodeFlutterMojo.ASSET_MANIFEST); + assertTrue(manifest.isFile()); + assertEquals(java.util.Arrays.asList("assets/2.5x/a.png", "assets/a.png", "packages/lib/assets/3x/b.png"), + Files.readAllLines(manifest.toPath(), java.nio.charset.StandardCharsets.UTF_8)); + + // Recorded like the assets: gone with them when the Flutter tree is. + mojo.removeStaleAssets(new HashSet()); + assertFalse(manifest.exists()); + } + + @Test + public void removingFlutterRemovesItsAssets() throws Exception { + asset("assets/a.png"); + asset("packages/lib/assets/b.png"); + mojo.copyAssets(); + assertTrue(flat("assets/a.png").isFile()); + + // What executeImpl does when src/main/flutter is gone. + mojo.removeStaleAssets(new HashSet()); + + assertFalse(flat("assets/a.png").exists()); + assertFalse(flat("packages/lib/assets/b.png").exists()); + } + + @Test + public void filesThePluginDidNotWriteAreNeverRemoved() throws Exception { + asset("assets/a.png"); + mojo.copyAssets(); + // The application's own resource, sharing the output directory and even + // the prefix: it is not in the plugin's record, so it is not the plugin's. + File own = new File(classes, "cn1f_app_owned.bin"); + Files.write(own.toPath(), new byte[] {1}); + + mojo.removeStaleAssets(new HashSet()); + + assertTrue(own.isFile()); + } + + /** + * The same vectors as the runtime's FlutterAssetsTest and the benchmark's + * stage_assets.py: the three encoders must agree name for name, or an asset + * the build writes is one the runtime never finds. + */ + @Test + public void encodingMatchesTheRuntimeAndIsPrefixFree() { + assertEquals("cn1f_assets_sstudies_sreply__card.png", + TranscodeFlutterMojo.flatAssetName("assets/studies/reply_card.png")); + assertEquals("cn1f_a___sb.png", TranscodeFlutterMojo.flatAssetName("a_/b.png")); + assertEquals("cn1f_a_s__b.png", TranscodeFlutterMojo.flatAssetName("a/_b.png")); + assertFalse(TranscodeFlutterMojo.flatAssetName("a_/b.png") + .equals(TranscodeFlutterMojo.flatAssetName("a/_b.png"))); + } + + private File asset(String key) throws Exception { + File f = new File(flutter, key); + f.getParentFile().mkdirs(); + Files.write(f.toPath(), new byte[] {0}); + return f; + } + + private File flat(String key) { + return new File(classes, TranscodeFlutterMojo.flatAssetName(key)); + } + + private void set(String name, Object value) throws Exception { + for (Class c = mojo.getClass(); c != null; c = c.getSuperclass()) { + try { + Field f = c.getDeclaredField(name); + f.setAccessible(true); + f.set(mojo, value); + return; + } catch (NoSuchFieldException keepLooking) { + // up the chain + } + } + throw new NoSuchFieldException(name); + } +} diff --git a/maven/core-unittests/pom.xml b/maven/core-unittests/pom.xml index aadaa09d05b..d26aa64b089 100644 --- a/maven/core-unittests/pom.xml +++ b/maven/core-unittests/pom.xml @@ -29,6 +29,25 @@ + + + maven-jar-plugin + + + + test-jar + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/maven/core-unittests/src/test/java/com/codename1/l10n/SimpleDateFormatTest.java b/maven/core-unittests/src/test/java/com/codename1/l10n/SimpleDateFormatTest.java index c72c820ee62..05eff41c4e8 100644 --- a/maven/core-unittests/src/test/java/com/codename1/l10n/SimpleDateFormatTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/l10n/SimpleDateFormatTest.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codename1.l10n; import com.codename1.junit.UITestBase; @@ -200,4 +223,53 @@ public String getShortMonthName(Date date) { return "Long"; } } + + @Test + void formatsInTheZoneItIsGiven() { + java.util.TimeZone saved = java.util.TimeZone.getDefault(); + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/New_York")); + try { + // 2024-03-10 02:30 UTC: the local 02:30 does not exist that morning in New York. + java.util.Calendar c = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + c.clear(); + c.set(2024, java.util.Calendar.MARCH, 10, 2, 30, 0); + SimpleDateFormat f = new SimpleDateFormat("HH:mm"); + f.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + assertEquals("02:30", f.format(c.getTime())); + assertEquals("UTC", f.getTimeZone().getID()); + SimpleDateFormat copy = (SimpleDateFormat) f.clone(); + assertEquals("02:30", copy.format(c.getTime()), "a clone keeps the zone"); + assertEquals("America/New_York", new SimpleDateFormat("HH:mm").getTimeZone().getID(), + "without one, the device's zone"); + } finally { + java.util.TimeZone.setDefault(saved); + } + } + @Test + void parsesInTheZoneItIsGiven() throws Exception { + java.util.TimeZone saved = java.util.TimeZone.getDefault(); + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/New_York")); + try { + java.util.Calendar c = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + c.clear(); + c.set(2024, java.util.Calendar.JANUARY, 1, 0, 0, 0); + SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + f.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + assertEquals(c.getTime().getTime(), f.parse("2024-01-01 00:00").getTime(), + "text without a zone is read in the formatter's zone, not the device's"); + // Summer, so the device zone's DST cannot leak into a zone without one. + c.set(2024, java.util.Calendar.JULY, 1, 12, 0, 0); + assertEquals(c.getTime().getTime(), f.parse("2024-07-01 12:00").getTime()); + assertEquals("2024-07-01 12:00", f.format(f.parse("2024-07-01 12:00")), "round trip"); + + java.util.Calendar ny = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("America/New_York")); + ny.clear(); + ny.set(2024, java.util.Calendar.JULY, 1, 12, 0, 0); + assertEquals(ny.getTime().getTime(), + new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2024-07-01 12:00").getTime(), + "without one, the device's zone"); + } finally { + java.util.TimeZone.setDefault(saved); + } + } } diff --git a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java index d8833551ac4..83d685ab32e 100644 --- a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java +++ b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java @@ -1637,6 +1637,16 @@ public void reset() { incomingConnections.clear(); resourceAsStreams.clear(); deviceDensity = Display.DENSITY_MEDIUM; + // Restored for the same reason the density above is, and it is the one + // device-shape flag whose leak is SILENT. A test that leaves this false + // does not fail; the next test asking for COMMAND_BEHAVIOR_BUTTON_BAR is + // rewritten to COMMAND_BEHAVIOR_SOFTKEY by + // CodenameOneImplementation.setCommandBehavior, its commands are drawn as + // soft buttons, and it fails on a missing button bar with nothing naming + // the cause. That is exactly how MenuBarDialogSideMenuTest failed in CI + // and passed on every developer machine: surefire's filesystem run order + // differs per checkout, so whether the leaking test ran first was luck. + touchDevice = true; displayWidth = 1080; displayHeight = 1920; desktopSize = new Dimension(displayWidth, displayHeight); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/DialogTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/DialogTest.java index c5e04ee1e3b..ba7ce719730 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/DialogTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/DialogTest.java @@ -392,4 +392,25 @@ private Button findButton(Container root, Command target) { } return null; } + + @FormTest + void aTimedOutDialogIsOnlyDisposedOnTheEdt() throws Exception { + final Dialog dlg = new Dialog("Timed"); + dlg.setTimeout(1); + Thread.sleep(5); // the deadline is now in the past + // invokeAndBlock's waiting thread polls isDisposed() while a modal show blocks. + // It must not act on the deadline there: disposing and deregistering the + // dialog's animation from that thread raced the EDT walking the same list. + final boolean[] offEdt = new boolean[1]; + Thread poller = new Thread(new Runnable() { + @Override + public void run() { + offEdt[0] = dlg.isDisposed(); + } + }); + poller.start(); + poller.join(); + assertFalse(offEdt[0], "the waiting thread leaves the deadline to the EDT"); + assertTrue(dlg.isDisposed(), "on the EDT the passed deadline disposes the dialog"); + } } diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/animations/ContainerTransformNoSourceTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/animations/ContainerTransformNoSourceTest.java new file mode 100644 index 00000000000..15822848295 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/animations/ContainerTransformNoSourceTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.animations; + +import com.codename1.junit.UITestBase; +import com.codename1.ui.Form; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ContainerTransformNoSourceTest extends UITestBase { + + @Test + void aTransitionWithNoSourceFormDoesNotThrow() { + // The first Form shown has nothing to transition from; the Transition contract + // allows a null source, and dereferencing it made that first show throw. + Form destination = new Form(); + destination.setWidth(200); + destination.setHeight(300); + final ContainerTransformTransition t = ContainerTransformTransition.create("anything", 200); + t.init(null, destination); + assertDoesNotThrow(t::initTransition); + assertFalse(t.animate(), "with nothing to transform from there is no animation"); + } + + @Test + void cleanupReleasesTheForms() { + // A caller can still hold the transition (Display.getRunningTransition) after + // it ends; cleanup must drop both forms and their component trees. + Form source = new Form(); + Form destination = new Form(); + final ContainerTransformTransition t = ContainerTransformTransition.create("anything", 200); + t.init(source, destination); + t.cleanup(); + assertNull(t.getSource()); + assertNull(t.getDestination()); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/animations/CubicBezierMotionTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/animations/CubicBezierMotionTest.java index 653aa19a724..6621e1e7d87 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/animations/CubicBezierMotionTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/animations/CubicBezierMotionTest.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codename1.ui.animations; import com.codename1.junit.UITestBase; @@ -120,4 +143,20 @@ void worksWithDecreasingRange() { assertEquals(1000, sample(m, 0)); assertEquals(0, sample(m, 1000)); } + + @Test + void aThreePointMotionWithAFlatSegmentStaysContinuousAtTheJoin() { + // Midpoint on the bottom edge: the first segment has no vertical extent. Its Y + // cannot be normalized, and substituting linear progress there made the value run + // up toward t -- about 499 of 1000 just before the join -- and then drop to 0 at + // it. The segment's own cubic is flat at 0 all the way. + Motion m = Motion.createThreePointCubicMotion(0, 1000, 1000, + 0.1f, 0f, 0.4f, 0f, + 0.5f, 0f, + 0.6f, 0.3f, 0.9f, 1f); + assertEquals(0, sample(m, 499), 20, "the flat first segment stays at its level"); + assertEquals(0, sample(m, 250), 20); + int atJoin = sample(m, 500); + assertTrue(Math.abs(atJoin - sample(m, 499)) <= 20, "no jump at the join"); + } } diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathRectangleEqualityTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathRectangleEqualityTest.java new file mode 100644 index 00000000000..ae8c8372cf9 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathRectangleEqualityTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.geom; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A path is equal to a rectangle only when it IS that rectangle. + * + *

This used to compare bounding boxes, so every non-rectangular path compared equal to + * its own bounding box. The clip bookkeeping asks exactly this question before deciding a + * setClip is a no-op, and Codename One narrows the clip to a component's bounds before + * painting it -- so a shaped clip filling that component arrived with bounds equal to the + * current rectangular clip, compared EQUAL, and was DISCARDED. The subtree then painted + * square with nothing reported.

+ */ +class GeneralPathRectangleEqualityTest { + + /** A 64-sided polygon inscribed in (x, y, w, h) -- a circle for these purposes. */ + private static GeneralPath circle(float x, float y, float w, float h) { + GeneralPath p = new GeneralPath(); + float rx = w / 2f; + float ry = h / 2f; + float cx = x + rx; + float cy = y + ry; + for (int i = 0; i < 64; i++) { + double a = 2 * Math.PI * i / 64; + float px = (float) (cx + rx * Math.cos(a)); + float py = (float) (cy + ry * Math.sin(a)); + if (i == 0) { + p.moveTo(px, py); + } else { + p.lineTo(px, py); + } + } + p.closePath(); + return p; + } + + private static GeneralPath rectPath(float x, float y, float w, float h) { + GeneralPath p = new GeneralPath(); + p.moveTo(x, y); + p.lineTo(x + w, y); + p.lineTo(x + w, y + h); + p.lineTo(x, y + h); + p.closePath(); + return p; + } + + @Test + void aCircleIsNotItsBoundingRectangle() { + GeneralPath c = circle(10, 20, 100, 100); + assertFalse(c.isRectangle(), "a 64-gon is not a rectangle"); + Rectangle bounds = new Rectangle(); + c.getBounds(bounds); + assertFalse(c.equals(bounds, null), + "a circle must not compare equal to the rectangle it is inscribed in"); + } + + @Test + void aTriangleIsNotItsBoundingRectangle() { + GeneralPath t = new GeneralPath(); + t.moveTo(50f, 0f); + t.lineTo(100f, 100f); + t.lineTo(0f, 100f); + t.closePath(); + Rectangle bounds = new Rectangle(); + t.getBounds(bounds); + assertFalse(t.equals(bounds, null)); + } + + /// The case the comparison exists for still answers yes. + @Test + void aRectangularPathStillEqualsThatRectangle() { + GeneralPath r = rectPath(10, 20, 100, 50); + assertTrue(r.isRectangle()); + assertTrue(r.equals(new Rectangle(10, 20, 100, 50), null)); + } + + @Test + void aRectangularPathDoesNotEqualADifferentRectangle() { + GeneralPath r = rectPath(10, 20, 100, 50); + assertFalse(r.equals(new Rectangle(10, 20, 100, 51), null)); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/DefaultLookAndFeelLazyGlyphsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/DefaultLookAndFeelLazyGlyphsTest.java new file mode 100644 index 00000000000..d6396060a62 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/DefaultLookAndFeelLazyGlyphsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.plaf; + +import com.codename1.junit.UITestBase; +import com.codename1.ui.Image; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/// DefaultLookAndFeel builds its check box, radio button and combo box glyphs the first +/// time something asks for them instead of on every theme install. What callers see +/// must not change: after a refresh the getters still return the images the theme +/// defines, and an image an application sets after a refresh is kept, not replaced by +/// the late build. +public class DefaultLookAndFeelLazyGlyphsTest extends UITestBase { + + @Test + public void imagesAreThereWhenAskedForAfterARefresh() { + DefaultLookAndFeel laf = (DefaultLookAndFeel) UIManager.getInstance().getLookAndFeel(); + laf.refreshTheme(true); + // The test implementation supports TrueType fonts, so the Material glyph + // fallback applies and every group has images. + assertNotNull(laf.getCheckBoxImages()); + assertNotNull(laf.getCheckBoxFocusImages()); + assertNotNull(laf.getRadioButtonImages()); + assertNotNull(laf.getRadioButtonFocusImages()); + } + + @Test + public void anImageSetAfterARefreshIsNotReplacedByTheLateBuild() { + DefaultLookAndFeel laf = (DefaultLookAndFeel) UIManager.getInstance().getLookAndFeel(); + laf.refreshTheme(true); + Image checked = Image.createImage(4, 4, 0xff00ff00); + Image unchecked = Image.createImage(4, 4, 0xffff0000); + laf.setCheckBoxImages(checked, unchecked, checked, unchecked); + laf.setRadioButtonImages(checked, unchecked, checked, unchecked); + // The first read after the setter must return the application's images. + assertSame(checked, laf.getCheckBoxImages()[1]); + assertSame(unchecked, laf.getCheckBoxImages()[0]); + assertSame(checked, laf.getRadioButtonImages()[1]); + // A new refresh re-derives them from the theme, as it always did. + laf.refreshTheme(true); + assertNotNull(laf.getCheckBoxImages()); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerDeferredBaseThemeTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerDeferredBaseThemeTest.java new file mode 100644 index 00000000000..f54b1242143 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerDeferredBaseThemeTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.plaf; + +import com.codename1.junit.UITestBase; +import org.junit.jupiter.api.Test; + +import java.util.Hashtable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// A deferred base theme (UIManager.setDeferredBaseTheme) must be invisible in +/// what an application sees: installed before anything reads the theme, never +/// installed when a theme replaces it outright, and installed first when a theme is +/// layered on it. The Linux and Windows ports defer their native theme this way +/// instead of installing it while the display initializes. +public class UIManagerDeferredBaseThemeTest extends UITestBase { + + /// Counts its runs and installs a base theme with one recognisable key. + private static final class Base implements Runnable { + private final UIManager manager; + int runs; + + Base(UIManager manager) { + this.manager = manager; + } + + @Override + public void run() { + runs++; + Hashtable base = new Hashtable(); + base.put("@deferredBaseMarker", "base"); + base.put("name", "DeferredBase"); + base.put("DeferredProbe.fgColor", "112233"); + manager.setThemeProps(base); + } + } + + @Test + public void aReadInstallsTheBaseThemeOnceBeforeAnswering() { + UIManager m = UIManager.createInstance(); + Base base = new Base(m); + m.setDeferredBaseTheme(base); + assertEquals(0, base.runs, "registering must not install"); + assertEquals("base", m.getThemeConstant("deferredBaseMarker", "missing"), + "the first read has to see the base theme"); + assertEquals(0x112233, m.getComponentStyle("DeferredProbe").getFgColor()); + assertEquals(1, base.runs, "installed exactly once"); + } + + @Test + public void everyKindOfReadTriggersIt() { + String[] kinds = {"style", "selected", "constant", "boolean", "image", "laf", "name", "zoom"}; + for (String kind : kinds) { + UIManager m = UIManager.createInstance(); + Base base = new Base(m); + m.setDeferredBaseTheme(base); + if ("style".equals(kind)) { + m.getComponentStyle("Label"); + } else if ("selected".equals(kind)) { + m.getComponentSelectedStyle("Label"); + } else if ("constant".equals(kind)) { + m.getThemeConstant("x", 0); + } else if ("boolean".equals(kind)) { + m.isThemeConstant("x", false); + } else if ("image".equals(kind)) { + m.getThemeImageConstant("x"); + } else if ("name".equals(kind)) { + assertEquals("DeferredBase", m.getThemeName(), "the name of the theme that will be installed"); + } else if ("zoom".equals(kind)) { + // Zooming must scale the base theme, not an empty table it then replaces. + m.zoomFonts(1.5f); + } else { + m.getLookAndFeel(); + } + assertEquals(1, base.runs, "a " + kind + " read must install the deferred base theme"); + } + } + + @Test + public void aThemeThatReplacesEverythingSupersedesIt() { + UIManager m = UIManager.createInstance(); + Base base = new Base(m); + m.setDeferredBaseTheme(base); + Hashtable app = new Hashtable(); + app.put("@appMarker", "app"); + m.setThemeProps(app); + assertEquals("app", m.getThemeConstant("appMarker", "missing")); + assertEquals("missing", m.getThemeConstant("deferredBaseMarker", "missing"), + "setThemeProps resets the table, so the base theme must not reappear"); + assertEquals(0, base.runs, "a superseded base theme is never installed"); + } + + @Test + public void aLayeredThemeGetsTheBaseUnderneath() { + UIManager m = UIManager.createInstance(); + Base base = new Base(m); + m.setDeferredBaseTheme(base); + Hashtable layer = new Hashtable(); + layer.put("@layerMarker", "layer"); + m.addThemeProps(layer); + assertEquals(1, base.runs); + assertEquals("base", m.getThemeConstant("deferredBaseMarker", "missing")); + assertEquals("layer", m.getThemeConstant("layerMarker", "missing")); + } + + @Test + public void aPendingBaseThemeCountsAsInstalled() { + // FullScreenAdService installs the native theme when none was installed; + // a deferred one is as good as installed and must not look missing. + UIManager m = UIManager.createInstance(); + Base base = new Base(m); + m.setDeferredBaseTheme(base); + assertTrue(m.wasThemeInstalled()); + assertEquals(0, base.runs, "asking must not install"); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/util/ResourcesParseBufferTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/util/ResourcesParseBufferTest.java new file mode 100644 index 00000000000..f42cf31ab60 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/util/ResourcesParseBufferTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.util; + +import com.codename1.junit.UITestBase; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Hashtable; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Resources parses a stream that is not a ByteArrayInputStream through a private +/// read buffer. A source that hands back a few bytes at a time -- as a network or +/// native file stream may -- must parse to exactly what the whole file in memory +/// parses to. +public class ResourcesParseBufferTest extends UITestBase { + + /// Returns at most three bytes per read and skips at most two, so every refill + /// and short-read path of the buffer is taken. + private static final class TrickleStream extends InputStream { + private final byte[] data; + private int pos; + + TrickleStream(byte[] data) { + this.data = data; + } + + @Override + public int read() { + return pos < data.length ? data[pos++] & 0xff : -1; + } + + @Override + public int read(byte[] b, int off, int len) { + if (pos >= data.length) { + return -1; + } + int n = Math.min(Math.min(len, 3), data.length - pos); + System.arraycopy(data, pos, b, off, n); + pos += n; + return n; + } + + @Override + public long skip(long n) { + int k = (int) Math.min(Math.min(n, 2), data.length - pos); + pos += k; + return k; + } + } + + /// A real theme: thousands of keys, fonts and images, which is the file shape the + /// buffer was put in for. Read from the checkout; the test fails rather than + /// skipping when it is missing. + private static byte[] themeFile() throws IOException { + java.io.File f = new java.io.File("../../CodenameOneDesigner/src/iPhoneTheme.res"); + assertTrue(f.isFile(), "fixture " + f.getAbsolutePath()); + InputStream in = new java.io.FileInputStream(f); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } finally { + in.close(); + } + } + + @Test + public void aTrickledStreamParsesLikeTheWholeFile() throws IOException { + byte[] data = themeFile(); + Resources whole = Resources.open(new ByteArrayInputStream(data)); + Resources trickled = Resources.open(new TrickleStream(data)); + + String[] themes = whole.getThemeResourceNames(); + assertTrue(themes.length > 0, "the fixture has a theme to compare"); + assertArrayEquals(themes, trickled.getThemeResourceNames()); + String[] images = whole.getImageResourceNames(); + String[] trickledImages = trickled.getImageResourceNames(); + Arrays.sort(images); + Arrays.sort(trickledImages); + assertArrayEquals(images, trickledImages); + for (String name : themes) { + Hashtable a = whole.getTheme(name); + Hashtable b = trickled.getTheme(name); + assertEquals(a.keySet(), b.keySet(), "theme " + name + " keys"); + } + } +} diff --git a/maven/dart-runtime/pom.xml b/maven/dart-runtime/pom.xml new file mode 100644 index 00000000000..e4b698bbe85 --- /dev/null +++ b/maven/dart-runtime/pom.xml @@ -0,0 +1,97 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-dart-runtime + 8.0-SNAPSHOT + jar + codenameone-dart-runtime + + Java implementation of dart:core / dart:math semantics used by Dart + code transpiled to Java by the Codename One Dart transpiler. App-grade + code: depends only on codenameone-core and compiles to Java 17 + bytecode, which all Codename One targets accept. + + + + UTF-8 + 17 + 17 + + + + + + maven-compiler-plugin + + + 17 + 17 + 17 + + + + + + + + + java17-fork + + + env.JAVA17_HOME + + + + + + maven-compiler-plugin + + + 17 + 17 + 17 + true + ${env.JAVA17_HOME}/bin/javac + + + + maven-surefire-plugin + + ${env.JAVA17_HOME}/bin/java + + + + + + + + + + com.codenameone + codenameone-core + provided + + + org.junit.jupiter + junit-jupiter + test + + + diff --git a/maven/dart-runtime/src/main/java/dart/async/Await.java b/maven/dart-runtime/src/main/java/dart/async/Await.java new file mode 100644 index 00000000000..259e66cde55 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/async/Await.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +/** + * The lowering target of Dart's {@code await} (M3 blocking model). + * + *

On the EDT the wait runs inside {@code Display.invokeAndBlock}, which + * keeps dispatching events — other Dart callbacks, timers and UI input keep + * running while this frame is parked, matching Dart's "other events run + * during an await" semantics. Off the EDT (background threads, headless + * tests) it is a plain monitor wait.

+ */ +public final class Await { + + private Await() { + } + + /** + * {@code await v} where v is a FutureOr or dynamic: a Future is awaited, anything + * else -- {@code await 42}, {@code await null} -- is the value itself, as in Dart. + * Only the Future overload existed, so awaiting a non-Future did not compile. + */ + @SuppressWarnings("unchecked") + public static Object awaitAny(Object v) { + return v instanceof Future ? await$((Future) v) : v; + } + + public static T await$(final Future f) { + if (f == null) { + throw new dart.core.TypeError("await on null Future"); + } + if (f.isDone()) { + return f.valueOrThrow(); + } + if (com.codename1.ui.Display.isInitialized() + && com.codename1.ui.Display.getInstance().isEdt()) { + com.codename1.ui.Display.getInstance().invokeAndBlock(new Runnable() { + @Override + public void run() { + parkUntilDone(f); + } + }); + } else { + parkUntilDone(f); + } + return f.valueOrThrow(); + } + + private static void parkUntilDone(Future f) { + synchronized (f.monitor()) { + while (!f.isDone()) { + try { + f.monitor().wait(500); + } catch (InterruptedException ignore) { + // spurious wakeup handling via the loop condition + } + } + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/async/Completer.java b/maven/dart-runtime/src/main/java/dart/async/Completer.java new file mode 100644 index 00000000000..ca166f55450 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/async/Completer.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +/** + * Dart's Completer<T>. + */ +public class Completer { + + private final Future future = new Future(); + + public Future future() { + return future; + } + + /** Dart's {@code complete()} with the value omitted: completes with null. */ + public void complete() { + future.complete(null); + } + + public void complete(T value) { + future.complete(value); + } + + public void completeError(Object error) { + future.completeError(error); + } + + /** + * Dart's {@code isCompleted}: true once complete or completeError has been + * called -- including complete(aFuture) while that future is still pending. + * Asking whether the returned future had SETTLED reported false through that + * adoption window, so the usual {@code if (!c.isCompleted) c.complete(...)} + * guard let a second completion through and it threw StateError. + */ + public boolean isCompleted() { + return future.isCompletedOrAdopting(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/async/Future.java b/maven/dart-runtime/src/main/java/dart/async/Future.java new file mode 100644 index 00000000000..a1161a1604b --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/async/Future.java @@ -0,0 +1,741 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +import dart.core.DartList; +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Dart's Future<T> for the M3 blocking-await model: the transpiler + * lowers {@code await f} to {@link Await#await$(Future)}, which parks the + * caller (legally, via invokeAndBlock on the EDT) until completion. Async + * function bodies therefore run to completion synchronously from the + * caller's perspective; completion ordering of simultaneously resumed + * awaits is a documented divergence until the M5 CPS backend. + * + *

Interop: {@link #fromAsyncResource} bridges CN1 async APIs.

+ */ +public class Future { + + private final Object lock = new Object(); + private boolean done; + /// Set while this future waits on another it was completed WITH. It counts + /// as completed for a second complete() -- Dart refuses that too -- but not + /// yet for listeners, who must see the adopted outcome. + private boolean adopting; + private T value; + private Throwable error; + private List listeners; + + Future() { + } + + /** + * Creates a future already completed with {@code v}. Lets an immediately + * available subclass in another package (e.g. Flutter foundation's + * {@code SynchronousFuture}) bridge in, since the no-arg constructor and + * {@link #complete} are package-private. + */ + protected Future(T v) { + complete(v); + } + + /** + * The completed value if this future has already resolved synchronously + * (e.g. a {@code SynchronousFuture}), otherwise null. Lets synchronous + * consumers such as the Localizations delegate pipeline read the result + * without parking the EDT. + */ + public T getNow() { + synchronized (lock) { + return done ? value : null; + } + } + + /** An already-completed future. */ + public static Future value(T v) { + Future f = new Future(); + f.complete(v); + return f; + } + + /** An already-failed future. */ + public static Future error(Object err) { + Future f = new Future(); + f.completeError(err); + return f; + } + + /** + * Dart's Future.delayed. With a live CN1 Display the callback fires on + * the EDT via CN.setTimeout; headless (tests, plain JVM) a short-lived + * thread sleeps and completes — no daemon flags, so the JVM can exit. + */ + public static Future delayed(dart.core.Duration duration) { + return delayed(duration, (Funcs.Func0) null); + } + + /** + * Future.delayed with a void computation body. Dart's {@code computation} + * returns {@code FutureOr}; a statement-body closure transpiles to a + * {@link Funcs.VoidFunc0}, so this overload lets those bind without forcing + * an artificial return value. + */ + public static Future delayed(dart.core.Duration duration, final Funcs.VoidFunc0 computation) { + return delayed(duration, new Funcs.Func0() { + @Override + public Object call() { + if (computation != null) { + computation.call(); + } + return null; + } + }); + } + + public static Future delayed(dart.core.Duration duration, final Funcs.Func0 computation) { + final Future f = new Future(); + // Dart treats a negative delay as zero. Passed through, it reached + // Thread.sleep -- which throws -- on the headless path, so the worker died + // without completing and an await on the result hung forever; Timer + // rejects it the same way on the Display path. + long ms = duration == null ? 0 : Math.max(0L, duration.inMilliseconds()); + final Runnable complete = new Runnable() { + @Override + public void run() { + try { + f.complete(computation == null ? null : computation.call()); + } catch (Throwable t) { + f.completeError(t); + } + } + }; + if (com.codename1.ui.Display.isInitialized()) { + Timers.schedule(ms, complete); + } else { + final long sleepMs = ms; + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(sleepMs); + } catch (InterruptedException ignore) { + // fall through and complete anyway + } + complete.run(); + } + }, "dart-future-delayed").start(); + } + return f; + } + + /** + * Dart's Future.wait: a future of every value, in input order, once all + * the inputs have completed -- or, if any failed, of the FIRST error, + * still only after all of them completed (Dart's default, non-eager + * behaviour; the later errors are dropped). + * + * It returns immediately and settles through listeners. It used to await + * each input before returning, which blocked the caller -- and when an + * input failed, the error was thrown from the call itself, so + * {@code Future.wait(fs).catchError(...)} never got a future to install + * its handler on. + */ + public static Future> wait(Iterable> futures) { + return wait(futures, false, null); + } + + /** + * Future.wait with its named arguments, which the transpiler used to drop. + * {@code eagerError} fails the result at the FIRST error instead of waiting for + * every input -- without it, an error beside an input that never completes left + * the result pending forever. {@code cleanUp}, once any input has failed, receives + * every non-null successful value: the ones already in and the ones that arrive + * later, as Dart hands them over. + */ + public static Future> wait(Iterable> futures, + final boolean eagerError, + final Funcs.VoidFunc1 cleanUp) { + final Future> result = new Future>(); + final List> inputs = new ArrayList>(); + try { + for (Future f : futures) { + inputs.add(f); + } + } catch (Throwable t) { + // Dart returns a future failed with the traversal's error; letting it escape + // made Future.wait(bad).catchError(...) throw from the call itself. + result.completeError(t); + return result; + } + final int count = inputs.size(); + if (count == 0) { + result.complete(new DartList()); + return result; + } + final Object[] values = new Object[count]; + final boolean[] arrived = new boolean[count]; + // Inputs still pending, and the first error. Guarded by `pending`: on + // the headless path inputs complete on their own threads. + final int[] pending = {count}; + final Throwable[] firstError = {null}; + for (int i = 0; i < count; i++) { + final int index = i; + final Future f = inputs.get(i); + f.onComplete(new Runnable() { + @Override + public void run() { + Throwable error; + boolean last; + boolean failNow = false; + List toClean = new ArrayList(); + synchronized (pending) { + if (f.error != null) { + if (firstError[0] == null) { + firstError[0] = f.error; + failNow = eagerError; + // The values already in are the application's to release. + for (int j = 0; j < count; j++) { + if (arrived[j] && values[j] != null) { + toClean.add(values[j]); + } + } + } + } else if (firstError[0] != null) { + if (f.value != null) { + toClean.add(f.value); + } + } else { + values[index] = f.value; + arrived[index] = true; + } + last = --pending[0] == 0; + error = firstError[0]; + } + if (cleanUp != null) { + for (Object v : toClean) { + try { + cleanUp.call(v); + } catch (Throwable t) { + // Uncaught in Dart too: nothing awaits a cleanUp callback. + com.codename1.io.Log.e(t); + } + } + } + if (failNow) { + result.completeError(error); + return; + } + if (!last || (error != null && eagerError)) { + return; + } + if (error != null) { + result.completeError(error); + return; + } + DartList out = new DartList(); + for (Object v : values) { + out.add(v); + } + result.complete(out); + } + }); + } + return result; + } + + /** Registers a completion callback (fires immediately if already done). */ + public Future then(final Funcs.Func1 onValue) { + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error != null) { + next.completeError(error); + return; + } + try { + next.complete(onValue.call(value)); + } catch (Throwable t) { + next.completeError(t); + } + } + }); + return next; + } + + /** + * Dart's {@code then(onValue, onError: handler)}. The handler receives THIS + * future's error, never one thrown by onValue -- that one fails the returned + * future, as in Dart. The named onError used to be dropped by the transpiler, so + * a failure skipped the handler and the chain stayed failed. The handler may take + * the error, or the error and a stack trace, and may return a value or nothing. + */ + public Future then(final Funcs.Func1 onValue, final Object onError) { + if (onError == null) { + return then(onValue); + } + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error != null) { + try { + next.complete(callErrorHandler(onError, error)); + } catch (Throwable t) { + next.completeError(t); + } + return; + } + try { + next.complete(onValue.call(value)); + } catch (Throwable t) { + next.completeError(t); + } + } + }); + return next; + } + + /** {@link #then(Funcs.Func1, Object)} with a void onValue body. */ + public Future then(final Funcs.VoidFunc1 onValue, final Object onError) { + return then(new Funcs.Func1() { + @Override + public Object call(T v) { + onValue.call(v); + return null; + } + }, onError); + } + + /** Invokes an error handler of any shape a Dart handler transpiles to. */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Object callErrorHandler(Object handler, Object thrown) { + // The handler gets what was thrown, not the carrier: Future.error('x') hands it 'x'. + Object err = thrown instanceof Throwable ? DartRuntime.caught((Throwable) thrown) : thrown; + if (handler instanceof Funcs.Func1) { + return ((Funcs.Func1) handler).call(err); + } + if (handler instanceof Funcs.VoidFunc1) { + ((Funcs.VoidFunc1) handler).call(err); + return null; + } + if (handler instanceof Funcs.Func2) { + return ((Funcs.Func2) handler).call(err, null); + } + if (handler instanceof Funcs.VoidFunc2) { + ((Funcs.VoidFunc2) handler).call(err, null); + return null; + } + throw new dart.core.ArgumentError("onError is not a function"); + } + + /** + * {@code then} with a void callback body — the common statement-body + * {@code .then((_) { ... })} shape, which transpiles to a + * {@link Funcs.VoidFunc1}. Mirrors {@link #then(Funcs.Func1)} but discards + * the (absent) callback result. + */ + public Future then(final Funcs.VoidFunc1 onValue) { + return then(new Funcs.Func1() { + @Override + public Object call(T v) { + onValue.call(v); + return null; + } + }); + } + + /** + * {@code catchError} with a void handler {@code (error) { ... }}: runs the + * handler if this future completed with an error, recovering the chain. + */ + public Future catchError(final Funcs.VoidFunc1 onError) { + // A NEW future, as Dart returns one: the handler recovers the chain, so + // `await op.catchError((e) { recover(); })` completes with null rather than + // rethrowing. Returning this future handed back the original failure, and a + // handler that threw escaped into listener dispatch instead of failing the + // chain. + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error == null) { + next.complete(value); + return; + } + try { + onError.call(DartRuntime.caught(error)); + next.complete(null); + } catch (Throwable t) { + next.completeError(t); + } + } + }); + return next; + } + + /** + * {@code catchError} with a value-returning handler {@code (error) => v}: + * substitutes the recovery value when this future completed with an error. + */ + public Future catchError(final Funcs.Func1 onError) { + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error != null) { + try { + next.complete(onError.call(DartRuntime.caught(error))); + } catch (Throwable t) { + next.completeError(t); + } + } else { + next.complete(value); + } + } + }); + return next; + } + + /** {@code catchError(onError, test: ...)} — the optional {@code test} filter is accepted + * for API shape (all errors are handled here). Void-handler form. */ + public Future catchError(final Funcs.VoidFunc1 onError, final Object test) { + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error == null) { + next.complete(value); + } else { + // The predicate runs inside the guard: when it throws, Dart completes + // the returned future with that error. Outside it, the throw escaped + // the completing call (or broke listener dispatch) and left this + // future pending forever. + try { + if (!catches(test, DartRuntime.caught(error))) { + next.completeError(error); + return; + } + onError.call(DartRuntime.caught(error)); + next.complete(null); + } catch (Throwable t) { + next.completeError(t); + } + } + } + }); + return next; + } + + /** {@code catchError(onError, test: ...)} -- value-handler form. */ + public Future catchError(final Funcs.Func1 onError, final Object test) { + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + if (error == null) { + next.complete(value); + } else { + // The predicate runs inside the guard: when it throws, Dart completes + // the returned future with that error. Outside it, the throw escaped + // the completing call (or broke listener dispatch) and left this + // future pending forever. + try { + if (!catches(test, DartRuntime.caught(error))) { + next.completeError(error); + return; + } + next.complete(onError.call(DartRuntime.caught(error))); + } catch (Throwable t) { + next.completeError(t); + } + } + } + }); + return next; + } + + /// Dart's catchError `test:`: when the predicate rejects the error, the handler + /// is NOT run and the error continues down the chain unchanged. Both overloads + /// used to ignore it and recover every failure, so a selective handler -- + /// `test: (e) => e is TimeoutException` -- swallowed unrelated errors and + /// turned a failed operation into a successful one. No test catches all. + @SuppressWarnings("unchecked") + private static boolean catches(Object test, Object err) { + if (test instanceof Funcs.Func1) { + Object verdict = ((Funcs.Func1) test).call(err); + return Boolean.TRUE.equals(verdict); + } + return true; + } + + public Future whenComplete(final Funcs.VoidFunc0 action) { + // A NEW future carrying this one's outcome -- unless the action throws, in + // which case Dart completes it with that error instead. Returning this + // future reported the old outcome regardless, and the throw either escaped + // synchronously (already completed) or broke listener dispatch on the + // completing thread, so later listeners were never told. + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + try { + action.call(); + } catch (Throwable t) { + next.completeError(t); + return; + } + if (error != null) { + next.completeError(error); + } else { + next.complete(value); + } + } + }); + return next; + } + + /** + * {@code whenComplete} whose action may return a value: when that value is a + * Future, Dart waits for it before forwarding this future's outcome, and a + * cleanup that fails replaces the outcome with its error. The void overload + * above discarded it, so {@code whenComplete(() => asyncCleanup())} + * completed while the cleanup was still running and lost its failure. A + * separate name rather than an overload, because a lambda that fits both a + * Func0 and a VoidFunc0 is ambiguous to javac. + */ + public Future whenCompleteFuture(final Funcs.Func0 action) { + final Future next = new Future(); + onComplete(new Runnable() { + @Override + public void run() { + Object r; + try { + r = action.call(); + } catch (Throwable t) { + next.completeError(t); + return; + } + if (r instanceof Future) { + final Future cleanup = (Future) r; + cleanup.onComplete(new Runnable() { + @Override + public void run() { + if (cleanup.error != null) { + next.completeError(cleanup.error); + } else { + forwardTo(next); + } + } + }); + return; + } + forwardTo(next); + } + }); + return next; + } + + private void forwardTo(Future next) { + if (error != null) { + next.completeError(error); + } else { + next.complete(value); + } + } + + void onComplete(Runnable r) { + boolean immediate; + synchronized (lock) { + immediate = done; + if (!done) { + if (listeners == null) { + listeners = new ArrayList(); + } + listeners.add(r); + } + } + if (immediate) { + r.run(); + } + } + + /// Completes with {@code v}, or -- when {@code v} is itself a Future -- + /// with whatever that future completes with. + /// + /// Dart types this {@code FutureOr}: a {@code then} callback that returns + /// a future, a {@code catchError} handler that recovers asynchronously and + /// {@code Completer.complete(someFuture)} all hand one over, and each must + /// wait for it and adopt its value OR its error. Storing the future itself + /// as the value completed {@code await a.then((v) => fetch(v))} at once with + /// a Future object where the fetched result belonged, and dropped any error + /// fetch raised. Adopting here covers every caller rather than each in turn. + void complete(T v) { + if (v instanceof Future) { + adopt((Future) v); + return; + } + synchronized (lock) { + if (done || adopting) { + throw new dart.core.StateError("Future already completed"); + } + } + settle(v); + } + + private void adopt(final Future source) { + synchronized (lock) { + if (done || adopting) { + throw new dart.core.StateError("Future already completed"); + } + adopting = true; + } + if (source == this) { + // What Dart reports for a future completed with itself; waiting would + // simply never finish. + settleError(new dart.core.TypeError("Chaining cycle detected: a Future was completed with itself")); + return; + } + source.onComplete(new Runnable() { + @Override + public void run() { + Throwable e; + Object val; + synchronized (source.lock) { + e = source.error; + val = source.value; + } + if (e != null) { + settleError(e); + } else { + @SuppressWarnings("unchecked") + T adopted = (T) val; + settle(adopted); + } + } + }); + } + + private void settle(T v) { + List toRun; + synchronized (lock) { + if (done) { + throw new dart.core.StateError("Future already completed"); + } + done = true; + adopting = false; + value = v; + toRun = listeners; + listeners = null; + lock.notifyAll(); + } + runAll(toRun); + } + + void completeError(Object err) { + synchronized (lock) { + if (done || adopting) { + throw new dart.core.StateError("Future already completed"); + } + } + settleError(err); + } + + private void settleError(Object err) { + List toRun; + synchronized (lock) { + if (done) { + throw new dart.core.StateError("Future already completed"); + } + done = true; + adopting = false; + error = err instanceof Throwable ? (Throwable) err : DartRuntime.asError(err); + toRun = listeners; + listeners = null; + lock.notifyAll(); + } + runAll(toRun); + } + + private void runAll(List rs) { + if (rs != null) { + for (Runnable r : rs) { + r.run(); + } + } + } + + /** True once completed, or once completed WITH a future that has not settled yet. */ + boolean isCompletedOrAdopting() { + synchronized (lock) { + return done || adopting; + } + } + + boolean isDone() { + synchronized (lock) { + return done; + } + } + + T valueOrThrow() { + synchronized (lock) { + if (error != null) { + if (error instanceof Error) { + throw (Error) error; // an await rethrows an Error as itself + } + throw error instanceof RuntimeException ? (RuntimeException) error + : new RuntimeException(error); + } + return value; + } + } + + Object monitor() { + return lock; + } + + /** Bridge from CN1's AsyncResource. */ + public static Future fromAsyncResource(com.codename1.util.AsyncResource ar) { + final Future f = new Future(); + ar.ready(new com.codename1.util.SuccessCallback() { + @Override + public void onSucess(T v) { + f.complete(v); + } + }); + ar.except(new com.codename1.util.SuccessCallback() { + @Override + public void onSucess(Throwable t) { + f.completeError(t); + } + }); + return f; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/async/Timer.java b/maven/dart-runtime/src/main/java/dart/async/Timer.java new file mode 100644 index 00000000000..0cec362f1b8 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/async/Timer.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** + * Dart's {@code dart:async} Timer: a one-shot callback scheduled after a + * {@link Duration}. With a live CN1 Display the callback fires on the EDT via + * {@code CN.setTimeout}; headless (tests, plain JVM) it falls back to a + * short-lived thread. {@link #cancel()} prevents a not-yet-fired callback. + */ +public final class Timer { + + private volatile boolean cancelled; + private volatile boolean fired; + /** Cleared by cancel(), so a cancelled timer no longer keeps its callback reachable. */ + volatile Funcs.VoidFunc0 callback; + /** The scheduled task on the Display path, so cancel() can unschedule it. */ + private Timers.Handle handle; + + public Timer(Duration duration, Funcs.VoidFunc0 callback) { + this.callback = callback; + // Dart fires a negative delay as soon as possible, as though it were zero. + // Passed through, it made the Display path throw from the scheduler and + // the headless thread die in Thread.sleep -- a timer that stayed active + // and never called back. + long ms = duration == null ? 0 : Math.max(0L, duration.inMilliseconds()); + final Runnable r = new Runnable() { + @Override + public void run() { + if (cancelled) { + return; + } + fired = true; + Funcs.VoidFunc0 cb = Timer.this.callback; + Timer.this.callback = null; + if (cb != null) { + cb.call(); + } + } + }; + if (com.codename1.ui.Display.isInitialized()) { + handle = Timers.schedule(ms, r); + } else { + final long delay = ms; + worker = new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(delay); + } catch (InterruptedException ignore) { + // cancel() interrupts the sleep; r checks the flag and does nothing + } + r.run(); + } + }, "dart-timer"); + worker.start(); + } + } + + /** The headless sleeping thread, so cancel() can end it early. */ + private Thread worker; + + public void cancel() { + cancelled = true; + callback = null; + Timers.Handle h = handle; + handle = null; + if (h != null) { + h.cancel(); + } + // Headless, a timer is a (non-daemon) thread asleep until the deadline. Only + // setting the flag left a cancelled hours-long timer holding the JVM open and + // its callback reachable until then; interrupting ends the thread now. + Thread w = worker; + worker = null; + if (w != null) { + w.interrupt(); + } + } + + public boolean isActive() { + return !cancelled && !fired; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/async/Timers.java b/maven/dart-runtime/src/main/java/dart/async/Timers.java new file mode 100644 index 00000000000..6c9baf4a136 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/async/Timers.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +/** + * Schedules on the EDT after any delay a Dart Duration can express. + * + *

CN.setTimeout takes an int of milliseconds, so a delay past Integer.MAX_VALUE + * -- about 24.8 days -- was narrowed to a negative timeout and threw from the + * scheduler: {@code Future.delayed(Duration(days: 30))} failed at once instead of + * completing in thirty days. Longer waits are chained in int-sized steps.

+ */ +final class Timers { + + /** + * Schedules one link: {@code CN.setTimeout} on the EDT. A seam only so the + * handle bookkeeping can be tested without a Display. + */ + interface Scheduler { + java.util.Timer schedule(int ms, Runnable r); + } + + static Scheduler scheduler = new Scheduler() { + @Override + public java.util.Timer schedule(int ms, Runnable r) { + return com.codename1.ui.CN.setTimeout(ms, r); + } + }; + + /** + * Cancels a scheduled task. CN.setTimeout answers the java.util.Timer that + * holds the task; dropping it left a cancelled task -- and the callback it + * reaches -- scheduled until its deadline, hours or days away. For a chained + * wait this tracks the link currently scheduled, and once cancelled no further + * link is scheduled. Every link runs on the EDT, as does a Dart cancel(), so + * the fields need no synchronization. + */ + static final class Handle { + private java.util.Timer current; + private boolean cancelled; + + void cancel() { + cancelled = true; + java.util.Timer t = current; + current = null; + if (t != null) { + t.cancel(); + } + } + } + + private Timers() { + } + + static Handle schedule(long ms, Runnable r) { + Handle h = new Handle(); + schedule(ms, r, h); + return h; + } + + private static void schedule(final long ms, final Runnable r, final Handle h) { + if (h.cancelled) { + return; + } + if (ms <= Integer.MAX_VALUE) { + h.current = scheduler.schedule((int) Math.max(0L, ms), r); + return; + } + h.current = scheduler.schedule(Integer.MAX_VALUE, new Runnable() { + @Override + public void run() { + schedule(ms - Integer.MAX_VALUE, r, h); + } + }); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/collection/IterableMixin.java b/maven/dart-runtime/src/main/java/dart/collection/IterableMixin.java new file mode 100644 index 00000000000..5d447d825a8 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/collection/IterableMixin.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.collection; + +import dart.runtime.Funcs; + +/** + * Dart's {@code dart:collection} {@code IterableMixin}. A transpiled class + * such as {@code class Board with IterableMixin} is emitted as a + * Java class that {@code implements IterableMixin} and supplies the + * single abstract member {@link #iterator()}; every other member of the + * Iterable protocol is reached here as an inherited default that iterates via + * the returned {@link Iterator}. + * + * @param the element type + */ +public interface IterableMixin { + + /** The applying class supplies this — the source of every default below. */ + Iterator iterator(); + + /** Dart's {@code Iterable.length}. */ + default long length() { + long count = 0; + Iterator it = iterator(); + while (it.moveNext()) { + count++; + } + return count; + } + + /** Dart's {@code Iterable.isEmpty}. */ + default boolean isEmpty() { + return !iterator().moveNext(); + } + + /** Dart's {@code Iterable.isNotEmpty}. */ + default boolean isNotEmpty() { + return iterator().moveNext(); + } + + // Dart's StateError, as DartIterable throws: an `on StateError` fallback in the + // application did not catch Java's NoSuchElementException/IllegalStateException. + + /** Dart's {@code Iterable.first}. */ + default E first() { + Iterator it = iterator(); + if (!it.moveNext()) { + throw new dart.core.StateError("No element"); + } + return it.current(); + } + + /** Dart's {@code Iterable.last}. */ + default E last() { + Iterator it = iterator(); + if (!it.moveNext()) { + throw new dart.core.StateError("No element"); + } + E result; + do { + result = it.current(); + } while (it.moveNext()); + return result; + } + + /** Dart's {@code Iterable.single}. */ + default E single() { + Iterator it = iterator(); + if (!it.moveNext()) { + throw new dart.core.StateError("No element"); + } + E result = it.current(); + if (it.moveNext()) { + throw new dart.core.StateError("Too many elements"); + } + return result; + } + + /** Dart's {@code Iterable.contains(element)}. */ + default boolean contains(Object element) { + Iterator it = iterator(); + while (it.moveNext()) { + E e = it.current(); + if (dart.runtime.DartRuntime.eq(e, element)) { // Dart's ==: 1 contains 1.0 + return true; + } + } + return false; + } + + /** Dart's {@code Iterable.forEach(action)}. */ + default void forEach(Funcs.VoidFunc1 action) { + Iterator it = iterator(); + while (it.moveNext()) { + action.call(it.current()); + } + } + + /** Dart's {@code Iterable.elementAt(index)}. */ + default E elementAt(long index) { + if (index < 0) { + throw new dart.core.RangeError("RangeError (index): Invalid value: Not in inclusive range: " + index); + } + Iterator it = iterator(); + long i = 0; + while (it.moveNext()) { + if (i == index) { + return it.current(); + } + i++; + } + throw new dart.core.RangeError("RangeError (index): Index out of range: index should be less than " + i + ": " + index); + } + + /** Dart's {@code Iterable.any(test)}. */ + default boolean any(Funcs.Func1 test) { + Iterator it = iterator(); + while (it.moveNext()) { + if (Boolean.TRUE.equals(test.call(it.current()))) { + return true; + } + } + return false; + } + + /** Dart's {@code Iterable.every(test)}. */ + default boolean every(Funcs.Func1 test) { + Iterator it = iterator(); + while (it.moveNext()) { + if (!Boolean.TRUE.equals(test.call(it.current()))) { + return false; + } + } + return true; + } + + /** Dart's {@code Iterable.join([separator])}. */ + default String join(String separator) { + StringBuilder sb = new StringBuilder(); + Iterator it = iterator(); + boolean firstElement = true; + while (it.moveNext()) { + if (!firstElement && separator != null) { + sb.append(separator); + } + E e = it.current(); + sb.append(e == null ? "null" : e.toString()); + firstElement = false; + } + return sb.toString(); + } + + /** Dart's {@code Iterable.join()} with no separator. */ + default String join() { + return join(""); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/collection/Iterator.java b/maven/dart-runtime/src/main/java/dart/collection/Iterator.java new file mode 100644 index 00000000000..e117534bbd6 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/collection/Iterator.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.collection; + +/** + * Dart's {@code Iterator} protocol: advance with {@link #moveNext()}, then + * read {@link #current()}. A transpiled {@code class X implements Iterator} + * supplies {@code moveNext} and the {@code current} property; the java-style + * {@link #hasNext()}/{@link #next()} pair is provided for interop. + * + *

All members have defaults so an applying class only needs to override the + * ones it declares (Dart's {@code moveNext} and {@code current}); anything it + * omits falls back to an inert default.

+ * + * @param the element type + */ +public interface Iterator { + + /** Advance to the next element; false when the iteration is exhausted. */ + default boolean moveNext() { + return false; + } + + /** The element reached by the most recent {@link #moveNext()}. */ + default E current() { + return null; + } + + /** Java-style peek: whether another element is available. */ + default boolean hasNext() { + return moveNext(); + } + + /** Java-style advance: returns {@link #current()} after moving. */ + default E next() { + moveNext(); + return current(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/ArgumentError.java b/maven/dart-runtime/src/main/java/dart/core/ArgumentError.java new file mode 100644 index 00000000000..774d2b7921f --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/ArgumentError.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; + +/** + * Dart's ArgumentError. + */ +public class ArgumentError extends RuntimeException { + private final Object invalidValue; + private final boolean hasValue; + private final String name; + + public ArgumentError(String message) { + super(message); + this.invalidValue = null; + this.hasValue = false; + this.name = null; + } + + public ArgumentError(Object value, String name, String message) { + super(message); + this.invalidValue = value; + this.hasValue = true; + this.name = name; + } + + public static ArgumentError value(Object value, String name, String message) { + return new ArgumentError(value, name, message); + } + + public Object invalidValue() { + return invalidValue; + } + + public String name() { + return name; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Invalid argument"); + if (name != null) { + sb.append(" (").append(name).append(")"); + } + if (getMessage() != null) { + sb.append(": ").append(getMessage()); + } + if (hasValue) { + sb.append(": ").append(DartRuntime.str(invalidValue)); + } + return sb.toString(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/ConcurrentModificationError.java b/maven/dart-runtime/src/main/java/dart/core/ConcurrentModificationError.java new file mode 100644 index 00000000000..4d70fc8f3ff --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/ConcurrentModificationError.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's ConcurrentModificationError -- a collection changed length while it was + * being iterated. + */ +public class ConcurrentModificationError extends RuntimeException { + public ConcurrentModificationError(String message) { + super(message); + } + + @Override + public String toString() { + return "Concurrent modification during iteration: " + getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DString.java b/maven/dart-runtime/src/main/java/dart/core/DString.java new file mode 100644 index 00000000000..98354d4dfdb --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DString.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Static helpers implementing Dart's String API over java.lang.String + * (used directly — no wrapper allocation). The transpiler maps Dart String + * members that have no direct java.lang.String equivalent to these. + */ +public final class DString { + + private DString() { + } + + public static long length(String s) { + return s.length(); + } + + public static boolean isEmpty(String s) { + return s.isEmpty(); + } + + public static boolean isNotEmpty(String s) { + return !s.isEmpty(); + } + + /** Dart's s[i] — single-character string. */ + public static String idx(String s, long index) { + RangeError.checkValidIndex(index, s.length()); + return String.valueOf(s.charAt((int) index)); + } + + public static long codeUnitAt(String s, long index) { + RangeError.checkValidIndex(index, s.length()); + return s.charAt((int) index); + } + + /** + * Dart's s * n operator -- repeat. A count of zero OR LESS is the empty string, not + * an error: measured on the Dart 3.9 VM, {@code 'x' * -1} is {@code ''}. + */ + public static String repeat(String s, long times) { + if (times <= 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (long i = 0; i < times; i++) { + sb.append(s); + } + return sb.toString(); + } + + public static String substring(String s, long start) { + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + return s.substring((int) start); + } + + public static String substring(String s, long start, long end) { + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + RangeError.checkValueInInterval(end, start, s.length(), "end"); + return s.substring((int) start, (int) end); + } + + public static String padLeft(String s, long width, String padding) { + StringBuilder sb = new StringBuilder(); + for (long i = s.length(); i < width; i++) { + sb.append(padding); + } + return sb.append(s).toString(); + } + + public static String padLeft(String s, long width) { + return padLeft(s, width, " "); + } + + public static String padRight(String s, long width, String padding) { + StringBuilder sb = new StringBuilder(s); + for (long i = s.length(); i < width; i++) { + sb.append(padding); + } + return sb.toString(); + } + + public static String padRight(String s, long width) { + return padRight(s, width, " "); + } + + public static DartList split(String s, String pattern) { + DartList out = new DartList<>(); + if (pattern.isEmpty()) { + for (int i = 0; i < s.length(); i++) { + out.add(String.valueOf(s.charAt(i))); + } + return out; + } + int start = 0; + int i; + while ((i = s.indexOf(pattern, start)) >= 0) { + out.add(s.substring(start, i)); + start = i + pattern.length(); + } + out.add(s.substring(start)); + return out; + } + + /** + * Dart's {@code String.split(Pattern)} for a RegExp, ported from the Dart + * VM's own algorithm so the edge cases agree: an empty input that the + * pattern matches splits into no parts at all, an empty match at the + * current split point is skipped rather than producing an empty part, and a + * match at the very end ends the split. Dart declares split (and the other + * Pattern methods below) with a Pattern, so {@code s.split(RegExp(','))} is + * ordinary source; with only the String overload it generated a call javac + * rejected. + */ + public static DartList split(String s, RegExp pattern) { + DartList out = new DartList<>(); + int length = s.length(); + java.util.Iterator matches = pattern.allMatches(s).iterator(); + if (length == 0 && matches.hasNext()) { + return out; + } + int startIndex = 0; + int previousIndex = 0; + while (true) { + if (startIndex == length || !matches.hasNext()) { + out.add(s.substring(previousIndex, length)); + break; + } + RegExpMatch match = matches.next(); + if (match.start() == length) { + out.add(s.substring(previousIndex, length)); + break; + } + int endIndex = (int) match.end(); + if (startIndex == endIndex && endIndex == previousIndex) { + ++startIndex; + continue; + } + out.add(s.substring(previousIndex, (int) match.start())); + startIndex = previousIndex = endIndex; + } + return out; + } + + public static long indexOf(String s, String other) { + return s.indexOf(other); + } + + public static long indexOf(String s, String other, long start) { + // Dart range-checks the start; Java clamps a negative one to 0 and answers + // -1 past the end, so 'abc'.indexOf('a', -1) found the 'a'. + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + return s.indexOf(other, (int) start); + } + + public static long lastIndexOf(String s, String other) { + return s.lastIndexOf(other); + } + + /** + * Dart's {@code lastIndexOf(pattern, start)}: the last match starting at or before + * {@code start}, which must lie in 0..length. Java clamps an out-of-range start + * instead of throwing, so it is range-checked as {@link #indexOf(String, String, long)} is. + */ + public static long lastIndexOf(String s, String other, long start) { + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + return s.lastIndexOf(other, (int) start); + } + + /** Dart's {@code indexOf(Pattern)} for a RegExp: where the first match starts. */ + public static long indexOf(String s, RegExp pattern) { + return indexOf(s, pattern, 0); + } + + /** Dart's {@code indexOf(Pattern, start)}: the first match found searching from start. */ + public static long indexOf(String s, RegExp pattern, long start) { + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + RegExpMatch m = pattern.matchFrom(s, (int) start); + return m == null ? -1 : m.start(); + } + + /** + * Dart's {@code lastIndexOf(Pattern)} for a RegExp: the greatest position + * at which the pattern matches starting exactly there, as Dart defines it. + */ + public static long lastIndexOf(String s, RegExp pattern) { + return lastIndexOf(s, pattern, s.length()); + } + + /** Dart's {@code lastIndexOf(Pattern, start)} for a RegExp: searching back from start. */ + public static long lastIndexOf(String s, RegExp pattern, long start) { + RangeError.checkValueInInterval(start, 0, s.length(), "start"); + for (int i = (int) start; i >= 0; i--) { + RegExpMatch m = pattern.matchFrom(s, i); + if (m != null && m.start() == i) { + return i; + } + } + return -1; + } + + /** Dart's {@code contains(Pattern)} for a RegExp. */ + public static boolean contains(String s, RegExp pattern) { + return pattern.hasMatch(s); + } + + /** + * Dart's {@code contains(pattern, startIndex)}: a match that starts at or after + * {@code startIndex}, which must lie in 0..length. The start used to be dropped, so + * {@code 'abc'.contains('a', 1)} searched from 0 and answered true. + */ + public static boolean contains(String s, RegExp pattern, long startIndex) { + RangeError.checkValueInInterval(startIndex, 0, s.length(), "startIndex"); + return pattern.matchFrom(s, (int) startIndex) != null; + } + + /** {@link #contains(String, RegExp, long)} for a String pattern. */ + public static boolean contains(String s, String other, long startIndex) { + RangeError.checkValueInInterval(startIndex, 0, s.length(), "startIndex"); + return s.indexOf(other, (int) startIndex) >= 0; + } + + /** + * Dart's {@code replaceAll(Pattern, String)} for a RegExp. The replacement + * is literal -- Dart does not expand group references here; that is + * replaceAllMapped's job -- so every match, empty ones included, is swapped + * for the same text: {@code 'abc'.replaceAll(RegExp(''), '-')} is -a-b-c-. + */ + public static String replaceAll(String s, RegExp pattern, String to) { + StringBuilder sb = new StringBuilder(); + int last = 0; + for (RegExpMatch m : pattern.allMatches(s)) { + sb.append(s, last, (int) m.start()).append(to); + last = (int) m.end(); + } + return sb.append(s, last, s.length()).toString(); + } + + /** Dart's {@code replaceFirst(Pattern, String)} for a RegExp; the replacement is literal. */ + public static String replaceFirst(String s, RegExp pattern, String to) { + RegExpMatch m = pattern.firstMatch(s); + if (m == null) { + return s; + } + return s.substring(0, (int) m.start()) + to + s.substring((int) m.end()); + } + + /** + * Dart's String.trim: strips Dart's whitespace -- Unicode spaces and line + * separators included, a non-breaking space among them. Java's trim only strips + * characters at or below U+0020, so text pasted with non-breaking spaces stayed + * padded after a trim. + */ + public static String trim(String s) { + int start = 0; + int end = s.length(); + while (start < end && isDartWhitespace(s.charAt(start))) { + start++; + } + while (end > start && isDartWhitespace(s.charAt(end - 1))) { + end--; + } + return start == 0 && end == s.length() ? s : s.substring(start, end); + } + + /** The code units Dart's trim treats as whitespace. */ + static boolean isDartWhitespace(char c) { + if (c <= 0x20) { + return c == 0x20 || (c >= 0x09 && c <= 0x0D); + } + return c == 0x85 || c == 0xA0 || c == 0x1680 || (c >= 0x2000 && c <= 0x200A) + || c == 0x2028 || c == 0x2029 || c == 0x202F || c == 0x205F + || c == 0x3000 || c == 0xFEFF; + } + + /** Dart's {@code startsWith(pattern, index)}: whether the pattern occurs at index. */ + public static boolean startsWith(String s, String pattern, long index) { + RangeError.checkValueInInterval(index, 0, s.length(), "index"); + return s.startsWith(pattern, (int) index); + } + + public static boolean contains(String s, String other) { + return s.contains(other); + } + + public static String replaceAll(String s, String from, String to) { + // Dart's replaceAll on a String pattern is literal, like Java's replace. + return s.replace(from, to); + } + + public static String replaceFirst(String s, String from, String to) { + return replaceFirst(s, from, to, 0); + } + + /** Dart's {@code replaceFirst(from, to, startIndex)}: the first match at or after the start. */ + public static String replaceFirst(String s, String from, String to, long startIndex) { + RangeError.checkValueInInterval(startIndex, 0, s.length(), "startIndex"); + int i = s.indexOf(from, (int) startIndex); + if (i < 0) { + return s; + } + return s.substring(0, i) + to + s.substring(i + from.length()); + } + + /** {@link #replaceFirst(String, String, String, long)} for a RegExp pattern. */ + public static String replaceFirst(String s, RegExp pattern, String to, long startIndex) { + RangeError.checkValueInInterval(startIndex, 0, s.length(), "startIndex"); + RegExpMatch m = pattern.matchFrom(s, (int) startIndex); + if (m == null) { + return s; + } + return s.substring(0, (int) m.start()) + to + s.substring((int) m.end()); + } + + /** Dart's int.parse. */ + public static long parseInt(String s) { + Long v = tryParseInt(s); + if (v == null) { + throw new dart.core.FormatException("Invalid radix-10 number: " + s); + } + return v.longValue(); + } + + /** Dart's int.parse with {@code radix:}. */ + public static long parseInt(String s, long radix) { + Long v = tryParseInt(s, radix); + if (v == null) { + throw new dart.core.FormatException("Invalid radix-" + radix + " number: " + s); + } + return v.longValue(); + } + + /** Dart's int.tryParse: decimal, or hexadecimal with a 0x prefix. */ + public static Long tryParseInt(String s) { + return parseSigned(s, -1); + } + + /** Dart's int.tryParse with {@code radix:} -- 2 to 36, no 0x prefix. */ + public static Long tryParseInt(String s, long radix) { + if (radix < 2 || radix > 36) { + throw new RangeError("Invalid value: Not in inclusive range 2..36: " + radix); + } + return parseSigned(s, (int) radix); + } + + /** + * Dart's integer syntax: surrounding whitespace, an optional sign, then digits + * in {@code radix} -- or, with no radix given, decimal digits or 0x and hex + * digits. Null when the text is not that. + */ + private static Long parseSigned(String s, int radix) { + if (s == null) { + return null; + } + // Dart's whitespace, not Java's: String.trim leaves U+00A0, U+2003, U+3000 and + // the rest in place, and 123 between two U+00A0 failed to parse. + String t = trim(s); + boolean negative = false; + if (t.startsWith("-") || t.startsWith("+")) { + negative = t.charAt(0) == '-'; + t = t.substring(1); + } + int r = radix; + if (r < 0) { + if (t.length() > 2 && t.charAt(0) == '0' && (t.charAt(1) == 'x' || t.charAt(1) == 'X')) { + t = t.substring(2); + r = 16; + } else { + r = 10; + } + } + if (t.isEmpty()) { + return null; + } + for (int i = 0; i < t.length(); i++) { + if (Character.digit(t.charAt(i), r) < 0) { + return null; // also rejects a second sign, which parseLong would take + } + } + try { + return Long.valueOf(Long.parseLong(negative ? "-" + t : t, r)); + } catch (NumberFormatException e) { + return null; // out of range + } + } + + /** + * Dart's String.toUpperCase: locale independent, one UTF-16 unit at a time, so + * a sharp s (U+00DF) is left alone, as Dart leaves it. Java's String.toUpperCase follows + * the default locale, and under Turkish folds i to a dotted capital. + */ + public static String toUpperCase(String s) { + char[] out = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + char u = Character.toUpperCase(c); + if (u != c) { + if (out == null) { + out = s.toCharArray(); + } + out[i] = u; + } + } + return out == null ? s : new String(out); + } + + /** Dart's String.toLowerCase: locale independent, as {@link #toUpperCase}. */ + public static String toLowerCase(String s) { + char[] out = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + char l = Character.toLowerCase(c); + if (l != c) { + if (out == null) { + out = s.toCharArray(); + } + out[i] = l; + } + } + return out == null ? s : new String(out); + } + + /** Dart's double.parse. */ + public static double parseDouble(String s) { + Double d = tryParseDouble(s); + if (d == null) { + throw new dart.core.FormatException("Invalid double: " + s); + } + return d.doubleValue(); + } + + /** + * Dart's double.tryParse: null for anything outside Dart's grammar -- an optional + * sign, then NaN, Infinity, or decimal digits with an optional fraction and + * exponent, with surrounding whitespace allowed. Java's parser also accepts a d or f + * suffix and hexadecimal floats, so '1d' and '0x1.0p0' parsed to 1.0 where Dart + * answers null. + */ + public static Double tryParseDouble(String s) { + if (s == null) { + return null; + } + String t = trim(s); + if (!isDartDouble(t)) { + return null; + } + try { + return Double.valueOf(Double.parseDouble(t)); + } catch (NumberFormatException e) { + return null; + } + } + + private static boolean isDartDouble(String t) { + int i = 0; + int n = t.length(); + if (i < n && (t.charAt(i) == '+' || t.charAt(i) == '-')) { + i++; + } + String rest = t.substring(i); + if (rest.equals("NaN") || rest.equals("Infinity")) { + return true; + } + int digits = 0; + while (i < n && t.charAt(i) >= '0' && t.charAt(i) <= '9') { + i++; + digits++; + } + if (i < n && t.charAt(i) == '.') { + i++; + while (i < n && t.charAt(i) >= '0' && t.charAt(i) <= '9') { + i++; + digits++; + } + } + if (digits == 0) { + return false; + } + if (i < n && (t.charAt(i) == 'e' || t.charAt(i) == 'E')) { + i++; + if (i < n && (t.charAt(i) == '+' || t.charAt(i) == '-')) { + i++; + } + int exp = 0; + while (i < n && t.charAt(i) >= '0' && t.charAt(i) <= '9') { + i++; + exp++; + } + if (exp == 0) { + return false; + } + } + return i == n; + } + + public static long compareTo(String a, String b) { + int r = a.compareTo(b); + return r < 0 ? -1 : (r > 0 ? 1 : 0); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartComparable.java b/maven/dart-runtime/src/main/java/dart/core/DartComparable.java new file mode 100644 index 00000000000..83042337e5e --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartComparable.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Static helpers for Dart's {@code Comparable}. Dart exposes + * {@code Comparable.compare(a, b)} as a static combinator; it delegates to the + * receivers' {@code compareTo}. Instances map onto {@link java.lang.Comparable}. + */ +public final class DartComparable { + + private DartComparable() { + } + + /** + * {@code Comparable.compare}: returns a negative value, zero, or a positive + * value as {@code a} orders before, equal to, or after {@code b}. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static long compare(Object a, Object b) { + if (a instanceof Number && b instanceof Number) { + return compareNumbers((Number) a, (Number) b); + } + if (a instanceof Comparable) { + return ((Comparable) a).compareTo(b); + } + throw new TypeError("type '" + (a == null ? "Null" : a.getClass().getName()) + + "' is not a subtype of type 'Comparable'"); + } + + /** + * Dart's {@code num.compareTo} across int and double. Java's wrappers only + * compare within their own class, so a List<num> holding 2 and 1.5 + * threw ClassCastException from Long.compareTo(Double) instead of sorting. + * Double.compare gives Dart's order between two doubles: -0.0 before 0.0, + * NaN after everything. An int against a double is compared exactly, as + * the Dart VM does: widening the int first rounds 2^53 + 1 onto the double + * 2^53 and reported them equal. + */ + private static long compareNumbers(Number a, Number b) { + boolean ai = isIntegral(a); + boolean bi = isIntegral(b); + if (ai && bi) { + long x = a.longValue(); + long y = b.longValue(); + return x < y ? -1 : x > y ? 1 : 0; + } + if (!ai && !bi) { + return Double.compare(a.doubleValue(), b.doubleValue()); + } + long l = ai ? a.longValue() : b.longValue(); + double d = ai ? b.doubleValue() : a.doubleValue(); + int c; + if (d != d) { + c = -1; // every int orders before NaN + } else { + c = compareIntDouble(l, d); + if (c == 0 && l == 0 && 1 / d < 0) { + c = 1; // 0.compareTo(-0.0) is 1 + } + } + return ai ? c : -c; + } + + /** + * The numeric order of an int and a non-NaN double, without widening the + * int: -1, 0 or 1 as {@code l} is below, equal to or above {@code d}. Zero + * and negative zero are the same value here; callers that order them + * (compareTo, but not the relational operators) do that themselves. + */ + public static int compareIntDouble(long l, double d) { + // [-2^63, 2^63) is the range of doubles a long can hold; outside it the + // (long) cast below would saturate and compare wrongly. + if (d >= 9.223372036854775808E18) { + return -1; + } + if (d < -9.223372036854775808E18) { + return 1; + } + double f = Math.floor(d); + long fl = (long) f; + if (l != fl) { + return l < fl ? -1 : 1; + } + return d > f ? -1 : 0; // l == floor(d): below d iff d has a fraction + } + + private static boolean isIntegral(Number n) { + return n instanceof Long || n instanceof Integer || n instanceof Short || n instanceof Byte; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartDoubleList.java b/maven/dart-runtime/src/main/java/dart/core/DartDoubleList.java new file mode 100644 index 00000000000..5e3acad86ee --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartDoubleList.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.Arrays; + +import dart.runtime.Funcs; + +/** + * A {@link DartList}<Double> backed by a primitive {@code double[]} — the transpiler targets this for + * Dart {@code List<double>}. The hot index/add paths ({@link #getDouble(long)}, {@link #setDouble(long, double)}, + * {@link #addDouble(double)}) never box; the inherited list surface still works (boxing only at that + * generic boundary) because {@link DartList} routes through the overridden {@code get/set/size/add}. + */ +public final class DartDoubleList extends DartList { + + private double[] a; + private int len; + + public DartDoubleList() { + super(true); + a = new double[8]; + } + + private DartDoubleList(double[] a, int len, boolean growable) { + this(a, len, growable, false); + } + + private DartDoubleList(double[] a, int len, boolean growable, boolean readOnly) { + super(growable, readOnly); + this.a = a; + this.len = len; + } + + /** + * A fixed-length list over {@code backing} itself, not a copy: writes through + * it land in the array. Dart's Float64List views -- vector_math's + * {@code Matrix4.storage} -- are exactly that. + */ + public static DartDoubleList view(double[] backing) { + return new DartDoubleList(backing, backing.length, false); + } + + /** Literal helper for Dart's <double>[a, b, c]. */ + public static DartDoubleList ofDoubles(double... elements) { + double[] backing = elements.length == 0 ? new double[8] : Arrays.copyOf(elements, Math.max(8, elements.length)); + return new DartDoubleList(backing, elements.length, true); + } + + /** Dart's List<double>.filled(length, fill). */ + public static DartDoubleList filled(long length, double fill, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + int n = (int) length; + double[] backing = new double[Math.max(8, n)]; + Arrays.fill(backing, 0, n, fill); + return new DartDoubleList(backing, n, growable); + } + + public static DartDoubleList filled(long length, double fill) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return filled(length, fill, false); + } + + // Named *Doubles (mirroring ofDoubles) to avoid an erasure clash with the + // inherited DartList generic generate/from statics. + + /** Dart's List<double>.generate(length, generator). */ + public static DartDoubleList generateDoubles(long length, Funcs.Func1 generator, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + int n = (int) length; + double[] backing = new double[Math.max(8, n)]; + for (int i = 0; i < n; i++) { + backing[i] = generator.call((long) i); + } + return new DartDoubleList(backing, n, growable); + } + + public static DartDoubleList generateDoubles(long length, Funcs.Func1 generator) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return generateDoubles(length, generator, true); + } + + /** Dart's List<double>.from(elements). */ + /** {@link #fromDoubles(Iterable)} with Dart's {@code growable:} flag. */ + public static DartDoubleList fromDoubles(Iterable elements, boolean growable) { + DartDoubleList grown = fromDoubles(elements); + return new DartDoubleList(grown.a, grown.len, growable); + } + + public static DartDoubleList fromDoubles(Iterable elements) { + DartDoubleList l = new DartDoubleList(); + for (Number e : elements) { + // As List.from: the element must BE a double; an int is not one in Dart. + if (!(e instanceof Double || e instanceof Float)) { + throw new TypeError("type '" + (e == null ? "Null" : "int") + + "' is not a subtype of type 'double'"); + } + l.addDouble(e.doubleValue()); + } + return l; + } + + /** Dart's {@code List.unmodifiable(source)}: a copy that refuses every write. */ + public static DartDoubleList unmodifiableDoubles(Iterable elements) { + DartDoubleList copy = fromDoubles(elements); + return new DartDoubleList(copy.a, copy.len, false, true); + } + + private void ensure(int cap) { + if (cap > a.length) { + a = Arrays.copyOf(a, Math.max(cap, a.length * 2)); + } + } + + // --- primitive fast paths (transpiler targets these for List) --- + + public double getDouble(long index) { + // Inline bounds check + cold throw helper — keeps this frameless (see + // RangeError.indexError / DartLongList.getLong). + if (index < 0 || index >= len) { + RangeError.indexError(index, len); + } + return a[(int) index]; + } + + public double setDouble(long index, double value) { + checkWritable(); + if (index < 0 || index >= len) { + RangeError.indexError(index, len); + } + a[(int) index] = value; + return value; + } + + public boolean addDouble(double value) { + checkGrowable("add"); + ensure(len + 1); + a[len++] = value; + modCount++; + return true; + } + + // --- storage accessors (boxed boundary for the inherited surface) --- + + @Override + public Double get(int index) { + RangeError.checkValidIndex(index, len); + return a[index]; + } + + @Override + public Double set(int index, Double element) { + checkWritable(); + RangeError.checkValidIndex(index, len); + double old = a[index]; + a[index] = element; + return old; + } + + @Override + public int size() { + return len; + } + + @Override + public boolean add(Double e) { + return addDouble(e); + } + + @Override + public void add(int index, Double element) { + checkGrowable("insert"); + ensure(len + 1); + System.arraycopy(a, index, a, index + 1, len - index); + a[index] = element; + len++; + modCount++; + } + + @Override + public Double remove(int index) { + checkGrowable("removeAt"); + RangeError.checkValidIndex(index, len); + double old = a[index]; + System.arraycopy(a, index + 1, a, index, len - index - 1); + len--; + modCount++; + return old; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartException.java b/maven/dart-runtime/src/main/java/dart/core/DartException.java new file mode 100644 index 00000000000..0be430b5d34 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's Exception('message') — the generic exception users throw. + */ +public class DartException extends RuntimeException { + public DartException(String message) { + super(message); + } + + @Override + public String toString() { + return "Exception: " + getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartIdentityMap.java b/maven/dart-runtime/src/main/java/dart/core/DartIdentityMap.java new file mode 100644 index 00000000000..f510c3d1b78 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartIdentityMap.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Dart's {@code Map.identity()}: keys match only when they are the SAME object, + * whatever their {@code ==} says. Insertion ordered, like every Dart map. + * + *

{@code Map.identity()} used to answer a plain DartMap, so two distinct but equal + * keys -- two separately built {@code ValueKey('a')}, say -- collapsed into one + * entry, which is exactly what an identity map exists to prevent.

+ * + *

The entries live in an inner map keyed by an identity wrapper; the inherited + * storage is never used. Every access path DartMap and generated code take -- + * get, put, containsKey, remove, size, clear and the three views -- is overridden + * to go through the inner map.

+ */ +public final class DartIdentityMap extends DartMap { + + /** A key compared by identity. */ + private static final class IdKey { + final Object key; + + IdKey(Object key) { + this.key = key; + } + + /// Dart's identical(), not Java's ==: two separately boxed 1000s are the same + /// Dart int, and were two keys here. + @Override + public boolean equals(Object o) { + return o instanceof IdKey && dart.runtime.DartRuntime.identical(((IdKey) o).key, key); + } + + /// Consistent with equals: a boxed number or bool hashes by value. + @Override + public int hashCode() { + if (key instanceof Long || key instanceof Double || key instanceof Boolean) { + return key.hashCode(); + } + return System.identityHashCode(key); + } + } + + private final LinkedHashMap inner = new LinkedHashMap(); + + @Override + public V get(Object key) { + return inner.get(new IdKey(key)); + } + + @Override + public boolean containsKey(Object key) { + return inner.containsKey(new IdKey(key)); + } + + @Override + public V put(K key, V value) { + IdKey k = new IdKey(key); + if (!inner.containsKey(k)) { + structuralChange(); + } + return inner.put(k, value); + } + + @Override + public void putAll(Map other) { + for (Map.Entry e : other.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public V remove(Object key) { + IdKey k = new IdKey(key); + if (inner.containsKey(k)) { + structuralChange(); + } + return inner.remove(k); + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public boolean containsValue(Object value) { + // Identity applies to the keys only; values still compare with Dart's ==. + return containsValueDart(inner.values(), value); + } + + @Override + public void clear() { + if (!inner.isEmpty()) { + structuralChange(); + } + inner.clear(); + } + + @Override + public Collection values() { + return inner.values(); + } + + @Override + public Set keySet() { + return new AbstractSet() { + @Override + public Iterator iterator() { + final Iterator it = inner.keySet().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + @SuppressWarnings("unchecked") + public K next() { + return (K) it.next().key; + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + @Override + public int size() { + return inner.size(); + } + }; + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + final Iterator> it = inner.entrySet().iterator(); + return new Iterator>() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Map.Entry next() { + final Map.Entry e = it.next(); + return new Map.Entry() { + @Override + @SuppressWarnings("unchecked") + public K getKey() { + return (K) e.getKey().key; + } + + @Override + public V getValue() { + return e.getValue(); + } + + @Override + public V setValue(V v) { + return e.setValue(v); + } + }; + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + @Override + public int size() { + return inner.size(); + } + }; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartIdentitySet.java b/maven/dart-runtime/src/main/java/dart/core/DartIdentitySet.java new file mode 100644 index 00000000000..ca52bd65aa4 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartIdentitySet.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.Iterator; + +/** + * Dart's {@code Set.identity()}: elements match only when they are the SAME object. + * Insertion ordered, like every Dart set. + * + *

{@code Set.identity()} used to answer a plain DartSet, which collapsed distinct + * but equal elements -- two separately built {@code ValueKey('a')} -- into one.

+ * + *

Stored in a {@link DartIdentityMap}; the inherited LinkedHashSet storage is never + * used, and every access path is overridden to go through the map.

+ */ +public final class DartIdentitySet extends DartSet { + + private final DartIdentityMap inner = new DartIdentityMap(); + + @Override + public boolean add(E e) { + if (inner.containsKey(e)) { + return false; + } + inner.put(e, Boolean.TRUE); + structuralChange(); + return true; + } + + @Override + public boolean contains(Object e) { + return inner.containsKey(e); + } + + @Override + public boolean remove(Object e) { + if (!inner.containsKey(e)) { + return false; + } + inner.remove(e); + structuralChange(); + return true; + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public void clear() { + if (!inner.isEmpty()) { + structuralChange(); + } + inner.clear(); + } + + @Override + public Iterator iterator() { + return inner.keySet().iterator(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartIterable.java b/maven/dart-runtime/src/main/java/dart/core/DartIterable.java new file mode 100644 index 00000000000..19a81bfaa8f --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartIterable.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Dart's Iterable<E>: lazy combinators over a source iterable. + * Implements java.lang.Iterable for zero-friction interop. + */ +public class DartIterable implements Iterable { + + private final Iterable source; + + protected DartIterable(Iterable source) { + this.source = source; + } + + public static DartIterable wrap(Iterable source) { + return source instanceof DartIterable di ? di : new DartIterable<>(source); + } + + /** + * A {@code sync*} generator's iterable. {@code body} runs the generator's body into + * the list it is given, and it runs when an iteration begins -- once per iteration, + * as Dart re-runs a generator for each iterator. It used to run once, when the + * function was called: side effects happened before anyone iterated, and a second + * iteration replayed the first one's values instead of running the body again. + * + *

Still a divergence: an iteration runs the WHOLE body before its first element, + * where Dart suspends at each {@code yield}. An infinite generator therefore does not + * terminate here. Suspending needs the body rewritten as a state machine, the same + * continuation-passing lowering the blocking async model defers.

+ */ + public static DartIterable syncStar(final Funcs.VoidFunc1> body) { + return wrap(new Iterable() { + @Override + public Iterator iterator() { + DartList out = new DartList(); + body.call(out); + return out.iterator(); + } + }); + } + + /** + * Dart's {@code Iterable.generate(count, [generator])} — a lazy iterable of + * {@code count} elements produced by {@code generator(index)}. With no + * generator Dart yields the indices themselves. A negative count is an empty + * iterable, not an error: measured on the Dart 3.9 VM, {@code Iterable.generate(-1)} + * neither throws nor yields anything (only {@code List.generate(-1)} throws). + */ + public static DartIterable generate(long count, Funcs.Func1 generator) { + return new DartIterable<>(() -> new Iterator() { + private long i; + + @Override + public boolean hasNext() { + return i < count; + } + + @Override + public E next() { + if (i >= count) { + throw new NoSuchElementException(); + } + return generator.call(i++); + } + }); + } + + @SuppressWarnings("unchecked") + public static DartIterable generate(long count) { + return generate(count, i -> (E) i); + } + + @Override + public Iterator iterator() { + return source.iterator(); + } + + public DartIterable map(Funcs.Func1 f) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator it = src.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public R next() { + return f.call(it.next()); + } + }); + } + + public DartIterable where(Funcs.Func1 test) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator it = src.iterator(); + private boolean ready; + private E next; + + private void advance() { + while (!ready && it.hasNext()) { + E candidate = it.next(); + if (Boolean.TRUE.equals(test.call(candidate))) { + next = candidate; + ready = true; + } + } + } + + @Override + public boolean hasNext() { + advance(); + return ready; + } + + @Override + public E next() { + advance(); + if (!ready) { + throw new NoSuchElementException(); + } + ready = false; + E r = next; + next = null; + return r; + } + }); + } + + public DartIterable take(long count) { + // Dart refuses a negative count; it silently meant "nothing" here, and the + // mirrored skip(-1) returned everything. + RangeError.checkNotNegative(count, "count"); + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator it = src.iterator(); + private long remaining = count; + + @Override + public boolean hasNext() { + return remaining > 0 && it.hasNext(); + } + + @Override + public E next() { + if (remaining <= 0) { + throw new NoSuchElementException(); + } + remaining--; + return it.next(); + } + }); + } + + public DartIterable skip(long count) { + RangeError.checkNotNegative(count, "count"); + Iterable src = this; + return new DartIterable<>(() -> { + Iterator it = src.iterator(); + for (long i = 0; i < count && it.hasNext(); i++) { + it.next(); + } + return it; + }); + } + + public long length() { + long n = 0; + for (E ignored : this) { + n++; + } + return n; + } + + public boolean isEmpty() { + return !iterator().hasNext(); + } + + public boolean isNotEmpty() { + return iterator().hasNext(); + } + + public E first() { + Iterator it = iterator(); + if (!it.hasNext()) { + throw new StateError("No element"); + } + return it.next(); + } + + public E last() { + Iterator it = iterator(); + if (!it.hasNext()) { + throw new StateError("No element"); + } + E e = it.next(); + while (it.hasNext()) { + e = it.next(); + } + return e; + } + + public E firstWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + for (E e : this) { + if (Boolean.TRUE.equals(test.call(e))) { + return e; + } + } + if (orElse != null) { + return orElse.call(); + } + throw new StateError("No element"); + } + + /** Dart's Iterable.elementAt(index) — the index-th element (0-based). */ + public E elementAt(long index) { + if (index < 0) { + throw new RangeError("index out of range: " + index); + } + long i = 0; + for (E e : this) { + if (i == index) { + return e; + } + i++; + } + throw new RangeError("index out of range: " + index); + } + + public boolean any(Funcs.Func1 test) { + for (E e : this) { + if (Boolean.TRUE.equals(test.call(e))) { + return true; + } + } + return false; + } + + public boolean every(Funcs.Func1 test) { + for (E e : this) { + if (!Boolean.TRUE.equals(test.call(e))) { + return false; + } + } + return true; + } + + public boolean contains(Object element) { + for (E e : this) { + if (dart.runtime.DartRuntime.eq(e, element)) { + return true; + } + } + return false; + } + + public void forEach(Funcs.VoidFunc1 action) { + for (E e : this) { + action.call(e); + } + } + + /** + * The name the emitter uses for Dart's forEach on every collection receiver -- + * DartList and DartSet need it to stay clear of java.lang.Iterable's own forEach. + * Without it here, forEach on an Iterable -- the result of map or where -- + * generated a call javac rejected. + */ + public void forEachDart(Funcs.VoidFunc1 action) { + forEach(action); + } + + public R fold(R initialValue, Funcs.Func2 combine) { + R acc = initialValue; + for (E e : this) { + acc = combine.call(acc, e); + } + return acc; + } + + /** Dart's {@code Iterable.reduce(combine)} — folds without a seed. */ + public E reduce(Funcs.Func2 combine) { + Iterator it = iterator(); + if (!it.hasNext()) { + throw new StateError("No element"); + } + E acc = it.next(); + while (it.hasNext()) { + acc = combine.call(acc, it.next()); + } + return acc; + } + + /** Dart's {@code Iterable.expand(f)} — flat-maps each element to an iterable. */ + public DartIterable expand(Funcs.Func1> f) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator outer = src.iterator(); + private Iterator inner; + + private void advance() { + while ((inner == null || !inner.hasNext()) && outer.hasNext()) { + Iterable next = f.call(outer.next()); + inner = next == null ? null : next.iterator(); + } + } + + @Override + public boolean hasNext() { + advance(); + return inner != null && inner.hasNext(); + } + + @Override + public R next() { + advance(); + if (inner == null || !inner.hasNext()) { + throw new NoSuchElementException(); + } + return inner.next(); + } + }); + } + + /** Dart's {@code Iterable.followedBy(other)} — lazy concatenation. */ + public DartIterable followedBy(Iterable other) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private Iterator it = src.iterator(); + private boolean second; + + @Override + public boolean hasNext() { + if (it.hasNext()) { + return true; + } + if (!second) { + second = true; + it = other == null ? java.util.Collections.emptyIterator() : other.iterator(); + } + return it.hasNext(); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return it.next(); + } + }); + } + + /** + * Dart's {@code Iterable.whereType<T>()} — the transpiler threads the + * requested type as a trailing {@code Class} witness. + */ + @SuppressWarnings("unchecked") + public DartIterable whereType(Class type) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator it = src.iterator(); + private boolean ready; + private T next; + + private void advance() { + while (!ready && it.hasNext()) { + E c = it.next(); + if (type == null ? c != null : type.isInstance(c)) { + next = (T) c; + ready = true; + } + } + } + + @Override + public boolean hasNext() { + advance(); + return ready; + } + + @Override + public T next() { + advance(); + if (!ready) { + throw new NoSuchElementException(); + } + ready = false; + T r = next; + next = null; + return r; + } + }); + } + + /** Dart's {@code Iterable.asMap()} — index-to-element map. */ + public DartMap asMap() { + DartMap m = new DartMap<>(); + long i = 0; + for (E e : this) { + m.put(i++, e); + } + return m; + } + + /** Dart's {@code Iterable.singleWhere(test, {orElse})}. */ + public E singleWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + E found = null; + boolean seen = false; + for (E e : this) { + if (Boolean.TRUE.equals(test.call(e))) { + if (seen) { + throw new StateError("Too many elements"); + } + found = e; + seen = true; + } + } + if (seen) { + return found; + } + if (orElse != null) { + return orElse.call(); + } + throw new StateError("No element"); + } + + /** Dart's {@code Iterable.lastWhere(test, {orElse})}. */ + public E lastWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + E found = null; + boolean seen = false; + for (E e : this) { + if (Boolean.TRUE.equals(test.call(e))) { + found = e; + seen = true; + } + } + if (seen) { + return found; + } + if (orElse != null) { + return orElse.call(); + } + throw new StateError("No element"); + } + + public DartIterable takeWhile(Funcs.Func1 test) { + Iterable src = this; + return new DartIterable<>(() -> new Iterator() { + private final Iterator it = src.iterator(); + private boolean done; + private boolean ready; + private E next; + + private void advance() { + if (!ready && !done && it.hasNext()) { + E c = it.next(); + if (Boolean.TRUE.equals(test.call(c))) { + next = c; + ready = true; + } else { + done = true; + } + } + } + + @Override + public boolean hasNext() { + advance(); + return ready; + } + + @Override + public E next() { + advance(); + if (!ready) { + throw new NoSuchElementException(); + } + ready = false; + return next; + } + }); + } + + public DartIterable skipWhile(Funcs.Func1 test) { + Iterable src = this; + return new DartIterable<>(() -> { + Iterator it = src.iterator(); + java.util.ArrayList buffered = new java.util.ArrayList<>(); + while (it.hasNext()) { + E c = it.next(); + if (!Boolean.TRUE.equals(test.call(c))) { + buffered.add(c); + break; + } + } + Iterator tail = it; + Iterator head = buffered.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return head.hasNext() || tail.hasNext(); + } + + @Override + public E next() { + return head.hasNext() ? head.next() : tail.next(); + } + }; + }); + } + + public String join(String separator) { + StringBuilder sb = new StringBuilder(); + boolean firstItem = true; + for (E e : this) { + if (!firstItem) { + sb.append(separator); + } + sb.append(dart.runtime.DartRuntime.str(e)); + firstItem = false; + } + return sb.toString(); + } + + public String join() { + return join(""); + } + + /** Dart's {@code toList(growable: ...)}: a fixed-length copy when growable is false. */ + public DartList toList(boolean growable) { + return growable ? toList() : DartList.from(this, false); + } + + public DartList toList() { + DartList l = new DartList<>(); + for (E e : this) { + l.add(e); + } + return l; + } + + public DartSet toSet() { + DartSet s = new DartSet<>(); + for (E e : this) { + s.add(e); + } + return s; + } + + @Override + public String toString() { + if (!DartRuntime.beginFormat(this)) { + return "(...)"; + } + try { + return "(" + join(", ") + ")"; + } finally { + DartRuntime.endFormat(this); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartList.java b/maven/dart-runtime/src/main/java/dart/core/DartList.java new file mode 100644 index 00000000000..13c75fdecf6 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartList.java @@ -0,0 +1,754 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.RandomAccess; + +/** + * Dart's List<E>: growable, insertion-ordered. Extends + * java.util.AbstractList so it interoperates with any Java/CN1 API for free, + * and adds the Dart API surface the transpiler targets. + * + *

Indexes in the Dart API arrive as {@code long} (Dart int); they are + * range-checked with Dart's RangeError semantics.

+ * + *

All the Dart-API methods route element access through the overridable + * {@link #get(int)}/{@link #set(int, Object)}/{@link #size()}/{@link #add(Object)} + * accessors, so a subclass backed by a primitive array (see + * {@link DartLongList}, {@link DartDoubleList}) inherits the whole surface + * while avoiding boxing on the hot index/add paths.

+ */ +public class DartList extends AbstractList implements RandomAccess { + + /// Identity, as Dart's own {@code List} has: {@code [1] == [1]} is false in Dart + /// unless a type overrides {@code operator ==}. The inherited Java equality is + /// structural, so two separately built collections compared equal and, used as + /// keys, collapsed into one entry of a map -- different control flow and lost + /// data in a transpiled application. Element-wise comparison is what Dart's + /// listEquals, mapEquals and setEquals are for, and they say so explicitly. + @Override + public boolean equals(Object other) { + return this == other; + } + + /// Consistent with {@link #equals}: identity. + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + private final ArrayList impl; + private final boolean growable; + /// Set for List.unmodifiable and the lists Dart hands out read-only (Uri.pathSegments): + /// every write is refused, element stores as well as length changes. + private final boolean readOnly; + + public DartList() { + this.impl = new ArrayList<>(); + this.growable = true; + this.readOnly = false; + } + + DartList(ArrayList impl, boolean growable) { + this(impl, growable, false); + } + + DartList(ArrayList impl, boolean growable, boolean readOnly) { + this.impl = impl; + this.growable = growable && !readOnly; + this.readOnly = readOnly; + } + + /** Subclass hook: primitive-backed lists pass their own storage marker. */ + DartList(boolean growable) { + this(growable, false); + } + + /** Subclass hook with a read-only flag: the primitive lists' List.unmodifiable. */ + DartList(boolean growable, boolean readOnly) { + this.impl = null; + this.growable = growable && !readOnly; + this.readOnly = readOnly; + } + + /** Refuses an element store on a read-only list; the primitive lists' setters call it too. */ + final void checkWritable() { + if (readOnly) { + throw new UnsupportedError("Cannot modify an unmodifiable list"); + } + } + + /** Literal helper: DartList.of(a, b, c) for Dart's [a, b, c]. */ + @SafeVarargs + public static DartList of(E... elements) { + DartList l = new DartList<>(); + for (E e : elements) { + l.impl.add(e); + } + return l; + } + + /** + * Dart's {@code List.from(elements, growable: ...)}. The flag used to be dropped, + * so a {@code growable: false} copy still accepted add and removeAt. + */ + public static DartList from(Iterable elements, boolean growable) { + ArrayList impl = new ArrayList(); + for (E e : elements) { + impl.add(e); + } + return new DartList(impl, growable); + } + + /** + * {@code List.from(elements)} with Dart's per-element type check: {@code type} is + * E's class. Java erases E, so the plain copy accepted anything -- + * {@code List.from([1])} stored a Long that failed only when a later + * typed read cast it. Dart throws TypeError from the constructor. + */ + @SuppressWarnings("unchecked") + public static DartList fromChecked(Object source, Class type, boolean nullable, + boolean growable) { + // Object, not Iterable: a dynamic source arrives untyped, as Dart allows. + if (!(source instanceof Iterable)) { + throw new TypeError("type '" + (source == null ? "Null" : source.getClass().getName()) + + "' is not a subtype of type 'Iterable'"); + } + ArrayList impl = new ArrayList(); + for (Object e : (Iterable) source) { + if (e == null ? !nullable : !type.isInstance(e)) { + String n = type.getName(); + throw new TypeError("type '" + (e == null ? "Null" : e.getClass().getName()) + + "' is not a subtype of type '" + n.substring(n.lastIndexOf('.') + 1) + "'"); + } + impl.add((E) e); + } + return new DartList(impl, growable); + } + + public static DartList from(Iterable elements) { + DartList l = new DartList<>(); + for (E e : elements) { + l.impl.add(e); + } + return l; + } + + /** Dart's List.filled(length, fill). */ + public static DartList filled(long length, E fill, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + ArrayList impl = new ArrayList<>(); + for (long i = 0; i < length; i++) { + impl.add(fill); + } + return new DartList<>(impl, growable); + } + + public static DartList filled(long length, E fill) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return filled(length, fill, false); + } + + /** Dart's List.generate(length, generator). */ + public static DartList generate(long length, Funcs.Func1 generator, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + ArrayList impl = new ArrayList<>(); + for (long i = 0; i < length; i++) { + impl.add(generator.call(i)); + } + return new DartList<>(impl, growable); + } + + public static DartList generate(long length, Funcs.Func1 generator) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return generate(length, generator, true); + } + + final boolean isGrowable() { + return growable; + } + + void checkGrowable(String op) { + if (readOnly) { + throw new UnsupportedError("Cannot " + op + " an unmodifiable list"); + } + if (!growable) { + throw new UnsupportedError(op + " on a fixed-length list"); + } + } + + // ------------------------------------------------------------------ + // java.util.List plumbing — the storage accessors (overridden by + // primitive-backed subclasses). Everything below routes through these. + // ------------------------------------------------------------------ + + @Override + public E get(int index) { + RangeError.checkValidIndex(index, impl.size()); + return impl.get(index); + } + + @Override + public E set(int index, E element) { + checkWritable(); + RangeError.checkValidIndex(index, impl.size()); + return impl.set(index, element); + } + + @Override + public int size() { + return impl.size(); + } + + @Override + public boolean add(E e) { + checkGrowable("add"); + // Every structural change moves AbstractList's modCount. The storage is a + // separate ArrayList, so without this the iterator never saw a change: + // `for (x in l) l.add(x)` chased the growing size forever where Dart throws. + modCount++; + return impl.add(e); + } + + @Override + public void add(int index, E element) { + checkGrowable("insert"); + modCount++; + impl.add(index, element); + } + + @Override + public E remove(int index) { + checkGrowable("removeAt"); + RangeError.checkValidIndex(index, size()); + modCount++; + return impl.remove(index); + } + + // ------------------------------------------------------------------ + // Dart API (long-indexed) — routed through the accessors above + // ------------------------------------------------------------------ + + /** Dart's list[i]. */ + public E idx(long index) { + RangeError.checkValidIndex(index, size()); + return get((int) index); + } + + /** Dart's list[i] = v. */ + public E idxSet(long index, E value) { + RangeError.checkValidIndex(index, size()); + set((int) index, value); + return value; + } + + public long length() { + return size(); + } + + public boolean isNotEmpty() { + return size() != 0; + } + + public E first() { + if (size() == 0) { + throw new StateError("No element"); + } + return get(0); + } + + public E last() { + if (size() == 0) { + throw new StateError("No element"); + } + return get(size() - 1); + } + + public void insert(long index, E element) { + checkGrowable("insert"); + RangeError.checkValueInInterval(index, 0, size(), "index"); + add((int) index, element); + } + + public E removeAt(long index) { + checkGrowable("removeAt"); + RangeError.checkValidIndex(index, size()); + return remove((int) index); + } + + public E removeLast() { + checkGrowable("removeLast"); + if (size() == 0) { + throw new RangeError("RangeError (index): Invalid value: Valid value range is empty: -1"); + } + return remove(size() - 1); + } + + /** Dart's List.remove(Object) — removes first match, returns whether found. */ + public boolean removeValue(Object value) { + checkGrowable("remove"); + for (int i = 0; i < size(); i++) { + if (DartRuntime.eq(get(i), value)) { + remove(i); + return true; + } + } + return false; + } + + /** Dart's {@code List + List}: a new list with the elements of {@code a} then {@code b}. */ + public static DartList concat(java.util.List a, java.util.List b) { + DartList r = new DartList(); + if (a != null) { + r.addAll(a); + } + if (b != null) { + r.addAll(b); + } + return r; + } + + /** Dart's List.addAll — named distinctly because java.util.List.addAll(Collection) makes the overload ambiguous. */ + public void addAllIterable(Iterable elements) { + checkGrowable("addAll"); + if (elements == this) { + // list.addAll(list) duplicates the ORIGINAL elements in Dart. Iterating + // this list while appending to it never ends here: the storage is the + // backing ArrayList, so AbstractList's modCount never moves, and the + // iterator neither snapshots the length nor fails fast -- it chases the + // end forever. Walking the original length by index is the snapshot, + // and reads through get() so the primitive-backed subclasses work too. + int n = size(); + for (int i = 0; i < n; i++) { + add(get(i)); + } + return; + } + for (E e : elements) { + add(e); + } + } + + /** Dart's {@code indexOf(element, start)}: the first match at or after start. */ + public long indexOfDart(E element, long start) { + // Decided on the long: narrowing first wrapped 2^32 to 0 and searched from the top. + if (start >= size()) { + return -1; + } + for (int i = (int) Math.max(0, start); i < size(); i++) { + if (DartRuntime.eq(get(i), element)) { + return i; + } + } + return -1; + } + + /** Dart's List.indexOf — long-typed; named to avoid clashing with java.util.List.indexOf(Object). */ + /** + * Dart's {@code lastIndexOf(element, [start])}: searching backward from {@code start} + * (clamped to the last index), with Dart's {@code ==}. + */ + public long lastIndexOfDart(E element, long start) { + long from = Math.min(start, (long) size() - 1); + for (int i = (int) from; i >= 0; i--) { + if (DartRuntime.eq(get(i), element)) { + return i; + } + } + return -1; + } + + public long lastIndexOfDart(E element) { + return lastIndexOfDart(element, size() - 1); + } + + public long indexOfDart(E element) { + for (int i = 0; i < size(); i++) { + if (DartRuntime.eq(get(i), element)) { + return i; + } + } + return -1; + } + + /** + * Dart's List.contains, with Dart's {@code ==} like removeValue and + * indexOfDart above. The inherited AbstractList version compares with Java + * equals, so a List<num> holding the int 1 answered false for 1.0. + */ + @Override + public boolean contains(Object element) { + for (int i = 0; i < size(); i++) { + if (DartRuntime.eq(get(i), element)) { + return true; + } + } + return false; + } + + /** Dart's {@code List.indexWhere(test, [start])}. */ + public long indexWhere(Funcs.Func1 test, long start) { + if (start >= size()) { + return -1; + } + for (int i = (int) Math.max(0, start); i < size(); i++) { + if (Boolean.TRUE.equals(test.call(get(i)))) { + return i; + } + } + return -1; + } + + public long indexWhere(Funcs.Func1 test) { + return indexWhere(test, 0); + } + + /** Dart's {@code List.lastIndexWhere(test, [start])}. */ + public long lastIndexWhere(Funcs.Func1 test) { + return lastIndexWhere(test, size() - 1); + } + + /** + * {@code lastIndexWhere(test, start)}: searches backward FROM {@code start}, which + * Dart clamps to the last index. The start was dropped, so the search always began + * at the end: [1, 2, 1].lastIndexWhere((x) => x == 1, 1) answered 2, not 0. + */ + public long lastIndexWhere(Funcs.Func1 test, long start) { + long from = Math.min(start, (long) size() - 1); + for (int i = (int) from; i >= 0; i--) { + if (Boolean.TRUE.equals(test.call(get(i)))) { + return i; + } + } + return -1; + } + + /** Dart's {@code List.removeWhere(test)} — removes every matching element. */ + public void removeWhere(Funcs.Func1 test) { + checkGrowable("removeWhere"); + filter(test, false); + } + + /** Dart's {@code List.retainWhere(test)} — keeps only matching elements. */ + public void retainWhere(Funcs.Func1 test) { + checkGrowable("retainWhere"); + filter(test, true); + } + + /** + * Keeps the elements whose test result equals {@code keepWhen}, calling the test on + * every element FRONT TO BACK, as Dart does, and compacting afterwards. Walking from + * the end reversed the callback order, so a predicate with state -- one alternating + * its answer, say -- was asked about 3, 2, 1 and removed different elements. A + * callback that changes the length is a ConcurrentModificationError, as in Dart. + */ + private void filter(Funcs.Func1 test, boolean keepWhen) { + int n = size(); + boolean[] keep = new boolean[n]; + for (int i = 0; i < n; i++) { + keep[i] = Boolean.TRUE.equals(test.call(get(i))) == keepWhen; + if (size() != n) { + throw new ConcurrentModificationError("list length changed from " + n + " to " + size()); + } + } + int j = 0; + for (int i = 0; i < n; i++) { + if (keep[i]) { + if (i != j) { + set(j, get(i)); + } + j++; + } + } + for (int i = n - 1; i >= j; i--) { + remove(i); + } + } + + public DartList sublist(long start, long end) { + RangeError.checkValueInInterval(start, 0, size(), "start"); + RangeError.checkValueInInterval(end, start, size(), "end"); + DartList l = new DartList<>(); + for (long i = start; i < end; i++) { + l.add(get((int) i)); + } + return l; + } + + public DartList sublist(long start) { + return sublist(start, size()); + } + + @SuppressWarnings("unchecked") + public void sort(Funcs.Func2 compare) { + int n = size(); + Object[] arr = new Object[n]; + for (int i = 0; i < n; i++) { + arr[i] = get(i); + } + if (compare == null) { + // Dart's default order is Comparable.compare, which compares int and + // double numerically; Arrays.sort's natural order throws across them. + java.util.Arrays.sort(arr, (a, b) -> { + long r = DartComparable.compare(a, b); + return r < 0 ? -1 : (r > 0 ? 1 : 0); + }); + } else { + java.util.Arrays.sort(arr, (a, b) -> { + long r = compare.call((E) a, (E) b); + return r < 0 ? -1 : (r > 0 ? 1 : 0); + }); + } + for (int i = 0; i < n; i++) { + set(i, (E) arr[i]); + } + } + + public void sortDefault() { + sort((Funcs.Func2) null); + } + + public DartIterable reversed() { + DartList self = this; + return DartIterable.wrap(() -> new java.util.Iterator() { + private int i = self.size() - 1; + + @Override + public boolean hasNext() { + return i >= 0; + } + + @Override + public E next() { + return self.get(i--); + } + }); + } + + // Dart iterable combinators, delegating to a lazy view. + + public DartIterable asIterable() { + return DartIterable.wrap(this); + } + + public DartIterable map(Funcs.Func1 f) { + return asIterable().map(f); + } + + public DartIterable where(Funcs.Func1 test) { + return asIterable().where(test); + } + + public E firstWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().firstWhere(test, orElse); + } + + /** Dart's Iterable.elementAt(index) — O(1) for the random-access list. */ + public E elementAt(long index) { + RangeError.checkValidIndex(index, size()); + return get((int) index); + } + + public boolean any(Funcs.Func1 test) { + return asIterable().any(test); + } + + public boolean every(Funcs.Func1 test) { + return asIterable().every(test); + } + + public R fold(R initialValue, Funcs.Func2 combine) { + return asIterable().fold(initialValue, combine); + } + + public E reduce(Funcs.Func2 combine) { + return asIterable().reduce(combine); + } + + public DartIterable expand(Funcs.Func1> f) { + return asIterable().expand(f); + } + + public DartIterable whereType(Class type) { + return asIterable().whereType(type); + } + + public DartIterable followedBy(Iterable other) { + return asIterable().followedBy(other); + } + + public DartIterable take(long count) { + return asIterable().take(count); + } + + public DartIterable skip(long count) { + return asIterable().skip(count); + } + + /** + * Dart's {@code List.asMap()}: an unmodifiable view backed by this list, so a later + * {@code list[0] = x} shows through and {@code map[0] = y} is refused. It used to be a + * mutable copy -- stale after the list changed, and writable without effect on it. + */ + public DartMap asMap() { + return new DartListMapView(this); + } + + public E lastWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().lastWhere(test, orElse); + } + + public E singleWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().singleWhere(test, orElse); + } + + /** + * Dart's {@code List.getRange(start, end)}: a lazy view over a sub-range. The range + * is checked now; the elements are read when iterated, so a change made to the list + * in between shows through. Copying through sublist snapshotted them instead. As in + * Dart, a change of length during iteration is a ConcurrentModificationError. + */ + public DartIterable getRange(long start, long end) { + RangeError.checkValueInInterval(start, 0, size(), "start"); + RangeError.checkValueInInterval(end, start, size(), "end"); + final int from = (int) start; + final int to = (int) end; + final DartList source = this; + return DartIterable.wrap(new Iterable() { + @Override + public java.util.Iterator iterator() { + final int length = source.size(); + return new java.util.Iterator() { + private int i = from; + + @Override + public boolean hasNext() { + checkLength(); + return i < to; + } + + @Override + public E next() { + checkLength(); + if (i >= to) { + throw new java.util.NoSuchElementException(); + } + return source.get(i++); + } + + @Override + public void remove() { + throw new UnsupportedError("Cannot remove from a range view"); + } + + private void checkLength() { + if (source.size() != length) { + throw new ConcurrentModificationError("list length changed from " + + length + " to " + source.size()); + } + } + }; + } + }); + } + + /** + * Dart's {@code List.unmodifiable(source)}: a copy that refuses EVERY write. It was + * only fixed-length, so {@code list[0] = x} still replaced an element of a list Dart + * guarantees nobody can change. + */ + public static DartList unmodifiable(Iterable source) { + ArrayList impl = new ArrayList<>(); + if (source != null) { + for (E e : source) { + impl.add(e); + } + } + return new DartList<>(impl, false, true); + } + + public String join(String separator) { + return asIterable().join(separator); + } + + public String join() { + return join(""); + } + + public void forEachDart(Funcs.VoidFunc1 action) { + // Named forEachDart because AbstractList inherits Java's forEach(Consumer). + // A callback that changes the length is a ConcurrentModificationError in Dart; + // the unchecked loop ignored an append and failed on a removal with an + // unrelated range error. + for (int i = 0, n = size(); i < n; i++) { + action.call(get(i)); + if (size() != n) { + throw new ConcurrentModificationError("list length changed from " + n + " to " + size()); + } + } + } + + /** Dart's {@code toList(growable: ...)}: a fixed-length copy when growable is false. */ + public DartList toList(boolean growable) { + return growable ? toList() : DartList.from(this, false); + } + + public DartList toList() { + return DartList.from(this); + } + + public DartSet toSet() { + DartSet s = new DartSet(); + s.addAll(this); + return s; + } + + @Override + public String toString() { + if (!DartRuntime.beginFormat(this)) { + return "[...]"; + } + try { + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(DartRuntime.str(get(i))); + } + return sb.append("]").toString(); + } finally { + DartRuntime.endFormat(this); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartListMapView.java b/maven/dart-runtime/src/main/java/dart/core/DartListMapView.java new file mode 100644 index 00000000000..9c6ed0f9994 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartListMapView.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Dart's {@code List.asMap()}: indices to elements, backed by the list and unmodifiable. + * Reads see the list as it is now; every write is refused with UnsupportedError. The + * inherited map storage is never used. + */ +final class DartListMapView extends DartMap { + + private final DartList list; + + DartListMapView(DartList list) { + this.list = list; + } + + private int index(Object key) { + if (key instanceof Long || key instanceof Integer) { + long i = ((Number) key).longValue(); + if (i >= 0 && i < list.size()) { + return (int) i; + } + } + return -1; + } + + @Override + public E get(Object key) { + int i = index(key); + return i < 0 ? null : list.get(i); + } + + @Override + public boolean containsKey(Object key) { + return index(key) >= 0; + } + + @Override + public boolean containsValue(Object value) { + return list.contains(value); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + private static UnsupportedError unmodifiable() { + return new UnsupportedError("Cannot modify an unmodifiable map"); + } + + @Override + public E put(Long key, E value) { + throw unmodifiable(); + } + + @Override + public void putAll(Map other) { + throw unmodifiable(); + } + + @Override + public E remove(Object key) { + throw unmodifiable(); + } + + @Override + public void clear() { + throw unmodifiable(); + } + + @Override + public Collection values() { + return java.util.Collections.unmodifiableList(list); + } + + @Override + public Set keySet() { + return new AbstractSet() { + @Override + public Iterator iterator() { + return new Iterator() { + int i; + + @Override + public boolean hasNext() { + return i < list.size(); + } + + @Override + public Long next() { + if (i >= list.size()) { + throw new NoSuchElementException(); + } + return Long.valueOf(i++); + } + }; + } + + @Override + public int size() { + return list.size(); + } + }; + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + int i; + + @Override + public boolean hasNext() { + return i < list.size(); + } + + @Override + public Map.Entry next() { + if (i >= list.size()) { + throw new NoSuchElementException(); + } + final int at = i++; + return new Map.Entry() { + @Override + public Long getKey() { + return Long.valueOf(at); + } + + @Override + public E getValue() { + return list.get(at); + } + + @Override + public E setValue(E v) { + throw unmodifiable(); + } + }; + } + }; + } + + @Override + public int size() { + return list.size(); + } + }; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartLongList.java b/maven/dart-runtime/src/main/java/dart/core/DartLongList.java new file mode 100644 index 00000000000..23eda8648fd --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartLongList.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.Arrays; + +import dart.runtime.Funcs; + +/** + * A {@link DartList}<Long> backed by a primitive {@code long[]} — the transpiler targets this for + * Dart {@code List<int>}. The hot index/add paths ({@link #getLong(long)}, {@link #setLong(long, long)}, + * {@link #addLong(long)}) never box; the inherited Dart/Java list surface still works (boxing only at + * that generic boundary) because {@link DartList} routes through the overridden {@code get/set/size/add}. + */ +public final class DartLongList extends DartList { + + private long[] a; + private int len; + + public DartLongList() { + super(true); + a = new long[8]; + } + + private DartLongList(long[] a, int len, boolean growable) { + this(a, len, growable, false); + } + + private DartLongList(long[] a, int len, boolean growable, boolean readOnly) { + super(growable, readOnly); + this.a = a; + this.len = len; + } + + /** Literal helper for Dart's <int>[a, b, c]. */ + public static DartLongList ofLongs(long... elements) { + long[] backing = elements.length == 0 ? new long[8] : Arrays.copyOf(elements, Math.max(8, elements.length)); + return new DartLongList(backing, elements.length, true); + } + + /** Dart's List<int>.filled(length, fill). */ + public static DartLongList filled(long length, long fill, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + int n = (int) length; + long[] backing = new long[Math.max(8, n)]; + Arrays.fill(backing, 0, n, fill); + return new DartLongList(backing, n, growable); + } + + public static DartLongList filled(long length, long fill) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return filled(length, fill, false); + } + + // Named *Longs (mirroring ofLongs) rather than reusing the DartList generic + // generate/from names: those would share a JVM erasure with the inherited + // static methods, which is not a legal hide/override relationship. + + /** Dart's List<int>.generate(length, generator). */ + public static DartLongList generateLongs(long length, Funcs.Func1 generator, boolean growable) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + int n = (int) length; + long[] backing = new long[Math.max(8, n)]; + for (int i = 0; i < n; i++) { + backing[i] = generator.call((long) i); + } + return new DartLongList(backing, n, growable); + } + + public static DartLongList generateLongs(long length, Funcs.Func1 generator) { + RangeError.checkNotNegative(length, "length"); + RangeError.checkAllocatable(length); + return generateLongs(length, generator, true); + } + + /** Dart's List<int>.from(elements). */ + /** {@link #fromLongs(Iterable)} with Dart's {@code growable:} flag. */ + public static DartLongList fromLongs(Iterable elements, boolean growable) { + DartLongList grown = fromLongs(elements); + return new DartLongList(grown.a, grown.len, growable); + } + + public static DartLongList fromLongs(Iterable elements) { + DartLongList l = new DartLongList(); + for (Number e : elements) { + // Dart checks each element's type: List.from([1.9]) throws, where + // longValue() quietly made it 1 -- and a double holding 1.0 is no int either. + if (!(e instanceof Long || e instanceof Integer || e instanceof Short || e instanceof Byte)) { + throw new TypeError("type '" + (e == null ? "Null" : "double") + + "' is not a subtype of type 'int'"); + } + l.addLong(e.longValue()); + } + return l; + } + + /** Dart's {@code List.unmodifiable(source)}: a copy that refuses every write. */ + public static DartLongList unmodifiableLongs(Iterable elements) { + DartLongList copy = fromLongs(elements); + return new DartLongList(copy.a, copy.len, false, true); + } + + private void ensure(int cap) { + if (cap > a.length) { + a = Arrays.copyOf(a, Math.max(cap, a.length * 2)); + } + } + + // --- primitive fast paths (transpiler targets these for List) --- + + public long getLong(long index) { + // Inline bounds check (against the logical length, which can be < backing + // capacity) + cold throw helper, so this stays a frameless method — see + // RangeError.indexError. Avoids a full-frame call per index access. + if (index < 0 || index >= len) { + RangeError.indexError(index, len); + } + return a[(int) index]; + } + + public long setLong(long index, long value) { + checkWritable(); + if (index < 0 || index >= len) { + RangeError.indexError(index, len); + } + a[(int) index] = value; + return value; + } + + public boolean addLong(long value) { + checkGrowable("add"); + ensure(len + 1); + a[len++] = value; + modCount++; + return true; + } + + // --- storage accessors (boxed boundary for the inherited surface) --- + + @Override + public Long get(int index) { + RangeError.checkValidIndex(index, len); + return a[index]; + } + + @Override + public Long set(int index, Long element) { + checkWritable(); + RangeError.checkValidIndex(index, len); + long old = a[index]; + a[index] = element; + return old; + } + + @Override + public int size() { + return len; + } + + @Override + public boolean add(Long e) { + return addLong(e); + } + + @Override + public void add(int index, Long element) { + checkGrowable("insert"); + ensure(len + 1); + System.arraycopy(a, index, a, index + 1, len - index); + a[index] = element; + len++; + modCount++; + } + + @Override + public Long remove(int index) { + checkGrowable("removeAt"); + RangeError.checkValidIndex(index, len); + long old = a[index]; + System.arraycopy(a, index + 1, a, index, len - index - 1); + len--; + modCount++; + return old; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartLongMap.java b/maven/dart-runtime/src/main/java/dart/core/DartLongMap.java new file mode 100644 index 00000000000..4ec8f8eb129 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartLongMap.java @@ -0,0 +1,555 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.Funcs; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A primitive {@code long}→{@code long}, insertion-ordered map — the transpiler + * targets this for Dart {@code Map}. The hot paths ({@link #putLong(long, long)}, + * {@link #getLongOr(long, long)}, {@link #containsKeyLong(long)}) never box; the generic + * {@link java.util.Map} surface (used for CN1 interop and the rare bare {@code m[k]} read) + * boxes only at that boundary. + * + *

Storage mirrors Dart's own {@code _CompactLinkedHashMap}: an open-addressing {@code int} + * hash index into a single insertion-ordered {@code data} array that holds each entry's key + * and value INTERLEAVED (key at {@code 2e}, value at {@code 2e+1}). Interleaving is the point: + * a lookup that finds a key at {@code data[2e]} then reads its value from {@code data[2e+1]} in + * the SAME cache line, instead of a second random miss into a separate {@code vals[]} array. + * Removal marks the entry as a hole and tombstones its index slot; holes are compacted away + * when they dominate.

+ */ +public final class DartLongMap extends AbstractMap { + + private static final int EMPTY = -1; + private static final int DELETED = -2; + + private long[] data; // interleaved [key0, val0, key1, val1, ...] in insertion order + private boolean[] present; // present[e] false == hole left by a removal + private int entryCount; // appended entries incl. holes + private int liveSize; // live (non-hole) entries + private int[] index; // hash slot -> entry index (or EMPTY/DELETED) + private int mask; // index.length - 1 (index length is a power of two) + + // The probe reads index[slot] (compact int table, cache-friendly) then confirms data[2e]. It + // does NOT re-check present[e]: index[slot] >= 0 already implies present[e] == true (appendEntry + // sets both; removeLong sets index[slot]=DELETED and rehash rebuilds index only from present + // entries), so the boolean[] load -- a separate random cache line at e -- is pure overhead and is + // dropped. (A slot-local key/value mirror was tried and REVERTED: it 5x'd the table memory and + // the cache/GC pressure on a large map outweighed avoiding the chase; interleaving key+value is + // the memory-neutral win instead -- it removes the second random miss without adding any array.) + /// Identity, like {@link DartMap}: Dart maps compare by identity unless a type + /// overrides {@code operator ==}. The emitter specialises a Map<int, int> to + /// this class, which inherited AbstractMap's structural equality, so two equal + /// literals compared == and collapsed into one key where Dart keeps two. + @Override + public boolean equals(Object other) { + return this == other; + } + + /// Consistent with {@link #equals}: identity. + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + public DartLongMap() { + data = new long[16]; // 8 entries * 2 slots + present = new boolean[8]; + index = new int[16]; + java.util.Arrays.fill(index, EMPTY); + mask = index.length - 1; + } + + /** Dart's {@code Map.of}/{@code .from}: a shallow copy of {@code src}. */ + public static DartLongMap from(java.util.Map src) { + DartLongMap m = new DartLongMap(); + if (src != null) { + for (java.util.Map.Entry e : src.entrySet()) { + m.putLong(e.getKey().longValue(), e.getValue().longValue()); + } + } + return m; + } + + /** Literal helper for Dart's <int, int>{a: b, ...}. Pairs are key0, val0, key1, val1, ... */ + public static DartLongMap ofLongs(long... pairs) { + DartLongMap m = new DartLongMap(); + for (int i = 0; i + 1 < pairs.length; i += 2) { + m.putLong(pairs[i], pairs[i + 1]); + } + return m; + } + + private static int hash(long k) { + // SplitMix64-style finaliser so sequential keys (a common Dart case) spread across slots. + long z = k; + z = (z ^ (z >>> 30)) * 0xbf58476d1ce4e5b9L; + z = (z ^ (z >>> 27)) * 0x94d049bb133111ebL; + z = z ^ (z >>> 31); + return (int) z; + } + + /** Returns the entry index for key, or -1 if absent. */ + private int find(long key) { + int slot = hash(key) & mask; + while (true) { + int e = index[slot]; + if (e == EMPTY) { + return -1; + } + if (e != DELETED && data[e << 1] == key) { + return e; + } + slot = (slot + 1) & mask; + } + } + + // --- primitive fast paths (transpiler targets these for Map) --- + + /** Dart's m[key] = value; returns value so it composes in expression position. */ + public long putLong(long key, long value) { + int slot = hash(key) & mask; + int firstDeleted = -1; + while (true) { + int e = index[slot]; + if (e == EMPTY) { + int target = firstDeleted >= 0 ? firstDeleted : slot; + appendEntry(target, key, value); + return value; + } + if (e == DELETED) { + if (firstDeleted < 0) { + firstDeleted = slot; + } + } else if (data[e << 1] == key) { + data[(e << 1) + 1] = value; + return value; + } + slot = (slot + 1) & mask; + } + } + + /// Bumped by every structural change -- a new key, a removal, a clear -- but not by + /// a value update, which Dart does not count as one. forEach and the iterators fail + /// fast when it moves under them: a callback that added entries used to be chased + /// forever along the growing entry count, hanging the app, where Dart throws. + private int version; + + private void checkVersion(int expected) { + if (version != expected) { + // Dart's error type, so an `on ConcurrentModificationError` clause catches it; + // Java's ConcurrentModificationException went straight past one. + throw new ConcurrentModificationError("map modified during iteration"); + } + } + + private void appendEntry(int slot, long key, long value) { + version++; + if (entryCount == present.length) { + data = java.util.Arrays.copyOf(data, data.length * 2); + present = java.util.Arrays.copyOf(present, present.length * 2); + } + int e = entryCount++; + data[e << 1] = key; + data[(e << 1) + 1] = value; + present[e] = true; + index[slot] = e; + liveSize++; + // Keep the index table under 0.75 load (live + tombstones vs capacity). + if ((entryCount) * 4 >= index.length * 3) { + rehash(); + } + } + + private void rehash() { + // Compact holes out of the insertion array first if they dominate, preserving order. + if (entryCount - liveSize > (liveSize >> 1)) { + int w = 0; + for (int r = 0; r < entryCount; r++) { + if (present[r]) { + data[w << 1] = data[r << 1]; + data[(w << 1) + 1] = data[(r << 1) + 1]; + present[w] = true; + w++; + } + } + for (int i = w; i < entryCount; i++) { + present[i] = false; + } + entryCount = w; + } + // Smallest power of two keeping the index table under ~0.5 load (avoids + // Integer.highestOneBit, which the ParparVM minimal JavaAPI lacks). + int newLen = 16; + int want = entryCount * 2; + while (newLen < want) { + newLen <<= 1; + } + index = new int[newLen]; + java.util.Arrays.fill(index, EMPTY); + mask = newLen - 1; + for (int e = 0; e < entryCount; e++) { + if (!present[e]) { + continue; + } + int slot = hash(data[e << 1]) & mask; + while (index[slot] != EMPTY) { + slot = (slot + 1) & mask; + } + index[slot] = e; + } + } + + /** Dart's m[key] ?? orElse without boxing. Value shares the key's cache line (interleaved). */ + public long getLongOr(long key, long orElse) { + int e = find(key); + return e < 0 ? orElse : data[(e << 1) + 1]; + } + + public boolean containsKeyLong(long key) { + return find(key) >= 0; + } + + /** Boxed read for the bare Dart {@code m[key]} (returns null when absent). */ + public Long idxLong(long key) { + int e = find(key); + return e < 0 ? null : data[(e << 1) + 1]; + } + + public long removeLong(long key) { + int slot = hash(key) & mask; + while (true) { + int e = index[slot]; + if (e == EMPTY) { + return 0; + } + if (e != DELETED && data[e << 1] == key) { + long old = data[(e << 1) + 1]; + present[e] = false; + index[slot] = DELETED; + liveSize--; + version++; + return old; + } + slot = (slot + 1) & mask; + } + } + + // --- Dart map surface --- + + /** + * Dart's {@code Map.remove(key)}: removes the entry and returns its former + * value, or null when the key was absent. + */ + public Long removeDart(long key) { + int e = find(key); + if (e < 0) { + return null; + } + Long old = data[(e << 1) + 1]; + removeLong(key); + return old; + } + + public Long idx(long key) { + return idxLong(key); + } + + public long length() { + return liveSize; + } + + public boolean isNotEmpty() { + return liveSize != 0; + } + + public boolean isEmptyDart() { + return liveSize == 0; + } + + public DartIterable keys() { + return DartIterable.wrap(new Iterable() { + public Iterator iterator() { + return keyIterator(); + } + }); + } + + public DartIterable valuesIterable() { + return DartIterable.wrap(new Iterable() { + public Iterator iterator() { + return valueIterator(); + } + }); + } + + // The Dart Map API the emitter calls on any Map receiver. A Map is + // specialised to this class, which had only part of it, so entries, addAll, + // idxSet, putIfAbsent, removeWhere and update on one generated calls javac + // rejected. Boxed, like the generic map; the primitive paths stay above. + + public DartIterable> entries() { + return DartMap.entriesOf(this); + } + + public void addAll(java.util.Map other) { + if (other != null) { + for (Map.Entry e : other.entrySet()) { + putLong(e.getKey().longValue(), e.getValue().longValue()); + } + } + } + + public void addEntries(Iterable> entries) { + if (entries != null) { + for (MapEntry e : entries) { + putLong(e.key().longValue(), e.value().longValue()); + } + } + } + + public Long idxSet(Long key, Long value) { + putLong(key.longValue(), value.longValue()); + return value; + } + + public Long putIfAbsentDart(Long key, Funcs.Func0 ifAbsent) { + if (containsKeyLong(key.longValue())) { + return idxLong(key.longValue()); + } + Long v = ifAbsent.call(); + putLong(key.longValue(), v.longValue()); + return v; + } + + public void removeWhere(Funcs.Func2 test) { + // Collected first, then removed: this map's views do not support removal + // through their iterators. + java.util.List doomed = new java.util.ArrayList(); + for (Map.Entry e : entrySet()) { + if (Boolean.TRUE.equals(test.call(e.getKey(), e.getValue()))) { + doomed.add(e.getKey()); + } + } + for (Long k : doomed) { + removeLong(k.longValue()); + } + } + + public Long update(Long key, Funcs.Func1 update, Funcs.Func0 ifAbsent) { + long k = key.longValue(); + if (containsKeyLong(k)) { + Long v = update.call(idxLong(k)); + putLong(k, v.longValue()); + return v; + } + if (ifAbsent != null) { + Long v = ifAbsent.call(); + putLong(k, v.longValue()); + return v; + } + throw new ArgumentError("Key not in map: " + key); + } + + public void forEachDart(Funcs.VoidFunc2 action) { + final int v = version; + for (int e = 0; e < entryCount; e++) { + if (present[e]) { + action.call(data[e << 1], data[(e << 1) + 1]); + checkVersion(v); + } + } + } + + // --- java.util.Map interop (boxed boundary) --- + + @Override + public int size() { + return liveSize; + } + + @Override + public boolean isEmpty() { + return liveSize == 0; + } + + @Override + public boolean containsKey(Object key) { + Long k = asKey(key); + return k != null && containsKeyLong(k.longValue()); + } + + /// Dart's {@code containsValue} compares with {@code ==}: {@code {0: 1}} contains + /// {@code 1.0}. AbstractMap's version used Long.equals, which never matches a Double. + @Override + public boolean containsValue(Object value) { + return DartMap.containsValueDart(values(), value); + } + + /// A key given as any number equal to an int, as Dart's {@code ==} finds it: the + /// {@code [1.0]} of a {@code Map} is its entry for 1. Null for anything + /// that equals no int. + private static Long asKey(Object key) { + if (key instanceof Long) { + return (Long) key; + } + if (key instanceof Integer || key instanceof Short || key instanceof Byte) { + return Long.valueOf(((Number) key).longValue()); + } + if (key instanceof Double || key instanceof Float) { + double d = ((Number) key).doubleValue(); + if (d >= -9.223372036854775808E18 && d < 9.223372036854775808E18 && (double) (long) d == d) { + return Long.valueOf((long) d); + } + } + return null; + } + + @Override + public Long get(Object key) { + Long k = asKey(key); + return k != null ? idxLong(k.longValue()) : null; + } + + @Override + public Long put(Long key, Long value) { + Long old = idxLong(key); + putLong(key, value); + return old; + } + + @Override + public Long remove(Object key) { + Long boxed = asKey(key); + if (boxed == null) { + return null; + } + long k = boxed.longValue(); + int e = find(k); + if (e < 0) { + return null; + } + Long old = data[(e << 1) + 1]; + removeLong(k); + return old; + } + + @Override + public void clear() { + version++; + entryCount = 0; + liveSize = 0; + java.util.Arrays.fill(index, EMPTY); + // present flags below entryCount are reset lazily as entries are re-appended; + // clear the live prefix so stale holes never read as present. + java.util.Arrays.fill(present, false); + } + + private Iterator keyIterator() { + return new Iterator() { + final int v = version; + int e = nextLive(0); + public boolean hasNext() { return e < entryCount; } + public Long next() { + checkVersion(v); + if (e >= entryCount) { throw new NoSuchElementException(); } + long k = data[e << 1]; e = nextLive(e + 1); return k; + } + }; + } + + private Iterator valueIterator() { + return new Iterator() { + final int ver = version; + int e = nextLive(0); + public boolean hasNext() { return e < entryCount; } + public Long next() { + checkVersion(ver); + if (e >= entryCount) { throw new NoSuchElementException(); } + long v = data[(e << 1) + 1]; e = nextLive(e + 1); return v; + } + }; + } + + private int nextLive(int from) { + int e = from; + while (e < entryCount && !present[e]) { + e++; + } + return e; + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + public int size() { + return liveSize; + } + public Iterator> iterator() { + return new Iterator>() { + final int v = version; + int e = nextLive(0); + public boolean hasNext() { + return e < entryCount; + } + public Map.Entry next() { + checkVersion(v); + if (e >= entryCount) { + throw new NoSuchElementException(); + } + Map.Entry en = + new AbstractMap.SimpleImmutableEntry(data[e << 1], data[(e << 1) + 1]); + e = nextLive(e + 1); + return en; + } + }; + } + }; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (int e = 0; e < entryCount; e++) { + if (!present[e]) { + continue; + } + if (!first) { + sb.append(", "); + } + first = false; + sb.append(data[e << 1]).append(": ").append(data[(e << 1) + 1]); + } + return sb.append("}").toString(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartMap.java b/maven/dart-runtime/src/main/java/dart/core/DartMap.java new file mode 100644 index 00000000000..a09f3c11517 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartMap.java @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Dart's Map<K,V>: insertion-ordered (Dart map literals preserve + * insertion order, matching LinkedHashMap). Extends LinkedHashMap so it + * interoperates with Java/CN1 APIs directly. + */ +public class DartMap extends LinkedHashMap { + + /// Identity, as Dart's own {@code Map} has: {@code [1] == [1]} is false in Dart + /// unless a type overrides {@code operator ==}. The inherited Java equality is + /// structural, so two separately built collections compared equal and, used as + /// keys, collapsed into one entry of a map -- different control flow and lost + /// data in a transpiled application. Element-wise comparison is what Dart's + /// listEquals, mapEquals and setEquals are for, and they say so explicitly. + @Override + public boolean equals(Object other) { + return this == other; + } + + /// Consistent with {@link #equals}: identity. + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + /// Dart's {@code containsValue} compares with {@code ==}, so {@code {0: 1}} + /// contains the value {@code 1.0}. The inherited LinkedHashMap version used the + /// wrappers' equals, which never matches a Long against a Double. + @Override + public boolean containsValue(Object value) { + return containsValueDart(values(), value); + } + + /// Whether {@code values} holds {@code value} under Dart's {@code ==}. + static boolean containsValueDart(Iterable values, Object value) { + for (Object v : values) { + if (DartRuntime.eq(v, value)) { + return true; + } + } + return false; + } + + /** + * The other boxed forms Dart's {@code ==} equates with a numeric key: 1 with + * 1.0, and 0.0 with -0.0 and 0. Empty for anything that is not a number, so a + * String key pays one instanceof. Java's wrappers are equal only within their + * own class, which made {@code {1: 'a'}[1.0]} null and {@code m[1.0] = 'b'} a + * second entry beside the int 1. + * + *

Only the EXACT twin: a lookup does not also probe the ints that merely round to + * a double. Dart's own maps find a key by hashCode first, and an int's hash is not + * the hash of the double it rounds to -- measured on the Dart 3.9 VM, + * {@code {9007199254740993: 'a'}[9007199254740992.0]} is null and the set + * equivalent is false, although those two keys are {@code ==}.

+ */ + static Object[] numericTwins(Object key) { + if (key instanceof Double) { + double d = ((Double) key).doubleValue(); + // Integral and inside the long range, tested with a cast: Math.rint is not + // in ParparVM's JavaAPI, and calling it broke every native build. + if (d >= -9.223372036854775808E18 && d < 9.223372036854775808E18 && (double) (long) d == d) { + Long asInt = Long.valueOf((long) d); + return d == 0 ? new Object[] {asInt, Double.valueOf(-d)} : new Object[] {asInt}; + } + return NO_TWINS; + } + if (key instanceof Long || key instanceof Integer) { + long l = ((Number) key).longValue(); + double d = (double) l; + if ((long) d == l && d < 9.223372036854775808E18) { + return l == 0 ? new Object[] {Double.valueOf(0.0), Double.valueOf(-0.0)} + : new Object[] {Double.valueOf(d)}; + } + } + return NO_TWINS; + } + + static final Object[] NO_TWINS = new Object[0]; + + /** + * The key this map already stores for {@code key} under Dart's {@code ==}, or + * {@code key} itself. Dart keeps the key an entry was first stored under, so + * {@code m[1.0] = 'b'} on a map holding 1 updates that entry and the key stays 1. + */ + private Object storedKey(Object key) { + if (!(key instanceof Number) || super.containsKey(key)) { + return key; + } + for (Object twin : numericTwins(key)) { + if (super.containsKey(twin)) { + return twin; + } + } + return key; + } + + @Override + public V get(Object key) { + return super.get(storedKey(key)); + } + + @Override + public boolean containsKey(Object key) { + return super.containsKey(storedKey(key)); + } + + /// Counts additions and removals of keys (not value updates), so forEachDart can + /// tell a structural change from a same-size swap: a callback that removed its key + /// and inserted another left the size alone, and on a one-entry map Java's iterator + /// had already finished, so the change went unreported. LinkedHashMap's own counter + /// is package-private to java.util. + private int structure; + + /// Records a structural change; subclasses with their own storage call it too. + protected final void structuralChange() { + structure++; + } + + @Override + @SuppressWarnings("unchecked") + public V put(K key, V value) { + Object stored = storedKey(key); + if (!super.containsKey(stored)) { + structure++; + } + return super.put((K) stored, value); + } + + @Override + public V remove(Object key) { + Object stored = storedKey(key); + if (super.containsKey(stored)) { + structure++; + } + return super.remove(stored); + } + + @Override + public void clear() { + if (!isEmpty()) { + structure++; + } + super.clear(); + } + + /** + * Through {@link #put}, so every copy -- addAll, Map.from, Map.of -- matches keys + * with Dart's ==. LinkedHashMap's own putAll bypasses the overridden put, so adding + * {1.0: 'b'} to {1: 'a'} made a second entry instead of updating the first. + */ + @Override + public void putAll(Map other) { + for (Map.Entry e : other.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + public DartMap() { + } + + /** Literal helper for {@code {a: 1, b: 2}}: pairs of key, value. */ + @SuppressWarnings("unchecked") + public static DartMap of(Object... pairs) { + DartMap m = new DartMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + m.put((K) pairs[i], (V) pairs[i + 1]); + } + return m; + } + + /** + * Dart's {@code Map.of(other)} / {@code Map.from(other)} — a new insertion-ordered + * map holding a shallow copy of {@code other}'s entries. The fixed-arity overload + * takes priority over the varargs {@link #of(Object...)} literal helper (a single + * {@code Map} argument is more specific), so map literals continue to resolve to + * the pairs form. + */ + public static DartMap of(Map other) { + return from(other); + } + + /** + * Dart's {@code Map.from(other)} — copy the entries of another map. Accepts any {@code Map} + * (Dart's {@code Map.from} takes an untyped map and the caller supplies K/V), casting the + * entries to the requested K/V per Dart's dynamic-map semantics. + */ + @SuppressWarnings("unchecked") + public static DartMap from(Map other) { + DartMap m = new DartMap<>(); + if (other != null) { + m.putAll((Map) other); + } + return m; + } + + /** + * Dart's {@code Map.fromIterable(iterable, {key, value})}. When {@code key} or + * {@code value} is null the element itself is used (Dart's identity default). + */ + @SuppressWarnings("unchecked") + public static DartMap fromIterable( + Iterable iterable, + Funcs.Func1 key, + Funcs.Func1 value) { + DartMap m = new DartMap<>(); + if (iterable != null) { + for (E e : iterable) { + K k = key != null ? key.call(e) : (K) e; + V v = value != null ? value.call(e) : (V) e; + m.put(k, v); + } + } + return m; + } + + /** Dart's {@code Map.fromEntries(entries)}. */ + public static DartMap fromEntries(Iterable> entries) { + DartMap m = new DartMap<>(); + if (entries != null) { + for (MapEntry e : entries) { + m.put(e.key(), e.value()); + } + } + return m; + } + + /** Dart's {@code Map.identity()}: keys match only when they are the same object. */ + public static DartMap identity() { + return new DartIdentityMap(); + } + + /** Dart's map[key]. */ + public V idx(K key) { + return get(key); + } + + /** Dart's map[key] = value. */ + public V idxSet(K key, V value) { + put(key, value); + return value; + } + + public long length() { + return size(); + } + + public boolean isNotEmpty() { + return !isEmpty(); + } + + public DartIterable keys() { + return DartIterable.wrap(keySet()); + } + + public DartIterable valuesIterable() { + return DartIterable.wrap(values()); + } + + public V putIfAbsentDart(K key, Funcs.Func0 ifAbsent) { + if (containsKey(key)) { + return get(key); + } + V v = ifAbsent.call(); + put(key, v); + return v; + } + + public void forEachDart(Funcs.VoidFunc2 action) { + // Checked after every callback, as Dart does: Java's iterator only notices a + // change on its next step, so a callback that added to a one-entry map ended the + // loop quietly instead of throwing ConcurrentModificationError. + int n = size(); + int s = structure; + for (Map.Entry e : entrySet()) { + action.call(e.getKey(), e.getValue()); + if (size() != n || structure != s) { + throw new ConcurrentModificationError("map changed during forEach"); + } + } + } + + /** Dart's Map.remove returns the removed value (or null). */ + public V removeDart(Object key) { + return remove(key); + } + + /** Dart's {@code Map.addAll(other)} — copies every entry of {@code other} in. */ + public void addAll(Map other) { + if (other != null) { + putAll(other); + } + } + + /** Dart's {@code Map.addEntries(entries)}. */ + public void addEntries(Iterable> entries) { + if (entries != null) { + for (MapEntry e : entries) { + put(e.key(), e.value()); + } + } + } + + /** Dart's {@code Map.entries} getter — an iterable of key/value pairs. */ + /** + * Dart's {@code map.entries}: a live view, as Dart's is -- an iterable kept and + * walked after the map changed sees the change. It used to copy the entries up + * front, so a retained view went stale. + */ + public DartIterable> entries() { + return entriesOf(this); + } + + /** A live MapEntry view over any Java map's entries. */ + static DartIterable> entriesOf(final Map map) { + return DartIterable.wrap(new Iterable>() { + @Override + public java.util.Iterator> iterator() { + final java.util.Iterator> it = map.entrySet().iterator(); + return new java.util.Iterator>() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public MapEntry next() { + Map.Entry e = it.next(); + return new MapEntry(e.getKey(), e.getValue()); + } + }; + } + }); + } + + /** Dart's {@code Map.removeWhere(test)}. */ + public void removeWhere(Funcs.Func2 test) { + java.util.Iterator> it = entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = it.next(); + if (Boolean.TRUE.equals(test.call(e.getKey(), e.getValue()))) { + it.remove(); + } + } + } + + /** Dart's {@code Map.update(key, update, {ifAbsent})}. */ + public V update(K key, Funcs.Func1 update, Funcs.Func0 ifAbsent) { + if (containsKey(key)) { + V v = update.call(get(key)); + put(key, v); + return v; + } + if (ifAbsent != null) { + V v = ifAbsent.call(); + put(key, v); + return v; + } + throw new ArgumentError("Key not in map: " + key); + } + + /** Dart's {@code Map.map(transform)} — returns a new map of transformed entries. */ + public DartMap mapEntries(Funcs.Func2> transform) { + DartMap m = new DartMap<>(); + for (Map.Entry e : entrySet()) { + MapEntry me = transform.call(e.getKey(), e.getValue()); + m.put(me.key(), me.value()); + } + return m; + } + + @Override + public String toString() { + if (!DartRuntime.beginFormat(this)) { + return "{...}"; // a map that contains itself, as Dart prints it + } + try { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry e : entrySet()) { + if (!first) { + sb.append(", "); + } + sb.append(DartRuntime.str(e.getKey())).append(": ").append(DartRuntime.str(e.getValue())); + first = false; + } + return sb.append("}").toString(); + } finally { + DartRuntime.endFormat(this); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartSet.java b/maven/dart-runtime/src/main/java/dart/core/DartSet.java new file mode 100644 index 00000000000..a56156c4ecc --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartSet.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +import java.util.LinkedHashSet; + +/** + * Dart's Set<E>: insertion-ordered (Dart set literals are LinkedHashSet). + * Extends {@link LinkedHashSet} for direct Java/CN1 interop and adds the Dart + * API surface the transpiler targets (set algebra plus the shared + * {@code Iterable} combinators, which delegate to a lazy {@link DartIterable} + * view so a {@code Set} used as an iterable emits the same call shapes as a + * {@code List}). + */ +public class DartSet extends LinkedHashSet { + + /// Identity, as Dart's own {@code Set} has: {@code [1] == [1]} is false in Dart + /// unless a type overrides {@code operator ==}. The inherited Java equality is + /// structural, so two separately built collections compared equal and, used as + /// keys, collapsed into one entry of a map -- different control flow and lost + /// data in a transpiled application. Element-wise comparison is what Dart's + /// listEquals, mapEquals and setEquals are for, and they say so explicitly. + @Override + public boolean equals(Object other) { + return this == other; + } + + /// Consistent with {@link #equals}: identity. + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + public DartSet() { + } + + /** + * The element this set already holds for {@code e} under Dart's {@code ==}, or + * {@code e} itself -- {@code {1}.contains(1.0)} is true in Dart, and adding + * 1.0 leaves the set as {1}. See {@link DartMap#numericTwins}. + */ + private Object storedElement(Object e) { + if (!(e instanceof Number) || super.contains(e)) { + return e; + } + for (Object twin : DartMap.numericTwins(e)) { + if (super.contains(twin)) { + return twin; + } + } + return e; + } + + @Override + public boolean contains(Object e) { + return super.contains(storedElement(e)); + } + + /// Additions and removals, for forEachDart; see DartMap's counter of the same name. + private int structure; + + protected final void structuralChange() { + structure++; + } + + @Override + public boolean add(E e) { + boolean added = storedElement(e) == e && super.add(e); + if (added) { + structure++; + } + return added; + } + + @Override + public boolean remove(Object e) { + boolean removed = super.remove(storedElement(e)); + if (removed) { + structure++; + } + return removed; + } + + @Override + public void clear() { + if (!isEmpty()) { + structure++; + } + super.clear(); + } + + @SafeVarargs + public static DartSet of(E... elements) { + DartSet s = new DartSet<>(); + for (E e : elements) { + s.add(e); + } + return s; + } + + /** Dart's {@code Set.from(iterable)} / {@code Set.of(iterable)} — copy elements. */ + public static DartSet from(Iterable elements) { + DartSet s = new DartSet<>(); + if (elements != null) { + for (E e : elements) { + s.add(e); + } + } + return s; + } + + /** Dart's {@code Set.identity()}: elements match only when they are the same object. */ + public static DartSet identity() { + return new DartIdentitySet(); + } + + public long length() { + return size(); + } + + public boolean isNotEmpty() { + return !isEmpty(); + } + + public DartIterable asIterable() { + return DartIterable.wrap(this); + } + + // --- set algebra --------------------------------------------------- + + /** Dart's {@code Set.difference(other)} — elements not in {@code other}. */ + public DartSet difference(java.util.Set other) { + DartSet s = new DartSet<>(); + for (E e : this) { + if (other == null || !other.contains(e)) { + s.add(e); + } + } + return s; + } + + /** Dart's {@code Set.intersection(other)} — elements also in {@code other}. */ + public DartSet intersection(java.util.Set other) { + DartSet s = new DartSet<>(); + for (E e : this) { + if (other != null && other.contains(e)) { + s.add(e); + } + } + return s; + } + + /** Dart's {@code Set.union(other)} — elements in either set. */ + public DartSet union(java.util.Set other) { + DartSet s = new DartSet<>(); + s.addAll(this); + if (other != null) { + s.addAll(other); + } + return s; + } + + /** Dart's {@code Set.containsAll(other)}. */ + public boolean containsAll(Iterable other) { + if (other != null) { + for (Object o : other) { + if (!contains(o)) { + return false; + } + } + } + return true; + } + + // --- mutators mirroring the transpiler's Iterable/Set intrinsics ---- + + /** Dart's {@code Set.remove(value)} — returns whether it was present. */ + public boolean removeValue(Object value) { + return remove(value); + } + + /** Dart's {@code Set.addAll(elements)} — named to avoid the Collection overload. */ + public void addAllIterable(Iterable elements) { + if (elements != null) { + for (E e : elements) { + add(e); + } + } + } + + public void removeAll(Iterable elements) { + if (elements != null) { + for (Object o : elements) { + remove(o); + } + } + } + + public void removeWhere(Funcs.Func1 test) { + java.util.Iterator it = iterator(); + while (it.hasNext()) { + if (Boolean.TRUE.equals(test.call(it.next()))) { + it.remove(); + } + } + } + + /** + * Dart's {@code Set.retainWhere(test)}: keeps only the matching elements. The + * transpiler routes it here for a Set as it does for a List, and only DartList + * declared it, so every Set call site failed to compile. + */ + public void retainWhere(Funcs.Func1 test) { + java.util.Iterator it = iterator(); + while (it.hasNext()) { + if (!Boolean.TRUE.equals(test.call(it.next()))) { + it.remove(); + } + } + } + + // --- Iterable combinators (delegate to the lazy view) --------------- + + public E first() { + return asIterable().first(); + } + + public E last() { + return asIterable().last(); + } + + public DartIterable map(Funcs.Func1 f) { + return asIterable().map(f); + } + + public DartIterable where(Funcs.Func1 test) { + return asIterable().where(test); + } + + /** Dart's {@code toList(growable: ...)}: a fixed-length copy when growable is false. */ + public DartList toList(boolean growable) { + return asIterable().toList(growable); + } + + public DartList toList() { + return asIterable().toList(); + } + + public DartSet toSet() { + return from(this); + } + + public String join(String separator) { + return asIterable().join(separator); + } + + public String join() { + return join(""); + } + + public boolean any(Funcs.Func1 test) { + return asIterable().any(test); + } + + public boolean every(Funcs.Func1 test) { + return asIterable().every(test); + } + + public R fold(R initialValue, Funcs.Func2 combine) { + return asIterable().fold(initialValue, combine); + } + + public E firstWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().firstWhere(test, orElse); + } + + public E elementAt(long index) { + return asIterable().elementAt(index); + } + + public void forEachDart(Funcs.VoidFunc1 action) { + // As DartMap.forEachDart: checked after every callback, not only on the next step. + int n = size(); + int s = structure; + for (E e : this) { + action.call(e); + if (size() != n || structure != s) { + throw new ConcurrentModificationError("set changed during forEach"); + } + } + } + + // --- lazy Iterable operations, delegated to the iterable view ------- + + public E lastWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().lastWhere(test, orElse); + } + + public E singleWhere(Funcs.Func1 test, Funcs.Func0 orElse) { + return asIterable().singleWhere(test, orElse); + } + + public E reduce(Funcs.Func2 combine) { + return asIterable().reduce(combine); + } + + public DartIterable expand(Funcs.Func1> f) { + return asIterable().expand(f); + } + + public DartIterable followedBy(Iterable other) { + return asIterable().followedBy(other); + } + + public DartIterable take(long count) { + return asIterable().take(count); + } + + public DartIterable skip(long count) { + return asIterable().skip(count); + } + + public DartMap asMap() { + return asIterable().asMap(); + } + + public DartIterable whereType(Class type) { + return asIterable().whereType(type); + } + + @Override + public String toString() { + if (!DartRuntime.beginFormat(this)) { + return "{...}"; // a set that contains itself, as Dart prints it + } + try { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (E e : this) { + if (!first) { + sb.append(", "); + } + sb.append(DartRuntime.str(e)); + first = false; + } + return sb.append("}").toString(); + } finally { + DartRuntime.endFormat(this); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartThrown.java b/maven/dart-runtime/src/main/java/dart/core/DartThrown.java new file mode 100644 index 00000000000..9b2eefac1dc --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartThrown.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Carries a thrown value that is not a Java throwable -- Dart can throw any object: + * {@code throw 'boom'}, {@code throw token}. The value itself is kept, and a catch + * clause binds it back ({@link dart.runtime.DartRuntime#caught}), so {@code catch (e)} + * sees the identical object with its type and fields. It used to be replaced by a + * DartException holding only its text: identity, {@code is} tests and fields were lost, + * and a thrown string printed as "Exception: boom". + */ +public final class DartThrown extends RuntimeException { + + private final Object value; + + public DartThrown(Object value) { + // The no-argument constructor: the (message, cause, suppression, stack) one is not + // in ParparVM's JavaAPI or CLDC11, and using it would break every native build. + super(); + this.value = value; + } + + /** The object the Dart code threw. */ + public Object value() { + return value; + } + + @Override + public String getMessage() { + return dart.runtime.DartRuntime.str(value); + } + + @Override + public String toString() { + return dart.runtime.DartRuntime.str(value); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartUnmodifiableMap.java b/maven/dart-runtime/src/main/java/dart/core/DartUnmodifiableMap.java new file mode 100644 index 00000000000..7396b1c982e --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartUnmodifiableMap.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.Funcs; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * An unmodifiable Dart map: a copy of the entries it is built from, whose every write + * -- map[k] = v, remove, clear, addAll, removeWhere, or a remove through one of its + * views -- is a Dart UnsupportedError, as Map.unmodifiable and the maps Dart hands out + * read-only (Uri.queryParameters) are. + */ +final class DartUnmodifiableMap extends DartMap { + + private final boolean frozen; + + DartUnmodifiableMap(Map entries) { + super.putAll(entries); + frozen = true; + } + + private static UnsupportedError refused() { + return new UnsupportedError("Cannot modify unmodifiable map"); + } + + @Override + public V put(K key, V value) { + if (frozen) { + throw refused(); + } + return super.put(key, value); + } + + @Override + public void putAll(Map other) { + if (frozen) { + throw refused(); + } + super.putAll(other); + } + + @Override + public V remove(Object key) { + throw refused(); + } + + @Override + public void clear() { + throw refused(); + } + + @Override + public void removeWhere(Funcs.Func2 test) { + throw refused(); + } + + @Override + public Set> entrySet() { + return frozen ? Collections.unmodifiableSet(super.entrySet()) : super.entrySet(); + } + + @Override + public Set keySet() { + return frozen ? Collections.unmodifiableSet(super.keySet()) : super.keySet(); + } + + @Override + public Collection values() { + return frozen ? Collections.unmodifiableCollection(super.values()) : super.values(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DartUri.java b/maven/dart-runtime/src/main/java/dart/core/DartUri.java new file mode 100644 index 00000000000..0408ef56d0e --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DartUri.java @@ -0,0 +1,575 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * A minimal {@code dart:core} {@code Uri}. The new_gallery app mainly uses + * {@code Uri.parse(String)} to hand a URL to {@code url_launcher} and reads it + * back via {@code toString()}, but Dart code also inspects {@code scheme}, + * {@code host}, {@code pathSegments} and {@code queryParameters}, so this holds + * the original text and parses those components on demand. The component + * accessors follow Dart semantics: {@code scheme} is lower-cased and empty when + * absent; {@code queryParameters} preserves insertion order and URL-decodes. + */ +public final class DartUri { + + private final String text; + private String scheme = ""; + private String host = ""; + private long port = 0; + /// Whether the authority named a port. Without one, port() answers the + /// scheme's default, as Dart's Uri.port does. + private boolean explicitPort; + private String path = ""; + private String query = ""; + private String fragment = ""; + + private DartUri(String text) { + this.text = normalize(text); + parse(); + } + + /** The parts of a URI whose characters are normalized, and what each allows as is. */ + private static final int USERINFO = 0; + private static final int PATH = 1; + private static final int QUERY = 2; + private static final int FRAGMENT = 3; + + /** + * The text with its user info, path, query and fragment normalized as Dart's + * Uri.parse does: a character the component does not allow is percent-encoded + * (as UTF-8, upper-case hex), an escape of an unreserved character is decoded, the + * hex of any other escape is upper-cased, a '%' that starts no valid escape becomes + * %25, and a backslash in the path is a '/'. Only the scheme and host used to be + * touched, so Uri.parse("https://x/a b") kept a literal space in its path and its + * text, and a launcher or HTTP client rejected what Dart would have sent. + */ + static String normalize(String t) { + int end = t.length(); + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + if (c == '/' || c == '?' || c == '#') { + end = i; + break; + } + } + StringBuilder out = new StringBuilder(t.length() + 8); + int pos = 0; + int colon = t.indexOf(':'); + if (colon > 0 && colon < end && isScheme(t.substring(0, colon))) { + out.append(t.substring(0, colon + 1)); + pos = colon + 1; + } + if (t.startsWith("//", pos)) { + int authEnd = t.length(); + for (int i = pos + 2; i < t.length(); i++) { + char c = t.charAt(i); + if (c == '/' || c == '?' || c == '#') { + authEnd = i; + break; + } + } + int at = t.lastIndexOf('@', authEnd - 1); + if (at >= pos + 2) { + out.append("//"); + component(t, pos + 2, at, USERINFO, out); + out.append(t.substring(at, authEnd)); + } else { + out.append(t.substring(pos, authEnd)); + } + pos = authEnd; + } + int hash = t.indexOf('#', pos); + int stop = hash >= 0 ? hash : t.length(); + int q = t.indexOf('?', pos); + if (q >= stop) { + q = -1; + } + component(t, pos, q >= 0 ? q : stop, PATH, out); + if (q >= 0) { + out.append('?'); + component(t, q + 1, stop, QUERY, out); + } + if (hash >= 0) { + out.append('#'); + component(t, hash + 1, t.length(), FRAGMENT, out); + } + return out.toString(); + } + + private static void component(String t, int from, int to, int kind, StringBuilder out) { + for (int i = from; i < to; i++) { + char c = t.charAt(i); + if (c == '%') { + if (i + 2 < to && hexValue(t.charAt(i + 1)) >= 0 && hexValue(t.charAt(i + 2)) >= 0) { + int v = hexValue(t.charAt(i + 1)) * 16 + hexValue(t.charAt(i + 2)); + if (isUnreserved(v)) { + out.append((char) v); + } else { + percent(v, out); + } + i += 2; + } else { + out.append("%25"); + } + } else if (c == '\\' && kind == PATH) { + out.append('/'); + } else if (c < 128 && allowed(c, kind)) { + out.append(c); + } else { + int cp = c; + if (Character.isHighSurrogate(c) && i + 1 < to && Character.isLowSurrogate(t.charAt(i + 1))) { + cp = ((c - 0xD800) << 10) + (t.charAt(i + 1) - 0xDC00) + 0x10000; + i++; + } + utf8(cp, out); + } + } + } + + private static boolean isUnreserved(int c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '-' || c == '.' || c == '_' || c == '~'; + } + + private static boolean allowed(char c, int kind) { + if (isUnreserved(c) || "!$&'()*+,;=".indexOf(c) >= 0 || c == ':') { + return true; + } + if (kind == USERINFO) { + return false; + } + if (c == '@' || c == '/') { + return true; + } + return c == '?' && kind != PATH; + } + + private static int hexValue(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; + } + + private static void percent(int b, StringBuilder out) { + String hex = "0123456789ABCDEF"; + out.append('%').append(hex.charAt((b >> 4) & 0xF)).append(hex.charAt(b & 0xF)); + } + + private static void utf8(int cp, StringBuilder out) { + if (cp < 0x80) { + percent(cp, out); + } else if (cp < 0x800) { + percent(0xC0 | (cp >> 6), out); + percent(0x80 | (cp & 0x3F), out); + } else if (cp < 0x10000) { + percent(0xE0 | (cp >> 12), out); + percent(0x80 | ((cp >> 6) & 0x3F), out); + percent(0x80 | (cp & 0x3F), out); + } else { + percent(0xF0 | (cp >> 18), out); + percent(0x80 | ((cp >> 12) & 0x3F), out); + percent(0x80 | ((cp >> 6) & 0x3F), out); + percent(0x80 | (cp & 0x3F), out); + } + } + + /** Dart's {@code Uri.parse}. */ + public static DartUri parse(String uri) { + String text = uri == null ? "" : uri; + String error = malformed(text); + if (error != null) { + throw new FormatException(error + ": " + text); + } + return new DartUri(text); + } + + /** Dart's {@code Uri.tryParse}: null for what {@link #parse} rejects. */ + public static DartUri tryParse(String uri) { + return uri == null || malformed(uri) != null ? null : new DartUri(uri); + } + + /** + * Why {@code text} is not a URI Dart would parse, or null. The parser used to be + * total, so tryParse never answered null and parse never threw: an unclosed IPv6 + * host or a non-numeric port came back as a URI with a mangled host and port, and + * validation code accepted it. These are the cases Dart rejects -- recorded from the + * Dart SDK; it is lenient about the rest (a stray space or a bad escape is encoded, + * an out-of-range port is kept), and so is this. + */ + static String malformed(String text) { + int end = text.length(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '/' || c == '?' || c == '#') { + end = i; + break; + } + } + int colon = text.indexOf(':'); + int rest = 0; + if (colon >= 0 && colon < end) { + // A colon before the first '/', '?' or '#' ends a scheme, so what precedes + // it has to be one. + if (colon == 0 || !isScheme(text.substring(0, colon))) { + return "Invalid scheme"; + } + rest = colon + 1; + } + if (!text.startsWith("//", rest)) { + return null; + } + int authStart = rest + 2; + int authEnd = text.length(); + for (int i = authStart; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '/' || c == '?' || c == '#') { + authEnd = i; + break; + } + } + String authority = text.substring(authStart, authEnd); + int at = authority.lastIndexOf('@'); + String hostPort = at >= 0 ? authority.substring(at + 1) : authority; + String port = null; + if (hostPort.startsWith("[")) { + int close = hostPort.indexOf(']'); + if (close < 0) { + return "Missing end `]` to match `[` in host"; + } + if (close + 1 < hostPort.length()) { + if (hostPort.charAt(close + 1) != ':') { + return "Invalid end of authority"; + } + port = hostPort.substring(close + 2); + } + } else { + int pc = hostPort.lastIndexOf(':'); + if (pc >= 0) { + port = hostPort.substring(pc + 1); + } + } + if (port != null && port.length() > 0) { + try { + Long.parseLong(port); + } catch (NumberFormatException e) { + return "Invalid port"; + } + } + return null; + } + + private void parse() { + String s = text; + int hash = s.indexOf('#'); + if (hash >= 0) { + fragment = s.substring(hash + 1); + s = s.substring(0, hash); + } + int q = s.indexOf('?'); + if (q >= 0) { + query = s.substring(q + 1); + s = s.substring(0, q); + } + int colon = s.indexOf(':'); + if (colon > 0 && isScheme(s.substring(0, colon))) { + scheme = asciiLower(s.substring(0, colon)); + s = s.substring(colon + 1); + } + if (s.startsWith("//")) { + s = s.substring(2); + int slash = s.indexOf('/'); + String authority = slash >= 0 ? s.substring(0, slash) : s; + s = slash >= 0 ? s.substring(slash) : ""; + int at = authority.indexOf('@'); + if (at >= 0) { + authority = authority.substring(at + 1); + } + // An IPv6 literal is bracketed, and its colons are not the port + // separator: http://[::1]:8080 took the FIRST colon as one, leaving the + // host "[" and a port that failed to parse. Dart's host is the address + // without its brackets, and the port follows the closing bracket. + int close = authority.startsWith("[") ? authority.indexOf(']') : -1; + int pc = close >= 0 ? authority.indexOf(':', close) : authority.indexOf(':'); + if (close >= 0) { + host = authority.substring(1, close); + if (pc >= 0) { + explicitPort = true; + try { + port = Long.parseLong(authority.substring(pc + 1)); + } catch (NumberFormatException ignored) { + port = 0; + } + } + } else if (pc >= 0) { + explicitPort = true; + host = authority.substring(0, pc); + try { + port = Long.parseLong(authority.substring(pc + 1)); + } catch (NumberFormatException ignored) { + port = 0; + } + } else { + host = authority; + } + // Dart canonicalises a registered name to lower case, so + // https://EXAMPLE.COM/ and https://example.com/ have the same host -- + // host allowlists, route matches and host-keyed caches depend on it. + host = asciiLower(host); + } + path = s; + } + + /** + * ASCII-only lower case. Schemes and host names are ASCII by definition, and + * String.toLowerCase is locale sensitive: on a Turkish device "HTTP" folds + * its I to a dotless i and no longer equals "http". + */ + private static String asciiLower(String s) { + if (s == null) { + return null; + } + char[] out = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'A' && c <= 'Z') { + if (out == null) { + out = s.toCharArray(); + } + out[i] = (char) (c + ('a' - 'A')); + } + } + return out == null ? s : new String(out); + } + + private static boolean isScheme(String s) { + if (s.isEmpty() || !Character.isLetter(s.charAt(0))) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '+' && c != '-' && c != '.') { + return false; + } + } + return true; + } + + /** Dart's {@code Uri.scheme} — lower-cased, empty when absent. */ + public String scheme() { + return scheme; + } + + /** Dart's {@code Uri.host}. */ + public String host() { + return host; + } + + /** Dart's {@code Uri.port}. */ + /// Dart's Uri.port: the explicit port, or the scheme's default when the URI + /// names none -- 80 for http, 443 for https, 0 otherwise. Returning 0 for + /// https://example.com/path sent code that splits a URL into host and port + /// to port 0 unless every URL spelled out :443. + public long port() { + if (explicitPort) { + return port; + } + if ("http".equals(scheme)) { + return 80; + } + if ("https".equals(scheme)) { + return 443; + } + return 0; + } + + /** Dart's {@code Uri.path}. */ + public String path() { + return path; + } + + /** Dart's {@code Uri.query}. */ + public String query() { + return query; + } + + /** Dart's {@code Uri.fragment}. */ + public String fragment() { + return fragment; + } + + /** Dart's {@code Uri.pathSegments} — the non-empty, decoded path segments. */ + public DartList pathSegments() { + DartList out = new DartList<>(); + String p = path; + if (p.startsWith("/")) { + p = p.substring(1); + } + if (!p.isEmpty()) { + for (String seg : p.split("/", -1)) { + out.add(decodeComponent(seg)); + } + } + // Unmodifiable, as Dart's is: editing it changed nothing about the URI. + return DartList.unmodifiable(out); + } + + /** Dart's {@code Uri.queryParameters} — insertion-ordered, decoded. */ + public DartMap queryParameters() { + DartMap out = new DartMap<>(); + if (!query.isEmpty()) { + for (String pair : query.split("&", -1)) { + if (pair.isEmpty()) { + continue; + } + int eq = pair.indexOf('='); + if (eq >= 0) { + out.put(decodeQueryComponent(pair.substring(0, eq)), + decodeQueryComponent(pair.substring(eq + 1))); + } else { + out.put(decodeQueryComponent(pair), ""); + } + } + } + // Unmodifiable, as Dart's is: a write to it changed nothing about the URI, so + // code that "added a parameter" went on as if it had. + return new DartUnmodifiableMap(out); + } + + /// Dart's {@code Uri.decodeComponent}, for path segments: a {@code +} is a + /// literal plus. URLDecoder is a form decoder and would turn it into a space, + /// so it is escaped first. + private static String decodeComponent(String s) { + try { + return java.net.URLDecoder.decode(s.replace("+", "%2B"), "UTF-8"); + } catch (Exception e) { + return s; + } + } + + /// Dart's {@code Uri.decodeQueryComponent}, for query keys and values, where + /// a {@code +} IS a space -- {@code ?q=hello+world} reads back as "hello world". + /// This is exactly URLDecoder's form decoding. The two used to share the + /// path rule, which kept every plus in a query literal. + private static String decodeQueryComponent(String s) { + try { + return java.net.URLDecoder.decode(s, "UTF-8"); + } catch (Exception e) { + return s; + } + } + + /** + * The text with its scheme and host folded to lower case, as Dart's Uri prints + * them: Uri.parse('HTTPS://EXAMPLE.COM/P').toString() is https://example.com/P. + * The path, query and fragment are case sensitive and stay as written. + */ + @Override + public String toString() { + if (canonical == null) { + canonical = canonicalText(text); + } + return canonical; + } + + private String canonical; + + /** + * Value equality over the canonical text, as Dart's Uri has: two separate parses + * of one URL are equal, and https://EXAMPLE.com/a equals https://example.com/a. + * Inherited identity made every parse unequal to every other, so a Uri could + * never find its entry in a map or set. + */ + @Override + public boolean equals(Object o) { + return o instanceof DartUri && toString().equals(o.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + private static String canonicalText(String t) { + int end = t.length(); + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + if (c == '?' || c == '#') { + end = i; + break; + } + } + char[] out = null; + int rest = 0; + int colon = t.indexOf(':'); + if (colon > 0 && colon < end && isScheme(t.substring(0, colon))) { + out = foldRange(t, out, 0, colon); + rest = colon + 1; + } + if (t.startsWith("//", rest)) { + int authStart = rest + 2; + int authEnd = end; + for (int i = authStart; i < end; i++) { + if (t.charAt(i) == '/') { + authEnd = i; + break; + } + } + int at = t.indexOf('@', authStart); + int hostStart = at >= 0 && at < authEnd ? at + 1 : authStart; + int hostEnd = authEnd; + if (hostStart < authEnd && t.charAt(hostStart) == '[') { + int close = t.indexOf(']', hostStart); + hostEnd = close >= 0 && close < authEnd ? close : authEnd; + } else { + int pc = t.indexOf(':', hostStart); + if (pc >= 0 && pc < authEnd) { + hostEnd = pc; + } + } + out = foldRange(t, out, hostStart, hostEnd); + } + return out == null ? t : new String(out); + } + + private static char[] foldRange(String t, char[] out, int from, int to) { + for (int i = from; i < to; i++) { + char c = t.charAt(i); + if (c >= 'A' && c <= 'Z') { + if (out == null) { + out = t.toCharArray(); + } + out[i] = (char) (c + ('a' - 'A')); + } + } + return out; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DateTime.java b/maven/dart-runtime/src/main/java/dart/core/DateTime.java new file mode 100644 index 00000000000..0f5846edab6 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DateTime.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Dart's {@code dart:core} DateTime: an instant on the timeline, stored as + * microseconds since the Unix epoch plus a UTC/local flag, as the Dart VM keeps + * it. Fields are computed on demand in the proleptic Gregorian calendar, as Dart's + * are; a local value takes its zone offset from {@link java.util.Calendar}. + * + *

It used to keep milliseconds and discard the microsecond argument, so two + * instants a microsecond apart compared equal, differed by zero, and a + * sub-millisecond Duration added nothing.

+ */ +public final class DateTime { + + private final long epochMicros; + private final boolean utc; + + private DateTime(long epochMicros, boolean utc) { + this.epochMicros = epochMicros; + this.utc = utc; + } + + /** Local-time constructor mirroring {@code DateTime(year, [month, day, ...])}. */ + public DateTime(long year, long month, long day, long hour, long minute, + long second, long millisecond, long microsecond) { + this(build(year, month, day, hour, minute, second, millisecond, false) * 1000L + microsecond, false); + } + + public static DateTime now() { + return new DateTime(System.currentTimeMillis() * 1000L, false); + } + + /** UTC constructor mirroring {@code DateTime.utc(year, [month, day, ...])}. */ + public static DateTime utc(long year, long month, long day, long hour, long minute, + long second, long millisecond, long microsecond) { + return new DateTime(build(year, month, day, hour, minute, second, millisecond, true) * 1000L + + microsecond, true); + } + + public static DateTime fromMillisecondsSinceEpoch(long millisecondsSinceEpoch, boolean isUtc) { + return new DateTime(millisecondsSinceEpoch * 1000L, isUtc); + } + + public static DateTime fromMicrosecondsSinceEpoch(long microsecondsSinceEpoch, boolean isUtc) { + return new DateTime(microsecondsSinceEpoch, isUtc); + } + + /** Milliseconds since the epoch, rounded toward negative infinity as Dart does. */ + private long epochMillis() { + long q = epochMicros / 1000L; + return (epochMicros % 1000L != 0 && epochMicros < 0) ? q - 1 : q; + } + + /** Milliseconds in a day. */ + private static final long DAY_MS = 86400000L; + + /** + * 1583-01-01T00:00Z. From here on java.util.Calendar's default Gregorian rules and + * Dart's agree; before it Calendar switches to the Julian calendar at its 1582 + * cutover while Dart stays proleptic Gregorian, so DateTime.utc(1582, 10, 10) + * came out as October 20 -- every field, the epoch value and every comparison + * ten days off. + */ + private static final long GREGORIAN_AGREES_MS = daysFromCivil(1583, 1, 1) * DAY_MS; + + private static long build(long year, long month, long day, long hour, long minute, + long second, long millisecond, boolean utc) { + // Out-of-range components are how Dart spells calendar arithmetic: + // DateTime(y, m + 1, 0) is the last day of month m and month 0 is the + // previous December. They are normalized here arithmetically. An omitted + // month or day arrives as 1, not 0, because the dart:core stub declares + // Dart's own defaults. + long y = year + floorDiv(month - 1, 12); + long m = floorMod(month - 1, 12) + 1; + long wall = (daysFromCivil(y, m, 1) + day - 1) * DAY_MS + + hour * 3600000L + minute * 60000L + second * 1000L + millisecond; + if (utc) { + return wall; + } + if (wall - 2 * DAY_MS < GREGORIAN_AGREES_MS) { + // No zone kept daylight time this early, so the raw offset is the offset. + return wall - TimeZone.getDefault().getRawOffset(); + } + return calendarBuild(year, month, day, hour, minute, second, millisecond); + } + + /** A local time through the device's Calendar, which knows its daylight-time rules. */ + private static long calendarBuild(long year, long month, long day, long hour, long minute, + long second, long millisecond) { + // CN1's Calendar has no clear(); every time-carrying field is set + // explicitly so no residual "now" component leaks in. The lenient + // Calendar normalizes out-of-range components as Dart does. + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, (int) year); + c.set(Calendar.MONTH, (int) month - 1); + c.set(Calendar.DAY_OF_MONTH, (int) day); + c.set(Calendar.HOUR_OF_DAY, (int) hour); + c.set(Calendar.MINUTE, (int) minute); + c.set(Calendar.SECOND, (int) second); + c.set(Calendar.MILLISECOND, (int) millisecond); + return c.getTime().getTime(); + } + + /** + * This instant's wall-clock milliseconds: the instant itself in UTC, shifted by + * the zone's offset for a local value. The offset of a modern local instant is + * read back from the Calendar that applies the zone's daylight-time rules. + */ + private long wallMillis() { + long ms = epochMillis(); + if (utc) { + return ms; + } + if (ms - 2 * DAY_MS < GREGORIAN_AGREES_MS) { + return ms + TimeZone.getDefault().getRawOffset(); + } + Calendar c = Calendar.getInstance(); + c.setTime(new Date(ms)); + long localAsUtc = daysFromCivil(c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH)) * DAY_MS + + c.get(Calendar.HOUR_OF_DAY) * 3600000L + c.get(Calendar.MINUTE) * 60000L + + c.get(Calendar.SECOND) * 1000L + c.get(Calendar.MILLISECOND); + return localAsUtc; + } + + /** {year, month, day} of the wall-clock day, proleptic Gregorian. */ + private long[] date() { + return civilFromDays(floorDiv(wallMillis(), DAY_MS)); + } + + private long timeOfDay() { + return floorMod(wallMillis(), DAY_MS); + } + + /** + * Days since 1970-01-01 of a proleptic Gregorian date, month 1..12 (Howard + * Hinnant's days_from_civil). + */ + static long daysFromCivil(long y, long m, long d) { + y -= m <= 2 ? 1 : 0; + long era = (y >= 0 ? y : y - 399) / 400; + long yoe = y - era * 400; + long doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; + } + + /** The inverse of {@link #daysFromCivil}: {year, month, day}. */ + static long[] civilFromDays(long z) { + z += 719468; + long era = (z >= 0 ? z : z - 146096) / 146097; + long doe = z - era * 146097; + long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + long doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + long mp = (5 * doy + 2) / 153; + long d = doy - (153 * mp + 2) / 5 + 1; + long m = mp < 10 ? mp + 3 : mp - 9; + return new long[] {yoe + era * 400 + (m <= 2 ? 1 : 0), m, d}; + } + + private static long floorDiv(long a, long b) { + long q = a / b; + return (a % b != 0 && (a < 0) != (b < 0)) ? q - 1 : q; + } + + private static long floorMod(long a, long b) { + return a - floorDiv(a, b) * b; + } + + public long year() { + return date()[0]; + } + + public long month() { + return date()[1]; + } + + public long day() { + return date()[2]; + } + + public long hour() { + return timeOfDay() / 3600000L; + } + + public long minute() { + return timeOfDay() / 60000L % 60; + } + + public long second() { + return timeOfDay() / 1000L % 60; + } + + public long millisecond() { + return timeOfDay() % 1000L; + } + + public long microsecond() { + return epochMicros - epochMillis() * 1000L; + } + + /** Dart weekday: Monday == 1 .. Sunday == 7. */ + public long weekday() { + // 1970-01-01 was a Thursday, weekday 4. + return floorMod(floorDiv(wallMillis(), DAY_MS) + 3, 7) + 1; + } + + public long millisecondsSinceEpoch() { + return epochMillis(); + } + + public long microsecondsSinceEpoch() { + return epochMicros; + } + + public DateTime add(Duration duration) { + return new DateTime(epochMicros + duration.inMicroseconds(), utc); + } + + public DateTime subtract(Duration duration) { + return new DateTime(epochMicros - duration.inMicroseconds(), utc); + } + + public Duration difference(DateTime other) { + return Duration.ofMicroseconds(epochMicros - other.epochMicros); + } + + public boolean isBefore(DateTime other) { + return epochMicros < other.epochMicros; + } + + public boolean isAfter(DateTime other) { + return epochMicros > other.epochMicros; + } + + public boolean isAtSameMomentAs(DateTime other) { + return epochMicros == other.epochMicros; + } + + public DateTime toLocal() { + return utc ? new DateTime(epochMicros, false) : this; + } + + public DateTime toUtc() { + return utc ? this : new DateTime(epochMicros, true); + } + + public long compareTo(DateTime other) { + return epochMicros < other.epochMicros ? -1 : (epochMicros > other.epochMicros ? 1 : 0); + } + + /** The underlying instant as a {@link java.util.Date} (used by DateFormat). */ + public Date toJavaDate() { + return new Date(epochMillis()); + } + + public boolean isUtc() { + return utc; + } + + @Override + public boolean equals(Object o) { + // The instant AND the time-zone mode, as Dart's operator == compares: + // a local value and its toUtc() are the same moment but not equal. + // isAtSameMomentAs is the instant-only comparison. Comparing instants + // alone made them equal here, so a local and a UTC value collapsed into + // one map or set key. + return o instanceof DateTime && ((DateTime) o).epochMicros == epochMicros + && ((DateTime) o).utc == utc; + } + + @Override + public int hashCode() { + return (int) (epochMicros ^ (epochMicros >>> 32)); + } + + @Override + /// Dart's own format: {@code 2024-01-15 10:30:00.000}, and for a UTC value the + /// same with a trailing {@code Z}. This returned java.util.Date's locale text in + /// the host's time zone, so one transpiled value printed differently on each + /// device, a UTC value lost its Z, and nothing expecting a Dart date string could + /// parse it back. Microseconds print as three more digits when they are not zero, + /// as Dart prints them. + public String toString() { + long micros = microsecond(); + String text = fourDigits(year()) + "-" + twoDigits(month()) + "-" + twoDigits(day()) + + " " + twoDigits(hour()) + ":" + twoDigits(minute()) + ":" + twoDigits(second()) + + "." + threeDigits(millisecond()) + (micros == 0 ? "" : threeDigits(micros)); + return utc ? text + "Z" : text; + } + + private static String fourDigits(long n) { + long abs = Math.abs(n); + String sign = n < 0 ? "-" : ""; + if (abs >= 1000) { + return "" + n; + } + if (abs >= 100) { + return sign + "0" + abs; + } + if (abs >= 10) { + return sign + "00" + abs; + } + return sign + "000" + abs; + } + + private static String threeDigits(long n) { + if (n >= 100) { + return "" + n; + } + if (n >= 10) { + return "0" + n; + } + return "00" + n; + } + + private static String twoDigits(long n) { + return n >= 10 ? "" + n : "0" + n; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/DateTimeRange.java b/maven/dart-runtime/src/main/java/dart/core/DateTimeRange.java new file mode 100644 index 00000000000..a6b6e700477 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/DateTimeRange.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's {@code DateTimeRange}: an inclusive-start, inclusive-end pair of + * {@link DateTime} instants used by the Material date-range picker. + */ +public final class DateTimeRange { + + private DateTime start; + private DateTime end; + + /** Named-parameter constructor {@code DateTimeRange({start, end})}. */ + public DateTimeRange(DateTime start, DateTime end) { + this.start = start; + this.end = end; + } + + public DateTime start() { + return start; + } + + public DateTime end() { + return end; + } + + public Duration duration() { + return end.difference(start); + } + + /** + * Flutter's {@code DateTimeRange ==}: equal endpoints. Inherited identity made a + * rebuilt, unchanged range differ from the one a controlled date-range picker + * already held, and two equal ranges separate keys of a map or set. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof DateTimeRange)) { + return false; + } + DateTimeRange r = (DateTimeRange) o; + return (start == null ? r.start == null : start.equals(r.start)) + && (end == null ? r.end == null : end.equals(r.end)); + } + + @Override + public int hashCode() { + return 31 * (start == null ? 0 : start.hashCode()) + (end == null ? 0 : end.hashCode()); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/Duration.java b/maven/dart-runtime/src/main/java/dart/core/Duration.java new file mode 100644 index 00000000000..c495f760548 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/Duration.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's Duration: an immutable span of time stored in microseconds. + */ +public final class Duration implements Comparable { + + public static final long microsecondsPerMillisecond = 1000; + public static final long microsecondsPerSecond = 1000000; + public static final long microsecondsPerMinute = 60 * microsecondsPerSecond; + public static final long microsecondsPerHour = 60 * microsecondsPerMinute; + public static final long microsecondsPerDay = 24 * microsecondsPerHour; + + public static final Duration zero = new Duration(0); + + private final long micros; + + private Duration(long micros) { + this.micros = micros; + } + + public static Duration ofMicroseconds(long micros) { + return new Duration(micros); + } + + /** Canonical constructor mirroring Dart's named parameters, in declared order. */ + public static Duration of(long days, long hours, long minutes, long seconds, long milliseconds, long microseconds) { + return new Duration(days * microsecondsPerDay + + hours * microsecondsPerHour + + minutes * microsecondsPerMinute + + seconds * microsecondsPerSecond + + milliseconds * microsecondsPerMillisecond + + microseconds); + } + + public long inMicroseconds() { + return micros; + } + + public long inMilliseconds() { + return micros / microsecondsPerMillisecond; + } + + public long inSeconds() { + return micros / microsecondsPerSecond; + } + + public long inMinutes() { + return micros / microsecondsPerMinute; + } + + public long inHours() { + return micros / microsecondsPerHour; + } + + public long inDays() { + return micros / microsecondsPerDay; + } + + public Duration plus(Duration other) { + return new Duration(micros + other.micros); + } + + public Duration minus(Duration other) { + return new Duration(micros - other.micros); + } + + public Duration times(long factor) { + return new Duration(micros * factor); + } + + public boolean isNegative() { + return micros < 0; + } + + public Duration abs() { + return micros < 0 ? new Duration(-micros) : this; + } + + @Override + public int compareTo(Duration other) { + return Long.compare(micros, other.micros); + } + + @Override + public boolean equals(Object o) { + return o instanceof Duration d && d.micros == micros; + } + + @Override + public int hashCode() { + return Long.hashCode(micros); + } + + @Override + public String toString() { + long us = micros; + String sign = ""; + if (us < 0) { + sign = "-"; + us = -us; + } + long hours = us / microsecondsPerHour; + long minutes = (us % microsecondsPerHour) / microsecondsPerMinute; + long seconds = (us % microsecondsPerMinute) / microsecondsPerSecond; + long microsRem = us % microsecondsPerSecond; + return sign + hours + ":" + pad2(minutes) + ":" + pad2(seconds) + "." + pad6(microsRem); + } + + private static String pad2(long v) { + return v < 10 ? "0" + v : Long.toString(v); + } + + private static String pad6(long v) { + StringBuilder sb = new StringBuilder(Long.toString(v)); + while (sb.length() < 6) { + sb.insert(0, '0'); + } + return sb.toString(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/FormatException.java b/maven/dart-runtime/src/main/java/dart/core/FormatException.java new file mode 100644 index 00000000000..dbc852137d9 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/FormatException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's FormatException (int.parse / double.parse failures etc.). + */ +public class FormatException extends RuntimeException { + public FormatException(String message) { + super(message); + } + + @Override + public String toString() { + return "FormatException: " + getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/LateInitializationError.java b/maven/dart-runtime/src/main/java/dart/core/LateInitializationError.java new file mode 100644 index 00000000000..5501fcc5c8d --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/LateInitializationError.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's LateInitializationError — a {@code late} variable was read before + * being assigned (or a late final assigned twice). + */ +public class LateInitializationError extends RuntimeException { + public LateInitializationError(String message) { + super(message); + } + + public static LateInitializationError notInitialized(String name) { + return new LateInitializationError("LateInitializationError: Field '" + name + "' has not been initialized."); + } + + public static LateInitializationError alreadyInitialized(String name) { + return new LateInitializationError("LateInitializationError: Field '" + name + "' has already been initialized."); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/MapEntry.java b/maven/dart-runtime/src/main/java/dart/core/MapEntry.java new file mode 100644 index 00000000000..49587414e8a --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/MapEntry.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; + +/** + * Dart's {@code MapEntry}: an immutable key/value pair. Produced by + * {@code Map.entries} and consumed by {@code Map.fromEntries}. + * + *

Dart exposes {@code key}/{@code value} as getters, so the transpiler + * emits them as no-arg method calls ({@link #key()} / {@link #value()}).

+ */ +public final class MapEntry { + + private final K key; + private final V value; + + /** Dart's {@code MapEntry(key, value)}. */ + public MapEntry(K key, V value) { + this.key = key; + this.value = value; + } + + public K key() { + return key; + } + + public V value() { + return value; + } + + @Override + public String toString() { + return "MapEntry(" + DartRuntime.str(key) + ": " + DartRuntime.str(value) + ")"; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/RangeError.java b/maven/dart-runtime/src/main/java/dart/core/RangeError.java new file mode 100644 index 00000000000..570d6b71961 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/RangeError.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's RangeError — a numeric argument was outside its valid range, + * including list index errors. + */ +public class RangeError extends ArgumentError { + + public RangeError(String message) { + super(message); + } + + /** + * Guard used by DartList index access; mirrors RangeError.checkValidIndex. + */ + public static long checkValidIndex(long index, long length) { + if (index < 0 || index >= length) { + throw new RangeError("RangeError (index): Invalid value: Not in inclusive range 0.." + (length - 1) + ": " + index); + } + return index; + } + + /** + * Cold throw helper for the primitive-list hot paths. The bounds comparison + * is done inline by the caller (a frameless method); only on failure is this + * called. Keeping the throw (which allocates a message + RangeError) out of + * the caller lets the caller stay a lightweight frameless method instead of + * paying a full method-stack frame on every in-range index access — the + * dominant cost of tight index loops (e.g. quicksort) on ParparVM. + */ + /** + * A list length no array can hold. Narrowed to an int, 2^32 wrapped to 0 and the + * list came back empty; Dart runs out of memory trying to allocate it, and so does + * this, rather than handing back a plausible wrong answer. + */ + public static void checkAllocatable(long length) { + if (length > Integer.MAX_VALUE - 8) { + throw new OutOfMemoryError("Cannot allocate a list of length " + length); + } + } + + /** Dart's {@code RangeError.checkNotNegative}. */ + public static long checkNotNegative(long value, String name) { + if (value < 0) { + throw new RangeError("Invalid value: Not greater than or equal to 0: " + value + + (name == null ? "" : " (" + name + ")")); + } + return value; + } + + public static void indexError(long index, long length) { + throw new RangeError("RangeError (index): Invalid value: Not in inclusive range 0.." + (length - 1) + ": " + index); + } + + public static long checkValueInInterval(long value, long minValue, long maxValue, String name) { + if (value < minValue || value > maxValue) { + throw new RangeError("RangeError (" + name + "): Invalid value: Not in inclusive range " + + minValue + ".." + maxValue + ": " + value); + } + return value; + } + + @Override + public String toString() { + return getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/RegExp.java b/maven/dart-runtime/src/main/java/dart/core/RegExp.java new file mode 100644 index 00000000000..e0ab2cb6188 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/RegExp.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import com.codename1.util.regex.RE; +import com.codename1.util.regex.RESyntaxException; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Dart's {@code dart:core} {@code RegExp}. + * + *

Backed by Codename One's own regex engine ({@link RE}) rather than + * {@code java.util.regex}: the latter does not exist on every Codename One + * target — an iOS build fails at runtime with "Pattern.compile() not + * implemented on this platform" — and a transpiled app must behave the same on + * all of them. {@link RE} is plain Java that translates like any app class.

+ * + *

Dart's grammar is JavaScript-flavoured ECMAScript, which overlaps with + * {@link RE}'s Perl5 syntax for the constructs apps actually use: anchors, + * character classes, quantifiers, alternation and capturing groups. The flag + * surface Dart exposes is mapped where the engine has an equivalent — + * {@code multiLine} and {@code caseSensitive}; {@code unicode} and + * {@code dotAll} are accepted and recorded but have no engine counterpart, so + * they are inert rather than silently changing the match.

+ */ +public final class RegExp { + + private final String source; + private boolean multiLine; + private boolean caseSensitive = true; + private boolean unicode; + private boolean dotAll; + private RE compiled; + + public RegExp(String source) { + this.source = source == null ? "" : source; + // Compiled now, as Dart does: RegExp('[') throws FormatException from the + // constructor. Compiling on first match moved the throw out of a try around + // the construction -- or dropped it, for a pattern never used. + engine(); + } + + public RegExp(String source, boolean multiLine, boolean caseSensitive, + boolean unicode, boolean dotAll) { + this.source = source == null ? "" : source; + this.multiLine = multiLine; + this.caseSensitive = caseSensitive; + this.unicode = unicode; + this.dotAll = dotAll; + engine(); // a malformed pattern throws here, as in Dart + } + + // Named-argument setters (used when the transpiler lowers named ctor args + // to post-construction assignments). Each invalidates the cached pattern. + public void multiLine(boolean value) { + this.multiLine = value; + this.compiled = null; + } + + public void caseSensitive(boolean value) { + this.caseSensitive = value; + this.compiled = null; + } + + public void unicode(boolean value) { + this.unicode = value; + this.compiled = null; + } + + public void dotAll(boolean value) { + this.dotAll = value; + this.compiled = null; + } + + private RE engine() { + if (compiled == null) { + int flags = RE.MATCH_NORMAL; + if (multiLine) { + flags |= RE.MATCH_MULTILINE; + } + if (!caseSensitive) { + flags |= RE.MATCH_CASEINDEPENDENT; + } + if (dotAll) { + flags |= RE.MATCH_SINGLELINE; // '.' also matches line terminators + } + try { + compiled = new RE(source, flags); + } catch (RESyntaxException e) { + throw new FormatException("Invalid regular expression: /" + source + "/: " + + e.getMessage()); + } + } + return compiled; + } + + /** Dart's {@code RegExp.pattern} getter — the original source string. */ + public String pattern() { + return source; + } + + /** Legacy alias for {@link #pattern()}. */ + public String getPattern() { + return source; + } + + /** Dart's {@code RegExp.hasMatch(input)}. */ + public boolean hasMatch(String input) { + return input != null && engine().match(input); + } + + /** Dart's {@code RegExp.firstMatch(input)} — null when there is no match. */ + public RegExpMatch firstMatch(String input) { + if (input == null) { + return null; + } + RE re = engine(); + return re.match(input) ? snapshot(re, input) : null; + } + + /** Dart's {@code RegExp.stringMatch(input)} — the matched substring or null. */ + public String stringMatch(String input) { + RegExpMatch m = firstMatch(input); + return m == null ? null : m.group(0); + } + + /** + * Dart's {@code RegExp.allMatches(input)}: lazy, as Dart's is. Each match is + * searched for when the iteration reaches it, so {@code allMatches(s).first} + * or {@code .take(1)} does one search, not one per match in the whole input, + * and every iteration searches afresh. + */ + public DartIterable allMatches(final String input) { + return new DartIterable(() -> new Iterator() { + private int from; + private RegExpMatch pending; + private boolean exhausted = input == null; + + @Override + public boolean hasNext() { + if (pending == null && !exhausted) { + pending = matchFrom(input, from); + if (pending == null) { + exhausted = true; + } else { + int start = (int) pending.start(); + int end = (int) pending.end(); + // An empty match must still advance, or this never terminates -- and + // it must advance past ITS OWN position, not the previous search + // offset. Comparing the end with `from` let an empty match found + // later than `from` (RegExp(r'$') on "abc", found at 3 from 0) set + // `from` to 3 and be found again there. + from = end > start ? end : end + 1; + } + } + return pending != null; + } + + @Override + public RegExpMatch next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + RegExpMatch m = pending; + pending = null; + return m; + } + }); + } + + /** Runs before every search; a seam for tests that count them. */ + static Runnable searchHook; + + /** + * The first match that starts at or after {@code from}, or null. Searching + * FROM the offset, not filtering allMatches by it: the two differ where + * matches overlap -- RegExp('aa') in "aaa" from 1 matches at 1, while the + * matches found from 0 are only the one at 0. String's Pattern methods + * (indexOf with a start, lastIndexOf) need the former. + */ + RegExpMatch matchFrom(String input, int from) { + if (input == null || from < 0 || from > input.length()) { + return null; + } + Runnable hook = searchHook; + if (hook != null) { + hook.run(); + } + RE re = engine(); + return re.match(input, from) ? snapshot(re, input) : null; + } + + /** + * Copies the engine's current match out of it: group text plus offsets. + * The engine reuses its state on the next match, so a Match that read + * through to it would change under the caller. + */ + private static RegExpMatch snapshot(RE re, String input) { + int count = Math.max(1, re.getParenCount()); + String[] groups = new String[count]; + int[] starts = new int[count]; + int[] ends = new int[count]; + for (int i = 0; i < count; i++) { + groups[i] = re.getParen(i); + starts[i] = re.getParenStart(i); + ends[i] = re.getParenEnd(i); + } + return new RegExpMatch(groups, starts, ends, input); + } + + @Override + public String toString() { + return "RegExp/" + source + "/"; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/RegExpMatch.java b/maven/dart-runtime/src/main/java/dart/core/RegExpMatch.java new file mode 100644 index 00000000000..99b20b5342d --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/RegExpMatch.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's {@code dart:core} {@code RegExpMatch} (a {@code Match}). + * + *

Group text and offsets are SNAPSHOTTED at construction. The regex engine + * behind {@link RegExp} keeps its match state on the compiled pattern object + * and overwrites it on the next match, so a Match holding a reference to it + * would silently change under the caller — a Dart Match is a value.

+ * + *

Dart group indices are 0-based with group 0 being the whole match; + * unmatched groups are {@code null}.

+ */ +public final class RegExpMatch { + + private final String[] groups; + private final int[] starts; + private final int[] ends; + private final String input; + + RegExpMatch(String[] groups, int[] starts, int[] ends, String input) { + this.groups = groups; + this.starts = starts; + this.ends = ends; + this.input = input; + } + + /** Dart's {@code Match.group(index)} — null for an unmatched group. */ + public String group(long index) { + int i = (int) index; + if (i < 0 || i >= groups.length) { + throw new RangeError("group index out of range: " + index); + } + return groups[i]; + } + + /** Dart's {@code match[index]} operator. */ + public String idx(long index) { + return group(index); + } + + /** Dart's {@code Match.groupCount} getter — number of capturing groups. */ + public long groupCount() { + return groups.length - 1; + } + + /** Dart's {@code Match.start} getter. */ + public long start() { + return starts[0]; + } + + /** Dart's {@code Match.end} getter. */ + public long end() { + return ends[0]; + } + + /** Dart's {@code Match.input} getter. */ + public String input() { + return input; + } + + /** Dart's {@code Match.groups(indices)} — the listed groups in order. */ + public DartList groups(java.util.List indices) { + DartList out = new DartList(); + for (Number n : indices) { + out.add(group(n.longValue())); + } + return out; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/StateError.java b/maven/dart-runtime/src/main/java/dart/core/StateError.java new file mode 100644 index 00000000000..e33006cdbc2 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/StateError.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's StateError — an object was in an invalid state for the operation + * (e.g. Iterable.first on an empty iterable). + */ +public class StateError extends RuntimeException { + public StateError(String message) { + super(message); + } + + @Override + public String toString() { + return "Bad state: " + getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/Stopwatch.java b/maven/dart-runtime/src/main/java/dart/core/Stopwatch.java new file mode 100644 index 00000000000..172fadca7e8 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/Stopwatch.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Java implementation of the Dart {@code dart:core} {@code Stopwatch}: a monotonic elapsed-time + * measurement backed by {@link System#nanoTime()}. Getters are exposed as methods (the transpiler + * lowers Dart getters to no-arg calls). + */ +public final class Stopwatch { + private long startNanos; + private long accumulatedNanos; + private boolean running; + + /** Creates a stopped stopwatch with zero elapsed time. */ + public Stopwatch() { + } + + /** Starts (or resumes) measuring. */ + public void start() { + if (!running) { + running = true; + startNanos = System.nanoTime(); + } + } + + /** Stops measuring, retaining the elapsed time. */ + public void stop() { + if (running) { + accumulatedNanos += System.nanoTime() - startNanos; + running = false; + } + } + + /** Resets the elapsed time to zero (keeps the running state). */ + public void reset() { + accumulatedNanos = 0; + startNanos = System.nanoTime(); + } + + private long elapsedNanos() { + return running ? accumulatedNanos + (System.nanoTime() - startNanos) : accumulatedNanos; + } + + /** Whether the stopwatch is currently running. */ + public boolean isRunning() { + return running; + } + + /** Elapsed whole microseconds. */ + public long elapsedMicroseconds() { + return elapsedNanos() / 1000L; + } + + /** Elapsed whole milliseconds. */ + public long elapsedMilliseconds() { + return elapsedNanos() / 1000000L; + } + + /** Elapsed raw ticks (nanoseconds, matching {@link #frequency()}). */ + public long elapsedTicks() { + return elapsedNanos(); + } + + /** Elapsed time as a {@link Duration}. */ + public Duration elapsed() { + return Duration.ofMicroseconds(elapsedMicroseconds()); + } + + /** Ticks per second (nanosecond resolution). */ + public long frequency() { + return 1000000000L; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/StringBuffer.java b/maven/dart-runtime/src/main/java/dart/core/StringBuffer.java new file mode 100644 index 00000000000..ea908d544e3 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/StringBuffer.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.runtime.DartRuntime; + +/** + * Dart's {@code dart:core} {@code StringBuffer} — a mutable sequence of + * characters used to build strings efficiently. Backed by a + * {@link java.lang.StringBuilder}. + * + *

Dart's {@code write}/{@code writeln} accept any {@code Object?} and append + * its Dart string representation (via {@link DartRuntime#str(Object)} so a Dart + * object's {@code toString()} semantics are honoured, and {@code null} renders + * as {@code "null"}). {@code writeCharCode} appends the UTF-16 code unit.

+ */ +public final class StringBuffer { + + private final StringBuilder sb = new StringBuilder(); + + public StringBuffer() { + } + + /** {@code StringBuffer([Object content = ""])} — seeds with the content's string. */ + public StringBuffer(Object content) { + sb.append(DartRuntime.str(content)); + } + + /** Dart's {@code StringBuffer.length} getter — number of UTF-16 code units. */ + public long length() { + return sb.length(); + } + + /** Dart's {@code StringBuffer.isEmpty} getter. */ + public boolean isEmpty() { + return sb.length() == 0; + } + + /** Dart's {@code StringBuffer.isNotEmpty} getter. */ + public boolean isNotEmpty() { + return sb.length() != 0; + } + + /** Dart's {@code StringBuffer.write(Object? object)}. */ + public void write(Object object) { + sb.append(DartRuntime.str(object)); + } + + /** Dart's {@code StringBuffer.writeln([Object? object = ""])}. */ + public void writeln() { + sb.append('\n'); + } + + public void writeln(Object object) { + sb.append(DartRuntime.str(object)); + sb.append('\n'); + } + + /** Dart's {@code StringBuffer.writeCharCode(int charCode)}. */ + public void writeCharCode(long charCode) { + if (charCode < 0 || charCode > 0x10FFFF) { + throw new RangeError("Invalid value: Not in inclusive range 0..1114111: " + charCode); + } + if (charCode > 0xFFFF) { + // A supplementary code point is TWO UTF-16 code units. Narrowing it to + // one char kept only the low bits: 0x1F600 became U+F600. + long v = charCode - 0x10000; + sb.append((char) (0xD800 + (v >> 10))).append((char) (0xDC00 + (v & 0x3FF))); + return; + } + sb.append((char) charCode); + } + + /** Dart's {@code StringBuffer.writeAll(Iterable objects, [String separator = ""])}. */ + public void writeAll(Iterable objects) { + writeAll(objects, ""); + } + + public void writeAll(Iterable objects, String separator) { + boolean first = true; + for (Object o : objects) { + if (!first && separator != null) { + sb.append(separator); + } + sb.append(DartRuntime.str(o)); + first = false; + } + } + + /** Dart's {@code StringBuffer.clear()}. */ + public void clear() { + sb.setLength(0); + } + + @Override + public String toString() { + return sb.toString(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/TypeError.java b/maven/dart-runtime/src/main/java/dart/core/TypeError.java new file mode 100644 index 00000000000..ead366f69f3 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/TypeError.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's TypeError (thrown by failed casts and the ! null-check operator). + */ +public class TypeError extends RuntimeException { + public TypeError(String message) { + super(message); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/UnimplementedError.java b/maven/dart-runtime/src/main/java/dart/core/UnimplementedError.java new file mode 100644 index 00000000000..b8a4b343d84 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/UnimplementedError.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's UnimplementedError (thrown by the UnimplementedError() idiom and + * by transpiler-generated stubs for members outside the supported subset). + */ +public class UnimplementedError extends UnsupportedError { + public UnimplementedError(String message) { + super(message); + } + + public UnimplementedError() { + super("Unimplemented"); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/core/UnsupportedError.java b/maven/dart-runtime/src/main/java/dart/core/UnsupportedError.java new file mode 100644 index 00000000000..ac0dd4213fb --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/core/UnsupportedError.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +/** + * Dart's UnsupportedError (e.g. mutating a fixed-length or unmodifiable list). + */ +public class UnsupportedError extends RuntimeException { + public UnsupportedError(String message) { + super(message); + } + + @Override + public String toString() { + return "Unsupported operation: " + getMessage(); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/math/DartMath.java b/maven/dart-runtime/src/main/java/dart/math/DartMath.java new file mode 100644 index 00000000000..dc47e278b8b --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/math/DartMath.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.math; + +import java.util.Random; + +/** + * Dart's dart:math library: statics over java.lang.Math plus Random with + * Dart semantics. + */ +public final class DartMath { + + private DartMath() { + } + + public static final double pi = Math.PI; + public static final double e = Math.E; + + public static long min(long a, long b) { + return Math.min(a, b); + } + + public static double min(double a, double b) { + return Math.min(a, b); + } + + public static long max(long a, long b) { + return Math.max(a, b); + } + + public static double max(double a, double b) { + return Math.max(a, b); + } + + public static double pow(double x, double exponent) { + return Math.pow(x, exponent); + } + + /** Dart's pow with int args and non-negative int exponent stays int. */ + public static long powInt(long x, long exponent) { + long result = 1; + long base = x; + long exp = exponent; + while (exp > 0) { + if ((exp & 1) == 1) { + result *= base; + } + base *= base; + exp >>= 1; + } + return result; + } + + /** + * Dart's {@code min} over two nums whose static types are not both int or + * both double. The result is one of the arguments, unconverted, so + * {@code min(1, 2.5)} is the int 1 -- converting both to double printed + * "1.0" and failed an {@code is int} test. As in Dart, NaN wins and -0.0 is + * below 0.0. + * + *

The comparison converts an int to double, as Dart's does: measured on the Dart + * 3.9 VM, {@code min(9007199254740993, 9007199254740992.0)} is the INT (the operands + * compare equal, so the first is kept) and {@code max(9007199254740992.0, + * 9007199254740993)} is the double -- exactly what this answers.

+ */ + public static Number minNum(Number a, Number b) { + if (isIntegral(a) && isIntegral(b)) { + return Long.valueOf(Math.min(a.longValue(), b.longValue())); + } + double x = a.doubleValue(); + double y = b.doubleValue(); + if (x != x) { + return a; + } + if (y != y) { + return b; + } + if (x != y) { + return x < y ? a : b; + } + return x == 0 && 1 / y < 0 ? b : a; + } + + /** Dart's {@code max} over two nums; see {@link #minNum}. 0.0 is above -0.0. */ + public static Number maxNum(Number a, Number b) { + if (isIntegral(a) && isIntegral(b)) { + return Long.valueOf(Math.max(a.longValue(), b.longValue())); + } + double x = a.doubleValue(); + double y = b.doubleValue(); + if (x != x) { + return a; + } + if (y != y) { + return b; + } + if (x != y) { + return x > y ? a : b; + } + return x == 0 && 1 / x < 0 ? b : a; + } + + /** + * Dart's {@code pow} over nums: an int raised to a non-negative int stays an + * int ({@code pow(2, 3)} is 8, not 8.0); anything else is a double. + */ + public static Number powNum(Number x, Number exponent) { + if (isIntegral(x) && isIntegral(exponent) && exponent.longValue() >= 0) { + return Long.valueOf(powInt(x.longValue(), exponent.longValue())); + } + return Double.valueOf(Math.pow(x.doubleValue(), exponent.doubleValue())); + } + + private static boolean isIntegral(Number n) { + return n instanceof Long || n instanceof Integer || n instanceof Short || n instanceof Byte; + } + + public static double sqrt(double x) { + return Math.sqrt(x); + } + + public static double sin(double x) { + return Math.sin(x); + } + + public static double cos(double x) { + return Math.cos(x); + } + + public static double tan(double x) { + return Math.tan(x); + } + + public static double asin(double x) { + return Math.asin(x); + } + + public static double acos(double x) { + return Math.acos(x); + } + + public static double atan(double x) { + return Math.atan(x); + } + + public static double atan2(double a, double b) { + return Math.atan2(a, b); + } + + public static double exp(double x) { + return Math.exp(x); + } + + public static double log(double x) { + return Math.log(x); + } + + /** + * Dart's Random. nextInt(max) returns 0..max-1. + */ + public static final class DartRandom { + private final Random impl; + + private static final long FULL_RANGE = 1L << 32; + + public DartRandom() { + impl = new Random(); + } + + public DartRandom(long seed) { + impl = new Random(seed); + } + + /// Dart's {@code Random.nextInt}, whose bound runs to {@code 1 << 32} + /// INCLUSIVE -- the whole unsigned 32-bit range, which is what a random + /// ARGB colour or a 32-bit identifier asks for. Java's nextInt(int) stops + /// at Integer.MAX_VALUE, and narrowing a larger bound to int made it zero + /// or negative, so {@code nextInt(1 << 32)} threw instead of answering. + public long nextInt(long max) { + if (max <= 0 || max > FULL_RANGE) { + throw new dart.core.RangeError("max must be in range 0 < max <= 2^32, was " + max); + } + if (max <= Integer.MAX_VALUE) { + return impl.nextInt((int) max); + } + // 32 unsigned bits, then rejection rather than a bare modulo: a + // modulo over 2^32 favours the low values whenever max does not + // divide it, and max here is never below 2^31, so the bias would be + // up to a factor of two rather than negligible. + long limit = FULL_RANGE - (FULL_RANGE % max); + long bits; + do { + bits = impl.nextInt() & 0xFFFFFFFFL; + } while (bits >= limit); + return bits % max; + } + + public double nextDouble() { + return impl.nextDouble(); + } + + public boolean nextBool() { + return impl.nextBoolean(); + } + } +} diff --git a/maven/dart-runtime/src/main/java/dart/math/DartPoint.java b/maven/dart-runtime/src/main/java/dart/math/DartPoint.java new file mode 100644 index 00000000000..6b519876b95 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/math/DartPoint.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.math; + +/** + * Dart's {@code dart:math} {@code Point}. The gallery only uses + * {@code Point}, so coordinates are stored as {@code double}; the type + * parameter {@code T} exists so the transpiler's {@code Point} type + * argument resolves. + * + * @param the (numeric) coordinate type; phantom in this runtime + */ +public final class DartPoint { + + private final double x; + private final double y; + + public DartPoint(double x, double y) { + this.x = x; + this.y = y; + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + /** + * Value equality, as Dart's Point has: Point(1, 2) == Point(1, 2). Inherited identity + * made every two points unequal, so equal points could not find each other's entries. + */ + @Override + public boolean equals(Object o) { + return o instanceof DartPoint && ((DartPoint) o).x == x && ((DartPoint) o).y == y; + } + + /** Consistent with equals, whose == makes 0.0 and -0.0 equal: hashed alike. */ + @Override + public int hashCode() { + long bits = Double.doubleToLongBits(x == 0.0 ? 0.0 : x) * 31 + + Double.doubleToLongBits(y == 0.0 ? 0.0 : y); + return (int) (bits ^ (bits >>> 32)); + } + + public double distanceTo(DartPoint other) { + double dx = x - other.x; + double dy = y - other.y; + return Math.sqrt(dx * dx + dy * dy); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/DartRuntime.java b/maven/dart-runtime/src/main/java/dart/runtime/DartRuntime.java new file mode 100644 index 00000000000..13c21d540d2 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/DartRuntime.java @@ -0,0 +1,1031 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +import dart.core.TypeError; + +/** + * Static helpers the transpiler-generated Java code calls into for Dart + * operator and core-language semantics that have no direct Java equivalent. + * + *

Signatures here are a stable contract with the code generator — do not + * change them without updating {@code JavaEmitter} in the dart-transpiler + * module and its golden tests.

+ */ +public final class DartRuntime { + + private DartRuntime() { + } + + /** Pluggable sink for Dart's top-level print(); tests capture output here. */ + private static Funcs.VoidFunc1 printSink; + + /** + * What the app was doing when a failure happens, appended to runtime + * errors. A UI framework built on this runtime sets it around a build; + * without it a {@code !} failure names no location at all, because the + * transpiled call sites are inlined into their caller's frame and the + * stack trace shows only the framework's own recursion. + */ + private static Object diagnosticContext; + + /** + * Sets (or clears, with null) the context appended to runtime errors. + * + *

Takes an OBJECT, not a formatted string. The context is set on every + * widget build and read only when something actually fails, so formatting + * it eagerly allocated one String per widget per frame to describe an + * error that almost never happens. A non-String value is described by its + * class when — and only when — a failure asks for it.

+ */ + public static void diagnosticContext(Object context) { + diagnosticContext = context; + } + + public static String diagnosticContext() { + Object c = diagnosticContext; + if (c == null) { + return null; + } + if (c instanceof String) { + return (String) c; + } + return "building " + c.getClass().getName(); + } + + /** The raw context value, for a caller that only wants to save and restore it. */ + public static Object diagnosticContextValue() { + return diagnosticContext; + } + + /** + * Dart's {@code x!} null-check operator. + */ + public static T nn(T v) { + if (v == null) { + String where = diagnosticContext(); + throw new TypeError("Null check operator used on a null value" + + (where == null ? "" : " (while " + where + ")")); + } + return v; + } + + /** + * Dart's {@code value is T?}: true for null as well as for a {@code type}. A helper + * rather than {@code v == null || v instanceof T} so the subject is evaluated once. + */ + public static boolean isOrNull(Object value, Class type) { + return value == null || type.isInstance(value); + } + + /** + * Dart's {@code value as T}, checked: a value that is not a {@code type} throws + * Dart's TypeError, which {@code on TypeError catch} can handle. A bare Java cast + * threw ClassCastException instead, and on iOS threw nothing at all -- ParparVM's + * CHECKCAST is unchecked, so the wrong object flowed on. The test is therefore an + * explicit isInstance, never a cast whose failure is caught. {@code nullable} is + * {@code as T?}, which lets null through; {@code dartType} names T in the message. + */ + @SuppressWarnings("unchecked") + public static T as(Object value, Class type, boolean nullable, String dartType) { + if (value == null) { + if (nullable) { + return null; + } + } else if (type.isInstance(value)) { + return (T) value; + } + throw new TypeError("type '" + typeName(value) + "' is not a subtype of type '" + + dartType + "' in type cast"); + } + + /** + * Dart's {@code ==} between reference values: null-safe, delegates to + * equals (user {@code operator ==} overrides equals). + */ + /** + * Dart's {@code Object.hashAll(objects)}: a hash of the ELEMENTS, in order, combined + * as java.util.Objects.hash combines its arguments. Passing the iterable to + * Objects.hash hashed the collection object itself -- by identity, for a Dart list -- + * so two equal value objects hashing {@code [a, b]} got different hashes and never + * found each other's map or set entries. + */ + public static long hashAll(Iterable objects) { + int h = 1; + if (objects != null) { + for (Object o : objects) { + h = 31 * h + hashOf(o); + } + } + return h; + } + + /** Dart's {@code Object.hash(a, b, ...)}: {@link #hashAll} over the arguments. */ + public static long hash(Object... objects) { + int h = 1; + if (objects != null) { + for (Object o : objects) { + h = 31 * h + hashOf(o); + } + } + return h; + } + + /** + * A hash consistent with {@link #eq}: Dart's {@code 1 == 1.0}, so 1.0 (and any double + * equal to an int) hashes as that int, and 0.0 as -0.0. Java's wrappers hash a Long + * and the equal Double differently, so a value class hashing a num field gave equal + * instances different hashes and they missed each other in maps and sets. + */ + public static int hashOf(Object o) { + if (o == null) { + return 0; + } + if (o instanceof Double || o instanceof Float) { + double d = ((Number) o).doubleValue(); + if (d >= -9.223372036854775808E18 && d < 9.223372036854775808E18 && (double) (long) d == d) { + return Long.valueOf((long) d).hashCode(); + } + return o.hashCode(); + } + if (o instanceof Integer || o instanceof Short || o instanceof Byte) { + return Long.valueOf(((Number) o).longValue()).hashCode(); + } + return o.hashCode(); + } + + /** + * Whether {@code t} is what Dart calls an Error, for {@code on Error}. This runtime's + * Dart errors -- StateError, ArgumentError, RangeError, TypeError, UnsupportedError, + * LateInitializationError and the rest -- are RuntimeExceptions, so the Java + * {@code catch (Error e)} that {@code on Error} used to become caught none of them. + * Everything thrown is an Error except the Dart Exceptions (DartException, + * FormatException) and Java's checked exceptions. + */ + public static boolean isDartError(Throwable t) { + if (t instanceof Error) { + return true; + } + return t instanceof RuntimeException && !(t instanceof dart.core.DartException) + && !(t instanceof dart.core.FormatException) && !(t instanceof dart.core.DartThrown); + } + + /** Throws {@code t} on unchanged; declared to return so a caller can write {@code throw rethrow(t)}. */ + public static RuntimeException rethrow(Throwable t) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + if (t instanceof Error) { + throw (Error) t; + } + throw new RuntimeException(t); + } + + /** Collections whose toString is running, innermost last; compared by identity. */ + private static final java.util.ArrayList FORMATTING = new java.util.ArrayList(); + + /** + * Starts formatting {@code collection}, or answers false when it is already being + * formatted further up -- a list that contains itself, directly or through another + * collection. Dart prints such a reference as {@code [...]} (or {@code {...}}, + * {@code (...)}); formatting it again recursed until the stack overflowed. + * Pair every true answer with {@link #endFormat}. + */ + public static boolean beginFormat(Object collection) { + for (int i = 0; i < FORMATTING.size(); i++) { + if (FORMATTING.get(i) == collection) { + return false; + } + } + FORMATTING.add(collection); + return true; + } + + public static void endFormat(Object collection) { + for (int i = FORMATTING.size() - 1; i >= 0; i--) { + if (FORMATTING.get(i) == collection) { + FORMATTING.remove(i); + return; + } + } + } + + /** + * Dart's {@code identical(a, b)}: the same object -- except that numbers and booleans + * are values, not objects, in Dart, so two boxes of the same int (or of the same + * double bits, or the same bool) are identical even when Java allocated them apart. + */ + public static boolean identical(Object a, Object b) { + if (a == b) { + return true; + } + if (a instanceof Long && b instanceof Long) { + return ((Long) a).longValue() == ((Long) b).longValue(); + } + if (a instanceof Double && b instanceof Double) { + return Double.doubleToLongBits(((Double) a).doubleValue()) + == Double.doubleToLongBits(((Double) b).doubleValue()); + } + if (a instanceof Boolean && b instanceof Boolean) { + return ((Boolean) a).booleanValue() == ((Boolean) b).booleanValue(); + } + return false; + } + + /** {@code t} as Dart's ConcurrentModificationError, converting Java's exception. */ + public static dart.core.ConcurrentModificationError asConcurrentModificationError(Throwable t) { + if (t instanceof dart.core.ConcurrentModificationError) { + return (dart.core.ConcurrentModificationError) t; + } + return new dart.core.ConcurrentModificationError(t.getMessage() == null ? "collection modified" : t.getMessage()); + } + + public static boolean eq(Object a, Object b) { + if (a instanceof Number && b instanceof Number) { + return numEq((Number) a, (Number) b); + } + return a == null ? b == null : a.equals(b); + } + + /** + * Dart's {@code ==} between two boxed numbers, which compares VALUES across + * int and double: {@code 1 == 1.0} is true. Java's wrapper equals is false + * for a Long against a Double, so a value that reached eq through dynamic, + * Object or a boxed collection element -- {@code List.contains(1.0)} + * holding the int 1 -- compared unequal. Double's equals is also wrong for + * Dart in both directions: NaN is never == itself and 0.0 == -0.0, which + * the primitive comparison below gets right. + * + * An int against a double is compared after converting the int to double -- + * measured on the Dart 3.9 VM, int.parse('9007199254740993') == 9007199254740992.0 + * is true, and so is the largest int against 2^63. This used to compare exactly + * and answered false for both, which no Dart program observes. (compareTo IS + * exact on the VM; see DartComparable.compareIntDouble. ==, <, >, min and max + * are not, and neither is this.) + */ + private static boolean numEq(Number a, Number b) { + boolean ai = isIntegral(a); + boolean bi = isIntegral(b); + if (ai && bi) { + return a.longValue() == b.longValue(); + } + if (!ai && !bi) { + return a.doubleValue() == b.doubleValue(); + } + long l = ai ? a.longValue() : b.longValue(); + double d = ai ? b.doubleValue() : a.doubleValue(); + return (double) l == d; + } + + private static boolean isIntegral(Number n) { + return n instanceof Long || n instanceof Integer || n instanceof Short || n instanceof Byte; + } + + // --- Operators on dynamic operands --------------------------------------- + // + // The transpiler emits Java operators when it knows the operand types. When + // an operand is `dynamic`, Dart picks the operator at run time from the + // value; these do the same for the built-in operand types. A user class's + // operator cannot be reached from here (there is no reflection on every + // port), so it fails with an error that says so rather than as invalid Java. + + /** + * Dart's binary {@code op} on values of unknown static type: arithmetic, + * {@code ~/} and {@code %}, the bitwise and shift operators, String + * {@code +} and {@code *}, and List {@code +}. int op int stays int, any + * double makes the result double, and {@code /} is always double. + */ + public static Object dynBinary(String op, Object a, Object b) { + if (a instanceof Number && b instanceof Number) { + Number x = (Number) a; + Number y = (Number) b; + boolean ints = isIntegral(x) && isIntegral(y); + if (op.equals("/")) { + return Double.valueOf(x.doubleValue() / y.doubleValue()); + } + if (op.equals("~/")) { + return Long.valueOf(ints ? tdiv(x.longValue(), y.longValue()) + : tdiv(x.doubleValue(), y.doubleValue())); + } + if (op.equals("%")) { + return ints ? (Object) Long.valueOf(mod(x.longValue(), y.longValue())) + : (Object) Double.valueOf(mod(x.doubleValue(), y.doubleValue())); + } + if (op.equals("+") || op.equals("-") || op.equals("*")) { + if (ints) { + long l = x.longValue(); + long r = y.longValue(); + return Long.valueOf(op.equals("+") ? l + r : op.equals("-") ? l - r : l * r); + } + double l = x.doubleValue(); + double r = y.doubleValue(); + return Double.valueOf(op.equals("+") ? l + r : op.equals("-") ? l - r : l * r); + } + if (ints) { + long l = x.longValue(); + long r = y.longValue(); + if (op.equals("&")) { + return Long.valueOf(l & r); + } + if (op.equals("|")) { + return Long.valueOf(l | r); + } + if (op.equals("^")) { + return Long.valueOf(l ^ r); + } + // Dart's shift-count rules, as the typed operators get -- changing + // only an operand's static type must not change the result. + if (op.equals("<<")) { + return Long.valueOf(shl(l, r)); + } + if (op.equals(">>")) { + return Long.valueOf(shr(l, r)); + } + if (op.equals(">>>")) { + return Long.valueOf(ushr(l, r)); + } + } + } + if (a instanceof String) { + if (op.equals("+")) { + if (!(b instanceof String)) { + throw new TypeError("type '" + typeName(b) + "' is not a subtype of type 'String'"); + } + return (String) a + (String) b; + } + if (op.equals("*") && b instanceof Number && isIntegral((Number) b)) { + return dart.core.DString.repeat((String) a, ((Number) b).longValue()); + } + } + if (op.equals("+") && a instanceof java.util.List && b instanceof java.util.List) { + return dart.core.DartList.concat((java.util.List) a, (java.util.List) b); + } + throw noOperator(op, a, b); + } + + /** Dart's relational {@code op} ({@code < > <= >=}) on values of unknown static type. */ + public static boolean dynCompare(String op, Object a, Object b) { + if (a instanceof Number && b instanceof Number) { + Number x = (Number) a; + Number y = (Number) b; + int c; + if (isIntegral(x) && isIntegral(y)) { + long l = x.longValue(); + long r = y.longValue(); + c = l < r ? -1 : l > r ? 1 : 0; + } else { + // An int against a double is compared as doubles, deliberately: that is + // what Dart's relational operators do. Measured on the Dart 3.9 VM, + // 9007199254740993 > 9007199254740992.0 is FALSE (statically, as num, and + // as dynamic), because the int is converted first. Only compareTo is exact. + double l = x.doubleValue(); + double r = y.doubleValue(); + if (Double.isNaN(l) || Double.isNaN(r)) { + return false; // every comparison with NaN is false + } + c = l < r ? -1 : l > r ? 1 : 0; + } + if (op.equals("<")) { + return c < 0; + } + if (op.equals(">")) { + return c > 0; + } + if (op.equals("<=")) { + return c <= 0; + } + if (op.equals(">=")) { + return c >= 0; + } + } + throw noOperator(op, a, b); + } + + /** + * Answers {@code rest} once {@code evaluated} has been evaluated. The transpiler + * chains it to evaluate call arguments in Dart's source order when that differs + * from the parameter order -- {@code seq($t1 = b(), seq($t2 = a(), true))} runs b + * before a, because Java evaluates arguments left to right. Not generic, so it + * never takes part in the call's type inference. + */ + public static boolean seq(Object evaluated, boolean rest) { + return rest; + } + + // --- int shifts and double rounding, with Dart's rules ---------------------- + // + // Java masks a long shift count to its low six bits, so 1 >> 64 was 1 and + // 1 << -1 a value, where Dart answers 0 and throws. Java's Math.round rounds a + // half toward positive infinity, so (-1.5).round() was -1 where Dart rounds half + // away from zero to -2. The emitter keeps a native shift for a constant count + // of 0..63, where the two agree. + + private static long checkShift(long count) { + if (count < 0) { + throw new dart.core.ArgumentError("Invalid argument: negative shift count " + count); + } + return count; + } + + /** Dart's {@code a << b}. */ + public static long shl(long a, long b) { + return checkShift(b) >= 64 ? 0L : a << b; + } + + /** Dart's {@code a >> b}: arithmetic, so a negative value shifts to -1. */ + public static long shr(long a, long b) { + return checkShift(b) >= 64 ? (a < 0 ? -1L : 0L) : a >> b; + } + + /** Dart's {@code a >>> b}. */ + public static long ushr(long a, long b) { + return checkShift(b) >= 64 ? 0L : a >>> b; + } + + /** + * Dart's {@code double.roundToDouble()}: half away from zero, keeping the sign + * of a zero result. Computed from the floor rather than as floor(d + 0.5), + * which rounds 0.49999999999999994 up to 1. + */ + public static double roundToDouble(double d) { + if (d != d || Double.isInfinite(d)) { + return d; + } + if (d < 0) { + return -roundHalfUpNonNegative(-d); + } + return roundHalfUpNonNegative(d); + } + + private static double roundHalfUpNonNegative(double d) { + double f = Math.floor(d); + return d - f >= 0.5 ? f + 1 : f; + } + + /** Dart's {@code double.round()}: as {@link #roundToDouble}, as an int. */ + public static long round(double d) { + if (d != d || Double.isInfinite(d)) { + throw new dart.core.UnsupportedError("Unsupported operation: " + doubleStr(d) + ".round()"); + } + return (long) roundToDouble(d); + } + + /** + * Dart's {@code double.toInt()} (and truncate): toward zero, but refusing NaN and + * the infinities with UnsupportedError. A Java (long) cast made NaN 0 and saturated + * the infinities, so validation around a bad value went quietly through. + */ + public static long toInt(double d) { + checkFinite(d, "toInt"); + return (long) d; + } + + /** Dart's {@code double.floor()}: an int, refusing NaN and the infinities. */ + public static long floor(double d) { + checkFinite(d, "floor"); + return (long) Math.floor(d); + } + + /** Dart's {@code double.ceil()}: an int, refusing NaN and the infinities. */ + public static long ceil(double d) { + checkFinite(d, "ceil"); + return (long) Math.ceil(d); + } + + private static void checkFinite(double d, String op) { + if (d != d || Double.isInfinite(d)) { + throw new dart.core.UnsupportedError("Unsupported operation: " + doubleStr(d) + "." + op + "()"); + } + } + + /** + * Dart's {@code int.clamp(lower, upper)}: ArgumentError when lower is above upper. + * Nested Math.min/max answered a plausible value instead -- 5.clamp(10, 0) was 0. + */ + public static long clamp(long v, long lower, long upper) { + if (lower > upper) { + throw new dart.core.ArgumentError("Invalid argument(s): " + upper + " < " + lower); + } + return v < lower ? lower : v > upper ? upper : v; + } + + /** Dart's {@code double.clamp}, ordered by num.compareTo (NaN compares greatest). */ + public static double clamp(double v, double lower, double upper) { + if (Double.compare(lower, upper) > 0) { + throw new dart.core.ArgumentError("Invalid argument(s): " + doubleStr(upper) + " < " + doubleStr(lower)); + } + if (Double.compare(v, lower) < 0) { + return lower; + } + return Double.compare(v, upper) > 0 ? upper : v; + } + + /** Dart's unary minus on a value of unknown static type. */ + public static Object dynNegate(Object a) { + if (a instanceof Number) { + Number x = (Number) a; + return isIntegral(x) ? (Object) Long.valueOf(-x.longValue()) : (Object) Double.valueOf(-x.doubleValue()); + } + throw noOperator("unary-", a, null); + } + + /** Dart's {@code ~} on a value of unknown static type. */ + public static Object dynBitNot(Object a) { + if (a instanceof Number && isIntegral((Number) a)) { + return Long.valueOf(~((Number) a).longValue()); + } + throw noOperator("~", a, null); + } + + /** A dynamic value used where Dart requires a bool: {@code !x}, a condition. */ + public static boolean dynBool(Object a) { + if (a instanceof Boolean) { + return ((Boolean) a).booleanValue(); + } + throw new TypeError("type '" + typeName(a) + "' is not a subtype of type 'bool'"); + } + + /** + * A postfix {@code x++} on a dynamic {@code x}: the transpiler emits + * {@code dynPostfix(x, x = dynBinary("+", x, 1))}, and Java evaluates the + * arguments left to right, so this answers the value from before the write. + */ + public static Object dynPostfix(Object before, Object after) { + return before; + } + + private static RuntimeException noOperator(String op, Object a, Object b) { + if (a == null) { + return new TypeError("Null check operator used on a null value (operator " + op + ")"); + } + return new dart.core.UnsupportedError("Operator '" + op + "' on a dynamic value of type " + + typeName(a) + (b == null ? "" : " and " + typeName(b)) + + " is not supported: the transpiler only dispatches the built-in number, String and" + + " List operators at run time. Give the operand a static type."); + } + + private static String typeName(Object o) { + if (o == null) { + return "Null"; + } + if (o instanceof Long || o instanceof Integer) { + return "int"; + } + if (o instanceof Double) { + return "double"; + } + if (o instanceof String) { + return "String"; + } + if (o instanceof Boolean) { + return "bool"; + } + return o.getClass().getName(); + } + + /** + * Dart's truncating division {@code ~/} — truncates toward zero, which + * matches Java integer division. + */ + public static long tdiv(long a, long b) { + if (b == 0) { + throw integerDivisionByZero(); + } + return a / b; + } + + public static long tdiv(double a, double b) { + double r = a / b; + if (Double.isNaN(r) || Double.isInfinite(r)) { + // Dart's UnsupportedError, which `on UnsupportedError` catches; Java's + // UnsupportedOperationException slipped past it into an `on Exception`. + throw new dart.core.UnsupportedError("Result of truncating division is " + r + ": " + a + " ~/ " + b); + } + return (long) r; + } + + /** + * Dart's IntegerDivisionByZeroException, an UnsupportedError. Checked here rather + * than left to the JVM: ParparVM answers 0 for an integer division or remainder by + * zero instead of throwing, so the device would have printed 0 where Dart throws. + */ + private static dart.core.UnsupportedError integerDivisionByZero() { + return new dart.core.UnsupportedError("IntegerDivisionByZeroException"); + } + + /** + * Dart's euclidean-style {@code %}: the result is always non-negative + * when the divisor is non-zero, unlike Java's remainder. + */ + public static long mod(long a, long b) { + if (b == 0) { + throw integerDivisionByZero(); // see tdiv: a remainder by zero throws too + } + long r = a % b; + return r < 0 ? r + Math.abs(b) : r; + } + + public static double mod(double a, double b) { + double r = a % b; + return r < 0 ? r + Math.abs(b) : r; + } + + /** + * Dart string conversion used by string interpolation and print: + * null prints as "null", doubles use Dart's formatting. + */ + public static String str(Object v) { + if (v == null) { + return "null"; + } + if (v instanceof Double d) { + return doubleStr(d); + } + if (v instanceof Float f) { + return doubleStr(f); + } + return v.toString(); + } + + public static String str(long v) { + return Long.toString(v); + } + + public static String str(double v) { + return doubleStr(v); + } + + public static String str(boolean v) { + return Boolean.toString(v); + } + + /** + * Dart's double.toString(): integral values keep a trailing ".0" + * (Dart prints 1.0, Java prints 1.0 too via Double.toString, but Java + * switches to scientific notation at different magnitudes). Values that + * are mathematically integral and within the safe range render as + * "<digits>.0"; everything else falls back to Java's shortest + * representation, with exponent formatting normalized to Dart's ("e+21" + * instead of "E21"). + */ + public static String doubleStr(double d) { + if (Double.isNaN(d)) { + return "NaN"; + } + if (Double.isInfinite(d)) { + return d > 0 ? "Infinity" : "-Infinity"; + } + // Integral test without Math.rint (absent from the ParparVM minimal JavaAPI): + // inside the |d| < 1e16 (< 2^53) guard the long truncation is exact, so an + // integral double round-trips through (long) unchanged. + if (Math.abs(d) < 1e16 && d == (double) (long) d) { + long l = (long) d; + // Negative-zero detection without doubleToRawLongBits (also absent): the + // non-raw doubleToLongBits (a supported native) yields the same bit pattern + // for -0.0 as raw, and NaN was already handled above. + if (l == 0 && Double.doubleToLongBits(d) != 0L) { + return "-0.0"; + } + return l + ".0"; + } + return dartNotation(d); + } + + /** + * The layout Dart gives a double's shortest digits -- the JavaScript + * Number-to-String rule: plain notation for magnitudes from 1e-7 up to 1e21, + * exponent notation outside it. Java's own thresholds are 1e-3 and 1e7, so its + * text was wrong in both bands: 0.0001 printed as 1e-4 and 12345678.9 as + * 1.23456789e+7. Java's digits are reused -- they are the shortest that round + * trip -- and only the point moves. + */ + private static String dartNotation(double d) { + String s = Double.toString(Math.abs(d)); + int ePos = s.indexOf('E'); + String mantissa = ePos < 0 ? s : s.substring(0, ePos); + int exp10 = ePos < 0 ? 0 : Integer.parseInt(s.substring(ePos + 1)); + int dot = mantissa.indexOf('.'); + String digits = dot < 0 ? mantissa : mantissa.substring(0, dot) + mantissa.substring(dot + 1); + // n: where the decimal point falls, counted in digits from the left. + int n = (dot < 0 ? mantissa.length() : dot) + exp10; + int lead = 0; + while (lead < digits.length() - 1 && digits.charAt(lead) == '0') { + lead++; + } + digits = digits.substring(lead); + n -= lead; + int end = digits.length(); + while (end > 1 && digits.charAt(end - 1) == '0') { + end--; + } + digits = digits.substring(0, end); + int k = digits.length(); + StringBuilder out = new StringBuilder(); + if (d < 0) { + out.append('-'); + } + if (k <= n && n <= 21) { + out.append(digits); + for (int i = k; i < n; i++) { + out.append('0'); + } + return out.append(".0").toString(); + } + if (0 < n && n <= 21) { + return out.append(digits, 0, n).append('.').append(digits, n, k).toString(); + } + if (-6 < n && n <= 0) { + out.append("0."); + for (int i = n; i < 0; i++) { + out.append('0'); + } + return out.append(digits).toString(); + } + int e = n - 1; + out.append(digits.charAt(0)); + if (k > 1) { + out.append('.').append(digits, 1, k); + } + return out.append('e').append(e < 0 ? '-' : '+').append(Math.abs(e)).toString(); + } + + /** + * Dart's {@code int.toRadixString}: a radix outside 2..36 is a RangeError. + * Java's Long.toString(v, radix) silently falls back to base 10 there, so + * 255.toRadixString(1) printed "255"; and the radix is checked as the Dart + * int it is, before the narrowing to a Java int could wrap it into range. + */ + public static String toRadixString(long v, long radix) { + dart.core.RangeError.checkValueInInterval(radix, 2, 36, "radix"); + return Long.toString(v, (int) radix); + } + + /** + * Dart's {@code num.toStringAsFixed(fractionDigits)}: exactly + * {@code fractionDigits} digits after the point, for 0 through 20. The + * digits are those of the double's EXACT binary value rounded half up, as + * Dart (and JavaScript's toFixed) define it -- so 1.005, which is really + * 1.00499999999999989..., gives 1.00 -- and a magnitude of 1e21 or more + * falls back to toString, as Dart does. + * + * The value is m * 2^e with an integer m, so value * 10^n is m * 10^n * + * 2^e, computed in the small multi-word integer below; ParparVM's minimal + * JavaAPI has no BigInteger or BigDecimal. The previous version scaled in a + * double and a long, which failed twice over: 1.0.toStringAsFixed(20) + * scaled to 1e20, past Long.MAX_VALUE, and printed 0.0922... instead of + * 1.000...; and scaling in floating point rounded where exact arithmetic + * would not. + */ + public static String toStringAsFixed(double d, long fractionDigits) { + if (fractionDigits < 0 || fractionDigits > 20) { + throw new dart.core.RangeError("Invalid value: fractionDigits must be in 0..20: " + + fractionDigits); + } + if (Double.isNaN(d)) { + return "NaN"; + } + if (Double.isInfinite(d)) { + return d > 0 ? "Infinity" : "-Infinity"; + } + if (Math.abs(d) >= 1e21) { + return str(d); + } + int n = (int) fractionDigits; + long bits = Double.doubleToLongBits(d); + int biased = (int) ((bits >>> 52) & 0x7ff); + long mantissa = bits & 0xfffffffffffffL; + int exp; + if (biased == 0) { + exp = -1074; + } else { + mantissa |= 1L << 52; + exp = biased - 1075; + } + int[] v = fromLong(mantissa); + for (int i = 0; i < n; i++) { + v = mulSmall(v, 10); + } + if (exp >= 0) { + v = shiftLeft(v, exp); + } else { + // Round half up: add the bit just below the cut, then drop the rest. + boolean half = testBit(v, -exp - 1); + v = shiftRight(v, -exp); + if (half) { + v = addOne(v); + } + } + String digits = toDecimal(v); + StringBuilder sb = new StringBuilder(); + // Dart keeps the sign of a negative value that rounds to zero: + // (-0.001).toStringAsFixed(2) is -0.00. + if (d < 0) { + sb.append('-'); + } + if (n == 0) { + return sb.append(digits).toString(); + } + StringBuilder padded = new StringBuilder(); + for (int i = digits.length(); i <= n; i++) { + padded.append('0'); + } + digits = padded.append(digits).toString(); + int split = digits.length() - n; + return sb.append(digits, 0, split).append('.').append(digits, split, digits.length()).toString(); + } + + // Minimal unsigned multi-word integers for toStringAsFixed: little-endian + // 32-bit words in an int[], no leading-zero words except for zero itself. + + private static int[] fromLong(long x) { + return trim(new int[] {(int) x, (int) (x >>> 32)}); + } + + private static int[] trim(int[] w) { + int len = w.length; + while (len > 1 && w[len - 1] == 0) { + len--; + } + if (len == w.length) { + return w; + } + int[] out = new int[len]; + System.arraycopy(w, 0, out, 0, len); + return out; + } + + private static int[] mulSmall(int[] w, int m) { + int[] out = new int[w.length + 1]; + long carry = 0; + for (int i = 0; i < w.length; i++) { + long p = (w[i] & 0xffffffffL) * m + carry; + out[i] = (int) p; + carry = p >>> 32; + } + out[w.length] = (int) carry; + return trim(out); + } + + private static int[] shiftLeft(int[] w, int k) { + int words = k >>> 5; + int bits = k & 31; + int[] out = new int[w.length + words + 1]; + for (int i = 0; i < w.length; i++) { + long x = (w[i] & 0xffffffffL) << bits; + out[i + words] |= (int) x; + out[i + words + 1] |= (int) (x >>> 32); + } + return trim(out); + } + + private static int[] shiftRight(int[] w, int k) { + int words = k >>> 5; + int bits = k & 31; + if (words >= w.length) { + return new int[] {0}; + } + int[] out = new int[w.length - words]; + for (int i = 0; i < out.length; i++) { + long lo = w[i + words] & 0xffffffffL; + long hi = i + words + 1 < w.length ? w[i + words + 1] & 0xffffffffL : 0L; + out[i] = (int) (((hi << 32) | lo) >>> bits); + } + return trim(out); + } + + private static boolean testBit(int[] w, int k) { + int word = k >>> 5; + return word < w.length && ((w[word] >>> (k & 31)) & 1) != 0; + } + + private static int[] addOne(int[] w) { + int[] out = new int[w.length + 1]; + long carry = 1; + for (int i = 0; i < w.length; i++) { + long s = (w[i] & 0xffffffffL) + carry; + out[i] = (int) s; + carry = s >>> 32; + } + out[w.length] = (int) carry; + return trim(out); + } + + private static String toDecimal(int[] w) { + if (w.length == 1) { + return Long.toString(w[0] & 0xffffffffL); + } + // Peel off nine decimal digits at a time. + StringBuilder chunks = new StringBuilder(); + int[] cur = w; + while (cur.length > 1 || cur[0] != 0) { + int[] q = new int[cur.length]; + long rem = 0; + for (int i = cur.length - 1; i >= 0; i--) { + long x = (rem << 32) | (cur[i] & 0xffffffffL); + q[i] = (int) (x / 1000000000L); + rem = x % 1000000000L; + } + cur = trim(q); + String part = Long.toString(rem); + boolean more = cur.length > 1 || cur[0] != 0; + if (more) { + for (int i = part.length(); i < 9; i++) { + part = "0" + part; + } + } + chunks.insert(0, part); + } + return chunks.toString(); + } + + /** + * Dart's top-level print(). Routed through a pluggable sink so + * behavioral tests can capture output deterministically. + */ + public static void print(Object v) { + String s = str(v); + Funcs.VoidFunc1 sink = printSink; + if (sink != null) { + sink.call(s); + } else { + System.out.println(s); + } + } + + public static void print(long v) { + print((Object) Long.toString(v)); + } + + public static void print(double v) { + print((Object) doubleStr(v)); + } + + public static void print(boolean v) { + print((Object) Boolean.toString(v)); + } + + /** Install a print sink (tests); pass null to restore System.out. */ + public static void setPrintSink(Funcs.VoidFunc1 sink) { + printSink = sink; + } + + /** + * Dart's `throw expr` accepts any object; wrap non-throwables so the + * JVM can propagate them while toString stays Dart-like. + */ + public static RuntimeException asError(Object thrown) { + if (thrown instanceof RuntimeException re) { + return re; + } + if (thrown instanceof Error err) { + throw err; // thrown as itself: an Error cannot be returned as a RuntimeException + } + if (thrown instanceof Throwable t) { + return new RuntimeException(t); + } + // Any other object is carried, not converted, so a catch gets it back. + return new dart.core.DartThrown(thrown); + } + + /** + * What a Dart catch clause binds for {@code t}: the thrown object itself when Dart + * code threw a non-exception value ({@link dart.core.DartThrown}), else {@code t}. + */ + public static Object caught(Throwable t) { + return t instanceof dart.core.DartThrown ? ((dart.core.DartThrown) t).value() : t; + } + + /** + * Whether {@code t} is what Dart calls an Exception, for {@code on Exception}: not a + * Dart error and not a thrown non-exception value -- a thrown String is neither. + */ + public static boolean isDartException(Throwable t) { + return !isDartError(t) && !(t instanceof dart.core.DartThrown); + } + + /** Marker for switch arms `dart analyze` proved unreachable. */ + public static RuntimeException unreachable() { + return new IllegalStateException("unreachable code reached — transpiler/analyzer mismatch"); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/Funcs.java b/maven/dart-runtime/src/main/java/dart/runtime/Funcs.java new file mode 100644 index 00000000000..172bb873858 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/Funcs.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +/** + * Canonical functional interfaces for transpiled Dart closures. + * + *

Dart function types are normalized by the transpiler to a canonical + * positional shape and bound to one of these interfaces. All are single + * abstract method interfaces so transpiled closures emit as Java lambdas + * or method references.

+ */ +public final class Funcs { + private Funcs() { + } + + @FunctionalInterface + public interface Func0 { + R call(); + } + + @FunctionalInterface + public interface Func1 { + R call(A a); + } + + @FunctionalInterface + public interface Func2 { + R call(A a, B b); + } + + @FunctionalInterface + public interface Func3 { + R call(A a, B b, C c); + } + + @FunctionalInterface + public interface Func4 { + R call(A a, B b, C c, D d); + } + + @FunctionalInterface + public interface Func5 { + R call(A a, B b, C c, D d, E e); + } + + @FunctionalInterface + public interface VoidFunc0 { + void call(); + } + + @FunctionalInterface + public interface VoidFunc1 { + void call(A a); + } + + @FunctionalInterface + public interface VoidFunc2 { + void call(A a, B b); + } + + @FunctionalInterface + public interface VoidFunc3 { + void call(A a, B b, C c); + } + + @FunctionalInterface + public interface VoidFunc4 { + void call(A a, B b, C c, D d); + } + + @FunctionalInterface + public interface VoidFunc5 { + void call(A a, B b, C c, D d, E e); + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/Ref.java b/maven/dart-runtime/src/main/java/dart/runtime/Ref.java new file mode 100644 index 00000000000..af56c0f3594 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/Ref.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +/** + * Holder for a captured mutable local of reference type. Dart closures may + * assign captured locals; Java lambdas require effectively-final captures, + * so the transpiler's CaptureBoxer rewrites such locals to a {@code final} + * holder whose {@code v} field is mutated instead. + */ +public final class Ref { + public T v; + + public Ref(T v) { + this.v = v; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/RefBool.java b/maven/dart-runtime/src/main/java/dart/runtime/RefBool.java new file mode 100644 index 00000000000..cb7c542db3d --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/RefBool.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +/** + * Primitive holder for a captured mutable Dart {@code bool} local. + */ +public final class RefBool { + public boolean v; + + public RefBool(boolean v) { + this.v = v; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/RefDouble.java b/maven/dart-runtime/src/main/java/dart/runtime/RefDouble.java new file mode 100644 index 00000000000..782019a72f3 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/RefDouble.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +/** + * Primitive holder for a captured mutable Dart {@code double} local. + */ +public final class RefDouble { + public double v; + + public RefDouble(double v) { + this.v = v; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/runtime/RefLong.java b/maven/dart-runtime/src/main/java/dart/runtime/RefLong.java new file mode 100644 index 00000000000..94e43f351bb --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/runtime/RefLong.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +/** + * Primitive holder for a captured mutable Dart {@code int} local + * (64-bit, mapped to Java {@code long}); avoids boxing in loops. + */ +public final class RefLong { + public long v; + + public RefLong(long v) { + this.v = v; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java b/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java new file mode 100644 index 00000000000..6ded6c563bc --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/typed_data/ByteData.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.typed_data; + +/** + * Dart's {@code dart:typed_data} ByteData: a fixed-length, random-access view + * over a byte buffer with typed accessors. Minimal big-endian implementation + * covering the 8- and 32-bit integer accessors. + */ +public final class ByteData { + + private final byte[] buffer; + + public ByteData(long length) { + dart.core.RangeError.checkNotNegative(length, "length"); + // Checked on the long, before narrowing: ByteData(2^32) cast to a zero-byte + // buffer instead of failing, as the list constructors already refuse to. + dart.core.RangeError.checkAllocatable(length); + this.buffer = new byte[(int) length]; + } + + /** + * The offset as an array index, after checking the whole access fits -- on the + * long, before narrowing. The cast came first before, so an offset of 2^32 wrapped + * to 0: getUint8 read byte zero and setUint8 overwrote it instead of throwing. + */ + private int at(long byteOffset, int width) { + // Against length - width, never offset + width: near Long.MAX_VALUE the sum + // overflowed negative, passed, and the narrowed index threw Java's + // ArrayIndexOutOfBoundsException instead of Dart's RangeError. + if (byteOffset < 0 || byteOffset > buffer.length - width) { + throw new dart.core.RangeError("Invalid value: Not in inclusive range 0.." + + (buffer.length - width) + ": " + byteOffset + " (byteOffset)"); + } + return (int) byteOffset; + } + + public long lengthInBytes() { + return buffer.length; + } + + public long getUint8(long byteOffset) { + return buffer[at(byteOffset, 1)] & 0xFF; + } + + public void setUint8(long byteOffset, long value) { + buffer[at(byteOffset, 1)] = (byte) value; + } + + public long getInt32(long byteOffset) { + int o = at(byteOffset, 4); + return ((buffer[o] & 0xFF) << 24) + | ((buffer[o + 1] & 0xFF) << 16) + | ((buffer[o + 2] & 0xFF) << 8) + | (buffer[o + 3] & 0xFF); + } + + public void setInt32(long byteOffset, long value) { + int o = at(byteOffset, 4); + buffer[o] = (byte) (value >> 24); + buffer[o + 1] = (byte) (value >> 16); + buffer[o + 2] = (byte) (value >> 8); + buffer[o + 3] = (byte) value; + } +} diff --git a/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java new file mode 100644 index 00000000000..b90b65951f3 --- /dev/null +++ b/maven/dart-runtime/src/main/java/dart/typed_data/Uint8List.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.typed_data; + +import java.util.List; + +/** + * Dart's {@code dart:typed_data} Uint8List: a fixed-length list of unsigned + * 8-bit integers backed by a Java {@code byte[]}. Only the surface used by + * the Flutter gallery (construction + length) is implemented. + */ +public final class Uint8List { + + private final byte[] bytes; + + public Uint8List(long length) { + // Checked on the long, before narrowing, as ByteData and the lists are: + // Uint8List(2^32) cast to an empty list instead of failing. + dart.core.RangeError.checkNotNegative(length, "length"); + dart.core.RangeError.checkAllocatable(length); + this.bytes = new byte[(int) length]; + } + + private Uint8List(byte[] bytes) { + this.bytes = bytes; + } + + /** {@code Uint8List.fromList(source)} with a source typed only dynamic. */ + public static Uint8List fromList(Object source) { + if (!(source instanceof List)) { + throw new dart.core.TypeError("type '" + (source == null ? "Null" : source.getClass().getName()) + + "' is not a subtype of type 'List'"); + } + return fromList((List) source); + } + + /** {@code Uint8List.fromList([...])}. */ + public static Uint8List fromList(List elements) { + byte[] b = new byte[elements.size()]; + for (int i = 0; i < b.length; i++) { + Object o = elements.get(i); + // The List contract is checked per element, as Dart does: a string + // became byte zero and a double was narrowed, where Dart throws TypeError. + if (!(o instanceof Long || o instanceof Integer || o instanceof Short || o instanceof Byte)) { + throw new dart.core.TypeError("type '" + (o == null ? "Null" : o instanceof Number ? "double" + : o.getClass().getSimpleName()) + "' is not a subtype of type 'int'"); + } + b[i] = ((Number) o).byteValue(); + } + return new Uint8List(b); + } + + public long length() { + return bytes.length; + } + + /** The raw backing array (used by image decoders). */ + public byte[] toBytes() { + return bytes; + } +} diff --git a/maven/dart-runtime/src/test/java/dart/async/FutureAdoptionTest.java b/maven/dart-runtime/src/test/java/dart/async/FutureAdoptionTest.java new file mode 100644 index 00000000000..32d4ec85639 --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/async/FutureAdoptionTest.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +import dart.runtime.Funcs; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// A future completed WITH another future waits for it and adopts its outcome, +/// the way Dart's FutureOr completion does. +public class FutureAdoptionTest { + + @Test + public void thenCallbackReturningAFutureAdoptsItsValue() { + final Completer inner = new Completer(); + Future chained = Future.value((Object) "a").then(new Funcs.Func1() { + @Override + public Object call(Object v) { + return inner.future(); + } + }); + // Still waiting: the value is not the inner Future object. + assertFalse(chained.isDone(), "the chain must wait for the returned future"); + inner.complete("fetched"); + assertTrue(chained.isDone()); + assertEquals("fetched", chained.valueOrThrow()); + } + + @Test + public void thenCallbackReturningAFailingFutureAdoptsTheError() { + final Completer inner = new Completer(); + Future chained = Future.value((Object) "a").then(new Funcs.Func1() { + @Override + public Object call(Object v) { + return inner.future(); + } + }); + inner.completeError(new IllegalStateException("fetch failed")); + RuntimeException thrown = assertThrows(RuntimeException.class, chained::valueOrThrow); + assertEquals("fetch failed", thrown.getMessage()); + } + + @Test + public void catchErrorRecoveringWithAFutureAdoptsIt() { + Future failed = new Future(); + failed.completeError(new IllegalStateException("boom")); + Future recovered = failed.catchError(new Funcs.Func1() { + @Override + public Object call(Object e) { + return Future.value((Object) "recovered"); + } + }); + assertEquals("recovered", recovered.valueOrThrow()); + } + + @Test + public void completingTwiceWhileAdoptingIsRefused() { + Completer c = new Completer(); + c.complete(new Completer().future()); + assertThrows(dart.core.StateError.class, () -> c.complete("again")); + } + + @Test + public void aFutureCompletedWithItselfFailsInsteadOfHanging() { + Future f = new Future(); + f.complete(f); + assertTrue(f.isDone()); + assertThrows(RuntimeException.class, f::valueOrThrow); + } + + @Test + public void plainValuesAreUnchanged() { + Future f = Future.value((Object) "v"); + assertSame("v", f.getNow()); + } + + @Test + public void voidCatchErrorRecoversTheChain() { + Future failed = new Future(); + failed.completeError(new IllegalStateException("boom")); + final boolean[] ran = {false}; + Future recovered = failed.catchError(new Funcs.VoidFunc1() { + @Override + public void call(Object e) { + ran[0] = true; + } + }); + assertTrue(ran[0]); + assertTrue(recovered.isDone()); + assertEquals(null, recovered.valueOrThrow(), "recovered with null, not rethrown"); + } + + @Test + public void aThrowingVoidCatchErrorFailsTheChainInstead() { + Future failed = new Future(); + failed.completeError(new IllegalStateException("boom")); + Future chained = failed.catchError(new Funcs.VoidFunc1() { + @Override + public void call(Object e) { + throw new IllegalArgumentException("handler failed"); + } + }); + RuntimeException thrown = assertThrows(RuntimeException.class, chained::valueOrThrow); + assertEquals("handler failed", thrown.getMessage()); + } + + @Test + public void whenCompleteCarriesTheOutcomeUnlessTheActionThrows() { + Future ok = Future.value((Object) "v"); + assertEquals("v", ok.whenComplete(new Funcs.VoidFunc0() { + @Override + public void call() { + } + }).valueOrThrow()); + Future cleanupFailed = ok.whenComplete(new Funcs.VoidFunc0() { + @Override + public void call() { + throw new IllegalStateException("cleanup failed"); + } + }); + RuntimeException thrown = assertThrows(RuntimeException.class, cleanupFailed::valueOrThrow); + assertEquals("cleanup failed", thrown.getMessage()); + } + + @Test + public void aNegativeDelayCompletesLikeZero() throws Exception { + Future f = Future.delayed(dart.core.Duration.of(0, 0, 0, 0, -50, 0), + new Funcs.Func0() { + @Override + public Object call() { + return "done"; + } + }); + long deadline = System.currentTimeMillis() + 2000; + while (!f.isDone() && System.currentTimeMillis() < deadline) { + Thread.sleep(5); + } + assertTrue(f.isDone(), "a negative delay must not leave the future pending forever"); + assertEquals("done", f.valueOrThrow()); + } + + @Test + public void aRejectingTestLetsTheErrorThrough() { + Future failed = new Future(); + failed.completeError(new IllegalStateException("unrelated")); + final boolean[] ran = {false}; + Future chained = failed.catchError(new Funcs.VoidFunc1() { + @Override + public void call(Object e) { + ran[0] = true; + } + }, new Funcs.Func1() { + @Override + public Object call(Object e) { + return e instanceof IllegalArgumentException; + } + }); + assertFalse(ran[0], "the handler must not run for an error its test rejects"); + RuntimeException thrown = assertThrows(RuntimeException.class, chained::valueOrThrow); + assertEquals("unrelated", thrown.getMessage()); + } + + @Test + public void anAcceptingTestRecovers() { + Future failed = new Future(); + failed.completeError(new IllegalArgumentException("mine")); + Future chained = failed.catchError(new Funcs.Func1() { + @Override + public Object call(Object e) { + return "recovered"; + } + }, new Funcs.Func1() { + @Override + public Object call(Object e) { + return e instanceof IllegalArgumentException; + } + }); + assertEquals("recovered", chained.valueOrThrow()); + } + + // --- Future.wait settles through listeners -------------------------------- + + // Bounded: the regression this guards is a wait that BLOCKS, which would + // otherwise hang the build instead of failing it -- in a separate thread, + // because Await parks through an interrupt. + @Test + @Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + public void waitReturnsBeforeItsInputsSettle() { + Completer a = new Completer(); + Completer b = new Completer(); + java.util.List> inputs = new java.util.ArrayList>(); + inputs.add(a.future()); + inputs.add(b.future()); + Future> all = Future.wait(inputs); + assertFalse(all.isDone(), "wait must hand back a future, not block on its inputs"); + b.complete("second"); + assertFalse(all.isDone()); + a.complete("first"); + dart.core.DartList values = all.valueOrThrow(); + assertEquals(2, values.size()); + assertEquals("first", values.get(0), "values keep input order, not completion order"); + assertEquals("second", values.get(1)); + } + + // Bounded: the regression this guards is a wait that BLOCKS, which would + // otherwise hang the build instead of failing it -- in a separate thread, + // because Await parks through an interrupt. + @Test + @Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + public void aFailingInputReachesCatchErrorOnTheReturnedFuture() { + Completer ok = new Completer(); + Completer bad = new Completer(); + java.util.List> inputs = new java.util.ArrayList>(); + inputs.add(ok.future()); + inputs.add(bad.future()); + final Object[] caught = {null}; + Future handled = Future.wait(inputs).catchError(new Funcs.Func1() { + @Override + public Object call(Object e) { + caught[0] = e; + return "handled"; + } + }); + final IllegalStateException boom = new IllegalStateException("boom"); + bad.completeError(boom); + assertFalse(handled.isDone(), "Dart's default waits for every input before reporting the error"); + ok.complete("fine"); + assertEquals("handled", handled.valueOrThrow()); + assertSame(boom, caught[0]); + } + + @Test + public void waitOnNothingCompletesWithAnEmptyList() { + assertEquals(0, Future.wait(new java.util.ArrayList>()).valueOrThrow().size()); + } + + // --- Completer and Timer --------------------------------------------------- + + @Test + public void aCompleterAdoptingAPendingFutureIsCompleted() { + Completer c = new Completer(); + Completer inner = new Completer(); + c.complete(inner.future()); + assertTrue(c.isCompleted(), "complete(pendingFuture) has been called, so it is completed"); + assertThrows(dart.core.StateError.class, () -> c.complete("again")); + inner.complete("v"); + assertEquals("v", c.future().valueOrThrow()); + } + + @Test + @Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + public void aNegativeTimerStillFires() throws Exception { + final java.util.concurrent.CountDownLatch fired = new java.util.concurrent.CountDownLatch(1); + new Timer(dart.core.Duration.ofMicroseconds(-5000L), new Funcs.VoidFunc0() { + @Override + public void call() { + fired.countDown(); + } + }); + assertTrue(fired.await(5, java.util.concurrent.TimeUnit.SECONDS), + "a negative delay fires as soon as possible, as though it were zero"); + } + + @Test + public void thenOnErrorHandlesOnlyTheSourceFailure() { + Future failed = Future.error(new IllegalStateException("source")); + Future handled = failed.then(new Funcs.Func1() { + @Override + public Object call(Object v) { + return "value"; + } + }, (Object) new Funcs.Func1() { + @Override + public Object call(Object e) { + return "handled"; + } + }); + assertEquals("handled", handled.valueOrThrow()); + + final IllegalArgumentException fromOnValue = new IllegalArgumentException("onValue"); + Future escaped = Future.value((Object) 1L).then(new Funcs.Func1() { + @Override + public Object call(Object v) { + throw fromOnValue; + } + }, (Object) new Funcs.Func1() { + @Override + public Object call(Object e) { + return "not me"; + } + }); + assertSame(fromOnValue, assertThrows(IllegalArgumentException.class, escaped::valueOrThrow)); + } + + @Test + public void whenCompleteWaitsForTheCleanupFutureThenForwardsTheOutcome() { + final Completer cleanup = new Completer(); + Future done = Future.value((Object) "result").whenCompleteFuture(new Funcs.Func0() { + @Override + public Object call() { + return cleanup.future(); + } + }); + assertFalse(done.isDone(), "the outcome waits for the cleanup"); + cleanup.complete(null); + assertTrue(done.isDone()); + assertEquals("result", done.valueOrThrow()); + } + + @Test + public void aFailingCleanupFutureReplacesTheOutcome() { + final Completer cleanup = new Completer(); + Future done = Future.value((Object) "result").whenCompleteFuture(new Funcs.Func0() { + @Override + public Object call() { + return cleanup.future(); + } + }); + cleanup.completeError(new IllegalStateException("cleanup failed")); + RuntimeException thrown = assertThrows(RuntimeException.class, done::valueOrThrow); + assertEquals("cleanup failed", thrown.getMessage()); + } + + @Test + public void aCatchErrorTestThatThrowsCompletesTheFutureWithItsError() { + Future failed = Future.error(new IllegalStateException("op")); + Future handled = failed.catchError(new Funcs.Func1() { + @Override + public Object call(Object e) { + return "recovered"; + } + }, new Funcs.Func1() { + @Override + public Object call(Object e) { + throw new IllegalArgumentException("predicate"); + } + }); + assertTrue(handled.isDone(), "the returned future completes"); + RuntimeException thrown = assertThrows(RuntimeException.class, handled::valueOrThrow); + assertEquals("predicate", thrown.getMessage()); + } +} diff --git a/maven/dart-runtime/src/test/java/dart/async/TimerCancelTest.java b/maven/dart-runtime/src/test/java/dart/async/TimerCancelTest.java new file mode 100644 index 00000000000..cfca5c355ab --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/async/TimerCancelTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.async; + +import dart.core.Duration; +import dart.runtime.Funcs; +import java.util.ArrayList; +import java.util.List; +import java.util.TimerTask; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Cancelling a timer unschedules the task CN.setTimeout created, including the +/// current link of a wait longer than an int of milliseconds, and releases the +/// callback. +public class TimerCancelTest { + + private final Timers.Scheduler original = Timers.scheduler; + private final List timers = new ArrayList(); + private final List delays = new ArrayList(); + private final List tasks = new ArrayList(); + + private void recordSchedules() { + Timers.scheduler = new Timers.Scheduler() { + @Override + public java.util.Timer schedule(int ms, Runnable r) { + java.util.Timer t = new java.util.Timer(true); + timers.add(t); + delays.add(Integer.valueOf(ms)); + tasks.add(r); + return t; + } + }; + } + + @AfterEach + public void restore() { + Timers.scheduler = original; + for (java.util.Timer t : timers) { + t.cancel(); + } + } + + /** A cancelled java.util.Timer refuses new tasks; a live one accepts them. */ + private static boolean isCancelled(java.util.Timer t) { + try { + t.schedule(new TimerTask() { + @Override + public void run() { + } + }, 3600000L); + return false; + } catch (IllegalStateException cancelled) { + return true; + } + } + + @Test + public void cancelUnschedulesTheTask() { + recordSchedules(); + Timers.Handle h = Timers.schedule(10, new Runnable() { + @Override + public void run() { + } + }); + assertEquals(1, timers.size()); + assertFalse(isCancelled(timers.get(0))); + h.cancel(); + assertTrue(isCancelled(timers.get(0)), "the scheduled task must be cancelled, not just flagged"); + } + + @Test + public void cancelReachesTheCurrentLinkOfAChainedWait() { + recordSchedules(); + final int[] ran = new int[1]; + Timers.Handle h = Timers.schedule(Integer.MAX_VALUE + 5L, new Runnable() { + @Override + public void run() { + ran[0]++; + } + }); + assertEquals(Integer.valueOf(Integer.MAX_VALUE), delays.get(0)); + tasks.get(0).run(); + assertEquals(2, timers.size()); + assertEquals(Integer.valueOf(5), delays.get(1)); + h.cancel(); + assertTrue(isCancelled(timers.get(1)), "the link scheduled now is the one to cancel"); + assertEquals(0, ran[0]); + } + + @Test + public void aLinkFiringAfterCancelSchedulesNothing() { + recordSchedules(); + Timers.Handle h = Timers.schedule(Integer.MAX_VALUE + 5L, new Runnable() { + @Override + public void run() { + } + }); + h.cancel(); + assertTrue(isCancelled(timers.get(0))); + // a link already handed to the EDT when cancel() ran + tasks.get(0).run(); + assertEquals(1, timers.size(), "a cancelled chain must not schedule its next link"); + } + + @Test + public void cancelReleasesTheCallback() { + Timer t = new Timer(Duration.of(0, 1, 0, 0, 0, 0), new Funcs.VoidFunc0() { + @Override + public void call() { + } + }); + assertTrue(t.isActive()); + t.cancel(); + assertFalse(t.isActive()); + assertNull(t.callback, "a cancelled timer must not keep its callback reachable"); + } + + @Test + public void aNegativeDelayIsScheduledAsZero() { + recordSchedules(); + Timers.schedule(-5, new Runnable() { + @Override + public void run() { + } + }); + assertEquals(1, delays.size()); + assertEquals(Integer.valueOf(0), delays.get(0)); + } +} diff --git a/maven/dart-runtime/src/test/java/dart/core/CollectionsTest.java b/maven/dart-runtime/src/test/java/dart/core/CollectionsTest.java new file mode 100644 index 00000000000..ae4ec9ed334 --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/core/CollectionsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CollectionsTest { + + @Test + public void listBasics() { + DartList l = DartList.of("a", "b", "c"); + assertEquals(3, l.length()); + assertEquals("a", l.idx(0)); + assertEquals("c", l.last()); + assertThrows(RangeError.class, () -> l.idx(3)); + assertThrows(RangeError.class, () -> l.idx(-1)); + assertEquals("[a, b, c]", l.toString()); + } + + @Test + public void listMapWhereAreLazyButCorrect() { + DartList l = DartList.of(1L, 2L, 3L, 4L); + assertEquals("[2, 4]", l.where(v -> v % 2 == 0).toList().toString()); + assertEquals("2, 4, 6, 8", l.map(v -> v * 2).join(", ")); + } + + @Test + public void fixedLengthListRejectsGrowth() { + DartList l = DartList.filled(2, 0L); + assertThrows(UnsupportedError.class, () -> l.add(1L)); + l.idxSet(1, 5L); + assertEquals("[0, 5]", l.toString()); + } + + @Test + public void listInterOpsWithJavaUtil() { + DartList l = DartList.of("x", "y"); + java.util.List asJava = l; + assertEquals(2, asJava.size()); + assertTrue(asJava.contains("y")); + } + + @Test + public void mapPreservesInsertionOrder() { + DartMap m = DartMap.of("z", 1L, "a", 2L, "m", 3L); + assertEquals("{z: 1, a: 2, m: 3}", m.toString()); + assertEquals("z, a, m", m.keys().join(", ")); + } + + @Test + public void stringHelpers() { + assertEquals("ababab", DString.repeat("ab", 3)); + assertEquals(" x", DString.padLeft("x", 3)); + assertEquals("[a, b]", DString.split("a-b", "-").toString()); + assertEquals(42L, DString.parseInt(" 42 ")); + assertThrows(FormatException.class, () -> DString.parseInt("nope")); + } +} diff --git a/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java b/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java new file mode 100644 index 00000000000..d53b0c6106a --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java @@ -0,0 +1,970 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import dart.math.DartMath; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// dart:core and dart:math behaviours that transpiled applications rely on and +/// that each once diverged from Dart. +public class DartCoreSemanticsTest { + + // --- DateTime normalizes out-of-range components -------------------------- + + @Test + public void dayZeroIsTheLastDayOfThePreviousMonth() { + DateTime d = new DateTime(2024, 3, 0, 0, 0, 0, 0, 0); + assertEquals(2, d.month()); + assertEquals(29, d.day(), "2024 is a leap year"); + } + + @Test + public void monthZeroIsThePreviousDecember() { + DateTime d = new DateTime(2024, 0, 15, 0, 0, 0, 0, 0); + assertEquals(2023, d.year()); + assertEquals(12, d.month()); + assertEquals(15, d.day()); + } + + @Test + public void lastDayOfMonthIdiom() { + // DateTime(y, m + 1, 0) -- the usual way Dart code asks for a month's length. + assertEquals(30, new DateTime(2023, 12, 0, 0, 0, 0, 0, 0).day()); + assertEquals(31, new DateTime(2024, 1, 0, 0, 0, 0, 0, 0).day()); + } + + // --- Uri query components decode '+' as a space; path segments do not ----- + + @Test + public void plusInAQueryIsASpace() { + DartUri u = DartUri.parse("https://example.com/search?q=hello+world&x=a%2Bb"); + assertEquals("hello world", u.queryParameters().get("q")); + assertEquals("a+b", u.queryParameters().get("x"), "an ENCODED plus stays a plus"); + } + + @Test + public void plusInAPathSegmentIsLiteral() { + DartUri u = DartUri.parse("https://example.com/c++/notes"); + assertEquals("c++", u.pathSegments().get(0)); + } + + // --- List.addAll of itself ------------------------------------------------ + + @Test + public void addAllOfItselfDuplicatesTheOriginal() { + DartList l = new DartList(); + l.add("a"); + l.add("b"); + l.addAllIterable(l); + assertEquals(4, l.size()); + assertEquals("a", l.get(2)); + assertEquals("b", l.get(3)); + } + + // --- Random.nextInt accepts Dart's whole range ---------------------------- + + @Test + public void nextIntAcceptsTheFullUnsigned32BitBound() { + DartMath.DartRandom r = new DartMath.DartRandom(42); + long max = 1L << 32; + for (int i = 0; i < 2000; i++) { + long v = r.nextInt(max); + assertTrue(v >= 0 && v < max, "out of range: " + v); + } + long big = (1L << 31) + 7; + for (int i = 0; i < 2000; i++) { + long v = r.nextInt(big); + assertTrue(v >= 0 && v < big, "out of range: " + v); + } + } + + @Test + public void nextIntReachesAboveTheSignedIntRange() { + DartMath.DartRandom r = new DartMath.DartRandom(7); + boolean sawHigh = false; + for (int i = 0; i < 200 && !sawHigh; i++) { + sawHigh = r.nextInt(1L << 32) > Integer.MAX_VALUE; + } + assertTrue(sawHigh, "values above 2^31 must be produced, not only the low half"); + } + + @Test + public void nextIntRejectsBoundsDartRejects() { + DartMath.DartRandom r = new DartMath.DartRandom(1); + assertThrows(RangeError.class, () -> r.nextInt(0)); + assertThrows(RangeError.class, () -> r.nextInt((1L << 32) + 1)); + } + + // --- DateTime.toString is Dart's format --------------------------------- + + @Test + public void dateTimePrintsDartsFormat() { + assertEquals("2024-03-05 07:08:09.010Z", + DateTime.utc(2024, 3, 5, 7, 8, 9, 10, 0).toString()); + assertEquals("2024-03-05 07:08:09.010", + new DateTime(2024, 3, 5, 7, 8, 9, 10, 0).toString(), + "local time, no Z"); + } + + // --- Uri with an IPv6 literal --------------------------------------------- + + @Test + public void bracketedIpv6AuthorityKeepsHostAndPort() { + DartUri u = DartUri.parse("http://[::1]:8080/path"); + assertEquals("::1", u.host()); + assertEquals(8080, u.port()); + assertEquals("/path", u.path()); + DartUri noPort = DartUri.parse("http://[2001:db8::7]/x"); + assertEquals("2001:db8::7", noPort.host()); + } + + // --- RegExp.allMatches of an empty pattern -------------------------------- + + @Test + public void anEmptyMatchIsReportedOnce() { + int count = 0; + for (RegExpMatch m : new RegExp("$").allMatches("abc")) { + count++; + assertEquals(3, m.start()); + } + assertEquals(1, count, "the end-of-input match must not be found twice"); + } + + @Test + public void allMatchesSearchesOnlyAsFarAsTheIterationGoes() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 100000; i++) { + sb.append('a'); + } + final int[] searches = new int[1]; + RegExp.searchHook = new Runnable() { + @Override + public void run() { + searches[0]++; + } + }; + try { + DartIterable all = new RegExp("a").allMatches(sb.toString()); + assertEquals(0, searches[0], "creating the iterable searches nothing"); + int seen = 0; + for (RegExpMatch m : all.take(1)) { + seen++; + assertEquals(0, m.start()); + assertEquals(1, m.end()); + } + assertEquals(1, seen); + assertEquals(1, searches[0], "take(1) is one search, not one per match"); + assertEquals(5, new RegExp("a").allMatches("aaaaa").length()); + } finally { + RegExp.searchHook = null; + } + } + + @Test + public void allMatchesIteratesAfreshAndKeepsItsOrder() { + DartIterable all = new RegExp("[0-9]+").allMatches("a1 b22 c333"); + StringBuilder first = new StringBuilder(); + for (RegExpMatch m : all) { + first.append(m.group(0)).append(','); + } + StringBuilder second = new StringBuilder(); + for (RegExpMatch m : all) { + second.append(m.group(0)).append(','); + } + assertEquals("1,22,333,", first.toString()); + assertEquals(first.toString(), second.toString()); + assertFalse(new RegExp("x").allMatches("abc").iterator().hasNext()); + assertFalse(new RegExp("x").allMatches(null).iterator().hasNext()); + } + + @Test + public void portDefaultsToTheSchemes() { + assertEquals(443, DartUri.parse("https://example.com/path").port()); + assertEquals(80, DartUri.parse("http://example.com/").port()); + assertEquals(8443, DartUri.parse("https://example.com:8443/").port(), "explicit wins"); + assertEquals(0, DartUri.parse("ftp://example.com/").port()); + } + + @Test + public void aLocalValueAndItsUtcFormAreNotEqual() { + DateTime local = new DateTime(2024, 3, 5, 7, 8, 9, 10, 0); + DateTime utc = local.toUtc(); + assertTrue(local.isAtSameMomentAs(utc), "the same instant"); + assertFalse(local.equals(utc), "but not equal: the time-zone mode differs"); + assertTrue(local.equals(utc.toLocal())); + } + + // --- String's Pattern methods accept a RegExp ----------------------------- + + @Test + public void splitOnARegExpFollowsDart() { + assertEquals(java.util.Arrays.asList("a", "b"), DString.split("a,b", new RegExp(","))); + assertEquals(java.util.Arrays.asList("a", "b", "c"), DString.split("a1b22c", new RegExp("[0-9]+"))); + assertEquals(java.util.Arrays.asList("a", "b", "b", "a"), DString.split("abba", new RegExp("")), + "empty matches split between characters, with no empty parts at the ends"); + assertEquals(0, DString.split("", new RegExp("")).size(), "an empty input the pattern matches has no parts"); + assertEquals(java.util.Arrays.asList(""), DString.split("", new RegExp(","))); + assertEquals(java.util.Arrays.asList("a", ""), DString.split("a,", new RegExp(","))); + } + + @Test + public void theOtherPatternMethodsAcceptARegExp() { + assertTrue(DString.contains("abc", new RegExp("b+"))); + assertFalse(DString.contains("abc", new RegExp("x"))); + assertEquals(1, DString.indexOf("xab", new RegExp("a"))); + assertEquals(1, DString.indexOf("aaa", new RegExp("aa"), 1), + "searches from the start offset, so an overlapping match is found"); + assertEquals(3, DString.lastIndexOf("abcab", new RegExp("ab"))); + assertEquals(-1, DString.lastIndexOf("abc", new RegExp("x"))); + assertEquals("-a-b-c-", DString.replaceAll("abc", new RegExp(""), "-")); + assertEquals("x-y-z", DString.replaceAll("x1y22z", new RegExp("[0-9]+"), "-")); + assertEquals("a$1c", DString.replaceAll("abc", new RegExp("(b)"), "$1"), "the replacement is literal"); + assertEquals("a-b2", DString.replaceFirst("a1b2", new RegExp("[0-9]"), "-")); + } + + // --- numbers as keys, sorting, text ---------------------------------------- + + @Test + public void aDoubleKeyFindsTheEqualIntKey() { + DartMap m = new DartMap(); + m.put(Long.valueOf(1), "a"); + assertEquals("a", m.get(Double.valueOf(1.0)), "{1: 'a'}[1.0]"); + m.put(Double.valueOf(1.0), "b"); + assertEquals(1, m.size(), "m[1.0] = 'b' updates the entry for 1"); + assertTrue(m.keySet().iterator().next() instanceof Long, "and the key stays the int 1"); + assertEquals("b", m.get(Long.valueOf(1))); + m.put(Double.valueOf(0.0), "zero"); + assertEquals("zero", m.get(Double.valueOf(-0.0)), "0.0 == -0.0"); + assertEquals("zero", m.remove(Long.valueOf(0))); + assertFalse(m.containsKey(Double.valueOf(0.0))); + } + + @Test + public void aSetHoldsOneOfEachEqualNumber() { + DartSet s = new DartSet(); + s.add(Long.valueOf(1)); + assertTrue(s.contains(Double.valueOf(1.0))); + assertFalse(s.add(Double.valueOf(1.0)), "adding 1.0 to {1} changes nothing"); + assertEquals(1, s.size()); + assertTrue(s.remove(Double.valueOf(1.0))); + assertTrue(s.isEmpty()); + } + + @Test + public void mixedNumbersSortNumerically() { + DartList l = new DartList(); + l.add(Long.valueOf(2)); + l.add(Double.valueOf(1.5)); + l.add(Long.valueOf(1)); + l.sortDefault(); // what the emitter calls for a bare sort() + assertEquals(java.util.Arrays.asList(Long.valueOf(1), Double.valueOf(1.5), Long.valueOf(2)), l); + assertTrue(DartComparable.compare(Long.valueOf(1), Double.valueOf(1.5)) < 0); + } + + @Test + public void equalIntMapsAreNotEqualMaps() { + DartLongMap a = new DartLongMap(); + DartLongMap b = new DartLongMap(); + a.putLong(1, 2); + b.putLong(1, 2); + assertFalse(a.equals(b), "{1: 2} == {1: 2} is false in Dart"); + assertTrue(a.equals(a)); + } + + @Test + public void writeCharCodeWritesASurrogatePairAboveTheBmp() { + StringBuffer sb = new StringBuffer(); + sb.writeCharCode(0x1F600); + assertEquals("\uD83D\uDE00", sb.toString()); + assertThrows(RangeError.class, () -> new StringBuffer().writeCharCode(0x110000)); + } + + @Test + public void theHostAndSchemeAreCanonicallyLowerCase() { + DartUri u = DartUri.parse("HTTPS://User@EXAMPLE.COM:8080/Path?Q=V#Frag"); + assertEquals("https", u.scheme()); + assertEquals("example.com", u.host()); + assertEquals("https://User@example.com:8080/Path?Q=V#Frag", u.toString(), + "the path, query, fragment and user info keep their case"); + assertEquals("fe80::1", DartUri.parse("http://[FE80::1]:80/").host()); + } + + @Test + public void padLeftRepeatsTheWholePaddingOncePerMissingPosition() { + // Recorded from the Dart SDK: 'x'.padLeft(4, 'ab') is 'abababx', NOT + // 'ababx'. The padding is prepended once per missing position whatever its + // length, which the Dart documentation spells out for multi-character + // padding such as ' '. + assertEquals("abababx", DString.padLeft("x", 4, "ab")); + assertEquals("xababab", DString.padRight("x", 4, "ab")); + } + + @Test + public void tryParseAnswersNullAndParseThrowsFormatException() { + assertEquals(null, DString.tryParseInt("x1")); + assertEquals(Long.valueOf(42), DString.tryParseInt(" 42 ")); + assertThrows(FormatException.class, () -> DString.parseInt("nope")); + } + + // --- values recorded from the Dart SDK ------------------------------------- + + @Test + public void aDateTimeKeepsItsMicroseconds() { + DateTime a = DateTime.utc(2024, 1, 15, 10, 30, 0, 0, 1); + DateTime b = DateTime.utc(2024, 1, 15, 10, 30, 0, 0, 999); + assertFalse(a.equals(b)); + assertEquals(998, b.difference(a).inMicroseconds()); + assertEquals(1, a.microsecond()); + assertEquals("2024-01-15 10:30:00.000001Z", a.toString()); + assertEquals("2024-01-15 10:30:00.000Z", DateTime.utc(2024, 1, 15, 10, 30, 0, 0, 0).toString()); + assertEquals(a.microsecondsSinceEpoch() + 5, + a.add(Duration.ofMicroseconds(5)).microsecondsSinceEpoch(), "a sub-millisecond add counts"); + DateTime early = DateTime.fromMicrosecondsSinceEpoch(-1, true); + assertEquals(-1, early.millisecondsSinceEpoch(), "milliseconds round toward negative infinity"); + assertEquals(999, early.microsecond()); + } + + @Test + public void anIdentityMapKeepsEqualKeysApart() { + DartMap m = DartMap.identity(); + String a = new String("k"); + String b = new String("k"); + m.put(a, "first"); + m.put(b, "second"); + assertEquals(2, m.size(), "equal but distinct keys are two entries"); + assertEquals("first", m.get(a)); + assertEquals("second", m.get(b)); + assertEquals(null, m.get("k")); + assertEquals(java.util.Arrays.asList(a, b), new java.util.ArrayList(m.keySet())); + m.remove(a); + assertEquals(1, m.size()); + } + + @Test + public void doublesPrintInDartNotation() { + // Recorded from the Dart SDK. + String[][] cases = { + {"0.0001", "0.0001"}, {"0.000001", "0.000001"}, {"1e-7", "1e-7"}, + {"1e16", "10000000000000000.0"}, {"1e20", "100000000000000000000.0"}, + {"1e21", "1e+21"}, {"1.5e22", "1.5e+22"}, {"12345678.9", "12345678.9"}, + {"123.456", "123.456"}, {"-0.0005", "-0.0005"}, {"2.5e-10", "2.5e-10"}, + {"0.1", "0.1"}, {"100.0", "100.0"}, {"1e300", "1e+300"}, + }; + for (String[] c : cases) { + assertEquals(c[1], dart.runtime.DartRuntime.doubleStr(Double.parseDouble(c[0])), c[0]); + } + } + + @Test + public void intParseTakesARadixAndHex() { + assertEquals(255, DString.parseInt("ff", 16)); + assertEquals(Long.valueOf(255), DString.tryParseInt("0xFF")); + assertEquals(Long.valueOf(-16), DString.tryParseInt("-0x10")); + assertEquals(Long.valueOf(1295), DString.tryParseInt("zz", 36)); + assertEquals(null, DString.tryParseInt("12", 2)); + assertEquals(null, DString.tryParseInt("--1")); + assertThrows(RangeError.class, () -> DString.tryParseInt("1", 37)); + } + + @Test + public void caseConversionIgnoresTheDefaultLocale() { + java.util.Locale saved = java.util.Locale.getDefault(); + try { + java.util.Locale.setDefault(new java.util.Locale("tr", "TR")); + assertEquals("ISTANBUL", DString.toUpperCase("istanbul"), "no dotted capital I"); + assertEquals("title", DString.toLowerCase("TITLE"), "no dotless i"); + } finally { + java.util.Locale.setDefault(saved); + } + assertEquals("STRA\u00dfE", DString.toUpperCase("stra\u00dfe"), "Dart leaves the sharp s alone"); + } + + @Test + public void indexOfRangeChecksItsStart() { + assertThrows(RangeError.class, () -> DString.indexOf("abc", "a", -1)); + assertThrows(RangeError.class, () -> DString.indexOf("abc", "a", 4)); + assertEquals(-1, DString.indexOf("abc", "c", 3), "the length itself is a valid start"); + } + + @Test + public void equivalentUrisAreEqual() { + DartUri a = DartUri.parse("https://EXAMPLE.com/a"); + DartUri b = DartUri.parse("https://example.com/a"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertFalse(a.equals(DartUri.parse("https://example.com/A")), "the path is case sensitive"); + } + + // --- collections, round 4 --------------------------------------------------- + + @Test + public void anIdentitySetKeepsEqualElementsApart() { + DartSet s = DartSet.identity(); + String a = new String("v"); + String b = new String("v"); + assertTrue(s.add(a)); + assertTrue(s.add(b), "an equal but distinct element is new"); + assertFalse(s.add(a)); + assertEquals(2, s.size()); + assertFalse(s.contains("v")); + assertTrue(s.remove(a)); + assertEquals(1, s.size()); + } + + @Test + public void entriesIsALiveView() { + DartMap m = new DartMap(); + m.put("a", 1L); + DartIterable> entries = m.entries(); + m.put("b", 2L); + assertEquals(2, entries.length(), "a retained view sees the later entry"); + } + + @Test + public void takeAndSkipRefuseANegativeCount() { + DartList l = new DartList(); + l.add("x"); + assertThrows(RangeError.class, () -> l.take(-1)); + assertThrows(RangeError.class, () -> l.skip(-1)); + assertEquals(1, l.take(5).length()); + } + + @Test + public void aListModifiedWhileIteratedFailsInsteadOfLooping() { + DartList l = new DartList(); + l.add("a"); + l.add("b"); + assertThrows(java.util.ConcurrentModificationException.class, () -> { + for (Object x : l) { + l.add(x); + } + }); + DartLongList longs = DartLongList.ofLongs(1, 2); + assertThrows(java.util.ConcurrentModificationException.class, () -> { + for (Long x : longs) { + longs.addLong(x); + } + }); + } + + @Test + public void aSpecialisedIntMapHasTheDartMapApi() { + DartLongMap m = DartLongMap.ofLongs(1, 10, 2, 20); + long sum = 0; + for (MapEntry e : m.entries()) { + sum += e.key() + e.value(); + } + assertEquals(33, sum); + m.update(1L, v -> v + 1, null); + assertEquals(Long.valueOf(11), m.idx(1)); + assertEquals(Long.valueOf(7), m.putIfAbsentDart(3L, () -> 7L)); + m.removeWhere((k, v) -> v > 15); + assertEquals(2, m.length(), "20 removed; 11 and 7 remain"); + m.idxSet(4L, 4L); + assertTrue(m.containsKeyLong(4)); + } + + @Test + public void anIterableHasForEachDart() { + DartList l = new DartList(); + l.add("a"); + l.add("b"); + final StringBuilder sb = new StringBuilder(); + l.asIterable().forEachDart(x -> sb.append(x)); + assertEquals("ab", sb.toString()); + } + + @Test + public void trimAndStartsWithFollowDart() { + assertEquals("padded", DString.trim("\u00a0 padded \u2003\u2028")); + assertEquals("x", DString.trim("\ufeffx")); + assertTrue(DString.startsWith("abc", "b", 1)); + assertThrows(RangeError.class, () -> DString.startsWith("abc", "a", 4)); + } + + @Test + public void fromHonoursGrowableAndIndexOfTakesAStart() { + DartList fixed = DartList.from(java.util.Arrays.asList("a", "b"), false); + assertThrows(UnsupportedError.class, () -> fixed.add("c")); + DartLongList longs = DartLongList.fromLongs(java.util.Arrays.asList(1L, 2L), false); + assertThrows(UnsupportedError.class, () -> longs.addLong(3)); + DartList l = DartList.of((Object) 1L, 2L, 1L); + assertEquals(2, l.indexOfDart(1L, 1)); + } + + // --- round 6 ---------------------------------------------------------------- + + @Test + public void negativeListLengthsAreRefused() { + assertThrows(RangeError.class, () -> DartList.filled(-1, "x")); + assertThrows(RangeError.class, () -> DartList.generate(-1, i -> "x")); + assertThrows(RangeError.class, () -> DartLongList.filled(-1, 0L)); + assertThrows(RangeError.class, () -> DartDoubleList.generateDoubles(-1, i -> 0.0)); + assertEquals(0, DartList.filled(0, "x").size()); + } + + @Test + public void byteDataChecksTheOffsetBeforeNarrowingIt() { + dart.typed_data.ByteData b = new dart.typed_data.ByteData(4); + b.setUint8(0, 7); + assertThrows(RangeError.class, () -> b.getUint8(4294967296L), "2^32 must not wrap to byte 0"); + assertThrows(RangeError.class, () -> b.setUint8(-1, 1)); + assertThrows(RangeError.class, () -> b.getInt32(1), "four bytes from 1 run past the end"); + assertEquals(7, b.getUint8(0)); + } + + @Test + public void malformedUrisAreRejectedAsDartRejectsThem() { + // Each expectation recorded from the Dart SDK. + assertEquals(null, DartUri.tryParse("http://[::1")); + assertEquals(null, DartUri.tryParse("http://host:abc/")); + assertEquals(null, DartUri.tryParse("::")); + assertEquals(null, DartUri.tryParse("1http://x")); + assertThrows(FormatException.class, () -> DartUri.parse("http://[::1")); + // ...and what it accepts, it keeps accepting. + assertTrue(DartUri.tryParse("http://host:99999/") != null); + assertTrue(DartUri.tryParse("a%zzb") != null); + assertTrue(DartUri.tryParse("mailto:a@b") != null); + assertTrue(DartUri.tryParse("http://[::1]:80/x") != null); + assertTrue(DartUri.tryParse("") != null); + } + + // --- round 7 ---------------------------------------------------------------- + + @Test + public void intMapIterationFailsFastWhenTheMapGrows() { + final DartLongMap m = DartLongMap.ofLongs(1, 1); + assertThrows(ConcurrentModificationError.class, + () -> m.forEachDart((k, v) -> m.putLong(m.length() + 1, v)), + "a callback that adds keys must not be chased forever"); + DartLongMap updating = DartLongMap.ofLongs(1, 1, 2, 2); + updating.forEachDart((k, v) -> updating.putLong(k, v + 10)); + assertEquals(Long.valueOf(11), updating.idx(1), "a value update is not a structural change"); + } + + @Test + public void addAllMatchesKeysWithDartEquality() { + DartMap m = new DartMap(); + m.put(Long.valueOf(1), "a"); + java.util.Map more = new java.util.HashMap(); + more.put(Double.valueOf(1.0), "b"); + m.addAll(more); + assertEquals(1, m.size(), "{1: 'a'}.addAll({1.0: 'b'}) is {1: b}"); + assertEquals("b", m.get(Long.valueOf(1))); + } + + @Test + public void aStartBeyondIntDoesNotWrap() { + DartList l = DartList.of((Object) 1L); + assertEquals(-1, l.indexOfDart(1L, 4294967296L), "2^32 must not wrap to 0"); + assertEquals(-1, l.indexWhere(x -> true, 4294967296L)); + } + + @Test + public void dartsOwnAnswersForNegativeRepeatAndGenerate() { + // Both recorded from the Dart SDK, against review findings that expected throws: + // 'ab' * -1 is the empty string, and Iterable.generate(-1) is empty. + assertEquals("", DString.repeat("ab", -1)); + assertEquals(0, DartIterable.generate(-1, i -> i).length()); + } + + // --- round 8 ---------------------------------------------------------------- + + @Test + public void aLengthNoArrayCanHoldIsRefusedNotWrapped() { + // Dart runs out of memory for List.filled(2^32, ..); narrowed first, it was an + // empty list. + assertThrows(OutOfMemoryError.class, () -> DartLongList.filled(4294967296L, 0L)); + assertThrows(OutOfMemoryError.class, () -> DartList.filled(4294967296L, "x")); + } + + @Test + public void asMapIsALiveUnmodifiableView() { + DartList l = DartList.of((Object) 1L, 2L); + DartMap m = l.asMap(); + l.set(0, 9L); + assertEquals(9L, m.get(0L), "the view reads the list as it is now"); + assertEquals(2, m.size()); + assertThrows(UnsupportedError.class, () -> m.put(0L, 5L)); + assertEquals(java.util.Arrays.asList(0L, 1L), new java.util.ArrayList(m.keySet())); + } + + @Test + public void doubleParseFollowsDartsGrammar() { + // Each recorded from the Dart SDK. + assertEquals(null, DString.tryParseDouble("1d")); + assertEquals(null, DString.tryParseDouble("0x1.0p0")); + assertEquals(null, DString.tryParseDouble("1e")); + assertEquals(Double.valueOf(1500.0), DString.tryParseDouble(" 1.5e3 ")); + assertEquals(Double.valueOf(0.5), DString.tryParseDouble(".5")); + assertEquals(Double.valueOf(5.0), DString.tryParseDouble("5.")); + assertEquals(Double.valueOf(1.0), DString.tryParseDouble("+1")); + assertTrue(DString.tryParseDouble("NaN").isNaN()); + assertEquals(Double.valueOf(Double.NEGATIVE_INFINITY), DString.tryParseDouble("-Infinity")); + assertThrows(FormatException.class, () -> DString.parseDouble("1f")); + } + + @Test + public void anIntAndADoubleCompareExactly() { + // 2^53 + 1 widens onto 2^53; Dart's compareTo still orders them. + assertEquals(1L, DartComparable.compare(Long.valueOf(9007199254740993L), Double.valueOf(9007199254740992.0))); + assertEquals(-1L, DartComparable.compare(Double.valueOf(9007199254740992.0), Long.valueOf(9007199254740993L))); + assertEquals(0L, DartComparable.compare(Long.valueOf(1), Double.valueOf(1.0))); + assertEquals(1L, DartComparable.compare(Long.valueOf(0), Double.valueOf(-0.0))); + assertEquals(-1L, DartComparable.compare(Long.valueOf(1), Double.valueOf(Double.NaN))); + assertEquals(-1L, DartComparable.compare(Long.valueOf(1), Double.valueOf(1.5))); + assertEquals(1L, DartComparable.compare(Long.valueOf(Long.MAX_VALUE), Double.valueOf(9.2e18))); + assertEquals(-1L, DartComparable.compare(Long.valueOf(Long.MAX_VALUE), Double.valueOf(9.3e18))); + assertEquals(1L, DartComparable.compare(Long.valueOf(-3), Double.valueOf(Double.NEGATIVE_INFINITY))); + } + + @Test + public void dotAllLetsTheDotMatchANewline() { + RegExp all = new RegExp("a.b"); + all.dotAll(true); + assertTrue(all.hasMatch("a\nb")); + assertFalse(new RegExp("a.b").hasMatch("a\nb")); + } + + @Test + public void toRadixStringRefusesARadixOutsideTwoToThirtySix() { + assertEquals("ff", dart.runtime.DartRuntime.toRadixString(255, 16)); + assertThrows(RangeError.class, () -> dart.runtime.DartRuntime.toRadixString(255, 1)); + assertThrows(RangeError.class, () -> dart.runtime.DartRuntime.toRadixString(255, 37)); + assertThrows(RangeError.class, () -> dart.runtime.DartRuntime.toRadixString(255, 4294967312L), + "a radix that would wrap into range as a Java int"); + } + + @Test + public void minMaxAndPowKeepIntsInts() { + assertEquals(Long.valueOf(1), DartMath.minNum(Long.valueOf(1), Double.valueOf(2.5))); + assertEquals(Double.valueOf(2.5), DartMath.maxNum(Long.valueOf(1), Double.valueOf(2.5))); + assertEquals(Long.valueOf(8), DartMath.powNum(Long.valueOf(2), Long.valueOf(3))); + assertEquals(Double.valueOf(0.5), DartMath.powNum(Long.valueOf(2), Long.valueOf(-1))); + assertEquals(Double.valueOf(-0.0), DartMath.minNum(Double.valueOf(0.0), Double.valueOf(-0.0))); + assertEquals(Double.valueOf(0.0), DartMath.maxNum(Double.valueOf(-0.0), Double.valueOf(0.0))); + assertTrue(Double.isNaN(DartMath.minNum(Long.valueOf(1), Double.valueOf(Double.NaN)).doubleValue())); + } + + @Test + public void getRangeIsALiveViewThatNoticesALengthChange() { + DartList list = new DartList(); + list.add(1L); + list.add(2L); + list.add(3L); + DartIterable range = list.getRange(0, 2); + list.set(0, 9L); + assertEquals("[9, 2]", range.toList().toString()); + java.util.Iterator it = range.iterator(); + it.next(); + list.add(4L); + assertThrows(ConcurrentModificationError.class, it::next); + assertThrows(RangeError.class, () -> list.getRange(1, 9)); + } + + @Test + public void mapsFindValuesAndIntKeysByDartEquality() { + DartMap m = new DartMap(); + m.put("a", 1L); + assertTrue(m.containsValue(1.0)); + assertFalse(m.containsValue(1.5)); + DartLongMap ints = DartLongMap.ofLongs(0, 1, 2, 3); + assertTrue(ints.containsValue(1.0)); + assertEquals(Long.valueOf(3), ints.get(2.0)); + assertTrue(ints.containsKey(2.0)); + assertFalse(ints.containsKey(2.5)); + assertEquals(Long.valueOf(3), ints.remove(2.0)); + assertFalse(ints.containsKey(2L)); + } + + @Test + public void uriParseNormalizesItsComponents() { + assertEquals("https://x/a%20b", DartUri.parse("https://x/a b").toString()); + assertEquals("/a%20b", DartUri.parse("https://x/a b").path()); + assertEquals("https://x/a%2Fb~A%25zz%254", DartUri.parse("https://x/a%2fb%7e%41%zz%4").toString()); + assertEquals("https://x/p?q=a%20b#f%20g", DartUri.parse("https://x/p?q=a b#f g").toString()); + assertEquals("https://x/a/b?c%5Cd", DartUri.parse("https://x/a\\b?c\\d").toString()); + assertEquals("https://x/caf%C3%A9%F0%9F%98%80", DartUri.parse("https://x/caf\u00e9\ud83d\ude00").toString()); + assertEquals("https://u%20s@x/p", DartUri.parse("https://u s@x/p").toString()); + assertEquals("mailto:a%20b@x.com", DartUri.parse("mailto:a b@x.com").toString()); + assertEquals("https://x/!$&'()*+,;=:@-._~", DartUri.parse("https://x/!$&'()*+,;=:@-._~").toString()); + assertEquals("a b", DartUri.parse("https://x/a b").pathSegments().get(0)); + } + + @Test + public void dateTimeIsProlepticGregorian() { + DateTime d = DateTime.utc(1582, 10, 10, 0, 0, 0, 0, 0); + assertEquals(1582L, d.year()); + assertEquals(10L, d.month()); + assertEquals(10L, d.day()); + assertEquals(-12219724800000L, d.millisecondsSinceEpoch()); + assertEquals(7L, d.weekday()); + assertEquals("-0001-12-31 23:59:59.999Z", DateTime.utc(-1, 12, 31, 23, 59, 59, 999, 0).toString()); + assertEquals("2024-03-01 00:00:00.000Z", DateTime.utc(2024, 2, 30, 0, 0, 0, 0, 0).toString()); + // A local value round-trips its fields on both sides of the old cutover. + DateTime old = new DateTime(1582, 10, 10, 12, 30, 0, 0, 0); + assertEquals("1582-10-10 12:30:00.000", old.toString()); + DateTime modern = new DateTime(2024, 7, 1, 12, 30, 0, 0, 0); + assertEquals("2024-07-01 12:30:00.000", modern.toString()); + } + + @Test + public void byteDataRefusesALengthItCannotAllocate() { + assertThrows(OutOfMemoryError.class, () -> new dart.typed_data.ByteData(4294967296L)); + assertThrows(OutOfMemoryError.class, () -> new dart.typed_data.Uint8List(4294967296L)); + assertThrows(RangeError.class, () -> new dart.typed_data.Uint8List(-1)); + assertEquals(8L, new dart.typed_data.ByteData(8).lengthInBytes()); + } + + @Test + public void typedListFromChecksEachElementsType() { + java.util.List mixed = new java.util.ArrayList(); + mixed.add(Double.valueOf(1.9)); + assertThrows(TypeError.class, () -> DartLongList.fromLongs(mixed)); + java.util.List ints = new java.util.ArrayList(); + ints.add(Long.valueOf(1)); + assertThrows(TypeError.class, () -> DartDoubleList.fromDoubles(ints)); + assertEquals(1L, DartLongList.fromLongs(ints).getLong(0)); + } + + @Test + public void forEachRefusesALengthChange() { + final DartList list = new DartList(); + list.add(1L); + list.add(2L); + assertThrows(ConcurrentModificationError.class, () -> list.forEachDart(e -> list.add(3L))); + } + + @Test + public void queryParametersAreReadOnly() { + DartMap q = DartUri.parse("https://x/?a=1").queryParameters(); + assertEquals("1", q.get("a")); + assertThrows(UnsupportedError.class, () -> q.put("b", "2")); + assertThrows(UnsupportedError.class, () -> q.idxSet("b", "2")); + assertThrows(UnsupportedError.class, () -> q.remove("a")); + assertThrows(UnsupportedError.class, q::clear); + assertThrows(UnsupportedOperationException.class, () -> q.keySet().clear()); + assertEquals(1, q.size()); + } + + @Test + public void dateTimeRangesCompareByTheirEndpoints() { + DateTime a = DateTime.utc(2024, 1, 1, 0, 0, 0, 0, 0); + DateTime b = DateTime.utc(2024, 1, 9, 0, 0, 0, 0, 0); + assertEquals(new DateTimeRange(a, b), new DateTimeRange(a, b)); + assertEquals(new DateTimeRange(a, b).hashCode(), new DateTimeRange(a, b).hashCode()); + assertFalse(new DateTimeRange(a, b).equals(new DateTimeRange(a, a))); + } + + @Test + public void containsHonoursItsStartIndex() { + assertFalse(DString.contains("abc", "a", 1)); + assertTrue(DString.contains("abc", "c", 1)); + assertFalse(DString.contains("abc", new RegExp("a"), 1)); + assertThrows(RangeError.class, () -> DString.contains("abc", "a", 4)); + } + + @Test + public void hashAllHashesTheElements() { + DartList one = new DartList(); + one.add(1L); + one.add("x"); + DartList two = new DartList(); + two.add(1L); + two.add("x"); + assertEquals(dart.runtime.DartRuntime.hashAll(one), dart.runtime.DartRuntime.hashAll(two)); + } + + @Test + public void aMalformedRegExpThrowsFromItsConstructor() { + assertThrows(FormatException.class, () -> new RegExp("[")); + assertThrows(FormatException.class, () -> new RegExp("[", false, true, false, false)); + assertTrue(new RegExp("a+").hasMatch("caab")); + } + + @Test + public void iterableMixinThrowsDartErrors() { + dart.collection.IterableMixin empty = new dart.collection.IterableMixin() { + @Override + public dart.collection.Iterator iterator() { + return new dart.collection.Iterator() { + @Override + public boolean moveNext() { + return false; + } + + @Override + public Object current() { + return null; + } + }; + } + }; + assertThrows(StateError.class, empty::first); + assertThrows(StateError.class, empty::last); + assertThrows(StateError.class, empty::single); + assertThrows(RangeError.class, () -> empty.elementAt(0)); + } + + @Test + public void aCancelledHeadlessTimerEndsItsThread() throws Exception { + dart.async.Timer t = new dart.async.Timer(Duration.of(0, 1, 0, 0, 0, 0), null); + t.cancel(); + long deadline = System.currentTimeMillis() + 2000; + while (liveTimerThreads() > 0 && System.currentTimeMillis() < deadline) { + Thread.sleep(10); + } + assertEquals(0, liveTimerThreads(), "cancel ends the hour-long sleep instead of waiting it out"); + assertFalse(t.isActive()); + } + + private static int liveTimerThreads() { + int n = 0; + for (Thread th : Thread.getAllStackTraces().keySet()) { + if ("dart-timer".equals(th.getName()) && th.isAlive()) { + n++; + } + } + return n; + } + + @Test + public void aThrownValueIsCarriedAndCaughtAsItself() { + Object token = new Object(); + RuntimeException carried = dart.runtime.DartRuntime.asError(token); + assertTrue(carried instanceof DartThrown); + assertTrue(dart.runtime.DartRuntime.caught(carried) == token, "the same object comes back"); + assertEquals("boom", dart.runtime.DartRuntime.asError("boom").toString()); + assertFalse(dart.runtime.DartRuntime.isDartError(carried), "a thrown string is not an Error"); + assertFalse(dart.runtime.DartRuntime.isDartException(carried), "nor an Exception"); + assertTrue(dart.runtime.DartRuntime.isDartException(new FormatException("f"))); + assertTrue(dart.runtime.DartRuntime.isDartError(new StateError("s"))); + assertThrows(StackOverflowError.class, () -> dart.runtime.DartRuntime.asError(new StackOverflowError()), + "an Error is thrown as itself"); + } + + @Test + public void unmodifiableListsRefuseEveryWrite() { + DartList source = new DartList(); + source.add(1L); + DartList ro = DartList.unmodifiable(source); + assertThrows(UnsupportedError.class, () -> ro.set(0, 9L)); + assertThrows(UnsupportedError.class, () -> ro.add(2L)); + assertThrows(UnsupportedError.class, ro::clear); + DartList segments = DartUri.parse("https://x/a/b").pathSegments(); + assertThrows(UnsupportedError.class, () -> segments.add("c")); + assertEquals("a", segments.get(0)); + } + + @Test + public void anIntMapChangedDuringForEachThrowsDartsError() { + final DartLongMap m = DartLongMap.ofLongs(1, 1); + assertThrows(ConcurrentModificationError.class, () -> m.forEachDart((k, v) -> m.put(k + 10, v))); + } + + @Test + public void identicalTreatsNumbersAsValues() { + assertTrue(dart.runtime.DartRuntime.identical(Long.valueOf(100000), Long.valueOf(100000))); + assertTrue(dart.runtime.DartRuntime.identical(Double.valueOf(0.5), Double.valueOf(0.5))); + assertFalse(dart.runtime.DartRuntime.identical(Double.valueOf(0.0), Double.valueOf(-0.0))); + assertFalse(dart.runtime.DartRuntime.identical(new StringBuilder("a").toString(), "a")); + } + + @Test + public void hashesAgreeWithDartEqualityAcrossIntAndDouble() { + assertEquals(dart.runtime.DartRuntime.hashOf(Long.valueOf(1)), dart.runtime.DartRuntime.hashOf(Double.valueOf(1.0))); + assertEquals(dart.runtime.DartRuntime.hashOf(Double.valueOf(0.0)), dart.runtime.DartRuntime.hashOf(Double.valueOf(-0.0))); + assertEquals(dart.runtime.DartRuntime.hash(Long.valueOf(2), "x"), dart.runtime.DartRuntime.hash(Double.valueOf(2.0), "x")); + } + + @Test + public void typedCopiesCheckTheirElements() { + java.util.List mixed = new java.util.ArrayList(); + mixed.add(Long.valueOf(1)); + assertThrows(TypeError.class, () -> DartList.fromChecked(mixed, String.class, false, true)); + java.util.List strings = new java.util.ArrayList(); + strings.add("a"); + strings.add(null); + assertThrows(TypeError.class, () -> DartList.fromChecked(strings, String.class, false, true), + "null is not a String"); + assertEquals(2, DartList.fromChecked(strings, String.class, true, true).size()); + java.util.List notInts = new java.util.ArrayList(); + notInts.add("x"); + assertThrows(TypeError.class, () -> dart.typed_data.Uint8List.fromList(notInts)); + } + + @Test + public void byteDataOffsetsNearTheLimitAreRangeErrors() { + dart.typed_data.ByteData bd = new dart.typed_data.ByteData(8); + assertThrows(RangeError.class, () -> bd.getInt32(Long.MAX_VALUE)); + assertThrows(RangeError.class, () -> bd.getInt32(5)); + } + + @Test + public void intParsingTrimsDartWhitespace() { + assertEquals(Long.valueOf(123), DString.tryParseInt("\u00A0123\u00A0")); + } + + @Test + @SuppressWarnings({"UnnecessaryBoxing", "removal"}) + public void identityCollectionsTreatEqualIntsAsOneKey() { + Long a = new Long(1000); + Long b = new Long(1000); + assertFalse(a == b, "two separate boxes"); + DartIdentityMap m = new DartIdentityMap(); + m.put(a, "x"); + m.put(b, "y"); + assertEquals(1, m.size()); + assertEquals("y", m.get(a)); + DartIdentitySet set = new DartIdentitySet(); + set.add(a); + set.add(b); + assertEquals(1, set.size()); + } + + @Test + public void aSameSizeSwapDuringForEachIsStillAModification() { + final DartMap m = new DartMap(); + m.put(1L, "a"); + assertThrows(ConcurrentModificationError.class, () -> m.forEachDart((k, v) -> { + m.remove(k); + m.put(2L, "b"); + })); + final DartSet s = new DartSet(); + s.add(1L); + assertThrows(ConcurrentModificationError.class, () -> s.forEachDart(e -> { + s.remove(e); + s.add(2L); + })); + final DartMap updating = new DartMap(); + updating.put(1L, "a"); + updating.forEachDart((k, v) -> updating.put(k, "b")); + assertEquals("b", updating.get(1L), "updating a value is not structural"); + } +} diff --git a/maven/dart-runtime/src/test/java/dart/core/DartLongMapTest.java b/maven/dart-runtime/src/test/java/dart/core/DartLongMapTest.java new file mode 100644 index 00000000000..94a08c0ce8a --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/core/DartLongMapTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Correctness coverage for the primitive long->long map (edge cases the benchmark never hits). */ +public class DartLongMapTest { + + @Test + public void putGetOverwrite() { + DartLongMap m = new DartLongMap(); + m.putLong(1, 10); + m.putLong(2, 20); + assertEquals(10, m.getLongOr(1, -1)); + assertEquals(20, m.getLongOr(2, -1)); + assertEquals(-1, m.getLongOr(99, -1)); + assertEquals(2, m.length()); + m.putLong(1, 111); // overwrite must not grow size + assertEquals(111, m.getLongOr(1, -1)); + assertEquals(2, m.length()); + } + + @Test + public void bareIndexReturnsNullWhenAbsent() { + DartLongMap m = new DartLongMap(); + m.putLong(5, 50); + assertEquals(Long.valueOf(50), m.idxLong(5)); + assertNull(m.idxLong(6)); + } + + @Test + public void containsAndRemove() { + DartLongMap m = new DartLongMap(); + m.putLong(7, 70); + assertTrue(m.containsKeyLong(7)); + assertFalse(m.containsKeyLong(8)); + assertEquals(70, m.removeLong(7)); + assertFalse(m.containsKeyLong(7)); + assertEquals(0, m.length()); + // re-add after remove (tombstone slot must be reusable) + m.putLong(7, 700); + assertEquals(700, m.getLongOr(7, -1)); + assertEquals(1, m.length()); + } + + @Test + public void insertionOrderPreserved() { + DartLongMap m = new DartLongMap(); + long[] order = {50, 3, 9, 1, 42, 7, 100, 2}; + for (long k : order) { + m.putLong(k, k * 2); + } + List keys = new ArrayList(); + for (Long k : m.keySet()) { + keys.add(k); + } + assertEquals(order.length, keys.size()); + for (int i = 0; i < order.length; i++) { + assertEquals(Long.valueOf(order[i]), keys.get(i), "key order at " + i); + } + // removing the middle key keeps the rest in order + m.removeLong(9); + keys.clear(); + for (Long k : m.keySet()) { + keys.add(k); + } + assertEquals(order.length - 1, keys.size()); + assertFalse(keys.contains(9L)); + assertEquals(Long.valueOf(50), keys.get(0)); + assertEquals(Long.valueOf(2), keys.get(keys.size() - 1)); + } + + @Test + public void growthAndReadbackManyEntries() { + DartLongMap m = new DartLongMap(); + int n = 5000; + for (int i = 0; i < n; i++) { + m.putLong(i, (long) i * 3 + 1); + } + assertEquals(n, m.length()); + long sum = 0; + for (int i = 0; i < n; i++) { + sum += m.getLongOr(i, 0); + } + long expect = 0; + for (int i = 0; i < n; i++) { + expect += (long) i * 3 + 1; + } + assertEquals(expect, sum); + } + + @Test + public void negativeAndZeroKeys() { + DartLongMap m = new DartLongMap(); + m.putLong(0, 100); + m.putLong(-1, 200); + m.putLong(Long.MIN_VALUE, 300); + m.putLong(Long.MAX_VALUE, 400); + assertEquals(100, m.getLongOr(0, -999)); + assertEquals(200, m.getLongOr(-1, -999)); + assertEquals(300, m.getLongOr(Long.MIN_VALUE, -999)); + assertEquals(400, m.getLongOr(Long.MAX_VALUE, -999)); + } + + @Test + public void mapInteropAndClear() { + DartLongMap m = new DartLongMap(); + m.putLong(1, 10); + m.putLong(2, 20); + assertEquals(Long.valueOf(10), m.get(1L)); + assertEquals(2, m.size()); + int entries = 0; + for (java.util.Map.Entry e : m.entrySet()) { + entries++; + assertEquals(Long.valueOf(m.getLongOr(e.getKey(), -1)), e.getValue()); + } + assertEquals(2, entries); + m.clear(); + assertEquals(0, m.length()); + assertFalse(m.containsKeyLong(1)); + m.putLong(3, 30); + assertEquals(30, m.getLongOr(3, -1)); + } +} diff --git a/maven/dart-runtime/src/test/java/dart/runtime/DartRuntimeTest.java b/maven/dart-runtime/src/test/java/dart/runtime/DartRuntimeTest.java new file mode 100644 index 00000000000..c6e598956d0 --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/runtime/DartRuntimeTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.runtime; + +import dart.core.TypeError; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DartRuntimeTest { + + @Test + public void modIsNonNegativeLikeDart() { + assertEquals(2, DartRuntime.mod(-3, 5)); + assertEquals(3, DartRuntime.mod(3, 5)); + assertEquals(2, DartRuntime.mod(-3, -5)); + assertEquals(1.5, DartRuntime.mod(-3.5, 5.0), 1e-9); + } + + @Test + public void tdivTruncatesTowardZero() { + assertEquals(-2, DartRuntime.tdiv(-7, 3)); + assertEquals(2, DartRuntime.tdiv(7, 3)); + assertEquals(-2, DartRuntime.tdiv(-7.5, 3.0)); + } + + @Test + public void doubleStrMatchesDartFormatting() { + assertEquals("1.0", DartRuntime.doubleStr(1.0)); + assertEquals("-1.0", DartRuntime.doubleStr(-1.0)); + assertEquals("0.0", DartRuntime.doubleStr(0.0)); + assertEquals("-0.0", DartRuntime.doubleStr(-0.0)); + assertEquals("2.5", DartRuntime.doubleStr(2.5)); + assertEquals("NaN", DartRuntime.doubleStr(Double.NaN)); + assertEquals("Infinity", DartRuntime.doubleStr(Double.POSITIVE_INFINITY)); + assertEquals("-Infinity", DartRuntime.doubleStr(Double.NEGATIVE_INFINITY)); + } + + @Test + public void strHandlesNullAndDoubles() { + assertEquals("null", DartRuntime.str((Object) null)); + assertEquals("3.0", DartRuntime.str((Object) Double.valueOf(3))); + assertEquals("7", DartRuntime.str(7L)); + assertEquals("true", DartRuntime.str(true)); + } + + @Test + public void nnThrowsDartTypeError() { + assertEquals("x", DartRuntime.nn("x")); + assertThrows(TypeError.class, () -> DartRuntime.nn(null)); + } + + @Test + public void eqIsNullSafe() { + assertTrue(DartRuntime.eq(null, null)); + assertFalse(DartRuntime.eq(null, "a")); + assertFalse(DartRuntime.eq("a", null)); + assertTrue(DartRuntime.eq("a", "a")); + } + + @Test + public void printSinkCapturesOutput() { + StringBuilder sb = new StringBuilder(); + DartRuntime.setPrintSink(s -> sb.append(s).append('\n')); + try { + DartRuntime.print("hello"); + DartRuntime.print(42L); + DartRuntime.print(1.0); + } finally { + DartRuntime.setPrintSink(null); + } + assertEquals("hello\n42\n1.0\n", sb.toString()); + } + + // --- == across int and double --------------------------------------------- + + @Test + public void eqComparesBoxedNumbersByValueAcrossIntAndDouble() { + assertTrue(DartRuntime.eq(Long.valueOf(1), Double.valueOf(1.0)), "1 == 1.0"); + assertTrue(DartRuntime.eq(Double.valueOf(1.0), Long.valueOf(1))); + assertTrue(DartRuntime.eq(Double.valueOf(0.0), Double.valueOf(-0.0)), "0.0 == -0.0"); + assertFalse(DartRuntime.eq(Double.valueOf(Double.NaN), Double.valueOf(Double.NaN)), "NaN is never == itself"); + // Recorded from the Dart 3.9 VM: == converts the int to double first. + assertTrue(DartRuntime.eq(Long.valueOf(9007199254740993L), Double.valueOf(9007199254740992.0)), + "2^53 + 1 == 2^53.0 on the VM"); + assertTrue(DartRuntime.eq(Long.valueOf(Long.MAX_VALUE), Double.valueOf(9.223372036854775807E18)), + "the largest int == 2^63 on the VM"); + assertFalse(DartRuntime.eq(Long.valueOf(1), Double.valueOf(1.5))); + dart.core.DartList nums = new dart.core.DartList(); + nums.add(Long.valueOf(1)); + assertTrue(nums.contains(Double.valueOf(1.0)), "List.contains(1.0) finds the int 1"); + } + + // --- toStringAsFixed ------------------------------------------------------ + + @Test + public void toStringAsFixedUsesTheExactValueAcrossTheWholeRange() { + assertEquals("1.00000000000000000000", DartRuntime.toStringAsFixed(1.0, 20), + "20 digits no longer overflows a long"); + assertEquals("0.10000000000000000555", DartRuntime.toStringAsFixed(0.1, 20), "the double's exact digits"); + assertEquals("1.00", DartRuntime.toStringAsFixed(1.005, 2), "1.005 is really 1.00499..."); + assertEquals("4321.123", DartRuntime.toStringAsFixed(4321.12345678, 3)); + assertEquals("4321.12346", DartRuntime.toStringAsFixed(4321.12345678, 5)); + assertEquals("123456789012345.000", DartRuntime.toStringAsFixed(123456789012345.0, 3)); + assertEquals("10000000000000000.0000", DartRuntime.toStringAsFixed(1e16, 4)); + assertEquals("3", DartRuntime.toStringAsFixed(2.5, 0), "an exact tie rounds up"); + assertEquals("-1.5", DartRuntime.toStringAsFixed(-1.5, 1)); + assertEquals("-0.00", DartRuntime.toStringAsFixed(-0.001, 2)); + assertEquals("0.00000000000000000000", DartRuntime.toStringAsFixed(Double.MIN_VALUE, 20)); + assertEquals("1e+21", DartRuntime.toStringAsFixed(1e21, 2), "1e21 and beyond print as toString"); + assertEquals("999999999999999868928.00", DartRuntime.toStringAsFixed(999999999999999900000.0, 2)); + assertThrows(dart.core.RangeError.class, () -> DartRuntime.toStringAsFixed(1.0, 21)); + assertThrows(dart.core.RangeError.class, () -> DartRuntime.toStringAsFixed(1.0, -1)); + } + + // --- shifts and rounding --------------------------------------------------- + + @Test + public void shiftsFollowDartsCountRules() { + assertEquals(0, DartRuntime.shr(1, 64)); + assertEquals(-1, DartRuntime.shr(-8, 64)); + assertEquals(0, DartRuntime.shl(1, 64)); + assertEquals(0, DartRuntime.ushr(-1, 64)); + assertEquals(8, DartRuntime.shl(1, 3)); + assertThrows(dart.core.ArgumentError.class, () -> DartRuntime.shl(1, -1)); + } + + @Test + public void roundingGoesHalfAwayFromZero() { + assertEquals(-2, DartRuntime.round(-1.5)); + assertEquals(3, DartRuntime.round(2.5)); + assertEquals(0, DartRuntime.round(0.49999999999999994), "not floor(d + 0.5)"); + assertEquals(-2.0, DartRuntime.roundToDouble(-1.5), 0.0); + assertEquals("-0.0", DartRuntime.doubleStr(DartRuntime.roundToDouble(-0.4)), "the sign of zero is kept"); + assertThrows(dart.core.UnsupportedError.class, () -> DartRuntime.round(Double.NaN)); + } + + @Test + public void dynamicShiftsFollowTheTypedRules() { + assertEquals(Long.valueOf(0), DartRuntime.dynBinary("<<", 1L, 64L)); + assertThrows(dart.core.ArgumentError.class, () -> DartRuntime.dynBinary("<<", 1L, -1L)); + } +} diff --git a/maven/dart-transpiler/pom.xml b/maven/dart-transpiler/pom.xml new file mode 100644 index 00000000000..6723940c292 --- /dev/null +++ b/maven/dart-transpiler/pom.xml @@ -0,0 +1,159 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-dart-transpiler + 8.0-SNAPSHOT + jar + codenameone-dart-transpiler + + Build-time Dart-to-Java transpiler used by the Codename One Maven + plugin to compile src/main/flutter/**.dart into Java source targeting + the codenameone-flutter-runtime and codenameone-dart-runtime modules. + Pure Java: Dart is parsed with an ANTLR4 front end (vendored + spec-derived grammar); no Dart SDK is required at build time. The + transpiler itself runs on Java 8 (it executes inside the Maven + plugin); the Java source it EMITS is Java 17. + + + + UTF-8 + 1.8 + 1.8 + + 4.9.3 + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + org.antlr + antlr4-maven-plugin + ${antlr.version} + + false + false + + + + + antlr4 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + org.antlr + com.codename1.dart.shaded.antlr + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + + org.antlr + antlr4-runtime + ${antlr.version} + + + org.junit.jupiter + junit-jupiter + test + + + + + + + + transpiler-tests + + [17,) + + + + com.codenameone + codenameone-flutter-runtime + ${project.version} + test + + + + + + + transpiler-tests-need-jdk17 + + (,17) + + + true + + + + diff --git a/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Lexer.g4 b/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Lexer.g4 new file mode 100644 index 00000000000..283f814bd6e --- /dev/null +++ b/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Lexer.g4 @@ -0,0 +1,245 @@ +/* Generated Mon, Jun 13, 2022 8:11:58 AM EST + * + * Copyright (c) 2022, 2023 Ken Domino + * Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + * + * This grammar is generated from the CFG contained in: + * https://github.com/dart-lang/language/blob/70eb85cf9a6606a9da0de824a5d55fd06de1287f/specification/dartLangSpec.tex + * + * The bash script used to scrape and the refactor the gramamr is here: + * https://github.com/kaby76/ScrapeDartSpec/blob/master/refactor.sh + * + * Note: the CFG in the Specification is in development, and is for approximately + * Dart version 2.15. The Specification is not up-to-date vis-a-vis the actual + * compiler code, located here: + * https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared/lib/src/parser + * Some of the refactorings that are applied are to bring the code into a working + * Antlr4 parser. Other refactorings replace some of the rules in the Spec because + * the Spec is incorrect, or incomplete. + * + * This grammar has been checked against a large subset (~370 Dart files) of the Dart SDK: + * https://github.com/dart-lang/sdk/tree/main/sdk/lib + * A copy of the SDK is provided in the examples for regression testing. + */ + +// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine +// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true + +lexer grammar Dart2Lexer; + +options { + superClass = Dart2LexerBase; +} + +// Insert here @header for C++ lexer. + +A : '&'; +AA : '&&'; +AE : '&='; +AT : '@'; +C : ','; +CB : ']'; +CBC : '}'; +CIR : '^'; +CIRE : '^='; +CO : ':'; +CP : ')'; +D : '.'; +DD : '..'; +DDD : '...'; +DDDQ : '...?'; +EE : '=='; +EG : '=>'; +EQ : '='; +GT : '>'; +LT : '<'; +LTE : '<='; +LTLT : '<<'; +LTLTE : '<<='; +ME : '-='; +MINUS : '-'; +MM : '--'; +NE : '!='; +NOT : '!'; +OB : '['; +OBC : '{'; +OP : '('; +P : '|'; +PC : '%'; +PE : '%='; +PL : '+'; +PLE : '+='; +PLPL : '++'; +PO : '#'; +POE : '|='; +PP : '||'; +QU : '?'; +QUD : '?.'; +QUDD : '?..'; +QUQU : '??'; +QUQUEQ : '??='; +SC : ';'; +SE : '/='; +SL : '/'; +SQS : '~/'; +SQSE : '~/='; +SQUIG : '~'; +ST : '*'; +STE : '*='; +ABSTRACT_ : 'abstract'; +AS_ : 'as'; +ASSERT_ : 'assert'; +ASYNC_ : 'async'; +AWAIT_ : 'await'; +BASE_ : 'base'; +BREAK_ : 'break'; +CASE_ : 'case'; +CATCH_ : 'catch'; +CLASS_ : 'class'; +CONST_ : 'const'; +CONTINUE_ : 'continue'; +COVARIANT_ : 'covariant'; +DEFAULT_ : 'default'; +DEFERRED_ : 'deferred'; +DO_ : 'do'; +DYNAMIC_ : 'dynamic'; +ELSE_ : 'else'; +ENUM_ : 'enum'; +EXPORT_ : 'export'; +EXTENDS_ : 'extends'; +EXTENSION_ : 'extension'; +EXTERNAL_ : 'external'; +FACTORY_ : 'factory'; +FALSE_ : 'false'; +FINAL_ : 'final'; +FINALLY_ : 'finally'; +FOR_ : 'for'; +FUNCTION_ : 'Function'; +GET_ : 'get'; +GTILDE_ : 'gtilde'; +HIDE_ : 'hide'; +IF_ : 'if'; +IMPLEMENTS_ : 'implements'; +IMPORT_ : 'import'; +IN_ : 'in'; +INTERFACE_ : 'interface'; +IS_ : 'is'; +LATE_ : 'late'; +LET_ : 'let'; +LIBRARY_ : 'library'; +MIXIN_ : 'mixin'; +NATIVE_ : 'native'; +NEW_ : 'new'; +NULL_ : 'null'; +OF_ : 'of'; +ON_ : 'on'; +OPERATOR_ : 'operator'; +PART_ : 'part'; +REQUIRED_ : 'required'; +RETHROW_ : 'rethrow'; +RETURN_ : 'return'; +SET_ : 'set'; +SHOW_ : 'show'; +STATIC_ : 'static'; +SUPER_ : 'super'; +SEALED_ : 'sealed'; +SWITCH_ : 'switch'; +SYNC_ : 'sync'; +THIS_ : 'this'; +THROW_ : 'throw'; +WHEN_ : 'when'; +TRUE_ : 'true'; +TRY_ : 'try'; +TYPEDEF_ : 'typedef'; +VAR_ : 'var'; +VOID_ : 'void'; +WHILE_ : 'while'; +WITH_ : 'with'; +YIELD_ : 'yield'; +NUMBER : DIGIT+ ( '.' DIGIT+)? EXPONENT? | '.' DIGIT+ EXPONENT?; +HEX_NUMBER : '0x' HEX_DIGIT+ | '0X' HEX_DIGIT+; +SingleLineString: + StringDQ + | StringSQ + | 'r\'' ~('\'' | '\n' | '\r')* '\'' + | 'r"' ~('"' | '\n' | '\r')* '"' +; +MultiLineString: + '"""' StringContentTDQ*? '"""' + | '\'\'\'' StringContentTSQ*? '\'\'\'' + | 'r"""' (~'"' | '"' ~'"' | '""' ~'"')* '"""' + | 'r\'\'\'' (~'\'' | '\'' ~'\'' | '\'\'' ~'\'')* '\'\'\'' +; +IDENTIFIER : IDENTIFIER_START IDENTIFIER_PART*; +WHITESPACE : ( '\t' | ' ' | NEWLINE)+ -> skip; +SINGLE_LINE_COMMENT : '//' ~[\r\n]* -> skip; +MULTI_LINE_COMMENT : '/*' ( MULTI_LINE_COMMENT | .)*? '*/' -> skip; +fragment EXPONENT : ( 'e' | 'E') ( '+' | '-')? DIGIT+; +fragment HEX_DIGIT : 'a' .. 'f' | 'A' .. 'F' | DIGIT; +fragment StringDQ : '"' StringContentDQ*? '"'; +fragment StringContentDQ: + ~('\\' | '"' | '\n' | '\r' | '$') + | '\\' ~('\n' | '\r') + | StringDQ + | '${' StringContentDQ*? '}' + | '$' { this.CheckNotOpenBrace() }? +; +fragment StringSQ: '\'' StringContentSQ*? '\''; +fragment StringContentSQ: + ~('\\' | '\'' | '\n' | '\r' | '$') + | '\\' ~('\n' | '\r') + | StringSQ + | '${' StringContentSQ*? '}' + | '$' { this.CheckNotOpenBrace() }? +; +fragment StringContentTDQ : ~('\\' | '"') | '"' ~'"' | '""' ~'"'; +fragment StringContentTSQ : '\'' ~'\'' | '\'\'' ~'\'' | .; +fragment ESCAPE_SEQUENCE: + '\n' + | '\r' + | '\\f' + | '\\b' + | '\t' + | '\\v' + | '\\x' HEX_DIGIT HEX_DIGIT + | '\\u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT + | '\\u{' HEX_DIGIT_SEQUENCE '}' +; +fragment HEX_DIGIT_SEQUENCE : HEX_DIGIT HEX_DIGIT? HEX_DIGIT? HEX_DIGIT? HEX_DIGIT? HEX_DIGIT?; +fragment NEWLINE : '\n' | '\r' | '\r\n'; +fragment BUILT_IN_IDENTIFIER: + 'abstract' + | 'as' + | 'covariant' + | 'deferred' + | 'dynamic' + | 'export' + | 'external' + | 'extension' + | 'factory' + | 'Function' + | 'get' + | 'implements' + | 'import' + | 'interface' + | 'late' + | 'library' + | 'mixin' + | 'operator' + | 'part' + | 'required' + | 'set' + | 'static' + | 'typedef' +; +fragment OTHER_IDENTIFIER : 'async' | 'hide' | 'of' | 'on' | 'show' | 'sync' | 'await' | 'yield'; +fragment IDENTIFIER_NO_DOLLAR : IDENTIFIER_START_NO_DOLLAR IDENTIFIER_PART_NO_DOLLAR*; +fragment IDENTIFIER_START_NO_DOLLAR : LETTER | '_'; +fragment IDENTIFIER_PART_NO_DOLLAR : IDENTIFIER_START_NO_DOLLAR | DIGIT; +fragment IDENTIFIER_START : IDENTIFIER_START_NO_DOLLAR | '$'; +fragment IDENTIFIER_PART : IDENTIFIER_START | DIGIT; +fragment LETTER : 'a' .. 'z' | 'A' .. 'Z'; +fragment DIGIT : '0' .. '9'; \ No newline at end of file diff --git a/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Parser.g4 b/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Parser.g4 new file mode 100644 index 00000000000..fa9e4a68f84 --- /dev/null +++ b/maven/dart-transpiler/src/main/antlr4/com/codename1/dart/transpiler/parser/Dart2Parser.g4 @@ -0,0 +1,1238 @@ +/* Generated Mon, Jun 13, 2022 8:11:58 AM EST + * + * Copyright (c) 2022, 2023 Ken Domino + * Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + * + * This grammar is generated from the CFG contained in: + * https://github.com/dart-lang/language/blob/70eb85cf9a6606a9da0de824a5d55fd06de1287f/specification/dartLangSpec.tex + * + * The bash script used to scrape and the refactor the gramamr is here: + * https://github.com/kaby76/ScrapeDartSpec/blob/master/refactor.sh + * + * Note: the CFG in the Specification is in development, and is for approximately + * Dart version 2.15. The Specification is not up-to-date vis-a-vis the actual + * compiler code, located here: + * https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared/lib/src/parser + * Some of the refactorings that are applied are to bring the code into a working + * Antlr4 parser. Other refactorings replace some of the rules in the Spec because + * the Spec is incorrect, or incomplete. + * + * This grammar has been checked against a large subset (~370 Dart files) of the Dart SDK: + * https://github.com/dart-lang/sdk/tree/main/sdk/lib + * A copy of the SDK is provided in the examples for regression testing. + */ + +// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging + +parser grammar Dart2Parser; + +options { + tokenVocab = Dart2Lexer; +} + +additiveExpression + : multiplicativeExpression (additiveOperator multiplicativeExpression)* + | SUPER_ ( additiveOperator multiplicativeExpression)+ + ; + +additiveOperator + : PL + | MINUS + ; + +argumentList + : namedArgument (C namedArgument)* + | expressionList ( C namedArgument)* + ; + +argumentPart + : typeArguments? arguments + ; + +arguments + : OP (argumentList C?)? CP + ; + +asOperator + : AS_ + ; + +assertion + : ASSERT_ OP expr (C expr)? C? CP + ; + +assertStatement + : assertion SC + ; + +assignableExpression + : primary assignableSelectorPart + | SUPER_ unconditionalAssignableSelector + | identifier + ; + +assignableSelector + : unconditionalAssignableSelector + | QUD identifier + | QU OB expr CB + ; + +assignableSelectorPart + : selector* assignableSelector + ; + +assignmentOperator + : EQ + | compoundAssignmentOperator + ; + +awaitExpression + : AWAIT_ unaryExpression + ; + +binaryOperator + : multiplicativeOperator + | additiveOperator + | shiftOperator + | relationalOperator + | EE + | bitwiseOperator + ; + +bitwiseAndExpression + : shiftExpression (A shiftExpression)* + | SUPER_ ( A shiftExpression)+ + ; + +bitwiseOperator + : A + | CIR + | P + ; + +bitwiseOrExpression + : bitwiseXorExpression (P bitwiseXorExpression)* + | SUPER_ ( P bitwiseXorExpression)+ + ; + +bitwiseXorExpression + : bitwiseAndExpression (CIR bitwiseAndExpression)* + | SUPER_ ( CIR bitwiseAndExpression)+ + ; + +block + : OBC statements CBC + ; + +booleanLiteral + : TRUE_ + | FALSE_ + ; + +breakStatement + : BREAK_ identifier? SC + ; + +cascade + : cascade DD cascadeSection + | conditionalExpression ( QUDD | DD) cascadeSection + ; + +cascadeAssignment + : assignmentOperator expressionWithoutCascade + ; + +cascadeSection + : cascadeSelector cascadeSectionTail + ; + +cascadeSectionTail + : cascadeAssignment + | selector* ( assignableSelector cascadeAssignment)? + ; + +cascadeSelector + : OB expr CB + | identifier + ; + +catchPart + : CATCH_ OP identifier (C identifier)? CP + ; + +classDeclaration + : classModifiers MIXIN_? CLASS_ typeIdentifier typeParameters? superclass? interfaces? OBC ( + metadata classMemberDeclaration + )* CBC + | classModifiers MIXIN_? CLASS_ mixinApplicationClass + ; + +// Dart 3 class modifiers — CN1 addition (spec: abstract/base/interface/final/sealed). +classModifiers + : (ABSTRACT_ | BASE_ | INTERFACE_ | FINAL_ | SEALED_)* + ; + +classMemberDeclaration + : declaration SC + | methodSignature functionBody + ; + +combinator + : SHOW_ identifierList + | HIDE_ identifierList + ; + +compilationUnit + : (libraryDeclaration | partDeclaration | expr | statement) EOF + ; + +compoundAssignmentOperator + : STE + | SE + | SQSE + | PE + | PLE + | ME + | LTLTE + | GT GT GT EQ + | GT GT EQ + | AE + | CIRE + | POE + | QUQUEQ + ; + +conditionalExpression + : ifNullExpression (QU expressionWithoutCascade CO expressionWithoutCascade)? + ; + +configurableUri + : uri configurationUri* + ; + +configurationUri + : IF_ OP uriTest CP uri + ; + +constantConstructorSignature + : CONST_ constructorName formalParameterList + ; + +constObjectExpression + : CONST_ constructorDesignation arguments + ; + +constructorDesignation + : typeIdentifier + | qualifiedName + | typeName typeArguments ( D identifier)? + ; + +constructorInvocation + : typeName typeArguments D identifier arguments + ; + +constructorName + : typeIdentifier (D identifier)? + ; + +constructorSignature + : constructorName formalParameterList + ; + +continueStatement + : CONTINUE_ identifier? SC + ; + +declaration + : ABSTRACT_? ( + EXTERNAL_ factoryConstructorSignature + | EXTERNAL_ constantConstructorSignature + | EXTERNAL_ constructorSignature + | ( EXTERNAL_ STATIC_?)? getterSignature + | ( EXTERNAL_ STATIC_?)? setterSignature + | ( EXTERNAL_ STATIC_?)? functionSignature + | EXTERNAL_? operatorSignature + | STATIC_ CONST_ type? staticFinalDeclarationList + | STATIC_ FINAL_ type? staticFinalDeclarationList + | STATIC_ LATE_ FINAL_ type? initializedIdentifierList + | STATIC_ LATE_? varOrType initializedIdentifierList + | COVARIANT_ LATE_ FINAL_ type? identifierList + | COVARIANT_ LATE_? varOrType initializedIdentifierList + | LATE_? FINAL_ type? initializedIdentifierList + | LATE_? varOrType initializedIdentifierList + | redirectingFactoryConstructorSignature + | constantConstructorSignature ( redirection | initializers)? + | constructorSignature ( redirection | initializers)? + ) + ; + +declaredIdentifier + : COVARIANT_? finalConstVarOrType identifier + ; + +defaultCase + : label* DEFAULT_ CO statements + ; + +defaultFormalParameter + : normalFormalParameter (EQ expr)? + ; + +defaultNamedParameter + : metadata REQUIRED_? normalFormalParameterNoMetadata (( EQ | CO) expr)? + ; + +doStatement + : DO_ statement WHILE_ OP expr CP SC + ; + +dottedIdentifierList + : identifier (D identifier)* + ; + +element + : expressionElement + | mapElement + | spreadElement + | ifElement + | forElement + ; + +elements + : element (C element)* C? + ; + +// Dart 2.17 enhanced enums: constants may carry arguments and the body may +// declare members after a `;` (getters/methods/fields/const constructors). +enumEntry + : metadata identifier (typeArguments? arguments)? + | metadata identifier D identifier arguments + ; + +enumType + : ENUM_ identifier typeParameters? mixins? interfaces? OBC + enumEntry (C enumEntry)* C? (SC (metadata classMemberDeclaration)*)? CBC + ; + +equalityExpression + : relationalExpression (equalityOperator relationalExpression)? + | SUPER_ equalityOperator relationalExpression + ; + +equalityOperator + : EE + | NE + ; + +expr + : assignableExpression assignmentOperator expr + | conditionalExpression + | cascade + | throwExpression + ; + +expressionElement + : expr + ; + +expressionList + : expr (C expr)* + ; + +expressionStatement + : expr? SC + ; + +expressionWithoutCascade + : assignableExpression assignmentOperator expressionWithoutCascade + | conditionalExpression + | throwExpressionWithoutCascade + ; + +extensionDeclaration + : EXTENSION_ identifier? typeParameters? ON_ type OBC (metadata classMemberDeclaration)* CBC + ; + +factoryConstructorSignature + : CONST_? FACTORY_ constructorName formalParameterList + ; + +fieldFormalParameter + : finalConstVarOrType? THIS_ D identifier (formalParameterPart QU?)? + ; + +// Dart 2.17 super parameters (e.g. `const MyApp({super.key});`) — CN1 addition. +superFormalParameter + : finalConstVarOrType? SUPER_ D identifier (formalParameterPart QU?)? + ; + +fieldInitializer + : (THIS_ D)? identifier EQ initializerExpression + ; + +finalConstVarOrType + : LATE_? FINAL_ type? + | CONST_ type? + | LATE_? varOrType + ; + +finallyPart + : FINALLY_ block + ; + +forElement + : AWAIT_? FOR_ OP forLoopParts CP element + ; + +forInitializerStatement + : localVariableDeclaration + | expr? SC + ; + +forLoopParts + : forInitializerStatement expr? SC expressionList? + | metadata declaredIdentifier IN_ expr + | metadata (FINAL_ | VAR_) pattern IN_ expr + | identifier IN_ expr + ; + +formalParameterList + : OP CP + | OP normalFormalParameters C? CP + | OP normalFormalParameters C optionalOrNamedFormalParameters CP + | OP optionalOrNamedFormalParameters CP + ; + +formalParameterPart + : typeParameters? formalParameterList + ; + +forStatement + : AWAIT_? FOR_ OP forLoopParts CP statement + ; + +functionBody + : NATIVE_ stringLiteral? SC + | ASYNC_? EG expr SC + | ( ASYNC_ ST? | SYNC_ ST)? block + ; + +functionExpression + : formalParameterPart functionExpressionBody + ; + +functionExpressionBody + : ASYNC_? EG expr + | ( ASYNC_ ST? | SYNC_ ST)? block + ; + +functionFormalParameter + : COVARIANT_? type? identifier formalParameterPart QU? + ; + +functionPrefix + : type? identifier + ; + +functionSignature + : type? identifier formalParameterPart + ; + +functionType + : functionTypeTails + | typeNotFunction functionTypeTails + ; + +functionTypeAlias + : functionPrefix formalParameterPart SC + ; + +functionTypeTail + : FUNCTION_ typeParameters? parameterTypeList + ; + +functionTypeTails + : functionTypeTail QU? functionTypeTails + | functionTypeTail + ; + +getterSignature + : type? GET_ identifier + ; + +identifier + : IDENTIFIER + | ABSTRACT_ + | AS_ + | COVARIANT_ + | DEFERRED_ + | DYNAMIC_ + | EXPORT_ + | EXTERNAL_ + | EXTENSION_ + | FACTORY_ + | FUNCTION_ + | GET_ + | IMPLEMENTS_ + | IMPORT_ + | INTERFACE_ + | LATE_ + | LIBRARY_ + | MIXIN_ + | OPERATOR_ + | PART_ + | REQUIRED_ + | SET_ + | STATIC_ + | TYPEDEF_ + | FUNCTION_ + | ASYNC_ + | HIDE_ + | OF_ + | ON_ + | SHOW_ + | SYNC_ + | AWAIT_ + | YIELD_ + | DYNAMIC_ + | NATIVE_ + | BASE_ + | SEALED_ + | WHEN_ + ; + +identifierList + : identifier (C identifier)* + ; + +ifElement + : IF_ OP expr CP element (ELSE_ element)? + ; + +ifNullExpression + : logicalOrExpression (QUQU logicalOrExpression)* + ; + +ifStatement + : IF_ OP expr (CASE_ guardedPattern)? CP statement (ELSE_ statement)? + ; + +importOrExport + : libraryImport + | libraryExport + ; + +importSpecification + : IMPORT_ configurableUri (DEFERRED_? AS_ identifier)? combinator* SC + ; + +incrementOperator + : PLPL + | MM + ; + +initializedIdentifier + : identifier (EQ expr)? + ; + +initializedIdentifierList + : initializedIdentifier (C initializedIdentifier)* + ; + +initializedVariableDeclaration + : declaredIdentifier (EQ expr)? (C initializedIdentifier)* + ; + +initializerExpression + : conditionalExpression + | cascade + ; + +initializerListEntry + : SUPER_ arguments + | SUPER_ D identifier arguments + | fieldInitializer + | assertion + ; + +initializers + : CO initializerListEntry (C initializerListEntry)* + ; + +interfaces + : IMPLEMENTS_ typeNotVoidList + ; + +isOperator + : IS_ NOT? + ; + +label + : identifier CO + ; + +letExpression + : LET_ staticFinalDeclarationList IN_ expr + ; + +libraryDeclaration + : libraryName? importOrExport* partDirective* (metadata topLevelDeclaration)* + ; + +libraryExport + : metadata EXPORT_ configurableUri combinator* SC + ; + +libraryImport + : metadata importSpecification + ; + +libraryName + : metadata LIBRARY_ dottedIdentifierList SC + ; + +listLiteral + : CONST_? typeArguments? OB elements? CB + ; + +literal + : nullLiteral + | booleanLiteral + | numericLiteral + | stringLiteral + | symbolLiteral + | listLiteral + | setOrMapLiteral + ; + +localFunctionDeclaration + : metadata functionSignature functionBody + ; + +localVariableDeclaration + : metadata initializedVariableDeclaration SC + ; + +logicalAndExpression + : equalityExpression (AA equalityExpression)* + ; + +logicalOrExpression + : logicalAndExpression (PP logicalAndExpression)* + ; + +mapElement + : expr CO expr + ; + +metadata + : (AT metadatum)* + ; + +metadatum + : identifier + | qualifiedName + | constructorDesignation arguments + ; + +methodSignature + : constructorSignature initializers? + | factoryConstructorSignature + | STATIC_? functionSignature + | STATIC_? getterSignature + | STATIC_? setterSignature + | operatorSignature + ; + +minusOperator + : MINUS + ; + +mixinApplication + : typeNotVoid mixins interfaces? + ; + +mixinApplicationClass + : identifier typeParameters? EQ mixinApplication SC + ; + +mixinDeclaration + : MIXIN_ typeIdentifier typeParameters? (ON_ typeNotVoidList)? interfaces? OBC ( + metadata classMemberDeclaration + )* CBC + ; + +mixins + : WITH_ typeNotVoidList + ; + +multilineString + : MultiLineString + ; + +multiplicativeExpression + : unaryExpression (multiplicativeOperator unaryExpression)* + | SUPER_ ( multiplicativeOperator unaryExpression)+ + ; + +multiplicativeOperator + : ST + | SL + | PC + | SQS + ; + +namedArgument + : label expr + ; + +namedFormalParameters + : OBC defaultNamedParameter (C defaultNamedParameter)* C? CBC + ; + +namedParameterType + : metadata REQUIRED_? typedIdentifier + ; + +namedParameterTypes + : OBC namedParameterType (C namedParameterType)* C? CBC + ; + +negationOperator + : NOT + ; + +newExpression + : NEW_ constructorDesignation arguments + ; + +nonLabelledStatement + : block + // yield / yield* must precede localVariableDeclaration: `yield` is also a legal identifier, so + // `yield i;` would otherwise parse as a variable declaration of a type named `yield`. + | yieldEachStatement + | yieldStatement + | localVariableDeclaration + | forStatement + | whileStatement + | doStatement + | switchStatement + | ifStatement + | rethrowStatement + | tryStatement + | breakStatement + | continueStatement + | returnStatement + | expressionStatement + | assertStatement + | localFunctionDeclaration + ; + +normalFormalParameter + : metadata normalFormalParameterNoMetadata + ; + +normalFormalParameterNoMetadata + : functionFormalParameter + | fieldFormalParameter + | superFormalParameter + | simpleFormalParameter + ; + +normalFormalParameters + : normalFormalParameter (C normalFormalParameter)* + ; + +normalParameterType + : metadata typedIdentifier + | metadata type + ; + +normalParameterTypes + : normalParameterType (C normalParameterType)* + ; + +nullLiteral + : NULL_ + ; + +numericLiteral + : NUMBER + | HEX_NUMBER + ; + +onPart + : catchPart block + | ON_ typeNotVoid catchPart? block + ; + +operator + : SQUIG + | binaryOperator + | OB CB + | OB CB EQ + ; + +operatorSignature + : type? OPERATOR_ operator formalParameterList + ; + +optionalOrNamedFormalParameters + : optionalPositionalFormalParameters + | namedFormalParameters + ; + +optionalParameterTypes + : optionalPositionalParameterTypes + | namedParameterTypes + ; + +optionalPositionalFormalParameters + : OB defaultFormalParameter (C defaultFormalParameter)* C? CB + ; + +optionalPositionalParameterTypes + : OB normalParameterTypes C? CB + ; + +parameterTypeList + : OP CP + | OP normalParameterTypes C optionalParameterTypes CP + | OP normalParameterTypes C? CP + | OP optionalParameterTypes CP + ; + +partDeclaration + : partHeader (metadata topLevelDeclaration)* + ; + +partDirective + : metadata PART_ uri SC + ; + +partHeader + : metadata PART_ OF_ (dottedIdentifierList | uri) SC + ; + +postfixExpression + : assignableExpression postfixOperator + | primary selector* + ; + +postfixOperator + : incrementOperator + ; + +prefixOperator + : minusOperator + | negationOperator + | tildeOperator + ; + +primary + : thisExpression + | SUPER_ unconditionalAssignableSelector + | SUPER_ argumentPart + | switchExpression + | functionExpression + | literal + | identifier + | newExpression + | constObjectExpression + | constructorInvocation + | recordLiteral + | OP expr CP + ; + +// ===== Dart 3 records — CN1 addition ===== +// A record literal needs either a named field or a comma, which is what +// distinguishes it from a parenthesized expression. +recordLiteral + : CONST_? OP recordField C (recordField (C recordField)* C?)? CP + | CONST_? OP identifier CO expr (C recordField)* C? CP + | CONST_? OP recordField C CP + ; + +recordField + : (identifier CO)? expr + ; + +qualifiedName + : typeIdentifier D identifier + | typeIdentifier D typeIdentifier D identifier + ; + +redirectingFactoryConstructorSignature + : CONST_? FACTORY_ constructorName formalParameterList EQ constructorDesignation + ; + +redirection + : CO THIS_ (D identifier)? arguments + ; + +relationalExpression + : bitwiseOrExpression (typeTest | typeCast | relationalOperator bitwiseOrExpression)? + | SUPER_ relationalOperator bitwiseOrExpression + ; + +relationalOperator + : GT EQ + | GT + | LTE + | LT + ; + +reserved_word + : ASSERT_ + | BREAK_ + | CASE_ + | CATCH_ + | CLASS_ + | CONST_ + | CONTINUE_ + | DEFAULT_ + | DO_ + | ELSE_ + | ENUM_ + | EXTENDS_ + | FALSE_ + | FINAL_ + | FINALLY_ + | FOR_ + | IF_ + | IN_ + | IS_ + | NEW_ + | NULL_ + | RETHROW_ + | RETURN_ + | SUPER_ + | SWITCH_ + | THIS_ + | THROW_ + | TRUE_ + | TRY_ + | VAR_ + | VOID_ + | WHILE_ + | WITH_ + ; + +rethrowStatement + : RETHROW_ SC + ; + +returnStatement + : RETURN_ expr? SC + ; + +selector + : NOT + | assignableSelector + | argumentPart + ; + +setOrMapLiteral + : CONST_? typeArguments? OBC elements? CBC + ; + +setterSignature + : type? SET_ identifier formalParameterList + ; + +shiftExpression + : additiveExpression (shiftOperator additiveExpression)* + | SUPER_ ( shiftOperator additiveExpression)+ + ; + +shiftOperator + : LTLT + | GT GT GT + | GT GT + ; + +simpleFormalParameter + : declaredIdentifier + | COVARIANT_? identifier + ; + +singleLineString + : SingleLineString + ; + +spreadElement + : (DDD | DDDQ) expr + ; + +statement + : label* nonLabelledStatement + ; + +statements + : statement* + ; + +staticFinalDeclaration + : identifier EQ expr + ; + +staticFinalDeclarationList + : staticFinalDeclaration (C staticFinalDeclaration)* + ; + +stringLiteral + : (multilineString | singleLineString)+ + ; + +superclass + : EXTENDS_ typeNotVoid mixins? + | mixins + ; + +switchCase + : label* CASE_ guardedPattern CO statements + ; + +switchStatement + : SWITCH_ OP expr CP OBC switchCase* defaultCase? CBC + ; + +// ===== Dart 3 patterns — CN1 addition (grammars-v4 is spec-2.15) ===== + +guardedPattern + : pattern (WHEN_ expr)? + ; + +switchExpression + : SWITCH_ OP expr CP OBC (switchExpressionCase (C switchExpressionCase)* C?)? CBC + ; + +switchExpressionCase + : guardedPattern EG expr + ; + +pattern + : logicalOrPattern + ; + +logicalOrPattern + : logicalAndPattern (PP logicalAndPattern)* + ; + +logicalAndPattern + : relationalPattern (AA relationalPattern)* + ; + +relationalPattern + : (EE | NE | LT | GT | LTE | GT EQ) bitwiseOrExpression + | unaryPattern + ; + +unaryPattern + : primaryPattern (AS_ type)? + ; + +primaryPattern + : constantPattern + | objectPattern + | recordPattern + | listPattern + | variablePattern + | OP pattern CP + ; + +// a bare `_` is the wildcard; `var x` / `final x` / `T x` bind +variablePattern + : (VAR_ | FINAL_ type? | type)? identifier + ; + +constantPattern + : nullLiteral + | booleanLiteral + | numericLiteral + | MINUS numericLiteral + | stringLiteral + | qualifiedName + ; + +objectPattern + : typeName typeArguments? OP (patternField (C patternField)* C?)? CP + ; + +recordPattern + : OP (patternField (C patternField)* C?)? CP + ; + +listPattern + : typeArguments? OB (pattern (C pattern)* C?)? CB + ; + +patternField + : (identifier? CO)? pattern + ; + +symbolLiteral + : PO (identifier ( D identifier)* | operator | VOID_) + ; + +thisExpression + : THIS_ + ; + +throwExpression + : THROW_ expr + ; + +throwExpressionWithoutCascade + : THROW_ expressionWithoutCascade + ; + +tildeOperator + : SQUIG + ; + +topLevelDeclaration + : classDeclaration + | mixinDeclaration + | extensionDeclaration + | enumType + | typeAlias + | EXTERNAL_ functionSignature SC + | EXTERNAL_ getterSignature SC + | EXTERNAL_ setterSignature SC + | functionSignature functionBody + | getterSignature functionBody + | setterSignature functionBody + | ( FINAL_ | CONST_) type? staticFinalDeclarationList SC + | LATE_ FINAL_ type? initializedIdentifierList SC + | LATE_? varOrType initializedIdentifierList SC + ; + +tryStatement + : TRY_ block (onPart+ finallyPart? | finallyPart) + ; + +type + : functionType QU? + | typeNotFunction + ; + +typeAlias + : TYPEDEF_ typeIdentifier typeParameters? EQ type SC + | TYPEDEF_ functionTypeAlias + ; + +typeArguments + : LT typeList GT + ; + +typeCast + : asOperator typeNotVoid + ; + +typedIdentifier + : type identifier + ; + +typeIdentifier + : IDENTIFIER + | ASYNC_ + | HIDE_ + | OF_ + | ON_ + | SHOW_ + | SYNC_ + | AWAIT_ + | YIELD_ + | DYNAMIC_ + | NATIVE_ + | FUNCTION_ + | BASE_ + | SEALED_ + | WHEN_ + ; + +typeList + : type (C type)* + ; + +typeName + : typeIdentifier (D typeIdentifier)? + ; + +typeNotFunction + : VOID_ + | typeNotVoidNotFunction + ; + +typeNotVoid + : functionType QU? + | typeNotVoidNotFunction + ; + +typeNotVoidList + : typeNotVoid (C typeNotVoid)* + ; + +typeNotVoidNotFunction + : typeName typeArguments? QU? + | FUNCTION_ QU? + ; + +typeNotVoidNotFunctionList + : typeNotVoidNotFunction (C typeNotVoidNotFunction)* + ; + +typeParameter + : metadata identifier (EXTENDS_ typeNotVoid)? + ; + +typeParameters + : LT typeParameter (C typeParameter)* GT + ; + +typeTest + : isOperator typeNotVoid + ; + +unaryExpression + : prefixOperator unaryExpression + | awaitExpression + | postfixExpression + | ( minusOperator | tildeOperator) SUPER_ + | incrementOperator assignableExpression + ; + +unconditionalAssignableSelector + : OB expr CB + | D identifier + ; + +uri + : stringLiteral + ; + +uriTest + : dottedIdentifierList (EE stringLiteral)? + ; + +varOrType + : VAR_ + | type + ; + +whileStatement + : WHILE_ OP expr CP statement + ; + +yieldEachStatement + : YIELD_ ST expr SC + ; + +yieldStatement + : YIELD_ expr SC + ; \ No newline at end of file diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/Program.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/Program.java new file mode 100644 index 00000000000..56b95957dbd --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/Program.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.analyze; + +import com.codename1.dart.transpiler.ast.Ast; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Whole-program model: every parsed user library plus lookup tables. + * All user code lands in one Java package, so class names are global. + */ +public final class Program { + + public final List libraries = new ArrayList(); + public final Map classes = new LinkedHashMap(); + public final Map enums = new LinkedHashMap(); + /** Top-level function name -> owning library. */ + public final Map functionOwners = new LinkedHashMap(); + public final Map functions = new LinkedHashMap(); + /** Top-level variable name -> owning library. */ + public final Map topLevelVarOwners = new LinkedHashMap(); + /** Top-level setter name -> owning library (Dart {@code set x(v)} at library scope). */ + public final Map topLevelSetters = new LinkedHashMap(); + public final Map topLevelVars = new LinkedHashMap(); + + /** + * Every library declaring a given top-level var / function name. Unlike the + * single-owner maps above (which only retain the last registration), these keep + * all owners so a same-name collision across studies (e.g. {@code homeRoute} in + * shrine/reply/rally routes.dart) can be resolved to the correct library via the + * importing library's prefix import. + */ + public final Map> topLevelVarOwnersByName = + new LinkedHashMap>(); + public final Map> functionOwnersByName = + new LinkedHashMap>(); + + public final List extensions = new ArrayList(); + + /** Top-level {@code typedef} name -> declaration, across all user libraries. */ + public final Map typedefs = new LinkedHashMap(); + + /** + * Union of every {@code import '...' as prefix;} prefix declared across all + * user libraries. Because all user code compiles into one Java package with + * global class / top-level names, a {@code prefix.member} access can be + * resolved against the whole program regardless of which library declared it. + */ + public final Set importPrefixes = new HashSet(); + + /** + * Every user class keyed by simple name, keeping ALL declarations that share a + * name. In the single-package model two libraries may each declare (say) a + * private {@code _FrontLayer} or a public {@code Backdrop}; {@link #classes} + * only retains the last one, so same-library-preferred lookups consult this. + */ + public final Map> classesByName = + new LinkedHashMap>(); + + public void add(Ast.Library lib) { + libraries.add(lib); + importPrefixes.addAll(lib.importPrefixes); + for (Ast.ClassDecl c : lib.classes) { + c.ownerLibrary = lib; + if (c.extensionOn != null) { + extensions.add(c); + } else { + classes.put(c.name, c); + List byName = classesByName.get(c.name); + if (byName == null) { + byName = new ArrayList(); + classesByName.put(c.name, byName); + } + byName.add(c); + } + } + for (Ast.EnumDecl e : lib.enums) { + enums.put(e.name, e); + } + for (Ast.FunctionDecl f : lib.functions) { + // A top-level setter shares its name with the getter/field it backs; keep the + // read-side (getter/plain function) in the lookup maps and track setters apart. + if (f.isSetter) { + topLevelSetters.put(f.name, lib); + continue; + } + functions.put(f.name, f); + functionOwners.put(f.name, lib); + addOwner(functionOwnersByName, f.name, lib); + } + for (Ast.FieldDecl v : lib.topLevelVars) { + topLevelVars.put(v.name, v); + topLevelVarOwners.put(v.name, lib); + addOwner(topLevelVarOwnersByName, v.name, lib); + } + for (Ast.TypedefDecl t : lib.typedefs) { + typedefs.put(t.name, t); + } + } + + /** + * Resolves a user class by simple name, preferring a declaration in {@code fromLibrary} + * when several libraries share the name (single-package name collision). Falls back to the + * last-registered declaration ({@link #classes}) when there is no same-library match. + */ + public Ast.ClassDecl resolveClass(String name, Ast.Library fromLibrary) { + List byName = classesByName.get(name); + if (byName == null || byName.isEmpty()) { + return classes.get(name); + } + if (byName.size() > 1 && fromLibrary != null) { + // 1. a class declared in the referencing library itself + for (Ast.ClassDecl c : byName) { + if (c.ownerLibrary == fromLibrary) { + return c; + } + } + // 2. a class declared in a library the referencing library imports. Dart resolves + // an unqualified name against the file's imports, so `Backdrop` in main.dart + // (which imports pages/backdrop.dart) must be that Backdrop, never an unrelated + // same-name class in a study file main.dart never imports. + for (String uri : fromLibrary.imports) { + Ast.Library target = resolveImportedLibrary(fromLibrary, uri); + if (target != null) { + for (Ast.ClassDecl c : byName) { + if (c.ownerLibrary == target) { + return c; + } + } + } + } + } + return byName.get(byName.size() - 1); + } + + private static void addOwner(Map> map, String name, Ast.Library lib) { + List owners = map.get(name); + if (owners == null) { + owners = new ArrayList(); + map.put(name, owners); + } + if (!owners.contains(lib)) { + owners.add(lib); + } + } + + /** + * Resolves which library owns a top-level var named {@code name}, referenced from + * {@code from} optionally through an import {@code prefix}. When several libraries + * declare the name, prefer the one the prefix import points to, then the referencing + * library itself, then a plainly-imported library, then the last registration. + */ + public Ast.Library resolveTopLevelVarOwner(String name, Ast.Library from, String prefix) { + return resolveOwner(topLevelVarOwnersByName.get(name), topLevelVarOwners.get(name), from, prefix); + } + + /** Same as {@link #resolveTopLevelVarOwner} for top-level functions / getters. */ + public Ast.Library resolveFunctionOwner(String name, Ast.Library from, String prefix) { + return resolveOwner(functionOwnersByName.get(name), functionOwners.get(name), from, prefix); + } + + private Ast.Library resolveOwner(List owners, Ast.Library fallback, + Ast.Library from, String prefix) { + if (owners == null || owners.isEmpty()) { + return fallback; + } + if (owners.size() == 1) { + return owners.get(0); + } + if (prefix != null && from != null) { + String uri = from.prefixImports.get(prefix); + Ast.Library target = resolveImportedLibrary(from, uri); + if (target != null && owners.contains(target)) { + return target; + } + } + if (from != null) { + if (owners.contains(from)) { + return from; + } + for (Ast.Library o : owners) { + for (String uri : from.imports) { + if (o == resolveImportedLibrary(from, uri)) { + return o; + } + } + } + } + return owners.get(owners.size() - 1); + } + + /** Resolves a (relative) import uri against the importing library's directory to a user library. */ + public Ast.Library resolveImportedLibrary(Ast.Library from, String uri) { + if (uri == null || from == null || uri.startsWith("dart:") || uri.startsWith("package:")) { + return null; + } + String base = from.fileName.replace('\\', '/'); + int slash = base.lastIndexOf('/'); + String dir = slash >= 0 ? base.substring(0, slash) : ""; + String combined = normalizePath(dir.isEmpty() ? uri : dir + "/" + uri); + for (Ast.Library lib : libraries) { + if (normalizePath(lib.fileName.replace('\\', '/')).equals(combined)) { + return lib; + } + } + return null; + } + + private static String normalizePath(String path) { + String[] parts = path.split("/"); + List out = new ArrayList(); + for (String p : parts) { + if (p.isEmpty() || p.equals(".")) { + continue; + } + if (p.equals("..")) { + if (!out.isEmpty() && !out.get(out.size() - 1).equals("..")) { + out.remove(out.size() - 1); + } else { + out.add(p); + } + } else { + out.add(p); + } + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < out.size(); i++) { + if (i > 0) { + sb.append('/'); + } + sb.append(out.get(i)); + } + return sb.toString(); + } + + /** Finds an extension member for the given receiver type name. */ + public Ast.ClassDecl findExtension(String typeName, String member, boolean getter) { + for (Ast.ClassDecl ext : extensions) { + if (!ext.extensionOn.name.equals(typeName)) { + continue; + } + for (Ast.MethodDecl m : ext.methods) { + if (m.name.equals(member) && m.isGetter == getter && !m.isSetter) { + return ext; + } + } + } + return null; + } + + /** + * Java class name hosting a library's top-level functions: main.dart -> MainLib. + * The FULL relative path is encoded (not just the basename) so libraries that share + * a basename across directories — e.g. the six {@code app.dart} / seven + * {@code routes.dart} files in the Flutter Gallery — get distinct classes instead of + * colliding into one and losing members. + */ + public static String libClassName(String fileName) { + String base = fileName.replace('\\', '/'); + if (base.endsWith(".dart")) { + base = base.substring(0, base.length() - 5); + } + StringBuilder sb = new StringBuilder(); + boolean up = true; + for (int i = 0; i < base.length(); i++) { + char c = base.charAt(i); + if (c == '_' || c == '-' || c == '.' || c == '/') { + up = true; + } else { + sb.append(up ? Character.toUpperCase(c) : c); + up = false; + } + } + if (sb.length() == 0) { + sb.append("Lib0"); + } + if (Character.isDigit(sb.charAt(0))) { + sb.insert(0, '_'); + } + return sb.append("Lib").toString(); + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java new file mode 100644 index 00000000000..cc22686fd1c --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/analyze/StubRegistry.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.analyze; + +import com.codename1.dart.transpiler.api.Diagnostics; +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.parser.AstBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The hand-written runtime API as seen from Dart: classes, enums and + * top-level functions declared in signature-stub .dart files (parsed with + * the same front end as user code). Every entry carries its Java name via + * the stub's {@code @JavaName} annotation. + */ +public final class StubRegistry { + + public final Map classes = new LinkedHashMap(); + public final Map enums = new LinkedHashMap(); + public final Map functions = new LinkedHashMap(); + public final Map topLevelVars = new LinkedHashMap(); + /** Stub extension declarations (`extension X on T { ... }`), keyed via their {@code on} type. */ + public final List extensions = new ArrayList(); + + /** Loads the embedded stub set (fallback when the classpath has none). */ + public static StubRegistry loadEmbedded(Diagnostics diags) { + StubRegistry r = new StubRegistry(); + r.loadBuiltins(diags); + r.loadResource("/com/codename1/dart/stubs/flutter_material.dart", diags); + return r; + } + + /** + * Loads transpiler built-in stubs that must be present regardless of the runtime stub + * classpath — currently the dart:collection mixins (IterableMixin/ListMixin/MapMixin/SetMixin), + * which user classes apply via {@code with IterableMixin}. + */ + private void loadBuiltins(Diagnostics diags) { + loadResource("/com/codename1/dart/stubs/dart_collection.dart", diags); + } + + /** + * Loads stubs from META-INF/dart/*.dart inside the given jars or + * directories (the runtime dependencies of the app being transpiled). + * Falls back to the embedded stub set when nothing contributes. + */ + public static StubRegistry loadFromClasspath(java.util.List entries, Diagnostics diags) { + StubRegistry r = new StubRegistry(); + r.loadBuiltins(diags); + int builtinClasses = r.classes.size(); + for (java.io.File entry : entries) { + try { + if (entry.isDirectory()) { + java.io.File dir = new java.io.File(entry, "META-INF/dart"); + java.io.File[] files = dir.listFiles(); + if (files != null) { + for (java.io.File f : files) { + if (f.getName().endsWith(".dart")) { + byte[] data = java.nio.file.Files.readAllBytes(f.toPath()); + r.load(f.getName(), new String(data, StandardCharsets.UTF_8), diags); + } + } + } + } else if (entry.getName().endsWith(".jar") && entry.exists()) { + java.util.zip.ZipFile zip = new java.util.zip.ZipFile(entry); + try { + java.util.Enumeration en = zip.entries(); + while (en.hasMoreElements()) { + java.util.zip.ZipEntry ze = en.nextElement(); + if (!ze.isDirectory() && ze.getName().startsWith("META-INF/dart/") + && ze.getName().endsWith(".dart")) { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + InputStream in = zip.getInputStream(ze); + byte[] chunk = new byte[8192]; + int n; + while ((n = in.read(chunk)) > 0) { + buf.write(chunk, 0, n); + } + r.load(entry.getName() + "!" + ze.getName(), + new String(buf.toByteArray(), StandardCharsets.UTF_8), diags); + } + } + } finally { + zip.close(); + } + } + } catch (IOException e) { + diags.error(entry.getName(), 0, 0, "E0903", "Failed scanning for Dart stubs: " + e); + } + } + // Only the always-loaded builtins contributed — no runtime stubs on the classpath. + if (r.classes.size() == builtinClasses && r.functions.isEmpty()) { + return loadEmbedded(diags); + } + return r; + } + + public void loadResource(String resource, Diagnostics diags) { + InputStream in = StubRegistry.class.getResourceAsStream(resource); + if (in == null) { + diags.error(resource, 0, 0, "E0901", "Missing embedded stub resource: " + resource); + return; + } + try { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = in.read(chunk)) > 0) { + buf.write(chunk, 0, n); + } + String src = new String(buf.toByteArray(), StandardCharsets.UTF_8); + load(resource, src, diags); + } catch (IOException e) { + diags.error(resource, 0, 0, "E0902", "Failed reading stub resource: " + e); + } + } + + public void load(String name, String source, Diagnostics diags) { + AstBuilder builder = new AstBuilder(diags); + Ast.Library lib = builder.parse(name, source); + for (Ast.ClassDecl c : lib.classes) { + if (c.extensionOn != null) { + extensions.add(c); + } else { + classes.put(c.name, c); + } + } + for (Ast.EnumDecl e : lib.enums) { + enums.put(e.name, e); + } + for (Ast.FunctionDecl f : lib.functions) { + functions.put(f.name, f); + } + for (Ast.FieldDecl v : lib.topLevelVars) { + if (v.javaName != null) { + topLevelVars.put(v.name, v); + } + } + } + + public boolean isStubClass(String dartName) { + return classes.containsKey(dartName); + } + + public boolean isStubEnum(String dartName) { + return enums.containsKey(dartName); + } + + /** + * Finds a stub extension declaring {@code member} for the given receiver type name. + * Matches the extension's {@code on} type against the receiver type or any of its stub + * supertypes (class chain), so an extension declared on a base type is still consulted. + */ + public Ast.ClassDecl findExtension(String typeName, String member, boolean getter) { + for (String t = typeName; t != null; ) { + for (Ast.ClassDecl ext : extensions) { + if (!ext.extensionOn.name.equals(t)) { + continue; + } + for (Ast.MethodDecl m : ext.methods) { + if (m.name.equals(member) && m.isGetter == getter && !m.isSetter) { + return ext; + } + } + } + Ast.ClassDecl c = classes.get(t); + t = c != null && c.superclass != null ? c.superclass.name : null; + } + return null; + } + + /** Walks the stub superclass chain looking for a member. */ + public Ast.MethodDecl findMethod(String className, String member, boolean getter) { + Ast.ClassDecl c = classes.get(className); + while (c != null) { + for (Ast.MethodDecl m : c.methods) { + if (m.name.equals(member) && m.isGetter == getter && !m.isSetter) { + return m; + } + } + c = c.superclass != null ? classes.get(c.superclass.name) : null; + } + return null; + } + + /** Walks the stub superclass chain looking for a declared setter (Dart {@code set x(v)}). */ + public Ast.MethodDecl findSetter(String className, String member) { + Ast.ClassDecl c = classes.get(className); + while (c != null) { + for (Ast.MethodDecl m : c.methods) { + if (m.name.equals(member) && m.isSetter) { + return m; + } + } + c = c.superclass != null ? classes.get(c.superclass.name) : null; + } + return null; + } + + /** The unnamed constructor of a stub class (or null). */ + public Ast.CtorDecl ctorOf(String className) { + Ast.ClassDecl c = classes.get(className); + return c == null ? null : c.defaultCtor(); + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/DartTranspiler.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/DartTranspiler.java new file mode 100644 index 00000000000..4ef0d2909e3 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/DartTranspiler.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +import com.codename1.dart.transpiler.analyze.Program; +import com.codename1.dart.transpiler.analyze.StubRegistry; +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.codegen.JavaEmitter; +import com.codename1.dart.transpiler.parser.AstBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Whole-program Dart-to-Java transpiler entry point (called by the + * Codename One Maven plugin's transcode-flutter goal and by tests). + * + *

Incrementality is whole-program: a digest over every input file plus + * the transpiler version is compared against {@code stateFile}; on match the + * transpile is skipped, otherwise everything regenerates. Output writes are + * content-stable — unchanged files are not rewritten, keeping downstream + * javac incremental.

+ */ +public final class DartTranspiler { + + /** Bumped whenever emission changes so stale state files don't skip. */ + private static final String VERSION = "m1-1"; + + public TranspileResult transpile(TranspileRequest req) { + Diagnostics diags = new Diagnostics(); + List dartFiles = new ArrayList(); + for (File root : req.sourceRoots) { + collectDartFiles(root, root, dartFiles); + } + Collections.sort(dartFiles); + + // The output PACKAGE is part of what was generated, so it is part of the + // key: changing cn1.flutter.package alone used to leave every generated + // source declared in the old package, because nothing else had changed. + String digest = digest(dartFiles) + stubDigest(req.stubClasspath) + + "-pkg:" + req.packageName; + if (req.stateFile != null && req.stateFile.exists() && req.outputDir != null && req.outputDir.exists()) { + try { + String prev = new String(Files.readAllBytes(req.stateFile.toPath()), StandardCharsets.UTF_8).trim(); + if (prev.equals(digest)) { + return new TranspileResult(diags.asList(), new ArrayList(), true); + } + } catch (IOException ignore) { + // fall through to full transpile + } + } + + Program program = new Program(); + AstBuilder builder = new AstBuilder(diags); + for (File f : dartFiles) { + try { + String src = new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8); + String rel = relativize(req.sourceRoots, f); + program.add(builder.parse(rel, src)); + } catch (IOException e) { + diags.error(f.getName(), 0, 0, "E0003", "Cannot read file: " + e); + } catch (RuntimeException | StackOverflowError e) { + // Front-end robustness: a parser/AST-builder gap must never abort the whole build. + // Record it as a diagnostic (with the crash site) so a SINGLE pass over a large real + // app yields the full gap inventory instead of dying on the first unhandled construct. + StackTraceElement[] st = e.getStackTrace(); + StackTraceElement top = null; + for (StackTraceElement s : st) { + if (s.getClassName().startsWith("com.codename1.dart.transpiler")) { top = s; break; } + } + String at = top == null ? "" : " @ " + + top.getClassName().substring(top.getClassName().lastIndexOf('.') + 1) + + "." + top.getMethodName() + ":" + top.getLineNumber(); + diags.error(f.getName(), 0, 0, "E0004", + "Front-end crash: " + e.getClass().getSimpleName() + + (e.getMessage() != null ? ": " + e.getMessage() : "") + at); + } + } + + StubRegistry stubs = req.stubClasspath.isEmpty() + ? StubRegistry.loadEmbedded(diags) + : StubRegistry.loadFromClasspath(req.stubClasspath, diags); + JavaEmitter emitter = new JavaEmitter(program, stubs, diags, req.packageName); + List files = emitter.emit(); + + if (!diags.hasErrors() && req.outputDir != null) { + writeOutput(req, files, diags); + if (req.stateFile != null && diags.hasErrors()) { + // A file failed to write, so the output does not match these inputs. + // Recording the digest anyway sent the next unchanged build down the + // up-to-date path with a missing or stale generated file and no error; + // removing the state (it may name an earlier, matching build) forces + // the next build to write everything again. + if (req.stateFile.exists() && !req.stateFile.delete()) { + diags.warn(null, "W0001", "Could not remove stale transpiler state file: " + req.stateFile); + } + } else if (req.stateFile != null) { + try { + req.stateFile.getParentFile().mkdirs(); + Files.write(req.stateFile.toPath(), digest.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + diags.warn(null, "W0001", "Could not write transpiler state file: " + e); + } + } + } + return new TranspileResult(diags.asList(), files, false); + } + + private void writeOutput(TranspileRequest req, List files, Diagnostics diags) { + File pkgDir = new File(req.outputDir, req.packageName.replace('.', File.separatorChar)); + removePreviousPackage(req, pkgDir); + pkgDir.mkdirs(); + Set expected = new HashSet(); + for (GeneratedFile gf : files) { + expected.add(gf.relativePath); + File out = new File(pkgDir, gf.relativePath); + out.getParentFile().mkdirs(); + try { + byte[] content = gf.content.getBytes(StandardCharsets.UTF_8); + if (out.exists() && Arrays.equals(Files.readAllBytes(out.toPath()), content)) { + continue; // content-stable: keep timestamp for incremental javac + } + Files.write(out.toPath(), content); + } catch (IOException e) { + diags.error(gf.relativePath, 0, 0, "E0004", "Cannot write generated file: " + e); + } + } + // sweep stale generated files + File[] existing = pkgDir.listFiles(); + if (existing != null) { + for (File f : existing) { + if (f.isFile() && f.getName().endsWith(".java") && !expected.contains(f.getName())) { + f.delete(); + } + } + } + } + + /** + * When the output package changed since the last run, deletes what that run generated + * into the OLD package's directory. The stale-file sweep below only looks in the new + * package's directory, so every source of the previous package stayed on the compile + * root and was compiled and packaged beside its replacement -- a second, obsolete copy + * of the whole generated tree. Only the generated .java files directly in that + * directory go, so an old package that is a parent of the new one keeps the new files. + */ + private void removePreviousPackage(TranspileRequest req, File pkgDir) { + if (req.stateFile == null || !req.stateFile.exists()) { + return; + } + String prev; + try { + prev = new String(Files.readAllBytes(req.stateFile.toPath()), StandardCharsets.UTF_8).trim(); + } catch (IOException e) { + return; + } + int at = prev.lastIndexOf("-pkg:"); + if (at < 0) { + return; + } + String prevPkg = prev.substring(at + 5); + if (prevPkg.isEmpty() || prevPkg.equals(req.packageName)) { + return; + } + File old = new File(req.outputDir, prevPkg.replace('.', File.separatorChar)); + File[] children = old.listFiles(); + if (children == null || old.equals(pkgDir)) { + return; + } + for (File f : children) { + if (f.isFile() && f.getName().endsWith(".java")) { + f.delete(); + } + } + String[] left = old.list(); + if (left != null && left.length == 0) { + old.delete(); + } + } + + private void collectDartFiles(File root, File dir, List out) { + File[] children = dir.listFiles(); + if (children == null) { + return; + } + for (File f : children) { + if (f.isDirectory()) { + // Only the asset roots themselves -- src/main/flutter/assets and + // src/main/flutter/packages, both of which TranscodeFlutterMojo bundles + // as assets -- are not source. Skipping every directory NAMED assets + // silently dropped real code such as lib/assets/generated.dart from the + // whole-program parse; not skipping packages/ parsed a .dart file bundled + // there as data, and its declarations could break the build. + if (!(dir.equals(root) && (f.getName().equals("assets") || f.getName().equals("packages")))) { + collectDartFiles(root, f, out); + } + } else if (f.getName().endsWith(".dart")) { + out.add(f); + } + } + } + + private String relativize(List roots, File f) { + for (File root : roots) { + String rootPath = root.getAbsolutePath(); + String path = f.getAbsolutePath(); + if (path.startsWith(rootPath)) { + String rel = path.substring(rootPath.length()); + if (rel.startsWith(File.separator)) { + rel = rel.substring(1); + } + return rel.replace(File.separatorChar, '/'); + } + } + return f.getName(); + } + + private String stubDigest(List stubClasspath) { + if (stubClasspath.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder("-stubs"); + for (File f : stubClasspath) { + sb.append('|').append(f.getAbsolutePath()).append('@').append(f.lastModified()); + if (f.isDirectory()) { + // A class directory -- a reactor or development build -- keeps its + // stubs under META-INF/dart, and editing one of those files does not + // touch the directory's own timestamp. The directory alone therefore + // hashed the same after a stub change and the transpile was skipped + // against the old runtime API. + appendStubFiles(new File(f, "META-INF/dart"), sb); + } + } + return Integer.toHexString(sb.toString().hashCode()); + } + + /** Every file under {@code dir}, in a stable order: path, size and timestamp. */ + private static void appendStubFiles(File dir, StringBuilder sb) { + File[] children = dir.listFiles(); + if (children == null) { + return; + } + Arrays.sort(children); + for (File c : children) { + if (c.isDirectory()) { + appendStubFiles(c, sb); + } else { + sb.append('|').append(c.getName()).append(':').append(c.length()) + .append('@').append(c.lastModified()); + } + } + } + + private static String fingerprint; + + /** + * A digest of the transpiler's OWN code, so that any change to what it emits + * invalidates a cached result without anyone having to remember to. + * + *

VERSION was meant to do this and had to be bumped by hand; it stayed + * "m1-1" through every emission change since the first milestone. A project + * that upgraded its plugin kept a state file whose digest still matched, so + * the transpile was skipped and the generated sources stayed whatever the + * previous transpiler had produced. Hashing the jar (or the class directory + * in a development build) the transpiler was loaded from makes that + * automatic. Computed once per JVM.

+ */ + private static synchronized String transpilerFingerprint() { + if (fingerprint != null) { + return fingerprint; + } + try { + File where = new File(DartTranspiler.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + List parts = new ArrayList(); + if (where.isDirectory()) { + collectFiles(where, parts); + Collections.sort(parts); + } else { + parts.add(where); + } + for (File f : parts) { + md.update(Files.readAllBytes(f.toPath())); + } + StringBuilder sb = new StringBuilder(); + for (byte b : md.digest()) { + sb.append(String.format("%02x", b)); + } + fingerprint = sb.toString(); + } catch (Exception e) { + // Unknown code source: never match a previous state, rather than risk + // reusing output from a different transpiler. + fingerprint = "unknown-" + System.nanoTime(); + } + return fingerprint; + } + + private static void collectFiles(File dir, List out) { + File[] children = dir.listFiles(); + if (children == null) { + return; + } + for (File c : children) { + if (c.isDirectory()) { + collectFiles(c, out); + } else { + out.add(c); + } + } + } + + private String digest(List files) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(VERSION.getBytes(StandardCharsets.UTF_8)); + md.update(transpilerFingerprint().getBytes(StandardCharsets.UTF_8)); + for (File f : files) { + md.update(f.getAbsolutePath().getBytes(StandardCharsets.UTF_8)); + md.update(Files.readAllBytes(f.toPath())); + } + StringBuilder sb = new StringBuilder(); + for (byte b : md.digest()) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + return "no-digest-" + System.nanoTime(); + } + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostic.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostic.java new file mode 100644 index 00000000000..4c67ccfc3f7 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostic.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +/** + * A transpiler diagnostic carrying the Dart source position. + */ +public final class Diagnostic { + + public enum Severity { + ERROR, WARNING, INFO + } + + public final String file; + public final int line; + public final int col; + public final Severity severity; + public final String code; // e.g. "E0101" + public final String message; + + public Diagnostic(String file, int line, int col, Severity severity, String code, String message) { + this.file = file; + this.line = line; + this.col = col; + this.severity = severity; + this.code = code; + this.message = message; + } + + @Override + public String toString() { + return file + ":[" + line + "," + col + "] " + message + " (dart2java:" + code + ")"; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostics.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostics.java new file mode 100644 index 00000000000..7a27ba0a75c --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/Diagnostics.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +import com.codename1.dart.transpiler.ast.Ast; + +import java.util.ArrayList; +import java.util.List; + +/** + * Mutable diagnostic collector threaded through the pipeline. + */ +public final class Diagnostics { + + private final List all = new ArrayList(); + + public void error(Ast.Node node, String code, String message) { + all.add(new Diagnostic(node == null ? "?" : node.file, node == null ? 0 : node.line, + node == null ? 0 : node.col, Diagnostic.Severity.ERROR, code, message)); + } + + public void error(String file, int line, int col, String code, String message) { + all.add(new Diagnostic(file, line, col, Diagnostic.Severity.ERROR, code, message)); + } + + public void warn(Ast.Node node, String code, String message) { + all.add(new Diagnostic(node == null ? "?" : node.file, node == null ? 0 : node.line, + node == null ? 0 : node.col, Diagnostic.Severity.WARNING, code, message)); + } + + public boolean hasErrors() { + for (Diagnostic d : all) { + if (d.severity == Diagnostic.Severity.ERROR) { + return true; + } + } + return false; + } + + public List asList() { + return all; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/GeneratedFile.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/GeneratedFile.java new file mode 100644 index 00000000000..3af5024f94c --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/GeneratedFile.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +/** + * One generated Java source file (path relative to the output package dir). + */ +public final class GeneratedFile { + public final String relativePath; + public final String content; + + public GeneratedFile(String relativePath, String content) { + this.relativePath = relativePath; + this.content = content; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileRequest.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileRequest.java new file mode 100644 index 00000000000..14123ec2b5f --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileRequest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Input to {@link DartTranspiler#transpile}. + */ +public final class TranspileRequest { + + public final List sourceRoots = new ArrayList(); + /** + * Jars/directories scanned for META-INF/dart/*.dart signature stubs + * (the runtime API as seen from Dart). When none contribute stubs the + * embedded copy is used. + */ + public final List stubClasspath = new ArrayList(); + public File outputDir; + /** Java package for generated sources. */ + public String packageName = "com.codename1.generated.flutter"; + /** Fast-skip digest state file; null disables the check. */ + public File stateFile; + + public TranspileRequest sourceRoot(File root) { + sourceRoots.add(root); + return this; + } + + public TranspileRequest stubClasspathEntry(File jarOrDir) { + stubClasspath.add(jarOrDir); + return this; + } + + public TranspileRequest outputDir(File dir) { + this.outputDir = dir; + return this; + } + + public TranspileRequest packageName(String pkg) { + this.packageName = pkg; + return this; + } + + public TranspileRequest stateFile(File f) { + this.stateFile = f; + return this; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileResult.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileResult.java new file mode 100644 index 00000000000..d795be503fa --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/api/TranspileResult.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.api; + +import java.util.ArrayList; +import java.util.List; + +/** + * Output of {@link DartTranspiler#transpile}. + */ +public final class TranspileResult { + + private final List diagnostics; + private final List generatedFiles; + private final boolean upToDate; + + public TranspileResult(List diagnostics, List generatedFiles, boolean upToDate) { + this.diagnostics = diagnostics; + this.generatedFiles = generatedFiles; + this.upToDate = upToDate; + } + + public List getDiagnostics() { + return diagnostics; + } + + public List getGeneratedFiles() { + return generatedFiles; + } + + public boolean isUpToDate() { + return upToDate; + } + + public boolean hasErrors() { + for (Diagnostic d : diagnostics) { + if (d.severity == Diagnostic.Severity.ERROR) { + return true; + } + } + return false; + } + + public List errors() { + List out = new ArrayList(); + for (Diagnostic d : diagnostics) { + if (d.severity == Diagnostic.Severity.ERROR) { + out.add(d); + } + } + return out; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java new file mode 100644 index 00000000000..b4cbfbc986d --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/ast/Ast.java @@ -0,0 +1,750 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.ast; + +import java.util.ArrayList; +import java.util.List; + +/** + * The transpiler's own immutable-ish AST. The ANTLR parse tree is converted + * to these nodes by AstBuilder and never escapes the parser package — this + * is the seam that absorbs grammar churn. + * + *

Node classes are deliberately compact: public fields, one file. Only + * the M1 language subset is modeled; AstBuilder reports a source-positioned + * diagnostic for anything else.

+ */ +public final class Ast { + + private Ast() { + } + + /** Source position carried by every node for diagnostics/source maps. */ + public static class Node { + public String file; + public int line; + public int col; + + public T at(String file, int line, int col) { + this.file = file; + this.line = line; + this.col = col; + @SuppressWarnings("unchecked") + T self = (T) this; + return self; + } + } + + // ------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------ + + /** A resolved-enough Dart type reference: name, args, nullability. */ + public static class TypeRef extends Node { + public String name; // "int", "String", "List", "Widget", "MyApp", "void", "var", "dynamic" + public List args = new ArrayList(); + public boolean nullable; + // For an inline function type (name == "Function", e.g. `void Function(int)`): + // the parsed signature so codegen can render a real Funcs.* SAM instead of Object. + public List funcParams; // non-null iff this is an inline function type + public TypeRef funcReturn; // return type of the inline function type + + public TypeRef(String name) { + this.name = name; + } + + public static TypeRef of(String name, TypeRef... args) { + TypeRef t = new TypeRef(name); + for (TypeRef a : args) { + t.args.add(a); + } + return t; + } + + public boolean is(String n) { + return name.equals(n); + } + + public TypeRef arg(int i) { + return i < args.size() ? args.get(i) : DYNAMIC; + } + + public static final TypeRef VAR = new TypeRef("var"); + public static final TypeRef DYNAMIC = new TypeRef("dynamic"); + public static final TypeRef VOID = new TypeRef("void"); + public static final TypeRef INT = new TypeRef("int"); + public static final TypeRef DOUBLE = new TypeRef("double"); + public static final TypeRef BOOL = new TypeRef("bool"); + public static final TypeRef STRING = new TypeRef("String"); + public static final TypeRef NULL = new TypeRef("Null"); + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(name); + if (!args.isEmpty()) { + sb.append('<'); + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(args.get(i)); + } + sb.append('>'); + } + if (nullable) { + sb.append('?'); + } + return sb.toString(); + } + } + + // ------------------------------------------------------------------ + // Declarations + // ------------------------------------------------------------------ + + public static class Library extends Node { + public String fileName; // e.g. "main.dart" (relative to source root) + public List imports = new ArrayList(); + /** Import prefix names introduced by `import '...' as name;` in this library. */ + public List importPrefixes = new ArrayList(); + /** `import '' as ;` — prefix name mapped to the (raw) import uri. */ + public java.util.Map prefixImports = + new java.util.LinkedHashMap(); + public List classes = new ArrayList(); + public List enums = new ArrayList(); + public List functions = new ArrayList(); + public List topLevelVars = new ArrayList(); + public List typedefs = new ArrayList(); + } + + public static class ClassDecl extends Node { + public String name; + public Library ownerLibrary; // the user library that declares this class (single-package model) + public String javaName; // from @JavaName('...') in stub files + public boolean isAbstract; + public boolean isSealed; // Dart 3: sealed class C { } + public boolean isMixin; // mixin M { } + public TypeRef extensionOn; // extension X on T { } — non-null marks an extension + public List mixins = new ArrayList(); // class C with M1, M2 + public TypeRef superclass; // null if none/Object + public List interfaces = new ArrayList(); + public List typeParams = new ArrayList(); + public List fields = new ArrayList(); + public List ctors = new ArrayList(); + public List methods = new ArrayList(); + + public FieldDecl field(String name) { + for (FieldDecl f : fields) { + if (f.name.equals(name)) { + return f; + } + } + return null; + } + + public MethodDecl method(String name) { + for (MethodDecl m : methods) { + if (m.name.equals(name) && !m.isGetter && !m.isSetter) { + return m; + } + } + return null; + } + + public MethodDecl getter(String name) { + for (MethodDecl m : methods) { + if (m.name.equals(name) && m.isGetter) { + return m; + } + } + return null; + } + + /** The canonical constructor parameter order: positional then named (declared order). */ + public CtorDecl defaultCtor() { + for (CtorDecl c : ctors) { + if (c.name == null) { + return c; + } + } + return null; + } + + public CtorDecl namedCtor(String name) { + for (CtorDecl c : ctors) { + if (name.equals(c.name)) { + return c; + } + } + return null; + } + + public boolean hasNamedNonFactoryCtor() { + for (CtorDecl c : ctors) { + if (c.name != null && !c.isFactory) { + return true; + } + } + return false; + } + } + + public static class EnumDecl extends Node { + public String name; + public String javaName; // from @JavaName('...') in stub files + public List entries = new ArrayList(); + // Dart 2.17 enhanced-enum body members (methods / getters, fields, constructors). + public List fields = new ArrayList(); + public List methods = new ArrayList(); + public List ctors = new ArrayList(); + + public MethodDecl method(String name) { + for (MethodDecl m : methods) { + if (m.name.equals(name) && !m.isGetter && !m.isSetter) { + return m; + } + } + return null; + } + + public MethodDecl getter(String name) { + for (MethodDecl m : methods) { + if (m.name.equals(name) && m.isGetter) { + return m; + } + } + return null; + } + + public boolean hasEntry(String name) { + return entries.contains(name); + } + } + + /** + * A top-level {@code typedef}. For a function-type alias + * ({@code typedef Name = Ret Function(A, B);}) {@link #paramTypes} and + * {@link #returnType} hold the signature; for a plain alias + * ({@code typedef Name = Map;}) {@link #aliased} holds the + * target type and the function fields are null. + */ + public static class TypedefDecl extends Node { + public String name; + public List typeParams = new ArrayList(); + public List paramTypes; // non-null for a function-type alias + public TypeRef returnType; // non-null for a function-type alias + public TypeRef aliased; // non-null for a plain (non-function) alias + } + + public static class FieldDecl extends Node { + public TypeRef type; // may be VAR + public String name; + public String javaName; // from @JavaName('...') in stub files (top-level vars) + public Expr initializer; // nullable + public boolean isFinal; + public boolean isConst; + public boolean isStatic; + public boolean isLate; + } + + public static class Param extends Node { + public TypeRef type; // may be VAR (inferred from field for this./super.) + public String name; + public boolean named; + public boolean required; + public boolean isThis; // this.x + public boolean isSuper; // super.x + public Expr defaultValue; // nullable + } + + public static class CtorDecl extends Node { + public String name; // named constructor, null for unnamed + public boolean isConst; + public boolean isFactory; + public List params = new ArrayList(); + public List fieldInits = new ArrayList(); // initializer list entries + public SuperInit superInit; // nullable + public Block body; // nullable (";" body) + } + + public static class FieldInit extends Node { + public String field; + public Expr value; + } + + public static class SuperInit extends Node { + public String namedCtor; // nullable + public Args args = new Args(); + } + + public static class MethodDecl extends Node { + public TypeRef returnType; // may be VAR (=> inferred) or VOID + public String name; + public List typeParams = new ArrayList(); // generic method: m(...) + public List params = new ArrayList(); + public boolean isStatic; + public boolean isGetter; + public boolean isSetter; + public boolean isOverride; // had @override metadata + public boolean isAbstract; // no body + public boolean isAsync; + public boolean isSyncStar; // sync* generator body + public Block body; // nullable when isAbstract or expression-bodied + public Expr exprBody; // for `=> expr` + } + + public static class FunctionDecl extends Node { + public TypeRef returnType; + public String name; + public String javaName; // from @JavaName('...') in stub files + public boolean isExternal; + public boolean isAsync; + public boolean isSyncStar; // sync* generator body + public boolean isGetter; // top-level `T get x => ...` + public boolean isSetter; // top-level `set x(v) { ... }` + public List typeParams = new ArrayList(); // generic function: fn(...) + public List params = new ArrayList(); + public Block body; + public Expr exprBody; + } + + // ------------------------------------------------------------------ + // Statements + // ------------------------------------------------------------------ + + public static abstract class Stmt extends Node { + } + + public static class Block extends Stmt { + public List statements = new ArrayList(); + } + + public static class ExprStmt extends Stmt { + public Expr expr; + } + + public static class VarDeclStmt extends Stmt { + public TypeRef type; // may be VAR + public boolean isFinal; + public String name; + public Expr initializer; // nullable + } + + /** int a = 1, b = 2; — emitted as sibling declarations, no brace scope. */ + public static class VarDeclGroup extends Stmt { + public List decls = new ArrayList(); + } + + public static class IfStmt extends Stmt { + public Expr condition; + public Pattern casePattern; // Dart 3 if-case: `if (expr case pattern)`; nullable + public Expr caseGuard; // optional `when` guard on the if-case; nullable + public Stmt thenStmt; + public Stmt elseStmt; // nullable + } + + public static class WhileStmt extends Stmt { + public Expr condition; + public Stmt body; + } + + public static class ForStmt extends Stmt { + public Stmt init; // VarDeclStmt or ExprStmt or null + public Expr condition; // nullable + public List updates = new ArrayList(); + public Stmt body; + } + + public static class ForInStmt extends Stmt { + public TypeRef varType; // may be VAR + public String varName; + public Pattern pattern; // Dart 3 pattern for-in (destructuring); null for simple var + public Expr iterable; + public Stmt body; + } + + public static class ReturnStmt extends Stmt { + public Expr value; // nullable + } + + /** yield expr; / yield* expr; inside a sync* generator body. */ + public static class YieldStmt extends Stmt { + public Expr value; + public boolean star; // yield* (delegates to a sub-iterable) + } + + public static class BreakStmt extends Stmt { + } + + public static class ContinueStmt extends Stmt { + } + + /** + * A function declared inside a method/function body: + * {@code Ret name(params) { ... }}. Lowered by the emitter to a local + * variable holding a lambda bound to a {@code Funcs.*} functional interface, + * so later {@code name(args)} calls and bare {@code name} tear-offs resolve + * against the local. + */ + public static class LocalFunc extends Stmt { + public TypeRef returnType; // may be VOID / VAR + public String name; + public List params = new ArrayList(); + public Block body; // nullable when expression-bodied + public Expr exprBody; // for `=> expr` + public boolean isAsync; + } + + // ------------------------------------------------------------------ + // Expressions + // ------------------------------------------------------------------ + + public static abstract class Expr extends Node { + } + + public static class IntLit extends Expr { + public long value; + } + + public static class DoubleLit extends Expr { + public double value; + } + + public static class BoolLit extends Expr { + public boolean value; + } + + public static class NullLit extends Expr { + } + + /** String literal made of literal text parts and interpolated expressions. */ + public static class StringLit extends Expr { + public List parts = new ArrayList(); // String | Expr + } + + public static class ListLit extends Expr { + public TypeRef elementType; // nullable + public List elements = new ArrayList(); + public boolean isConst; + } + + public static class MapLit extends Expr { + public TypeRef keyType; // nullable + public TypeRef valueType; + public List keys = new ArrayList(); + public List values = new ArrayList(); + // Structured elements (MapEntry / IfElement / ForElement / SpreadElement) when the map + // literal contains collection if/for/spread; when non-empty the emitter uses a builder. + public List elements = new ArrayList(); + public boolean structured; + public boolean isConst; + } + public static class SetLit extends Expr { + public TypeRef elementType; // nullable + public List elements = new ArrayList(); // plain / spread / if / for + public boolean isConst; + } + + public static class Ident extends Expr { + public String name; + } + + public static class ThisExpr extends Expr { + } + + public static class SuperExpr extends Expr { + } + + /** throw expr (statement position only in M2). */ + public static class ThrowExpr extends Expr { + public Expr value; + } + + /** await expr */ + public static class AwaitExpr extends Expr { + public Expr operand; + } + + public static class CatchClause extends Node { + public TypeRef onType; // null for bare catch + public String exceptionVar; // null when `on T { }` has no catch part + public String stackVar; // nullable + public Block body; + } + + public static class TryStmt extends Stmt { + public Block tryBlock; + public List catches = new ArrayList(); + public Block finallyBlock; // nullable + } + + /** target..a()..b = c — target evaluated once, sections applied to it. */ + public static class Cascade extends Expr { + public Expr target; + /** {@code a?..b}: a null target skips every section. */ + public boolean nullAware; + /** Each section is an Expr tree rooted at a CascadeTarget marker. */ + public List sections = new ArrayList(); + } + + /** Marker for the implicit receiver inside a cascade section. */ + public static class CascadeTarget extends Expr { + } + + /** ...expr / ...?expr inside a collection literal. */ + public static class SpreadElement extends Expr { + public Expr expr; + public boolean nullAware; + } + + /** key: value entry inside a map literal (used when a map has structured if/for/spread elements). */ + public static class MapEntry extends Expr { + public Expr key; + public Expr value; + } + + /** if (cond) elem [else elem] inside a collection literal. */ + public static class IfElement extends Expr { + public Expr condition; + public Expr thenElement; + public Expr elseElement; // nullable + } + + /** for (…) elem inside a collection literal (for-in or classic). */ + public static class ForElement extends Expr { + public TypeRef varType; // for-in var (may be VAR); null for classic + public String varName; // for-in variable; null for classic + public Pattern pattern; // Dart 3 pattern for-in (destructuring); null otherwise + public Expr iterable; // for-in source; null for classic + public Stmt init; // classic parts (VarDeclStmt/ExprStmt) + public Expr condition; + public List updates = new ArrayList(); + public Expr body; // the element produced per iteration + } + + /** target.name (or target?.name when nullAware). */ + public static class PropertyGet extends Expr { + public Expr target; + public String name; + public boolean nullAware; + } + + /** target.name(args) — method call, or function/ctor call when target == null. */ + public static class Call extends Expr { + public Expr target; // null for bare calls + public String name; // method or function/class name; null when calling an expression value + public Args args = new Args(); + public boolean nullAware; + public List typeArgs = new ArrayList(); + } + + /** Explicit `new X(...)`/`const X(...)`/`X.named(...)` when syntactically unambiguous. */ + public static class CtorCall extends Expr { + public TypeRef type; + public String ctorName; // nullable (unnamed) + public Args args = new Args(); + public boolean isConst; + } + + public static class Args { + public List positional = new ArrayList(); + public List named = new ArrayList(); + } + + public static class NamedArg { + public String name; + public Expr value; + } + + public static class IndexGet extends Expr { + public Expr target; + public Expr index; + } + + public static class Assign extends Expr { + public Expr lhs; // Ident | PropertyGet | IndexGet + public String op; // "=", "+=", "-=", "*=", "/=", "~/=", "%=", "??=" + public Expr rhs; + } + + public static class Binary extends Expr { + public Expr left; + public String op; // + - * / ~/ % == != < > <= >= && || ?? + public Expr right; + } + + public static class Unary extends Expr { + public String op; // "-", "!", "~" + public Expr operand; + } + + /** ++x / --x / x++ / x-- */ + public static class IncDec extends Expr { + public Expr operand; + public boolean increment; + public boolean prefix; + } + + public static class Conditional extends Expr { + public Expr condition; + public Expr thenExpr; + public Expr elseExpr; + } + + /** x! */ + public static class NotNullAssert extends Expr { + public Expr operand; + } + + /** x is T / x is! T */ + public static class IsTest extends Expr { + public Expr operand; + public TypeRef type; + public boolean negated; + } + + /** x as T */ + public static class AsCast extends Expr { + public Expr operand; + public TypeRef type; + } + + public static class Lambda extends Expr { + public boolean isAsync; + public List params = new ArrayList(); + public Block body; // nullable + public Expr exprBody; // for `=> expr` + } + + public static class ParenExpr extends Expr { + public Expr inner; + } + + // ------------------------------------------------------------------ + // Dart 3: switch statements / expressions and patterns + // ------------------------------------------------------------------ + + /** A `switch (e) { case p when g: stmts; default: stmts; }` statement. */ + public static class SwitchStmt extends Stmt { + public Expr subject; + public List cases = new ArrayList(); + } + + /** One case (or the default) of a switch statement. */ + public static class SwitchCase extends Node { + public Pattern pattern; // null for the default case + public Expr guard; // optional `when` guard + public List body = new ArrayList(); + public boolean isDefault; + } + + /** A `switch (e) { p when g => v, _ => v }` expression. */ + public static class SwitchExpr extends Expr { + public Expr subject; + public List cases = new ArrayList(); + } + + /** One `pattern when guard => value` arm of a switch expression. */ + public static class SwitchExprCase extends Node { + public Pattern pattern; + public Expr guard; + public Expr value; + public boolean isDefault; // the `_` wildcard arm + } + + /** Base for Dart 3 patterns. */ + public abstract static class Pattern extends Node { + } + + /** A constant pattern: a literal or a (possibly qualified) constant reference. */ + public static class ConstantPattern extends Pattern { + public Expr value; + } + + /** A variable / wildcard pattern: `var x`, `final T x`, `T x`, or `_`. */ + public static class VariablePattern extends Pattern { + public TypeRef type; // null when untyped (`var x` / bare) + public String name; + public boolean wildcard; // true for `_` + } + + /** An object pattern: `Type(field: subpattern, ...)`. */ + public static class ObjectPattern extends Pattern { + public TypeRef type; + public List fields = new ArrayList(); + } + + /** A record pattern: `(subpattern, name: subpattern, ...)`. */ + public static class RecordPattern extends Pattern { + public List fields = new ArrayList(); + } + + /** A list pattern: `[p0, p1, ...]`. */ + public static class ListPattern extends Pattern { + public List elements = new ArrayList(); + } + + /** A relational pattern: `> 5`, `== x`, `<= y`, etc. */ + public static class RelationalPattern extends Pattern { + public String op; + public Expr operand; + } + + /** A cast pattern: `subpattern as T`. */ + public static class CastPattern extends Pattern { + public Pattern inner; + public TypeRef type; + } + + /** A logical-or pattern: `a || b || c`. */ + public static class OrPattern extends Pattern { + public List alternatives = new ArrayList(); + } + + /** A logical-and pattern: `a && b`. */ + public static class AndPattern extends Pattern { + public List parts = new ArrayList(); + } + + /** A field of an object or record pattern: an optional name plus a sub-pattern. */ + public static class PatternField extends Node { + public String name; // getter/field name (positional record field: null) + public Pattern pattern; + } + + // ------------------------------------------------------------------ + // Dart 3: record literals + // ------------------------------------------------------------------ + + /** A record literal: `(a, b, name: c)`. */ + public static class RecordLit extends Expr { + public List fields = new ArrayList(); + } + + /** One field of a record literal (name null for a positional field). */ + public static class RecordField extends Node { + public String name; + public Expr value; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/CaptureScan.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/CaptureScan.java new file mode 100644 index 00000000000..4f31671f0f2 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/CaptureScan.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.codegen; + +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.ast.Ast.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Conservative capture analysis for one method body: a local must be boxed + * into a Ref holder when it is referenced inside a closure AND assigned + * anywhere in the method (Java lambdas require effectively-final captures). + * + *

Conservative means: a name declared inside the closure itself that is + * also assigned gets boxed too — semantically correct, marginally less + * pretty output.

+ */ +final class CaptureScan { + + private final Set assigned = new HashSet(); + private final Set referencedInLambda = new HashSet(); + private final Set allReferenced = new HashSet(); + private int lambdaDepth; + + private CaptureScan() { + } + + static Set boxedLocals(Block body) { + CaptureScan scan = new CaptureScan(); + if (body != null) { + scan.walkBlock(body); + } + Set boxed = new HashSet(scan.assigned); + boxed.retainAll(scan.referencedInLambda); + return boxed; + } + + static Set boxedLocals(Expr exprBody) { + CaptureScan scan = new CaptureScan(); + if (exprBody != null) { + scan.walkExpr(exprBody); + } + Set boxed = new HashSet(scan.assigned); + boxed.retainAll(scan.referencedInLambda); + return boxed; + } + + /** + * True when {@code name} is referenced from inside a closure within + * {@code body}. A C-style for loop's index is reassigned by the loop's + * update clause, so a closure that captures it needs a per-iteration + * effectively-final copy (Dart binds the loop variable fresh each pass). + */ + static boolean readInLambda(Stmt body, String name) { + CaptureScan scan = new CaptureScan(); + scan.walkStmt(body); + return scan.referencedInLambda.contains(name); + } + + /** Every identifier name referenced anywhere in an expression. */ + static Set referencedNames(Expr e) { + CaptureScan scan = new CaptureScan(); + if (e != null) { + scan.walkExpr(e); + } + return scan.allReferenced; + } + + private void walkBlock(Block b) { + for (Stmt s : b.statements) { + walkStmt(s); + } + } + + private void walkStmt(Stmt s) { + if (s == null) { + return; + } + if (s instanceof Block) { + walkBlock((Block) s); + } else if (s instanceof ExprStmt) { + walkExpr(((ExprStmt) s).expr); + } else if (s instanceof VarDeclStmt) { + VarDeclStmt v = (VarDeclStmt) s; + if (v.initializer != null) { + walkExpr(v.initializer); + } + } else if (s instanceof IfStmt) { + IfStmt i = (IfStmt) s; + walkExpr(i.condition); + walkStmt(i.thenStmt); + walkStmt(i.elseStmt); + } else if (s instanceof WhileStmt) { + walkExpr(((WhileStmt) s).condition); + walkStmt(((WhileStmt) s).body); + } else if (s instanceof ForStmt) { + ForStmt f = (ForStmt) s; + walkStmt(f.init); + walkExpr(f.condition); + for (Expr e : f.updates) { + walkExpr(e); + } + walkStmt(f.body); + } else if (s instanceof ForInStmt) { + ForInStmt f = (ForInStmt) s; + walkExpr(f.iterable); + walkStmt(f.body); + } else if (s instanceof ReturnStmt) { + walkExpr(((ReturnStmt) s).value); + } else if (s instanceof Ast.LocalFunc) { + // A nested function is lowered to a lambda, so its body is a closure + // context: outer locals it references-and-mutates must be boxed too. + Ast.LocalFunc lf = (Ast.LocalFunc) s; + lambdaDepth++; + if (lf.body != null) { + walkBlock(lf.body); + } + walkExpr(lf.exprBody); + lambdaDepth--; + } + } + + private void walkExprs(List list) { + for (Expr e : list) { + walkExpr(e); + } + } + + private void walkExpr(Expr e) { + if (e == null) { + return; + } + if (e instanceof Ident) { + String nm = ((Ident) e).name; + allReferenced.add(nm); + if (lambdaDepth > 0) { + referencedInLambda.add(nm); + } + } else if (e instanceof Assign) { + Assign a = (Assign) e; + if (a.lhs instanceof Ident) { + assigned.add(((Ident) a.lhs).name); + } + walkExpr(a.lhs); + walkExpr(a.rhs); + } else if (e instanceof IncDec) { + IncDec i = (IncDec) e; + if (i.operand instanceof Ident) { + assigned.add(((Ident) i.operand).name); + } + walkExpr(i.operand); + } else if (e instanceof Lambda) { + Lambda l = (Lambda) e; + lambdaDepth++; + if (l.body != null) { + walkBlock(l.body); + } + walkExpr(l.exprBody); + lambdaDepth--; + } else if (e instanceof Binary) { + walkExpr(((Binary) e).left); + walkExpr(((Binary) e).right); + } else if (e instanceof Unary) { + walkExpr(((Unary) e).operand); + } else if (e instanceof Conditional) { + Conditional c = (Conditional) e; + walkExpr(c.condition); + walkExpr(c.thenExpr); + walkExpr(c.elseExpr); + } else if (e instanceof PropertyGet) { + walkExpr(((PropertyGet) e).target); + } else if (e instanceof Call) { + Call c = (Call) e; + if (c.name != null) { + allReferenced.add(c.name); + } + walkExpr(c.target); + walkExprs(c.args.positional); + for (NamedArg na : c.args.named) { + walkExpr(na.value); + } + } else if (e instanceof CtorCall) { + CtorCall c = (CtorCall) e; + walkExprs(c.args.positional); + for (NamedArg na : c.args.named) { + walkExpr(na.value); + } + } else if (e instanceof IndexGet) { + walkExpr(((IndexGet) e).target); + walkExpr(((IndexGet) e).index); + } else if (e instanceof ListLit) { + walkExprs(((ListLit) e).elements); + } else if (e instanceof MapLit) { + walkExprs(((MapLit) e).keys); + walkExprs(((MapLit) e).values); + } else if (e instanceof StringLit) { + for (Object part : ((StringLit) e).parts) { + if (part instanceof Expr) { + walkExpr((Expr) part); + } + } + } else if (e instanceof NotNullAssert) { + walkExpr(((NotNullAssert) e).operand); + } else if (e instanceof IsTest) { + walkExpr(((IsTest) e).operand); + } else if (e instanceof AsCast) { + walkExpr(((AsCast) e).operand); + } else if (e instanceof ParenExpr) { + walkExpr(((ParenExpr) e).inner); + } + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java new file mode 100644 index 00000000000..2530ff45db9 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -0,0 +1,9478 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.codegen; + +import com.codename1.dart.transpiler.analyze.Program; +import com.codename1.dart.transpiler.analyze.StubRegistry; +import com.codename1.dart.transpiler.api.Diagnostics; +import com.codename1.dart.transpiler.api.GeneratedFile; +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.ast.Ast.*; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Emits Java 17 source from the transpiler AST. + * + *

M1 emitter notes: + *

    + *
  • Type resolution is folded into emission (each expression returns its + * code and static type); the standalone resolver pipeline arrives with + * M2 when inference needs grow.
  • + *
  • Stub-class constructors with named arguments emit as + * allocate-then-setter sequences (ANF); program-class constructors use + * canonical positional order with defaults inlined at call sites.
  • + *
  • Known M1 semantic divergence: for stub widgets, the constructor runs + * before named-argument expressions are evaluated (widgets are pure + * config objects, so this is unobservable in practice).
  • + *
+ */ +public final class JavaEmitter { + + private final Program program; + private final StubRegistry stubs; + private final Diagnostics diags; + private final String pkg; + /** Distinct record shapes encountered during emission; one Java record class is generated per shape. */ + private final Map recordShapes = new LinkedHashMap(); + + /** The structural shape of a Dart record: positional arity plus the sorted names of named fields. */ + private static final class RecordShape { + int positional; + List named; + } + + public JavaEmitter(Program program, StubRegistry stubs, Diagnostics diags, String pkg) { + this.program = program; + this.stubs = stubs; + this.diags = diags; + this.pkg = pkg; + } + + // ================================================================== + // Top level + // ================================================================== + + // Codegen robustness: a resolver/emit gap on one declaration must not abort the whole build. + // Record it (with the crash site) so a single pass yields the full gap inventory. + private void emitCrash(Library lib, String what, Throwable ex) { + StackTraceElement top = null; + for (StackTraceElement s : ex.getStackTrace()) { + if (s.getClassName().startsWith("com.codename1.dart.transpiler")) { top = s; break; } + } + String at = top == null ? "" : " @ " + + top.getClassName().substring(top.getClassName().lastIndexOf('.') + 1) + + "." + top.getMethodName() + ":" + top.getLineNumber(); + diags.error(lib.fileName, 0, 0, "E0005", + "Codegen crash on " + what + ": " + ex.getClass().getSimpleName() + + (ex.getMessage() != null ? ": " + ex.getMessage() : "") + at); + } + + public List emit() { + List out = new ArrayList(); + String mainLib = null; + for (Library lib : program.libraries) { + for (ClassDecl c : lib.classes) { + try { + if (c.extensionOn != null) { + out.add(emitExtension(c)); + } else if (c.isMixin) { + out.add(emitMixin(c)); + } else { + out.add(emitClass(c)); + } + } catch (RuntimeException | StackOverflowError ex) { + emitCrash(lib, "class " + c.name, ex); + } + } + for (EnumDecl e : lib.enums) { + try { + out.add(emitEnum(e)); + } catch (RuntimeException | StackOverflowError ex) { + emitCrash(lib, "enum " + e.name, ex); + } + } + if (!lib.functions.isEmpty() || !lib.topLevelVars.isEmpty()) { + try { + out.add(emitLibClass(lib)); + } catch (RuntimeException | StackOverflowError ex) { + emitCrash(lib, "library " + lib.fileName, ex); + } + for (FunctionDecl f : lib.functions) { + if (f.name.equals("main")) { + mainLib = Program.libClassName(lib.fileName); + } + } + } + } + if (mainLib != null) { + out.add(emitRegistry(mainLib)); + } + // record classes are discovered lazily while emitting bodies, so generate them last + for (Map.Entry e : recordShapes.entrySet()) { + out.add(emitRecordClass(e.getKey(), e.getValue())); + } + return out; + } + + /** Registers a record shape (idempotent) and returns its generated class name. */ + private String registerRecordShape(int positional, List namedSorted) { + StringBuilder n = new StringBuilder("Rec$").append(positional); + for (String nm : namedSorted) { + n.append('$').append(nm); + } + String cn = n.toString(); + if (!recordShapes.containsKey(cn)) { + RecordShape s = new RecordShape(); + s.positional = positional; + s.named = namedSorted; + recordShapes.put(cn, s); + } + return cn; + } + + /** Emits a generic Java record class for a record shape (component types are the type parameters). */ + private GeneratedFile emitRecordClass(String cn, RecordShape s) { + int total = s.positional + s.named.size(); + StringBuilder tp = new StringBuilder(); + StringBuilder comps = new StringBuilder(); + for (int i = 0; i < total; i++) { + if (i > 0) { + tp.append(", "); + comps.append(", "); + } + tp.append("T").append(i); + String comp = i < s.positional ? "$" + (i + 1) : s.named.get(i - s.positional); + comps.append("T").append(i).append(' ').append(comp); + } + StringBuilder sb = new StringBuilder(); + sb.append("package ").append(pkg).append(";\n\n"); + sb.append("/** Generated Dart record type (structural shape ").append(cn).append("). */\n"); + sb.append("public record ").append(cn); + if (total > 0) { + sb.append('<').append(tp).append('>'); + } + sb.append('(').append(comps).append(") {\n}\n"); + return new GeneratedFile(cn + ".java", sb.toString()); + } + + private Out emitRecordLit(RecordLit r, Ctx ctx) { + List positional = new ArrayList(); + List named = new ArrayList(); + for (RecordField f : r.fields) { + if (f.name == null) { + positional.add(f); + } else { + named.add(f); + } + } + named.sort((a, b) -> a.name.compareTo(b.name)); + List namedNames = new ArrayList(); + for (RecordField f : named) { + namedNames.add(f.name); + } + String cn = registerRecordShape(positional.size(), namedNames); + TypeRef t = new TypeRef(cn); + StringBuilder args = new StringBuilder(); + boolean first = true; + for (RecordField f : positional) { + if (!first) { + args.append(", "); + } + first = false; + Out o = emitExpr(f.value, null, ctx); + args.append(boxIfPrimitive(o, ctx)); + t.args.add(boxType(o.type)); + } + for (RecordField f : named) { + if (!first) { + args.append(", "); + } + first = false; + Out o = emitExpr(f.value, null, ctx); + args.append(boxIfPrimitive(o, ctx)); + t.args.add(boxType(o.type)); + } + return new Out("new " + cn + "<>(" + args + ")", t); + } + + /** The static type of a record component accessed by name (`$1`, `$2`, or a named field). */ + private TypeRef recordComponentType(TypeRef recordType, String name) { + RecordShape s = recordShapes.get(recordType.name); + if (s == null) { + return TypeRef.DYNAMIC; + } + int idx = -1; + if (name.length() > 1 && name.charAt(0) == '$') { + try { + idx = Integer.parseInt(name.substring(1)) - 1; + } catch (NumberFormatException ignored) { + idx = -1; + } + } else { + int at = s.named.indexOf(name); + if (at >= 0) { + idx = s.positional + at; + } + } + return idx >= 0 && idx < recordType.args.size() ? recordType.args.get(idx) : TypeRef.DYNAMIC; + } + + private GeneratedFile emitRegistry(String mainLib) { + StringBuilder sb = new StringBuilder(); + sb.append("package ").append(pkg).append(";\n\n"); + sb.append("/** Generated entry-point registry for transpiled Flutter code. */\n"); + sb.append("public final class FlutterRegistry {\n"); + sb.append(" private FlutterRegistry() {\n }\n\n"); + sb.append(" /** Invokes the Dart main() of the application's main library. */\n"); + sb.append(" public static void invokeMain() {\n"); + sb.append(" ").append(mainLib).append(".main$();\n"); + sb.append(" }\n"); + sb.append("}\n"); + return new GeneratedFile("FlutterRegistry.java", sb.toString()); + } + + private GeneratedFile emitEnum(EnumDecl e) { + StringBuilder sb = new StringBuilder(); + sb.append("package ").append(pkg).append(";\n\n"); + // Dart 2.17 enhanced enums carry a body (fields, methods, constructors). Emit the + // members through the same machinery as a class, with a synthetic ClassDecl standing + // in for `this`-typing and own-member resolution inside the bodies. + boolean enhanced = !e.methods.isEmpty() || !e.fields.isEmpty() || !e.ctors.isEmpty(); + ClassDecl syn = new ClassDecl(); + syn.name = e.name; + syn.fields = e.fields; + syn.methods = e.methods; + syn.ctors = e.ctors; + Ctx ctx = new Ctx(syn); + StringBuilder body = new StringBuilder(); + for (FieldDecl f : e.fields) { + TypeRef ft = fieldType(f, ctx); + String jt = javaType(ft, false, ctx); + if (f.isStatic) { + body.append(f.name.startsWith("_") ? " static " : " public static "); + } else { + body.append(" private "); + } + if ((f.isFinal || f.isConst)) { + body.append("final "); + } + body.append(jt).append(' ').append(f.name).append(";\n"); + if (!f.name.startsWith("_") && !f.isStatic) { + body.append(" public ").append(jt).append(" get$").append(f.name) + .append("() {\n return ").append(f.name).append(";\n }\n"); + } + } + for (CtorDecl ct : e.ctors) { + body.append(emitCtor(syn, ct, ctx)); + } + for (MethodDecl m : e.methods) { + Method mm = new Method(); + mm.name = m.name; + mm.isStatic = m.isStatic; + mm.isGetter = m.isGetter; + mm.isSetter = m.isSetter; + // Enum methods may override Enum.toString etc.; @Override is optional in Java, so + // omit it rather than risk annotating a method that overrides nothing. + mm.isOverride = false; + mm.isAbstract = m.isAbstract; + mm.isAsync = m.isAsync; + mm.isSyncStar = m.isSyncStar; + mm.returnType = m.returnType; + mm.params = m.params; + mm.typeParams = m.typeParams; + mm.body = m.body; + mm.exprBody = m.exprBody; + body.append(emitMethodLike(mm, ctx, false)); + } + for (String imp : ctx.imports.values()) { + sb.append("import ").append(imp).append(";\n"); + } + if (!ctx.imports.isEmpty()) { + sb.append('\n'); + } + sb.append(dartRef(e)).append("\n"); + sb.append("public enum ").append(e.name).append(" {\n "); + StringBuilder names = new StringBuilder(); + for (int i = 0; i < e.entries.size(); i++) { + if (i > 0) { + sb.append(", "); + names.append(", "); + } + sb.append(e.entries.get(i)); + // the constant's simple name (enhanced enums carry `name(args)`) + String entry = e.entries.get(i); + int paren = entry.indexOf('('); + names.append((paren >= 0 ? entry.substring(0, paren) : entry).trim()); + } + // Dart's `EnumType.values` is a `List`; expose it as a DartList field + // (coexisting with Java's implicit values() method) so `.values.idx(i)` resolves. + sb.append(";\n\n"); + sb.append(" public static final dart.core.DartList<").append(e.name) + .append("> values = dart.core.DartList.<").append(e.name).append(">of(") + .append(names).append(");\n\n"); + sb.append(body); + sb.append("}\n"); + return new GeneratedFile(e.name + ".java", sb.toString()); + } + + /** + * Whether a top-level variable must initialise LAZILY, on first read. + * + *

Dart initialises every top-level and static variable on first access, + * so the order they are written in cannot matter. Java runs static + * initialisers top to bottom, so the same source emitted as plain fields + * silently reads a not-yet-assigned neighbour as null. Shrine's theme is + * the shape of it:

+ * + *
+     *   final ThemeData shrineTheme = _buildShrineTheme();   // reads the scheme
+     *   final ColorScheme _shrineColorScheme = ColorScheme(...);
+     * 
+ * + *

which threw {@code ExceptionInInitializerError} on the class's very + * first use — the whole study failed to open. Only initialisers that + * cannot depend on anything (a bare literal) stay plain fields, so the + * common {@code const kPadding = 8.0} keeps reading as a constant.

+ */ + private static boolean isLazyTopLevel(FieldDecl v) { + return v.initializer != null && !isSelfContainedLiteral(v.initializer); + } + + /** + * A class's static field initialised lazily, for the same reason as a top-level + * one and more: Java initialises every static of a class the first time ANY of them + * is touched, while Dart initialises each on its own first read. Reordering the + * statics by dependency fixed the null-neighbour half and not this one -- reading + * a harmless {@code C.ready} still ran an unrelated + * {@code static final expensive = fail()} and threw, or ran side effects Dart would + * have deferred until {@code expensive} was read. + */ + /** + * A {@code late} field with no initializer, which Dart checks: reading it before it is + * assigned throws LateInitializationError, and so does assigning a {@code late final} + * one twice. Emitted as a plain field it read as 0, false or null and could be + * reassigned at will. Such a field is reached only through the get$/set$ pair + * {@link #emitLateAccessors} writes, so every write form -- `=`, `op=`, `??=`, `++` -- + * goes through the existing accessor-pair lowering. Mixin fields already live behind + * accessors of their own and are left as they are. + */ + private static boolean isLateChecked(FieldDecl f, ClassDecl owner) { + return f.isLate && f.initializer == null && !f.isConst && owner != null && !owner.isMixin; + } + + private static String emitLateAccessors(FieldDecl f, String jt) { + String st = f.isStatic ? "static " : ""; + String vis = f.name.startsWith("_") ? " " : " public "; + String flag = f.name + "$late"; + String self = f.isStatic ? "" : "this."; + StringBuilder b = new StringBuilder(); + b.append(" private ").append(st).append("boolean ").append(flag).append(";\n"); + b.append(vis).append(st).append(jt).append(" get$").append(f.name).append("() {\n") + .append(" if (!").append(flag).append(") {\n") + .append(" throw dart.core.LateInitializationError.notInitialized(\"") + .append(f.name).append("\");\n }\n") + .append(" return ").append(self).append(f.name).append(";\n }\n"); + b.append(vis).append(st).append(jt).append(" set$").append(f.name).append("(").append(jt).append(" v) {\n"); + if (f.isFinal) { + b.append(" if (").append(flag).append(") {\n") + .append(" throw dart.core.LateInitializationError.alreadyInitialized(\"") + .append(f.name).append("\");\n }\n"); + } + b.append(" ").append(flag).append(" = true;\n") + .append(" ").append(self).append(f.name).append(" = v;\n") + .append(" return v;\n }\n"); + return b.toString(); + } + + /** A constructor's store into its own field: through set$ for a checked late field. */ + private static String fieldStore(ClassDecl c, String field, String value) { + FieldDecl f = c.field(field); + if (f != null && isLateChecked(f, c)) { + return "this.set$" + field + "(" + value + ");"; + } + return "this." + field + " = " + value + ";"; + } + + private static boolean isLazyStatic(FieldDecl f) { + return f.isStatic && isLazyTopLevel(f); + } + + /** A literal whose value cannot reference any other declaration. */ + private static boolean isSelfContainedLiteral(Expr e) { + if (e instanceof Ast.IntLit || e instanceof Ast.DoubleLit + || e instanceof Ast.BoolLit || e instanceof Ast.NullLit) { + return true; + } + if (e instanceof Ast.StringLit) { + for (Object part : ((Ast.StringLit) e).parts) { + if (!(part instanceof String)) { + return false; // interpolation can read anything + } + } + return true; + } + if (e instanceof Ast.Unary) { + return isSelfContainedLiteral(((Ast.Unary) e).operand); + } + return false; + } + + /** + * A top-level variable as a lazily-initialised accessor pair. Named + * {@code get$x} / {@code set$x} so reads and writes route through the + * emitter's existing accessor handling — {@code x = v} becomes + * {@code Lib.set$x(v)} with no special case at the assignment site. + */ + private String emitLazyTopLevel(FieldDecl v, TypeRef vt, String jt, Ctx ctx) { + ctx.pushWriter(4); + Out init = emitExpr(v.initializer, vt, ctx); + String lifted = ctx.popWriter(); + StringBuilder sb = new StringBuilder(); + sb.append(" private static ").append(jt).append(' ').append(v.name) + .append("$value;\n"); + sb.append(" private static boolean ").append(v.name).append("$ready;\n\n"); + sb.append(" /** Dart `").append(v.name) + .append("` -- initialised on first read, as Dart does. */\n"); + sb.append(" public static ").append(jt).append(" get$").append(v.name) + .append("() {\n"); + sb.append(" if (!").append(v.name).append("$ready) {\n"); + // Marked ready BEFORE the initialiser runs: a variable whose own + // initialiser reads it back is a cycle, and returning the zero value + // beats recursing until the stack goes. + sb.append(" ").append(v.name).append("$ready = true;\n"); + // An initialiser that throws leaves the variable uninitialised in Dart, and the + // next read runs it again. Left marked ready, every later read answered the + // Java zero value -- null, 0 or false, even in a non-nullable variable. + sb.append(" try {\n"); + sb.append(lifted); + sb.append(" ").append(v.name).append("$value = ") + .append(coerce(init, vt, ctx)).append(";\n"); + sb.append(" } catch (Throwable $e) {\n"); + sb.append(" ").append(v.name).append("$ready = false;\n"); + sb.append(" throw $e;\n"); + sb.append(" }\n"); + sb.append(" }\n"); + sb.append(" return ").append(v.name).append("$value;\n"); + sb.append(" }\n\n"); + // Answers the value stored, as a Dart assignment expression does, so an + // accessor write can stand where a value is needed (`x ??= v`, `y = x = v`). + sb.append(" public static ").append(jt).append(" set$").append(v.name).append('(') + .append(jt).append(" $v) {\n"); + sb.append(" ").append(v.name).append("$ready = true;\n"); + sb.append(" ").append(v.name).append("$value = $v;\n"); + sb.append(" return $v;\n"); + sb.append(" }\n\n"); + return sb.toString(); + } + + private GeneratedFile emitLibClass(Library lib) { + Ctx ctx = new Ctx(null); + ctx.currentLibrary = lib; + String cls = Program.libClassName(lib.fileName); + StringBuilder body = new StringBuilder(); + for (FieldDecl v : lib.topLevelVars) { + TypeRef vt = fieldType(v, ctx); + String jt = javaType(vt, false, ctx); + if (isLazyTopLevel(v)) { + body.append(emitLazyTopLevel(v, vt, jt, ctx)); + continue; + } + body.append(" public static ").append(jt).append(' ').append(v.name); + if (v.initializer != null) { + ctx.pushWriter(2); + Out init = emitExpr(v.initializer, vt, ctx); + String lifted = ctx.popWriter(); + if (lifted.isEmpty()) { + body.append(" = ").append(coerce(init, vt, ctx)).append(";\n"); + } else { + body.append(";\n\n static {\n").append(lifted) + .append(" ").append(v.name).append(" = ") + .append(coerce(init, vt, ctx)).append(";\n }\n"); + } + } else { + body.append(" = ").append(zeroValue(vt)).append(";\n"); + } + } + if (!lib.topLevelVars.isEmpty()) { + body.append('\n'); + } + for (FunctionDecl f : lib.functions) { + Method m = new Method(); + m.isStatic = true; + m.isAsync = f.isAsync; + m.isSyncStar = f.isSyncStar; + m.name = f.name.equals("main") ? "main$" : f.name; + m.returnType = f.returnType; + m.params = f.params; + m.typeParams = f.typeParams; + m.body = f.body; + m.exprBody = f.exprBody; + body.append(emitMethodLike(m, ctx, false)); + } + return finishClassFile(cls, "public final class " + cls, null, null, + " private " + cls + "() {\n }\n\n" + body, ctx, lib.fileName); + } + + /** + * extension X on T { ... } — a final class of static methods whose first + * parameter is the receiver ($self). Bodies see `this` as $self; bare + * member CALLS are probed against the receiver type (intrinsics, stubs, + * other extensions); bare PROPERTY access needs explicit `this.`. + */ + private GeneratedFile emitExtension(ClassDecl ext) { + Ctx ctx = new Ctx(null); + ctx.currentLibrary = ext.ownerLibrary; + ctx.extensionSelfType = ext.extensionOn; + StringBuilder body = new StringBuilder(); + body.append(" private ").append(ext.name).append("() {\n }\n\n"); + for (MethodDecl m : ext.methods) { + ctx.pushScope(); + TypeRef rt = m.returnType == null || m.returnType.is("var") ? TypeRef.DYNAMIC : m.returnType; + StringBuilder sig = new StringBuilder(); + sig.append(" public static ").append(m.isSetter ? "void" : javaType(rt, false, ctx)) + .append(' ').append(m.name).append('(') + .append(javaType(ext.extensionOn, false, ctx)).append(" $self"); + for (Param pm : m.params) { + TypeRef pt = pm.type == null || pm.type.is("var") ? TypeRef.DYNAMIC : pm.type; + sig.append(", ").append(javaType(pt, false, ctx)).append(' ') + .append(ctx.declareShadowSafe(pm.name, pt)); + } + sig.append(") {\n"); + body.append(sig); + ctx.pushWriter(2); + ctx.methodReturnType = rt; + if (m.body != null) { + emitStatements(m.body, ctx); + } else if (m.exprBody != null) { + Out o = emitExpr(m.exprBody, rt.is("void") ? null : rt, ctx); + if (rt.is("void")) { + ctx.writer().line(statementize(o.code) + ";"); + } else { + ctx.writer().line("return " + coerce(o, rt, ctx) + ";"); + } + } + body.append(ctx.popWriter()); + ctx.methodReturnType = null; + ctx.popScope(); + body.append(" }\n\n"); + } + return finishClassFile(ext.name, "public final class " + ext.name, null, null, body, ctx, ext.file); + } + + /** + * mixin M { ... } — a Java interface with default methods; mixin fields + * become abstract get$x/set$x accessor pairs that the applying class + * synthesizes (Java interfaces hold no state). + */ + private GeneratedFile emitMixin(ClassDecl mx) { + Ctx ctx = new Ctx(mx); + StringBuilder body = new StringBuilder(); + for (FieldDecl f : mx.fields) { + TypeRef ft = fieldType(f, ctx); + String jt = javaType(ft, false, ctx); + body.append(" ").append(jt).append(" get$").append(f.name).append("();\n"); + body.append(" void set$").append(f.name).append('(').append(jt).append(" v);\n\n"); + } + for (MethodDecl m : mx.methods) { + if (m.isStatic) { + diags.error(m, "E0401", "Static mixin members are not supported yet"); + continue; + } + ctx.pushScope(); + TypeRef rt = m.returnType == null || m.returnType.is("var") ? TypeRef.DYNAMIC : m.returnType; + body.append(" default ").append(m.isSetter ? "void" : javaType(rt, false, ctx)) + .append(' ').append(m.isGetter ? m.name : m.name).append('('); + for (int i = 0; i < m.params.size(); i++) { + Param pm = m.params.get(i); + TypeRef pt = pm.type == null || pm.type.is("var") ? TypeRef.DYNAMIC : pm.type; + if (i > 0) { + body.append(", "); + } + body.append(javaType(pt, false, ctx)).append(' ') + .append(ctx.declareShadowSafe(pm.name, pt)); + } + body.append(") {\n"); + ctx.pushWriter(2); + ctx.methodReturnType = rt; + ctx.inAsyncBody = m.isAsync; + if (m.body != null) { + emitStatements(m.body, ctx); + } else if (m.exprBody != null) { + Out o = emitExpr(m.exprBody, rt.is("void") ? null : rt, ctx); + if (rt.is("void")) { + ctx.writer().line(statementize(o.code) + ";"); + } else { + ctx.writer().line("return " + coerce(o, rt, ctx) + ";"); + } + } + body.append(ctx.popWriter()); + ctx.methodReturnType = null; + ctx.inAsyncBody = false; + ctx.popScope(); + body.append(" }\n\n"); + } + return finishClassFile(mx.name, "public interface " + mx.name, null, null, body, ctx, mx.file); + } + + private GeneratedFile emitClass(ClassDecl c) { + Ctx ctx = new Ctx(c); + StringBuilder body = new StringBuilder(); + + // fields — Dart initializes statics lazily and order-independently, but Java runs + // static field initializers top-to-bottom, so a static whose initializer reads a + // later static reads null. Reorder statics so dependencies initialize first. + for (FieldDecl f : orderStaticFieldsByDependency(c)) { + TypeRef ft = fieldType(f, ctx); + String jt = javaType(ft, false, ctx); + if (isLazyStatic(f)) { + body.append(emitLazyTopLevel(f, ft, jt, ctx)); + continue; + } + // Instance fields are private (accessed via get$/set$ accessors). Static fields + // are read directly as ClassName.field with no accessor, so a public Dart static + // (no leading underscore) must be public here; a library-private (_x) static must + // be package-private so sibling classes in the same generated package can reach it. + if (f.isStatic) { + body.append(f.name.startsWith("_") ? " static " : " public static "); + } else { + body.append(" private "); + } + if ((f.isFinal || f.isConst) && f.initializer != null) { + body.append("final "); + } + body.append(jt).append(' ').append(f.name); + if (f.initializer != null) { + ctx.pushWriter(2); + Out init = emitExpr(f.initializer, ft, ctx); + String lifted = ctx.popWriter(); + if (lifted.isEmpty()) { + body.append(" = ").append(coerce(init, ft, ctx)).append(";\n"); + } else { + // complex initializer (e.g. named-arg constructor): move it + // into an initializer block, which runs for every ctor + body.append(";\n\n ").append(f.isStatic ? "static {" : "{").append('\n'); + body.append(lifted); + body.append(" ").append(f.isStatic ? "" : "this.").append(f.name) + .append(" = ").append(coerce(init, ft, ctx)).append(";\n"); + body.append(" }\n"); + } + } else { + body.append(";\n"); + } + if (isLateChecked(f, c)) { + body.append(emitLateAccessors(f, jt)); + body.append('\n'); + continue; + } + // Accessors for instance fields. A Dart library-private (`_x`) field is still + // reachable from sibling classes in the same library, so emit its accessor + // package-private (all generated classes share one package) rather than skip it; + // cross-instance reads compile to `x.get$_field()`. + if (!f.isStatic) { + boolean priv = f.name.startsWith("_"); + String vis = priv ? " " : " public "; + body.append(vis).append(jt).append(" get$").append(f.name).append("() {\n") + .append(" return ").append(f.name).append(";\n }\n"); + if (!f.isFinal && !f.isConst) { + body.append(vis).append("void set$").append(f.name).append("(").append(jt).append(" v) {\n") + .append(" this.").append(f.name).append(" = v;\n }\n"); + } + } + body.append('\n'); + } + + // constructors + if (c.hasNamedNonFactoryCtor()) { + body.append(" /** Marker distinguishing named-constructor instantiation. */\n"); + body.append(" private static final class $NamedCtor {\n private $NamedCtor() {\n }\n }\n\n"); + body.append(" private ").append(javaClassName(c)).append("($NamedCtor $marker) {\n }\n\n"); + } + for (CtorDecl ct : c.ctors) { + body.append(emitCtor(c, ct, ctx)); + } + + // Dart operator== ($eq) overrides Java equals via a bridge + MethodDecl eqOp = c.method("$eq"); + if (eqOp != null && eqOp.params.size() == 1) { + String otherType = javaType(eqOp.params.get(0).type == null + ? TypeRef.DYNAMIC : eqOp.params.get(0).type, true, ctx); + body.append(" @Override\n public boolean equals(Object $o) {\n") + .append(" return $o instanceof ").append(otherType) + .append(" && $eq((").append(otherType).append(") $o);\n }\n\n"); + } + + // methods + for (MethodDecl m : c.methods) { + Method mm = new Method(); + mm.name = m.name; + mm.isStatic = m.isStatic; + mm.isGetter = m.isGetter; + mm.isSetter = m.isSetter; + mm.isOverride = javaOverrides(c, m); + mm.isAbstract = m.isAbstract; + mm.isAsync = m.isAsync; + mm.isSyncStar = m.isSyncStar; + mm.returnType = m.returnType; + // Dart lets a value-returning method override a void one; Java forbids it, so pin the + // override's return to void to keep it a valid override. + if (overriddenReturnsVoid(c, m)) { + mm.returnType = TypeRef.VOID; + } + mm.params = m.params; + mm.typeParams = m.typeParams; + mm.body = m.body; + mm.exprBody = m.exprBody; + body.append(emitMethodLike(mm, ctx, c.isAbstract)); + } + + // mixin applications: implement each mixin interface and synthesize + // the state (field + accessors) the mixin's abstract accessors need + StringBuilder impls = new StringBuilder(); + for (TypeRef mixRef : c.mixins) { + ClassDecl mx = program.classes.get(mixRef.name); + if (mx == null || !mx.isMixin) { + // A mixin supplied by the hand-written runtime (a stub) maps to a + // Java interface (its @JavaName) with default-method behaviour and no + // synthesized state; the applying class simply implements it. Bare + // calls to the mixin's members resolve through emitBareCall. + if (stubs.isStubClass(mixRef.name)) { + if (impls.length() > 0) { + impls.append(", "); + } + impls.append(javaType(mixRef, false, ctx)); + emitChangeNotifierState(mixRef, body); + continue; + } + diags.error(c, "E0402", "Unknown mixin: " + mixRef.name); + continue; + } + if (impls.length() > 0) { + impls.append(", "); + } + impls.append(mixRef.name); + for (FieldDecl f : mx.fields) { + TypeRef ft = fieldType(f, ctx); + String jt = javaType(ft, false, ctx); + body.append(" private ").append(jt).append(' ').append(f.name); + if (f.initializer != null) { + ctx.pushWriter(2); + Out init = emitExpr(f.initializer, ft, ctx); + String lifted = ctx.popWriter(); + if (lifted.isEmpty()) { + body.append(" = ").append(coerce(init, ft, ctx)); + } else { + diags.error(f, "E0403", "Complex mixin field initializers are not supported yet"); + } + } + body.append(";\n"); + body.append(" public ").append(jt).append(" get$").append(f.name) + .append("() {\n return ").append(f.name).append(";\n }\n"); + body.append(" public void set$").append(f.name).append('(').append(jt) + .append(" v) {\n this.").append(f.name).append(" = v;\n }\n\n"); + } + } + // Dart `implements X` clauses (c.interfaces): the runtime interface the class satisfies + // (e.g. `implements Iterator` / `PreferredSizeWidget`). Emitted as Java `implements`. + for (TypeRef itf : c.interfaces) { + if (impls.length() > 0) { + impls.append(", "); + } + impls.append(javaType(itf, false, ctx)); + emitChangeNotifierState(itf, body); + } + body.append(emitStubGetterBridges(c, ctx)); + // Dart 3 sealed → Java sealed: a sealed class with subtypes lists them in a permits clause and + // its direct subtypes are marked non-sealed. Falls back to a plain abstract class when the + // hierarchy has no subtypes (a permits-less sealed class is illegal in Java). + List subtypes = directSubtypes(c.name); + boolean sealedSelf = c.isSealed && !subtypes.isEmpty(); + String modifier = ""; + if (!c.isSealed) { + List supers = new ArrayList(c.interfaces); + if (c.superclass != null) { + supers.add(c.superclass); + } + for (TypeRef sr : supers) { + ClassDecl sup = program.classes.get(sr.name); + if (sup != null && sup.isSealed && !directSubtypes(sup.name).isEmpty()) { + modifier = "non-sealed "; + break; + } + } + } + StringBuilder typeParamsSb = new StringBuilder(); + if (c.typeParams != null && !c.typeParams.isEmpty()) { + typeParamsSb.append('<'); + for (int i = 0; i < c.typeParams.size(); i++) { + if (i > 0) { + typeParamsSb.append(", "); + } + typeParamsSb.append(c.typeParams.get(i)); + } + typeParamsSb.append('>'); + } + String jname = javaClassName(c); + String decl = "public " + modifier + (sealedSelf ? "sealed " : "") + + (c.isAbstract ? "abstract " : "") + "class " + jname + typeParamsSb; + String ext = null; + if (c.superclass != null) { + ext = javaType(c.superclass, false, ctx); + } + String permits = null; + if (sealedSelf) { + StringBuilder pb = new StringBuilder(); + for (int i = 0; i < subtypes.size(); i++) { + if (i > 0) { + pb.append(", "); + } + pb.append(subtypes.get(i)); + } + permits = pb.toString(); + } + return finishClassFile(jname, decl, ext, impls.length() == 0 ? null : impls.toString(), + permits, body, ctx, c.file); + } + + /** Names of the classes that directly extend or implement the named class (whole-program). */ + private List directSubtypes(String name) { + List subs = new ArrayList(); + for (ClassDecl c : program.classes.values()) { + if (c.extensionOn != null || c.isMixin) { + continue; + } + boolean extendsIt = c.superclass != null && name.equals(c.superclass.name); + boolean implementsIt = false; + for (TypeRef itf : c.interfaces) { + if (name.equals(itf.name)) { + implementsIt = true; + break; + } + } + if (extendsIt || implementsIt) { + // The EMITTED name, not the Dart one. A subtype whose name collides with a + // stub class is emitted under a library-qualified name, and a permits clause + // naming the Dart name then refers to a class that does not exist - which is + // not a niche case, since the Flutter stubs declare Rect, Size, Color and + // plenty of other names an app will reasonably use for its own sealed types. + subs.add(javaClassName(c)); + } + } + return subs; + } + + private GeneratedFile finishClassFile(String name, String decl, String ext, String impls, + CharSequence body, Ctx ctx, String dartFile) { + return finishClassFile(name, decl, ext, impls, null, body, ctx, dartFile); + } + + private GeneratedFile finishClassFile(String name, String decl, String ext, String impls, + String permits, CharSequence body, Ctx ctx, String dartFile) { + StringBuilder sb = new StringBuilder(); + sb.append("package ").append(pkg).append(";\n\n"); + for (String imp : ctx.imports.values()) { + sb.append("import ").append(imp).append(";\n"); + } + if (!ctx.imports.isEmpty()) { + sb.append('\n'); + } + sb.append("// Generated from ").append(dartFile).append(" — do not edit.\n"); + sb.append(decl); + if (ext != null) { + sb.append(" extends ").append(ext); + } + if (impls != null && !impls.isEmpty()) { + sb.append(" implements ").append(impls); + } + if (permits != null && !permits.isEmpty()) { + sb.append(" permits ").append(permits); + } + sb.append(" {\n\n").append(body).append("}\n"); + return new GeneratedFile(name + ".java", sb.toString()); + } + + private String dartRef(Node n) { + return "// Generated from " + n.file + " — do not edit."; + } + + // ================================================================== + // Constructors + // ================================================================== + + /** Canonical parameter order: positional as declared, then named as declared. */ + private String emitCtor(ClassDecl c, CtorDecl ct, Ctx ctx) { + if (ct.isFactory) { + return emitFactoryCtor(c, ct, ctx); + } + if (ct.name != null) { + return emitNamedCtor(c, ct, ctx); + } + StringBuilder sb = new StringBuilder(); + sb.append(" public ").append(javaClassName(c)).append('('); + ctx.pushScope(); + List params = ct.params; + for (int i = 0; i < params.size(); i++) { + Param p = params.get(i); + TypeRef pt = paramType(c, p, ctx); + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(pt, false, ctx)).append(' ').append(ctx.declareShadowSafe(p.name, pt)); + } + sb.append(") {\n"); + Ctx.Writer w = ctx.pushWriter(2); + + // super(...) initializer for program superclasses + ClassDecl progSuper = c.superclass != null ? program.classes.get(c.superclass.name) : null; + if (progSuper != null) { + Args superArgs = ct.superInit != null ? ct.superInit.args : new Args(); + // super.x params contribute as named args + for (Param p : params) { + if (p.isSuper) { + NamedArg na = new NamedArg(); + na.name = p.name; + Ident id = new Ident(); + id.name = p.name; + na.value = id; + superArgs.named.add(na); + } + } + CtorDecl superCtor = progSuper.defaultCtor(); + // Nothing may precede super(...), so its arguments cannot be sequenced + // through lifted temps; they keep the parameter order. + inSuperInitializer = true; + try { + w.line("super(" + canonicalArgs(superCtor, superArgs, ctx) + ");"); + } finally { + inSuperInitializer = false; + } + } else if (ct.superInit != null && ct.superInit.args != null + && stubClassOf(c.superclass) != null) { + for (NamedArg na : ct.superInit.args.named) { + Out v = emitExpr(na.value, null, ctx); + w.line("this." + na.name + "(" + v.code + ");"); + } + } + // super.x params against stub superclasses -> inherited setters + if (progSuper == null) { + for (Param p : params) { + if (p.isSuper) { + w.line("this." + p.name + "(" + javaIdent(p.name) + ");"); + } + } + } + // this.x params + for (Param p : params) { + if (p.isThis) { + w.line(fieldStore(c, p.name, javaIdent(p.name))); + } + } + // initializer list entries + for (FieldInit fi : ct.fieldInits) { + Out v = emitExpr(fi.value, typeOfField(c, fi.field, ctx), ctx); + w.line(fieldStore(c, fi.field, v.code)); + } + if (ct.body != null) { + emitStatements(ct.body, ctx); + } + sb.append(ctx.popWriter()); + ctx.popScope(); + sb.append(" }\n\n"); + return sb.toString(); + } + + /** + * factory Foo(...) / factory Foo.name(...) — a static method returning + * the class; unnamed factories become {@code $create} and call sites + * route through it. + */ + private String emitFactoryCtor(ClassDecl c, CtorDecl ct, Ctx ctx) { + StringBuilder sb = new StringBuilder(); + String name = ct.name == null ? "$create" : javaIdent(ct.name); + ctx.pushScope(); + sb.append(" public static ").append(javaClassName(c)).append(' ').append(name).append('('); + appendParams(sb, c, ct.params, ctx); + sb.append(") {\n"); + ctx.pushWriter(2); + ctx.methodReturnType = new TypeRef(c.name); + if (ct.body != null) { + emitStatements(ct.body, ctx); + } + sb.append(ctx.popWriter()); + ctx.methodReturnType = null; + ctx.popScope(); + sb.append(" }\n\n"); + return sb.toString(); + } + + /** + * Dart named constructor C.name(...) — a private marker constructor + * (field initializers still run), an instance $init$name carrying the + * body with normal {@code this} semantics, and a public static factory + * with the constructor's name that call sites invoke. + */ + private String emitNamedCtor(ClassDecl c, CtorDecl ct, Ctx ctx) { + StringBuilder sb = new StringBuilder(); + ctx.pushScope(); + StringBuilder paramSig = new StringBuilder(); + StringBuilder argList = new StringBuilder(); + for (int i = 0; i < ct.params.size(); i++) { + Param p = ct.params.get(i); + TypeRef pt = paramType(c, p, ctx); + if (i > 0) { + paramSig.append(", "); + argList.append(", "); + } + String jn = ctx.declareShadowSafe(p.name, pt); + paramSig.append(javaType(pt, false, ctx)).append(' ').append(jn); + argList.append(jn); + } + sb.append(" public static ").append(javaClassName(c)).append(' ').append(javaIdent(ct.name)) + .append('(').append(paramSig).append(") {\n"); + sb.append(" ").append(javaClassName(c)).append(" $self = new ").append(javaClassName(c)).append("(($NamedCtor) null);\n"); + sb.append(" $self.$init$").append(ct.name).append('(').append(argList).append(");\n"); + sb.append(" return $self;\n }\n\n"); + sb.append(" private void $init$").append(ct.name).append('(').append(paramSig).append(") {\n"); + ctx.pushWriter(2); + Ctx.Writer w = ctx.writer(); + for (Param p : ct.params) { + if (p.isThis) { + w.line(fieldStore(c, p.name, javaIdent(p.name))); + } + if (p.isSuper) { + diags.error(p, "E0206", "super parameters are not supported on named constructors yet"); + } + } + for (FieldInit fi : ct.fieldInits) { + Out v = emitExpr(fi.value, typeOfField(c, fi.field, ctx), ctx); + w.line(fieldStore(c, fi.field, v.code)); + } + if (ct.body != null) { + emitStatements(ct.body, ctx); + } + sb.append(ctx.popWriter()); + ctx.popScope(); + sb.append(" }\n\n"); + return sb.toString(); + } + + private void appendParams(StringBuilder sb, ClassDecl c, List params, Ctx ctx) { + for (int i = 0; i < params.size(); i++) { + Param p = params.get(i); + TypeRef pt = paramType(c, p, ctx); + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(pt, false, ctx)).append(' ').append(ctx.declareShadowSafe(p.name, pt)); + } + } + + // ================================================================== + // Methods + // ================================================================== + + private static class Method { + String name; + boolean isStatic; + boolean isGetter; + boolean isSetter; + boolean isOverride; + boolean isAbstract; + boolean isAsync; + boolean isSyncStar; + TypeRef returnType; + List typeParams = new ArrayList(); + List params = new ArrayList(); + Block body; + Expr exprBody; + } + + /** + * Whether a plain method marked {@code @override} in Dart actually overrides + * a Java-visible super/stub method with an IDENTICAL signature. Java rejects + * {@code @Override} on a covariantly-narrowed parameter (e.g. + * {@code updateShouldNotify(PageStatus)} against + * {@code updateShouldNotify(InheritedWidget)}), which Dart allows via + * {@code covariant}. Dropping the annotation when no identical-signature + * target is found is always compile-safe ({@code @Override} is optional). + * Getters/setters/static keep their prior behavior. + */ + private boolean javaOverrides(ClassDecl c, MethodDecl m) { + if (!m.isOverride) { + return false; + } + if (m.isStatic || m.isGetter || m.isSetter) { + return true; + } + // program super chain + ClassDecl p = c; + while (p != null) { + for (TypeRef mix : p.mixins) { + if (stubSigMatches(mix.name, m)) { + return true; + } + } + for (TypeRef itf : p.interfaces) { + if (stubSigMatches(itf.name, m)) { + return true; + } + } + if (p.superclass == null) { + break; + } + ClassDecl sp = program.classes.get(p.superclass.name); + if (sp != null) { + MethodDecl sm = sp.method(m.name); + if (sm != null && sameParamTypes(sm.params, m.params)) { + return true; + } + p = sp; + continue; + } + // superclass is a stub (or unknown): walk the stub chain + return stubSigMatches(p.superclass.name, m); + } + return false; + } + + /** + * Whether the method {@code m} overrides a base method that returns {@code void}. Dart permits + * overriding a {@code void} method with a value-returning one; Java does not, so such an + * override must be emitted with a {@code void} return to stay a valid override. + */ + private boolean overriddenReturnsVoid(ClassDecl c, MethodDecl m) { + if (!m.isOverride || m.isStatic || m.isGetter || m.isSetter) { + return false; + } + ClassDecl p = c; + while (p != null) { + if (p.superclass == null) { + break; + } + ClassDecl sp = program.classes.get(p.superclass.name); + if (sp != null) { + MethodDecl sm = sp.method(m.name); + // Match by name+arity: a base param typed with the class's type variable won't + // name-match the override's concrete substitution. + if (sm != null && sm.params.size() == m.params.size() && !sm.isGetter && !sm.isSetter) { + return sm.returnType != null && sm.returnType.is("void"); + } + p = sp; + continue; + } + return stubMethodReturnsVoid(p.superclass.name, m); + } + return false; + } + + /** + * Bridges a program class's field or getter onto the method name a stub + * interface declares for it. + * + *

The two halves of the contract name the same property differently. A + * stub's instance getter {@code E get current} is a Java method + * {@code current()}; a program class's field {@code current} is a private + * field with {@code get$current()} accessors. A class that satisfies the + * stub interface with a field therefore does not implement the interface + * method at all — Java silently keeps the interface's default. + * + *

That is not a compile error and it does not throw. It just answers the + * default forever: {@code Board with IterableMixin} iterated correctly and + * handed every element back as null, so the 2D-transformations demo's + * painter died on the first {@code boardPoint!} and the entire board — the + * only content on that screen — never drew. + */ + private String emitStubGetterBridges(ClassDecl c, Ctx ctx) { + StringBuilder out = new StringBuilder(); + Set done = new HashSet(); + List supers = new ArrayList(c.interfaces); + supers.addAll(c.mixins); + for (TypeRef ref : supers) { + Ast.ClassDecl sc = stubs.classes.get(ref.name); + while (sc != null) { + for (Ast.MethodDecl sm : sc.methods) { + if (!sm.isGetter || sm.isStatic || done.contains(sm.name)) { + continue; + } + // Only when the property is a FIELD. A Dart getter is already + // emitted under the interface's own name, so bridging it would + // declare the method twice; a class that supplies neither is a + // gap the interface's default is entitled to fill. + FieldDecl f = c.field(sm.name); + if (f == null) { + continue; + } + boolean declaresMethod = false; + for (MethodDecl m : c.methods) { + if (sm.name.equals(m.name) && (m.isGetter + || (!m.isSetter && m.params.isEmpty()))) { + declaresMethod = true; + break; + } + } + if (declaresMethod) { + continue; + } + done.add(sm.name); + String jt = javaType(fieldType(f, ctx), false, ctx); + out.append(" @Override\n"); + out.append(" public ").append(jt).append(' ').append(sm.name) + .append("() {\n return get$").append(sm.name) + .append("();\n }\n\n"); + } + sc = sc.superclass != null ? stubs.classes.get(sc.superclass.name) : null; + } + } + return out.toString(); + } + + /** As {@link #stubSigMatches} but reports whether the matched stub method returns {@code void}. */ + private boolean stubMethodReturnsVoid(String stubClassName, MethodDecl m) { + Ast.ClassDecl sc = stubs.classes.get(stubClassName); + while (sc != null) { + for (Ast.MethodDecl sm : sc.methods) { + if (!sm.isGetter && !sm.isSetter && sm.name.equals(m.name) + && sm.params.size() == m.params.size()) { + return sm.returnType != null && sm.returnType.is("void"); + } + } + sc = sc.superclass != null ? stubs.classes.get(sc.superclass.name) : null; + } + return false; + } + + /** A same-name, same-arity, identical-param-type method anywhere on a stub class's chain. */ + private boolean stubSigMatches(String stubClassName, MethodDecl m) { + Ast.ClassDecl sc = stubs.classes.get(stubClassName); + while (sc != null) { + for (Ast.MethodDecl sm : sc.methods) { + if (!sm.isGetter && !sm.isSetter && sm.name.equals(m.name) + && sameParamTypes(sm.params, m.params)) { + return true; + } + } + sc = sc.superclass != null ? stubs.classes.get(sc.superclass.name) : null; + } + return false; + } + + private boolean sameParamTypes(List a, List b) { + if (a.size() != b.size()) { + return false; + } + for (int i = 0; i < a.size(); i++) { + if (!paramTypeName(a.get(i)).equals(paramTypeName(b.get(i)))) { + return false; + } + } + return true; + } + + private String paramTypeName(Param p) { + return p.type == null || p.type.is("var") ? "dynamic" : p.type.name; + } + + private String emitMethodLike(Method m, Ctx ctx, boolean classIsAbstract) { + StringBuilder sb = new StringBuilder(); + TypeRef rt = m.returnType == null || m.returnType.is("var") ? TypeRef.DYNAMIC : m.returnType; + // Dart's `int get hashCode` / `int compareTo(...)` map to Java's Object.hashCode / + // Comparable.compareTo, which return primitive `int` (not the `long` Dart int uses). + // Emit a Java `int` return (not `long`) so the override is valid; the body's long + // result is narrowed with an explicit cast. + boolean forceIntReturn = ("hashCode".equals(m.name) && m.params.isEmpty() && !m.isSetter) + || ("compareTo".equals(m.name) && m.params.size() == 1 && !m.isSetter); + if (m.isOverride) { + sb.append(" @Override\n"); + } + // Dart privacy is library-scoped, not class-scoped: a `_name` member is visible to + // every other class in the same Dart library. All generated classes land in one Java + // package, so emit `_`-prefixed members package-private (no modifier) rather than + // `private`, so sibling classes can still reach them. + sb.append(" ").append(m.name.startsWith("_") ? "" : "public "); + if (m.isStatic) { + sb.append("static "); + } + if (m.isAbstract) { + sb.append("abstract "); + } + ctx.pushScope(); + if (m.typeParams != null && !m.typeParams.isEmpty()) { + sb.append('<'); + for (int i = 0; i < m.typeParams.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(m.typeParams.get(i)); + } + sb.append("> "); + } + String rjt = m.isSetter ? "void" : (forceIntReturn ? "int" : javaType(rt, false, ctx)); + sb.append(rjt).append(' ').append(m.name).append('('); + // The Java parameter list and names, kept for a sync* body's helper method. + StringBuilder paramDecls = new StringBuilder(); + StringBuilder paramNames = new StringBuilder(); + for (int i = 0; i < m.params.size(); i++) { + Param p = m.params.get(i); + TypeRef pt = p.type == null || p.type.is("var") ? TypeRef.DYNAMIC : p.type; + if (i > 0) { + sb.append(", "); + } + String jn = ctx.declareShadowSafe(p.name, pt); + String decl = javaType(pt, false, ctx) + ' ' + jn; + sb.append(decl); + paramDecls.append(", ").append(decl); + paramNames.append(", ").append(jn); + } + sb.append(')'); + if (m.isAbstract) { + sb.append(";\n\n"); + ctx.popScope(); + return sb.toString(); + } + sb.append(" {\n"); + ctx.pushWriter(2); + ctx.methodReturnType = rt; + ctx.inAsyncBody = m.isAsync; + ctx.boxedLocals.clear(); + ctx.boxedLocals.addAll(m.body != null + ? CaptureScan.boxedLocals(m.body) : CaptureScan.boxedLocals(m.exprBody)); + if (m.isSyncStar && !rt.is("List") && !m.isSetter) { + // sync* generator. The body moves into a private helper that yields into the list + // it is given (`yield x` -> out.add(x), `yield* xs` -> out.addAllIterable(xs), and a + // bare `return;` simply ends it); the method returns DartIterable.syncStar over it, + // which runs the body when an iteration BEGINS and again for every iteration, as + // Dart runs a generator. Running it at the call -- as this did -- performed its side + // effects before anyone iterated and made a second iteration replay the first. + // (Suspension at each yield is not modelled; see DartIterable.syncStar.) + ctx.importClass("dart.core.DartList"); + ctx.importClass("dart.core.DartIterable"); + TypeRef elem = rt.is("Iterable") && !rt.args.isEmpty() ? rt.arg(0) : TypeRef.DYNAMIC; + String ej = javaType(elem, true, ctx); + String out = ctx.newTemp(); + String helper = m.name + "$sync"; + ctx.writer().line("return DartIterable.syncStar((DartList<" + ej + "> " + out + ") -> " + + helper + "(" + out + paramNames + "));"); + sb.append(ctx.popWriter()); + sb.append(" }\n\n"); + StringBuilder hb = new StringBuilder(" private "); + if (m.isStatic) { + hb.append("static "); + } + if (m.typeParams != null && !m.typeParams.isEmpty()) { + hb.append('<'); + for (int i = 0; i < m.typeParams.size(); i++) { + if (i > 0) { + hb.append(", "); + } + hb.append(m.typeParams.get(i)); + } + hb.append("> "); + } + hb.append("void ").append(helper).append("(DartList<").append(ej).append("> ").append(out) + .append(paramDecls).append(") {\n"); + ctx.pushWriter(2); + ctx.methodReturnType = TypeRef.VOID; + String savedList = ctx.syncStarList; + TypeRef savedElem = ctx.syncStarElem; + ctx.syncStarList = out; + ctx.syncStarElem = elem; + if (m.body != null) { + emitStatements(m.body, ctx); + } + ctx.syncStarList = savedList; + ctx.syncStarElem = savedElem; + hb.append(ctx.popWriter()); + hb.append(" }\n\n"); + sb.append(hb); + ctx.popScope(); + ctx.methodReturnType = null; + ctx.inAsyncBody = false; + return sb.toString(); + } + if (m.isSyncStar) { + // sync* declared as a List (not valid Dart, kept as before): collect yielded values + // into a DartList and return it. + ctx.importClass("dart.core.DartList"); + TypeRef elem = (rt.is("Iterable") || rt.is("List") || rt.is("Set")) && !rt.args.isEmpty() + ? rt.arg(0) : TypeRef.DYNAMIC; + String lst = ctx.newTemp(); + ctx.writer().line("DartList<" + javaType(elem, true, ctx) + "> " + lst + " = new DartList<>();"); + String savedList = ctx.syncStarList; + TypeRef savedElem = ctx.syncStarElem; + ctx.syncStarList = lst; + ctx.syncStarElem = elem; + if (m.body != null) { + emitStatements(m.body, ctx); + } + // DartList is a java Iterable but not a DartIterable; wrap when the declared return type + // maps to DartIterable (Dart `Iterable`). A `List` return can return the list directly. + if (rt.is("List")) { + ctx.writer().line("return " + lst + ";"); + } else { + ctx.importClass("dart.core.DartIterable"); + ctx.writer().line("return DartIterable.wrap(" + lst + ");"); + } + ctx.syncStarList = savedList; + ctx.syncStarElem = savedElem; + sb.append(ctx.popWriter()); + ctx.popScope(); + ctx.methodReturnType = null; + ctx.inAsyncBody = false; + sb.append(" }\n\n"); + return sb.toString(); + } + boolean asyncFuture = m.isAsync && (rt.is("Future") || rt.is("FutureOr")); + boolean savedNarrow = ctx.narrowReturnToInt; + ctx.narrowReturnToInt = forceIntReturn; + // An async function's exceptions belong to the Future it returns. Emitted + // bare, a throw in the body -- or an await rethrowing a failed future -- + // left the Java method before any Future existed, so the caller got a + // synchronous exception where Dart hands back a failed future to await + // or catchError. The body is bracketed so a throw becomes Future.error. + // RuntimeException, as an untyped Dart `catch` is emitted: every Dart + // throw surfaces as one (DartRuntime.asError wraps non-exceptions), and + // a VM error such as a stack overflow is not turned into a value. + String asyncErr = null; + if (asyncFuture && (m.body != null || m.exprBody != null)) { + ctx.importClass("dart.async.Future"); + asyncErr = ctx.newTemp(); + ctx.writer().line("try {"); + ctx.indent(1); + } + if (m.body != null) { + emitStatements(m.body, ctx); + if (asyncFuture && !endsWithJump(m.body)) { + ctx.importClass("dart.async.Future"); + ctx.writer().line("return Future.value(null);"); + } + } else if (m.exprBody != null) { + if (asyncFuture) { + Out o = emitExpr(m.exprBody, null, ctx); + ctx.importClass("dart.async.Future"); + ctx.writer().line("return " + (o.type.is("Future") ? o.code + : "Future.value(" + boxIfPrimitive(o, ctx) + ")") + ";"); + } else { + Out o = emitExpr(m.exprBody, rt.is("void") ? null : rt, ctx); + if (rt.is("void")) { + ctx.writer().line(statementize(o.code) + ";"); + } else if (forceIntReturn) { + ctx.writer().line("return (int) (" + o.code + ");"); + } else { + ctx.writer().line("return " + coerce(o, rt, ctx) + ";"); + } + } + } + if (asyncErr != null) { + ctx.indent(-1); + ctx.writer().line("} catch (RuntimeException " + asyncErr + ") {"); + ctx.indent(1); + ctx.writer().line("return Future.error(" + asyncErr + ");"); + ctx.indent(-1); + ctx.writer().line("}"); + } + ctx.narrowReturnToInt = savedNarrow; + sb.append(ctx.popWriter()); + ctx.popScope(); + ctx.methodReturnType = null; + ctx.inAsyncBody = false; + sb.append(" }\n\n"); + return sb.toString(); + } + + /** Shallow check: does the block's last statement definitely leave the method? */ + private boolean endsWithJump(Block b) { + if (b.statements.isEmpty()) { + return false; + } + Stmt last = b.statements.get(b.statements.size() - 1); + if (last instanceof ReturnStmt) { + return true; + } + return last instanceof ExprStmt && ((ExprStmt) last).expr instanceof ThrowExpr; + } + + // ================================================================== + // Statements + // ================================================================== + + private void emitStatements(Block b, Ctx ctx) { + for (Stmt s : b.statements) { + emitStatement(s, ctx); + } + } + + private void emitStatement(Stmt s, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + if (s instanceof Block) { + w.line("{"); + ctx.indent(1); + ctx.pushScope(); + emitStatements((Block) s, ctx); + ctx.popScope(); + ctx.indent(-1); + w.line("}"); + } else if (s instanceof VarDeclStmt) { + VarDeclStmt v = (VarDeclStmt) s; + TypeRef declared = v.type; + Out init = null; + if (v.initializer != null) { + init = emitExpr(v.initializer, declared != null && !declared.is("var") ? declared : null, ctx); + } + TypeRef t = declared == null || declared.is("var") + ? (init != null ? init.type : TypeRef.DYNAMIC) : declared; + // untyped closure locals get a SAM type by arity + if (t.is("Function") && v.initializer instanceof Lambda) { + Lambda l = (Lambda) v.initializer; + if (l.params.isEmpty()) { + ctx.importClass("dart.runtime.Funcs"); + String jn = ctx.declareShadowSafe(v.name, t); + w.line("Funcs.VoidFunc0 " + jn + " = " + init.code + ";"); + return; + } + diags.error(v, "E0138", "Annotate this closure variable's type (only zero-arg closures are inferred in M1)"); + } + String jn = ctx.declareShadowSafe(v.name, t); + if (ctx.boxedLocals.contains(v.name)) { + ctx.markBoxed(v.name); + String holder = refHolder(t, ctx); + String initCode = init != null ? coerce(init, t, ctx) : zeroValue(t); + w.line("final " + holder + " " + jn + " = new " + holder.split("<")[0] + + (holder.startsWith("Ref<") ? "<>" : "") + "(" + initCode + ");"); + return; + } + if ((v.type == null || v.type.is("var")) && init != null && containsDynamic(t)) { + // let javac infer generics the Dart-side inference doesn't track + w.line("var " + jn + " = " + init.code + ";"); + return; + } + String jt = javaType(t, false, ctx); + if (init != null) { + w.line(jt + " " + jn + " = " + coerce(init, t, ctx) + ";"); + } else { + w.line(jt + " " + jn + " = " + zeroValue(t) + ";"); + } + } else if (s instanceof VarDeclGroup) { + for (VarDeclStmt v : ((VarDeclGroup) s).decls) { + emitStatement(v, ctx); + } + } else if (s instanceof ExprStmt) { + Expr ex = ((ExprStmt) s).expr; + if (ex instanceof ThrowExpr) { + ctx.importClass("dart.runtime.DartRuntime"); + Out v = emitExpr(((ThrowExpr) ex).value, null, ctx); + w.line("throw DartRuntime.asError(" + v.code + ");"); + return; + } + // A conditional used as a statement (`cond ? f() : g();`) — when its arms are void + // (e.g. controller.reverse()/forward()) the ternary is not a valid Java expression + // statement, so lower it to an if/else. + if (ex instanceof Conditional) { + Conditional c = (Conditional) ex; + // Each arm's lifted statements are kept apart and placed inside its own + // branch; when the arms are not void they are discarded, since the + // conditional is emitted again below as an ordinary expression. + Lazy thenL = emitLazyOperand(c.thenExpr, null, ctx, 1); + Lazy elseL = emitLazyOperand(c.elseExpr, null, ctx, 1); + Out thenO = thenL.out; + Out elseO = elseL.out; + boolean voidArms = (thenO.type != null && thenO.type.is("void")) + || (elseO.type != null && elseO.type.is("void")); + if (voidArms) { + Out cond = emitExpr(c.condition, TypeRef.BOOL, ctx); + w.line("if (" + cond.code + ") {"); + w.raw(thenL.lifted); + ctx.indent(1); + String tc = statementize(thenO.code); + if (!tc.isEmpty()) { + w.line(tc + ";"); + } + ctx.indent(-1); + w.line("} else {"); + w.raw(elseL.lifted); + ctx.indent(1); + String ec = statementize(elseO.code); + if (!ec.isEmpty()) { + w.line(ec + ";"); + } + ctx.indent(-1); + w.line("}"); + return; + } + } + if (ex instanceof Assign && isNullAwareMemberAssign((Assign) ex) && !needsShort((Assign) ex)) { + emitNullAwareAssignStatement((Assign) ex, ctx); + return; + } + Out o = emitStatementExpr(ex, ctx); + String code = statementize(o.code); + if (!code.isEmpty()) { + w.line(code + ";"); + } + } else if (s instanceof YieldStmt) { + YieldStmt y = (YieldStmt) s; + if (ctx.syncStarList == null) { + diags.error(y, "E0304", "yield outside a sync* generator body"); + } else if (y.star) { + Out o = emitExpr(y.value, null, ctx); + w.line(ctx.syncStarList + ".addAllIterable(" + o.code + ");"); + } else { + Out o = emitExpr(y.value, ctx.syncStarElem, ctx); + w.line(ctx.syncStarList + ".add(" + coerce(o, ctx.syncStarElem, ctx) + ");"); + } + } else if (s instanceof ReturnStmt) { + ReturnStmt r = (ReturnStmt) s; + TypeRef rt = ctx.methodReturnType; + if (ctx.inAsyncBody && rt != null && (rt.is("Future") || rt.is("FutureOr"))) { + // async body: returned values wrap into a completed Future + ctx.importClass("dart.async.Future"); + TypeRef inner = rt.args.isEmpty() ? TypeRef.DYNAMIC : rt.arg(0); + if (r.value == null) { + w.line("return Future.value(null);"); + } else { + Out o = emitExpr(r.value, inner, ctx); + String code = o.code; + // await already unwraps; returning a Future directly passes through + if (o.type.is("Future")) { + w.line("return " + code + ";"); + } else { + w.line("return Future.value(" + boxIfPrimitive(o, ctx) + ");"); + } + } + return; + } + if (r.value == null) { + w.line("return;"); + } else if (rt != null && rt.is("void")) { + // Dart allows `return expr;` from a method Java-typed void (it overrides a void + // base). Keep a side-effecting call; drop a pure read (not a valid Java statement). + Out o = emitExpr(r.value, null, ctx); + String code = statementize(o.code); + if (!code.isEmpty() && code.contains("(")) { + w.line(code + ";"); + } + w.line("return;"); + } else if (ctx.narrowReturnToInt) { + Out o = emitExpr(r.value, null, ctx); + w.line("return (int) (" + o.code + ");"); + } else { + Out o = emitExpr(r.value, rt, ctx); + w.line("return " + (rt != null ? coerce(o, rt, ctx) : o.code) + ";"); + } + } else if (s instanceof TryStmt) { + TryStmt t = (TryStmt) s; + w.line("try {"); + ctx.indent(1); + ctx.pushScope(); + emitStatements(t.tryBlock, ctx); + ctx.popScope(); + ctx.indent(-1); + if (!t.catches.isEmpty()) { + emitCatchChain(t.catches, ctx); + } + if (t.finallyBlock != null) { + w.line("} finally {"); + ctx.indent(1); + ctx.pushScope(); + emitStatements(t.finallyBlock, ctx); + ctx.popScope(); + ctx.indent(-1); + } + w.line("}"); + } else if (s instanceof IfStmt && ((IfStmt) s).casePattern != null) { + emitIfCaseStmt((IfStmt) s, ctx); + } else if (s instanceof IfStmt) { + IfStmt i = (IfStmt) s; + Out c = emitExpr(i.condition, TypeRef.BOOL, ctx); + w.line("if (" + c.code + ") {"); + ctx.indent(1); + ctx.pushScope(); + // `if (x is T)` flow-promotes x to T inside the then-branch. + List undo = applyGuardPromotions(i.condition, ctx); + emitStatement(unwrapBlock(i.thenStmt), ctx); + restorePromotions(undo, ctx); + ctx.popScope(); + ctx.indent(-1); + if (i.elseStmt != null) { + w.line("} else {"); + ctx.indent(1); + ctx.pushScope(); + emitStatement(unwrapBlock(i.elseStmt), ctx); + ctx.popScope(); + ctx.indent(-1); + } + w.line("}"); + } else if (s instanceof WhileStmt) { + WhileStmt wh = (WhileStmt) s; + Lazy c = emitLazyOperand(wh.condition, TypeRef.BOOL, ctx, 1); + if (!c.lifts()) { + w.line("while (" + c.out.code + ") {"); + ctx.indent(1); + } else { + // The condition's lifted statements belong to every test, not to the + // first one only: run them at the top of each pass, then test. + w.line("while (true) {"); + w.raw(c.lifted); + ctx.indent(1); + w.line("if (!(" + c.out.code + ")) {"); + w.line(" break;"); + w.line("}"); + } + ctx.pushScope(); + ctx.pushBreakTarget(null); + emitStatement(unwrapBlock(wh.body), ctx); + ctx.popBreakTarget(); + ctx.popScope(); + ctx.indent(-1); + w.line("}"); + } else if (s instanceof ForStmt) { + ForStmt f = (ForStmt) s; + ctx.pushScope(); + // lift the init before the loop; conditions/updates must be lift-free in M1 + String initCode = ""; + String forVarDart = null; + String forVarJava = null; + TypeRef forVarType = null; + if (f.init instanceof VarDeclStmt) { + VarDeclStmt v = (VarDeclStmt) f.init; + Out init = v.initializer != null ? emitExpr(v.initializer, v.type, ctx) : null; + TypeRef t = v.type == null || v.type.is("var") + ? (init != null ? init.type : TypeRef.DYNAMIC) : v.type; + String loopVar = ctx.declareShadowSafe(v.name, t); + forVarDart = v.name; + forVarJava = loopVar; + forVarType = t; + initCode = javaType(t, false, ctx) + " " + loopVar + " = " + + (init != null ? coerce(init, t, ctx) : zeroValue(t)); + } else if (f.init instanceof ExprStmt) { + initCode = statementize(emitStatementExpr(((ExprStmt) f.init).expr, ctx).code); + } + final String forVarDart$ = forVarDart; + final String forVarJava$ = forVarJava; + final TypeRef forVarType$ = forVarType; + final ForStmt f$ = f; + emitCForLoop(initCode, f.condition, f.updates, ctx, new Runnable() { + public void run() { + // Dart binds the loop variable fresh each iteration, so a closure in the + // body captures a distinct value per pass. The Java loop variable is + // reassigned by the update clause (not effectively final), so emit a + // per-iteration final alias and route body references through it. + if (forVarDart$ != null && CaptureScan.readInLambda(f$.body, forVarDart$)) { + String alias = ctx.declareShadowSafe(forVarDart$, forVarType$); + ctx.writer().line("final " + javaType(forVarType$, false, ctx) + " " + alias + + " = " + forVarJava$ + ";"); + } + ctx.pushBreakTarget(null); + emitStatement(unwrapBlock(f$.body), ctx); + ctx.popBreakTarget(); + } + }); + ctx.popScope(); + } else if (s instanceof ForInStmt + && isIndexedRecordFor(((ForInStmt) s).pattern, ((ForInStmt) s).iterable)) { + // `for (final (int i, E e) in xs.indexed)` — Dart's Iterable.indexed pairs each + // element with its position. There is no runtime `indexed`, so lower to a counted + // loop that binds the index and element subpatterns directly. + final ForInStmt f = (ForInStmt) s; + emitIndexedFor(f.pattern, f.iterable, ctx, new Runnable() { + public void run() { + ctx.pushBreakTarget(null); + emitStatement(unwrapBlock(f.body), ctx); + ctx.popBreakTarget(); + } + }); + } else if (s instanceof ForInStmt) { + ForInStmt f = (ForInStmt) s; + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef elem = f.varType != null && !f.varType.is("var") + ? f.varType + : (iter.type != null && (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set")) + ? iter.type.arg(0) : TypeRef.DYNAMIC); + ctx.pushScope(); + ctx.pushBreakTarget(null); + if (f.pattern != null) { + // Dart 3 pattern for-in: bind a temp per element, then destructure into the pattern. + String loopVar = ctx.newTemp(); + w.line("for (" + javaType(elem, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + ctx.declare(loopVar, elem); + List binds = new ArrayList(); + patternMatch(f.pattern, loopVar, elem, ctx, binds); + for (String b : binds) { + w.line(b); + } + emitStatement(unwrapBlock(f.body), ctx); + ctx.indent(-1); + w.line("}"); + } else { + String loopVar = ctx.declareShadowSafe(f.varName, elem); + w.line("for (" + javaType(elem, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + emitStatement(unwrapBlock(f.body), ctx); + ctx.indent(-1); + w.line("}"); + } + ctx.popBreakTarget(); + ctx.popScope(); + } else if (s instanceof BreakStmt) { + String bl = ctx.currentBreakLabel(); + w.line(bl != null ? "break " + bl + ";" : "break;"); + } else if (s instanceof ContinueStmt) { + w.line("continue;"); + } else if (s instanceof SwitchStmt) { + emitSwitchStmt((SwitchStmt) s, ctx); + } else if (s instanceof Ast.LocalFunc) { + emitLocalFunc((Ast.LocalFunc) s, ctx); + } else if (s != null) { + diags.error(s, "E0127", "Unsupported statement in emitter"); + } + } + + /** + * A nested function declaration, lowered to a local variable holding a lambda + * bound to the matching {@code Funcs.*} functional interface. Registering the + * local in scope lets later {@code name(args)} calls (via {@code emitBareCall}) + * and bare {@code name} tear-offs (via {@code emitIdent}) resolve against it. + */ + private void emitLocalFunc(Ast.LocalFunc lf, Ctx ctx) { + int arity = lf.params.size(); + if (arity > 5) { + diags.error(lf, "E0139", "Nested functions with more than 5 parameters are not supported yet"); + return; + } + String samType = funcSamType(lf.returnType, lf.params, ctx); + // Reuse the lambda machinery (capture/box handling, param typing) by + // building an equivalent Lambda and emitting it against the SAM type. + Lambda l = new Lambda(); + l.file = lf.file; + l.line = lf.line; + l.col = lf.col; + l.isAsync = lf.isAsync; + l.params = lf.params; + l.body = lf.body; + l.exprBody = lf.exprBody; + // Declare the local first so a recursive body can reference the name. + String jn = ctx.declareShadowSafe(lf.name, new TypeRef("Function")); + Out init = emitExpr(l, funcTypeRef(lf.returnType, lf.params), ctx); + ctx.writer().line(samType + " " + jn + " = " + init.code + ";"); + } + + /** The {@code Funcs.*} functional-interface Java type for an inline function-type signature. */ + private String funcSamTypeFromRefs(List params, TypeRef ret, Ctx ctx) { + ctx.importClass("dart.runtime.Funcs"); + int arity = params == null ? 0 : params.size(); + boolean voidRet = ret == null || ret.is("void"); + if (voidRet) { + if (arity == 0) { + return "Funcs.VoidFunc0"; + } + StringBuilder sb = new StringBuilder("Funcs.VoidFunc").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(params.get(i), true, ctx)); + } + return sb.append('>').toString(); + } + TypeRef r = ret.is("var") || ret.is("dynamic") ? TypeRef.DYNAMIC : ret; + StringBuilder sb = new StringBuilder("Funcs.Func").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + sb.append(javaType(params.get(i), true, ctx)).append(", "); + } + sb.append(javaType(r, true, ctx)); + return sb.append('>').toString(); + } + + /** The {@code Funcs.*} functional-interface Java type for a function shape. */ + private String funcSamType(TypeRef ret, List params, Ctx ctx) { + ctx.importClass("dart.runtime.Funcs"); + int arity = params.size(); + boolean voidRet = ret == null || ret.is("void"); + if (voidRet) { + if (arity == 0) { + return "Funcs.VoidFunc0"; + } + StringBuilder sb = new StringBuilder("Funcs.VoidFunc").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(paramValueType(params.get(i)), true, ctx)); + } + return sb.append('>').toString(); + } + TypeRef r = ret.is("var") || ret.is("dynamic") ? TypeRef.DYNAMIC : ret; + StringBuilder sb = new StringBuilder("Funcs.Func").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + sb.append(javaType(paramValueType(params.get(i)), true, ctx)).append(", "); + } + sb.append(javaType(r, true, ctx)); + return sb.append('>').toString(); + } + + /** A named typedef-shaped {@link TypeRef} used to give lambda params their real types. */ + private TypeRef funcTypeRef(TypeRef ret, List params) { + // Not a registered typedef name, but emitLambda only reads typedefSig(expected.name); + // an unregistered name yields null there, which is fine — params carry their own types. + return new TypeRef("Function"); + } + + private static TypeRef paramValueType(Param p) { + return p.type == null || p.type.is("var") ? TypeRef.DYNAMIC : p.type; + } + + // ------------------------------------------------------------------ + // Dart 3: switch statements / expressions + pattern matching + // ------------------------------------------------------------------ + + /** + * Lowers a switch statement to a labeled block of independent {@code if} tests. Cases do not fall + * through (Dart semantics), so each match runs its body and breaks the label. A {@code when} guard + * is a nested test inside the matched block, so a matched-but-guard-failed case falls through to + * the following cases. + */ + /** + * Lowers a Dart 3 if-case statement {@code if (e case p [when g]) S1 else S2}. The scrutinee is + * lifted into a temp, the pattern match becomes the condition (with its bindings in scope for the + * guard and the then-branch), and a match-flag routes a failed match/guard to the else-branch. + */ + private void emitIfCaseStmt(IfStmt i, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + w.line("{"); + ctx.indent(1); + ctx.pushScope(); + Out subj = emitExpr(i.condition, null, ctx); + String temp = ctx.newTemp(); + w.line(javaType(subj.type, true, ctx) + " " + temp + " = " + subj.code + ";"); + ctx.declare(temp, subj.type); + List binds = new ArrayList(); + String cond = patternMatch(i.casePattern, temp, subj.type, ctx, binds); + boolean guarded = i.caseGuard != null; + // Emit a structural if/else so javac's definite-return analysis holds. A guard that fails must + // route to the else-branch, which requires emitting the else in two spots (pattern miss and + // guard miss). The common unguarded case emits it once. + w.line("if (" + cond + ") {"); + ctx.indent(1); + ctx.pushScope(); + for (String b : binds) { + w.line(b); + } + if (guarded) { + Out g = emitExpr(i.caseGuard, TypeRef.BOOL, ctx); + w.line("if (" + g.code + ") {"); + ctx.indent(1); + ctx.pushScope(); + emitStatement(unwrapBlock(i.thenStmt), ctx); + ctx.popScope(); + ctx.indent(-1); + if (i.elseStmt != null) { + w.line("} else {"); + ctx.indent(1); + ctx.pushScope(); + emitStatement(unwrapBlock(i.elseStmt), ctx); + ctx.popScope(); + ctx.indent(-1); + } + w.line("}"); + } else { + emitStatement(unwrapBlock(i.thenStmt), ctx); + } + ctx.popScope(); + ctx.indent(-1); + if (i.elseStmt != null) { + w.line("} else {"); + ctx.indent(1); + ctx.pushScope(); + emitStatement(unwrapBlock(i.elseStmt), ctx); + ctx.popScope(); + ctx.indent(-1); + } + w.line("}"); + ctx.popScope(); + ctx.indent(-1); + w.line("}"); + } + + private void emitSwitchStmt(SwitchStmt sw, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + Out subj = emitExpr(sw.subject, null, ctx); + String s = ctx.newTemp(); + String label = "$sw" + s.substring(2); + ctx.pushScope(); + w.line(javaType(subj.type, true, ctx) + " " + s + " = " + subj.code + ";"); + ctx.declare(s, subj.type); + w.line(label + ": {"); + ctx.indent(1); + ctx.pushBreakTarget(label); + SwitchCase defaultCase = null; + // Tracks whether control can leave the switch block normally (a case that `break`s to + // after the block rather than returning/throwing). When every case returns and there is + // no default, a Dart-exhaustive switch leaves the block unreachable — a trailing throw + // then keeps a value-returning method/lambda definitely-assigned in Java. + boolean anyFallThrough = false; + boolean hasContentCase = false; + // an empty non-default case falls through to the next case's body (Dart's only fallthrough) + List pending = new ArrayList(); + for (SwitchCase c : sw.cases) { + if (c.isDefault) { + defaultCase = c; + continue; + } + ctx.pushScope(); + List binds = new ArrayList(); + String cond = patternMatch(c.pattern, s, subj.type, ctx, binds); + if (c.body.isEmpty() && c.guard == null) { + if (!binds.isEmpty()) { + diags.error(c, "E0436", "An empty fall-through case cannot bind variables"); + } + pending.add(cond); + ctx.popScope(); + continue; + } + String full = cond; + if (!pending.isEmpty()) { + StringBuilder sb = new StringBuilder("("); + for (String pc : pending) { + sb.append(pc).append(" || "); + } + full = sb.append(cond).append(")").toString(); + pending.clear(); + } + w.line("if (" + full + ") {"); + ctx.indent(1); + for (String b : binds) { + w.line(b); + } + boolean guarded = c.guard != null; + if (guarded) { + Out g = emitExpr(c.guard, TypeRef.BOOL, ctx); + w.line("if (" + g.code + ") {"); + ctx.indent(1); + } + hasContentCase = true; + for (Stmt bs : c.body) { + emitStatement(bs, ctx); + } + // only emit the implicit break when the body does not already jump (else Java flags it + // as an unreachable statement) + if (!endsWithTerminator(c.body)) { + w.line("break " + label + ";"); + anyFallThrough = true; + } + if (guarded) { + ctx.indent(-1); + w.line("}"); + } + ctx.indent(-1); + w.line("}"); + ctx.popScope(); + } + if (defaultCase != null) { + ctx.pushScope(); + for (Stmt bs : defaultCase.body) { + emitStatement(bs, ctx); + } + ctx.popScope(); + } + ctx.popBreakTarget(); + ctx.indent(-1); + w.line("}"); + // Dart-exhaustive switch (an enum subject, no default) where every arm returns/throws: + // the post-block fall-through is unreachable in Dart, so emit an unreachable throw to + // satisfy Java's definite-return analysis. Only for enum subjects — a String/int switch is + // never exhaustive and legitimately falls through to code after it. + boolean enumSubject = subj.type != null + && (program.enums.containsKey(subj.type.name) || stubs.isStubEnum(subj.type.name)); + if (defaultCase == null && hasContentCase && !anyFallThrough && enumSubject) { + ctx.importClass("dart.runtime.DartRuntime"); + w.line("throw DartRuntime.asError(\"No matching switch case\");"); + } + ctx.popScope(); + } + + /** True when a statement list definitely transfers control (so a trailing break is unreachable). */ + private boolean endsWithTerminator(List body) { + if (body.isEmpty()) { + return false; + } + return stmtTerminates(body.get(body.size() - 1)); + } + + /** + * True when a single statement definitely transfers control. Recurses into a + * trailing block so `case x: { ...; break; }` (the gen-l10n locale lookup shape) + * is recognized as terminating and doesn't get a second, unreachable break. + */ + private boolean stmtTerminates(Stmt last) { + if (last instanceof ReturnStmt || last instanceof BreakStmt || last instanceof ContinueStmt) { + return true; + } + if (last instanceof ExprStmt && ((ExprStmt) last).expr instanceof ThrowExpr) { + return true; + } + if (last instanceof Block) { + List ss = ((Block) last).statements; + return !ss.isEmpty() && stmtTerminates(ss.get(ss.size() - 1)); + } + return false; + } + + /** + * Reorders a class's fields so that a static field whose initializer reads + * another static field of the same class is emitted after it. Dart evaluates + * static initializers lazily (order-independent); Java runs them top-to-bottom, + * so source order can make a static read a not-yet-initialized sibling as null. + * Stable: fields with no unmet dependency keep their original relative order, + * and any cycle falls back to source order (its remaining fields appended). + */ + private List orderStaticFieldsByDependency(ClassDecl c) { + List fields = c.fields; + java.util.Set staticNames = new HashSet(); + for (FieldDecl f : fields) { + if (f.isStatic) { + staticNames.add(f.name); + } + } + if (staticNames.isEmpty()) { + return fields; + } + // Static methods of this class: a static field whose initializer calls one of them + // can transitively read any static field (the method body isn't analyzed here), so it + // must initialize only after every other static field — covers the common + // `themeData(colorScheme)` builder that reads sibling `_textTheme`/`_colorScheme` statics. + java.util.Set staticMethods = new HashSet(); + for (MethodDecl m : c.methods) { + if (m.isStatic) { + staticMethods.add(m.name); + } + } + int staticCount = staticNames.size(); + List pending = new ArrayList(fields); + List ordered = new ArrayList(fields.size()); + java.util.Set placed = new HashSet(); + int placedStatics = 0; + boolean progress = true; + while (!pending.isEmpty() && progress) { + progress = false; + for (int i = 0; i < pending.size(); i++) { + FieldDecl f = pending.get(i); + boolean ready = true; + if (f.isStatic && f.initializer != null) { + java.util.Set refs = CaptureScan.referencedNames(f.initializer); + // direct dependency on a same-class static that is not yet placed + for (String ref : refs) { + if (!ref.equals(f.name) && staticNames.contains(ref) && !placed.contains(ref)) { + ready = false; + break; + } + } + // calls a same-class static method -> wait for every other static field + if (ready) { + for (String ref : refs) { + if (staticMethods.contains(ref) && placedStatics < staticCount - 1) { + ready = false; + break; + } + } + } + } + if (ready) { + ordered.add(f); + placed.add(f.name); + if (f.isStatic) { + placedStatics++; + } + pending.remove(i); + progress = true; + break; + } + } + } + // cycle (or a dependency that never resolves): keep the rest in source order + ordered.addAll(pending); + return ordered; + } + + /** + * Lowers a switch expression to a lifted result temp assigned inside a labeled block (the same + * shape as a switch statement). A non-exhaustive switch that matches nothing throws, mirroring + * Dart's runtime behavior. + */ + private Out emitSwitchExpr(SwitchExpr sw, TypeRef expected, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + Out subj = emitExpr(sw.subject, null, ctx); + TypeRef resultType = expected != null && !expected.is("var") && !expected.is("dynamic") + ? expected : TypeRef.DYNAMIC; + // No context type (e.g. `switch (t) {...}.present()`): infer a common result type + // from the arms so a member access on the switch value resolves against a real type. + if (resultType.is("dynamic")) { + TypeRef common = inferSwitchResultType(sw, ctx); + if (common != null) { + resultType = common; + } + } + String s = ctx.newTemp(); + String res = ctx.newTemp(); + String label = "$sw" + s.substring(2); + ctx.pushScope(); + w.line(javaType(subj.type, true, ctx) + " " + s + " = " + subj.code + ";"); + ctx.declare(s, subj.type); + w.line(javaType(resultType, true, ctx) + " " + res + ";"); + w.line(label + ": {"); + ctx.indent(1); + SwitchExprCase defaultCase = null; + for (SwitchExprCase c : sw.cases) { + if (c.isDefault && c.guard == null) { + defaultCase = c; // emitted unconditionally, last (Dart requires it last anyway) + continue; + } + ctx.pushScope(); + List binds = new ArrayList(); + String cond = patternMatch(c.pattern, s, subj.type, ctx, binds); + w.line("if (" + cond + ") {"); + ctx.indent(1); + for (String b : binds) { + w.line(b); + } + boolean guarded = c.guard != null; + if (guarded) { + Out g = emitExpr(c.guard, TypeRef.BOOL, ctx); + w.line("if (" + g.code + ") {"); + ctx.indent(1); + } + Out v = emitExpr(c.value, resultType, ctx); + w.line(res + " = " + coerce(v, resultType, ctx) + ";"); + w.line("break " + label + ";"); + if (guarded) { + ctx.indent(-1); + w.line("}"); + } + ctx.indent(-1); + w.line("}"); + ctx.popScope(); + } + if (defaultCase != null) { + ctx.pushScope(); + Out v = emitExpr(defaultCase.value, resultType, ctx); + w.line(res + " = " + coerce(v, resultType, ctx) + ";"); + ctx.popScope(); + } else { + ctx.importClass("dart.runtime.DartRuntime"); + w.line("throw DartRuntime.asError(\"No matching switch expression case\");"); + } + ctx.indent(-1); + w.line("}"); + ctx.popScope(); + return new Out(res, resultType); + } + + /** + * A common static type for every arm of a switch expression, or null when the arms + * disagree or an arm's type can't be inferred without side effects. Used only when the + * switch appears without a context type; keeps a member access on the switch value + * (e.g. {@code switch (t) {...}.present()}) resolvable. + */ + private TypeRef inferSwitchResultType(SwitchExpr sw, Ctx ctx) { + TypeRef common = null; + for (SwitchExprCase c : sw.cases) { + TypeRef at = inferExprTypeQuiet(c.value, ctx); + if (at == null || at.is("dynamic") || at.is("void")) { + return null; + } + if (common == null) { + common = at; + } else if (!common.name.equals(at.name)) { + return null; + } + } + return common; + } + + /** + * Best-effort static type of an expression without emitting it (no side effects). Handles + * the simple cases needed for switch-arm unification — a local variable or a field + * (own, or inherited from a program superclass) referenced by a bare identifier. Returns + * null for anything it can't resolve cheaply. + */ + private TypeRef inferExprTypeQuiet(Expr e, Ctx ctx) { + if (e instanceof Ident) { + String nm = ((Ident) e).name; + TypeRef local = ctx.lookup(nm); + if (local != null) { + return local; + } + ClassDecl cc = ctx.currentClass; + if (cc != null) { + FieldDecl f = cc.field(nm); + if (f != null) { + return fieldType(f, ctx); + } + FieldDecl inhF = findInheritedField(cc, nm); + if (inhF != null) { + return fieldType(inhF, ctx); + } + } + } + return null; + } + + /** + * True for a {@code (i, e) in xs.indexed} loop shape: the iterable is a plain {@code .indexed} + * access and the pattern is a two-field positional record. Shared by the statement for-in and + * the collection-literal for-element forms. + */ + private boolean isIndexedRecordFor(Pattern pattern, Expr iterable) { + if (!(pattern instanceof RecordPattern) || !(iterable instanceof PropertyGet)) { + return false; + } + PropertyGet pg = (PropertyGet) iterable; + if (!pg.name.equals("indexed") || pg.nullAware) { + return false; + } + int positional = 0; + for (PatternField pf : ((RecordPattern) pattern).fields) { + if (pf.name != null) { + return false; + } + positional++; + } + return positional == 2; + } + + /** + * Emits the header of an {@code xs.indexed} loop — a {@code long} counter and an + * enhanced-for over the base iterable — binding the record's index and element + * subpatterns, then runs {@code body} for the loop body and closes the loop. Dart's + * {@code Iterable.indexed} has no runtime counterpart, so this counted lowering stands in. + */ + private void emitIndexedFor(Pattern pattern, Expr iterable, Ctx ctx, Runnable body) { + Ctx.Writer w = ctx.writer(); + PropertyGet pg = (PropertyGet) iterable; + Out base = emitExpr(pg.target, null, ctx); + TypeRef bt = base.type; + TypeRef elemT = bt != null && (bt.is("List") || bt.is("Iterable") || bt.is("Set")) + && !bt.args.isEmpty() ? bt.arg(0) : TypeRef.DYNAMIC; + RecordPattern rp = (RecordPattern) pattern; + ctx.pushScope(); + String counter = ctx.newTemp(); + String el = ctx.newTemp(); + w.line("long " + counter + " = 0;"); + w.line("for (" + javaType(elemT, true, ctx) + " " + el + " : " + base.code + ") {"); + ctx.indent(1); + ctx.declare(el, elemT); + List binds = new ArrayList(); + patternMatch(rp.fields.get(0).pattern, counter, TypeRef.INT, ctx, binds); + patternMatch(rp.fields.get(1).pattern, el, elemT, ctx, binds); + for (String b : binds) { + w.line(b); + } + body.run(); + w.line(counter + "++;"); + ctx.indent(-1); + w.line("}"); + ctx.popScope(); + } + + /** + * Emits the binding declarations for a pattern matched against {@code subj} (a temp holding the + * scrutinee) into {@code binds}, and returns the boolean match condition. Bound variables are + * declared into the current scope so the case body and guard can reference them. + */ + private String patternMatch(Pattern p, String subj, TypeRef subjType, Ctx ctx, List binds) { + if (p instanceof VariablePattern) { + VariablePattern v = (VariablePattern) p; + if (v.wildcard) { + return "true"; + } + // An unqualified identifier pattern whose name is a constant of the enum being + // switched over is a CONSTANT pattern in Dart, not a variable binding (e.g. + // `switch (this) { study => ..., material || cupertino => ... }`). Compare by + // enum identity and bind nothing so it composes inside or-patterns. + if (v.type == null && subjType != null && program.enums.containsKey(subjType.name) + && program.enums.get(subjType.name).hasEntry(v.name)) { + return subj + " == " + subjType.name + "." + v.name; + } + if (v.type != null && isReferenceType(v.type)) { + String jt = javaType(v.type, true, ctx); + String nm = ctx.declareShadowSafe(v.name, v.type); + binds.add(jt + " " + nm + " = (" + jt + ") " + subj + ";"); + // Tested erased: Java has no instanceof for a parameterized type. + return subj + " instanceof " + erasedJavaType(v.type, ctx); + } + TypeRef bt = v.type != null ? v.type : subjType; + String jt = javaType(bt, false, ctx); + String nm = ctx.declareShadowSafe(v.name, bt); + binds.add(jt + " " + nm + " = " + castSubject(subj, subjType, bt, ctx) + ";"); + return v.type != null ? instanceofCheck(subj, v.type, ctx) : "true"; + } + if (p instanceof ConstantPattern) { + ctx.importClass("dart.runtime.DartRuntime"); + Out val = emitExpr(((ConstantPattern) p).value, subjType, ctx); + return "DartRuntime.eq(" + subj + ", " + val.code + ")"; + } + if (p instanceof RelationalPattern) { + RelationalPattern r = (RelationalPattern) p; + Out operand = emitExpr(r.operand, subjType, ctx); + if ("==".equals(r.op) || "!=".equals(r.op)) { + ctx.importClass("dart.runtime.DartRuntime"); + String eq = "DartRuntime.eq(" + subj + ", " + operand.code + ")"; + return "==".equals(r.op) ? eq : "!(" + eq + ")"; + } + String num = numericValue(subj, subjType); + return "(" + num + " " + r.op + " " + operand.code + ")"; + } + if (p instanceof CastPattern) { + CastPattern c = (CastPattern) p; + String jt = javaType(c.type, true, ctx); + String cast = "((" + jt + ") " + subj + ")"; + return joinAnd(subj + " instanceof " + erasedJavaType(c.type, ctx), + patternMatch(c.inner, cast, c.type, ctx, binds)); + } + if (p instanceof ObjectPattern) { + ObjectPattern o = (ObjectPattern) p; + String jt = javaType(o.type, true, ctx); + String cond = subj + " instanceof " + erasedJavaType(o.type, ctx); + String cast = "((" + jt + ") " + subj + ")"; + for (PatternField f : o.fields) { + String access = cast + "." + fieldAccess(o.type, f.name) + "()"; + TypeRef ft = fieldTypeOf(o.type, f.name); + cond = joinAnd(cond, patternMatch(f.pattern, access, ft, ctx, binds)); + } + return cond; + } + if (p instanceof AndPattern) { + String cond = "true"; + for (Pattern part : ((AndPattern) p).parts) { + cond = joinAnd(cond, patternMatch(part, subj, subjType, ctx, binds)); + } + return cond; + } + if (p instanceof OrPattern) { + // or-patterns must not bind (Dart requires identical bindings on every branch); the + // common use is alternative constants, which bind nothing. + List throwaway = new ArrayList(); + StringBuilder cond = new StringBuilder("("); + List alts = ((OrPattern) p).alternatives; + for (int i = 0; i < alts.size(); i++) { + if (i > 0) { + cond.append(" || "); + } + cond.append(patternMatch(alts.get(i), subj, subjType, ctx, throwaway)); + } + if (!throwaway.isEmpty()) { + diags.error(p, "E0433", "Variable bindings inside an or-pattern are not supported"); + } + return cond.append(")").toString(); + } + if (p instanceof ListPattern) { + ListPattern l = (ListPattern) p; + ctx.importClass("java.util.List"); + String cast = "((List) " + subj + ")"; + String cond = subj + " instanceof List && " + cast + ".size() == " + l.elements.size(); + for (int i = 0; i < l.elements.size(); i++) { + String access = cast + ".get(" + i + ")"; + cond = joinAnd(cond, patternMatch(l.elements.get(i), access, TypeRef.DYNAMIC, ctx, binds)); + } + return cond; + } + if (p instanceof RecordPattern) { + RecordPattern rp = (RecordPattern) p; + List positional = new ArrayList(); + List named = new ArrayList(); + for (PatternField f : rp.fields) { + if (f.name == null) { + positional.add(f); + } else { + named.add(f); + } + } + named.sort((a, b) -> a.name.compareTo(b.name)); + List namedNames = new ArrayList(); + for (PatternField f : named) { + namedNames.add(f.name); + } + String cn = registerRecordShape(positional.size(), namedNames); + String cast = "((" + cn + ") " + subj + ")"; + String cond = subj + " instanceof " + cn; + for (int i = 0; i < positional.size(); i++) { + cond = joinAnd(cond, patternMatch(positional.get(i).pattern, + cast + ".$" + (i + 1) + "()", TypeRef.DYNAMIC, ctx, binds)); + } + for (PatternField f : named) { + cond = joinAnd(cond, patternMatch(f.pattern, + cast + "." + f.name + "()", TypeRef.DYNAMIC, ctx, binds)); + } + return cond; + } + diags.error(p, "E0434", "Unsupported pattern in emitter"); + return "false"; + } + + private static String joinAnd(String a, String b) { + if ("true".equals(a)) { + return b; + } + if ("true".equals(b)) { + return a; + } + return a + " && " + b; + } + + /** True for a type that maps to a Java reference type (so {@code instanceof} + cast is legal). */ + private boolean isReferenceType(TypeRef t) { + String n = t.name; + return !(n.equals("int") || n.equals("double") || n.equals("num") || n.equals("bool")); + } + + private String instanceofCheck(String subj, TypeRef type, Ctx ctx) { + if (isReferenceType(type)) { + return subj + " instanceof " + erasedJavaType(type, ctx); + } + // primitive-typed variable pattern over a dynamic subject: check the boxed form + String boxed = type.is("bool") ? "Boolean" + : type.is("double") ? "Double" + : type.is("num") ? "Number" : "Long"; + return subj + " instanceof " + boxed; + } + + /** Casts/unboxes a scrutinee temp to the target bind type when they differ. */ + private String castSubject(String subj, TypeRef from, TypeRef to, Ctx ctx) { + if (from != null && from.name.equals(to.name)) { + return subj; + } + String jt = javaType(to, false, ctx); + if (to.is("int")) { + return "((Number) " + subj + ").longValue()"; + } + if (to.is("double")) { + return "((Number) " + subj + ").doubleValue()"; + } + return "(" + jt + ") " + subj; + } + + private String numericValue(String subj, TypeRef subjType) { + if (subjType != null && (subjType.is("int") || subjType.is("double") || subjType.is("num"))) { + return subj; + } + return "((Number) " + subj + ").doubleValue()"; + } + + /** The accessor call (without trailing {@code ()}) for a field/getter of a class in a pattern. */ + private String fieldAccess(TypeRef ownerType, String name) { + ClassDecl cd = program.classes.get(ownerType.name); + if (cd != null) { + if (cd.field(name) != null) { + return "get$" + name; + } + if (cd.getter(name) != null) { + return name; + } + } + return "get$" + name; + } + + private TypeRef fieldTypeOf(TypeRef ownerType, String name) { + ClassDecl cd = program.classes.get(ownerType.name); + if (cd != null) { + FieldDecl f = cd.field(name); + if (f != null && f.type != null) { + return f.type; + } + MethodDecl g = cd.getter(name); + if (g != null && g.returnType != null) { + return g.returnType; + } + } + return TypeRef.DYNAMIC; + } + + /** Blocks nested under if/while/for are emitted inline (the brace is already written). */ + private Stmt unwrapBlock(Stmt s) { + return s; + } + + // ================================================================== + // Expressions + // ================================================================== + + /** Emitted expression: Java code + inferred Dart static type. */ + private static final class Out { + final String code; + final TypeRef type; + /** + * True when this value's type fell to {@code dynamic} because a diagnostic was + * already reported for it (an unresolved identifier/member/method/constructor). + * Member/method access on such a receiver is suppressed from re-diagnosing, so a + * single root cause is reported once instead of cascading down the whole chain. + */ + final boolean fromError; + /** + * Dart null-shorting: when non-null, this names a temp whose nullness shorts the + * WHOLE selector chain this Out belongs to. Set by a null-aware access (`a?.b`) and + * propagated through trailing plain selectors (`.c()`, `.d`), then materialized into a + * `(guard == null ? null : code)` conditional when the value is finally consumed. + */ + final String shortGuard; + + Out(String code, TypeRef type) { + this(code, type, false, null); + } + + Out(String code, TypeRef type, boolean fromError) { + this(code, type, fromError, null); + } + + Out(String code, TypeRef type, boolean fromError, String shortGuard) { + this.code = code; + this.type = type == null ? TypeRef.DYNAMIC : type; + this.fromError = fromError; + this.shortGuard = shortGuard; + } + + /** This Out re-tagged so its whole chain is shorted by {@code guard} being null. */ + Out withShort(String guard) { + return guard == null ? this : new Out(code, type, fromError, guard); + } + } + + /** + * Emits an expression as a consumed VALUE: any pending Dart null-short guard + * (from a `?.` selector chain) is materialized into a conditional here. Selector + * emitters that want to extend the chain call {@link #emitExprRaw} for their target + * instead, so the guard propagates until the chain ends. + */ + private Out emitExpr(Expr e, TypeRef expected, Ctx ctx) { + return materializeShort(emitExprRaw(e, expected, ctx)); + } + + /** Wraps a guard-carrying Out in its `(guard == null ? null : code)` conditional. */ + private Out materializeShort(Out o) { + if (o != null && o.shortGuard != null) { + return new Out("(" + o.shortGuard + " == null ? null : " + o.code + ")", + boxType(o.type), o.fromError); + } + return o; + } + + private Out emitExprRaw(Expr e, TypeRef expected, Ctx ctx) { + if (e instanceof IntLit) { + long v = ((IntLit) e).value; + if (expected != null && expected.is("double")) { + return new Out(v + ".0", TypeRef.DOUBLE); + } + return new Out(v + "L", TypeRef.INT); + } + if (e instanceof DoubleLit) { + double v = ((DoubleLit) e).value; + String s = Double.toString(v); + return new Out(s, TypeRef.DOUBLE); + } + if (e instanceof BoolLit) { + return new Out(String.valueOf(((BoolLit) e).value), TypeRef.BOOL); + } + if (e instanceof NullLit) { + return new Out("null", TypeRef.NULL); + } + if (e instanceof StringLit) { + return emitString((StringLit) e, ctx); + } + if (e instanceof ListLit) { + return emitListLit((ListLit) e, expected, ctx); + } + if (e instanceof MapLit) { + return emitMapLit((MapLit) e, expected, ctx); + } + if (e instanceof SetLit) { + return emitSetLit((SetLit) e, expected, ctx); + } + if (e instanceof Ident) { + return emitIdent((Ident) e, expected, ctx); + } + if (e instanceof ThisExpr) { + if (ctx.extensionSelfType != null) { + return new Out("$self", ctx.extensionSelfType); + } + return new Out("this", ctx.currentClass != null ? new TypeRef(ctx.currentClass.name) : TypeRef.DYNAMIC); + } + if (e instanceof SuperExpr) { + TypeRef sup = ctx.currentClass != null && ctx.currentClass.superclass != null + ? ctx.currentClass.superclass : TypeRef.DYNAMIC; + return new Out("super", sup); + } + if (e instanceof CascadeTarget) { + return ctx.cascadeTarget(); + } + if (e instanceof Cascade) { + Cascade cas = (Cascade) e; + Out target = emitExpr(cas.target, expected, ctx); + String tmp; + if (target.code.equals("this")) { + tmp = "this"; + } else { + tmp = ctx.newTemp(); + ctx.writer().line("var " + tmp + " = " + target.code + ";"); + } + if (cas.nullAware && !tmp.equals("this")) { + // `a?..b()..c = v`: a null target skips every section, and the value is null. + ctx.writer().line("if (" + tmp + " != null) {"); + ctx.indent(1); + } + ctx.pushCascadeTarget(new Out(tmp, cas.nullAware ? copyNonNull(target.type) : target.type)); + for (Expr section : cas.sections) { + Out o = emitStatementExpr(section, ctx); + String code = statementize(o.code); + if (!code.isEmpty()) { + ctx.writer().line(code + ";"); + } + } + ctx.popCascadeTarget(); + if (cas.nullAware && !tmp.equals("this")) { + ctx.indent(-1); + ctx.writer().line("}"); + } + return new Out(tmp, target.type); + } + if (e instanceof ThrowExpr) { + diags.error(e, "E0205", "throw is only supported in statement position in M2"); + return new Out("null", TypeRef.DYNAMIC); + } + if (e instanceof AwaitExpr) { + Out o = emitExpr(((AwaitExpr) e).operand, null, ctx); + // Awaiting a void-typed operand (e.g. a `void` stub call) yields nothing; wrapping it + // in Await.await$(...) is a "void not allowed here" error, so emit the bare call. + if (o.type != null && o.type.is("void")) { + return new Out(o.code, TypeRef.VOID); + } + ctx.importClass("dart.async.Await"); + if (o.type != null && o.type.is("Future")) { + return new Out("Await.await$(" + o.code + ")", boxType(o.type.arg(0))); + } + if (o.type == null || isDynamicType(o.type) || o.type.is("FutureOr")) { + // Might be a Future, might be a value: decided at run time. + TypeRef inner = o.type != null && o.type.is("FutureOr") && !o.type.args.isEmpty() + ? o.type.arg(0) : TypeRef.DYNAMIC; + String v = "Await.awaitAny(" + o.code + ")"; + return new Out(isDynamicType(inner) ? v : "((" + javaType(boxType(inner), true, ctx) + ") " + v + ")", + boxType(inner)); + } + // A statically non-Future value: `await 42` is 42. + return new Out(o.code, o.type); + } + if (e instanceof ParenExpr) { + Out inner = emitExpr(((ParenExpr) e).inner, expected, ctx); + return new Out("(" + inner.code + ")", inner.type); + } + if (e instanceof SwitchExpr) { + return emitSwitchExpr((SwitchExpr) e, expected, ctx); + } + if (e instanceof RecordLit) { + return emitRecordLit((RecordLit) e, ctx); + } + if (e instanceof PropertyGet) { + return emitPropertyGet((PropertyGet) e, ctx); + } + if (e instanceof Call) { + return emitCall((Call) e, expected, ctx); + } + if (e instanceof CtorCall) { + CtorCall cc = (CtorCall) e; + if (cc.ctorName != null) { + // List.generate / .filled / .from — dart:core intrinsic factories, + // routed to the primitive Dart*List when E is a non-nullable int/double. + if (cc.type.name.equals("List") + && (cc.ctorName.equals("generate") || cc.ctorName.equals("filled") + || cc.ctorName.equals("from") || cc.ctorName.equals("unmodifiable"))) { + return emitListFactory(cc, ctx); + } + // Map/Set/Iterable named factory constructors — dart:core intrinsics + // routed to the existing statics on Dart{Map,Set,Iterable}. + Out coreFactory = emitCoreCollectionFactory(cc, ctx); + if (coreFactory != null) { + return coreFactory; + } + // Future named constructors (Future.delayed / Future.value / Future.error), + // e.g. `Future.delayed(Duration(...), () { ... })`. Routed to the + // dart.async.Future statics; the element type witness is dropped (erased). + if (cc.type.name.equals("Future")) { + Out future = emitFutureNamedCtor(cc, ctx); + if (future != null) { + return future; + } + } + ClassDecl pc = program.classes.get(cc.type.name); + if (pc != null && pc.namedCtor(cc.ctorName) != null) { + return new Out(javaClassName(pc) + "." + javaIdent(cc.ctorName) + "(" + + canonicalArgs(pc.namedCtor(cc.ctorName), cc.args, ctx) + ")", + new TypeRef(cc.type.name)); + } + Ast.ClassDecl sc = stubs.classes.get(cc.type.name); + if (sc != null) { + // stub named ctors are declared as static methods + Ast.MethodDecl m = stubs.findMethod(cc.type.name, cc.ctorName, false); + if (m != null && m.isStatic) { + String call = stubSimpleName(cc.type.name, ctx) + "." + cc.ctorName + "(" + + stubMethodArgs(m, cc.args, ctx) + ")"; + // `Provider.value(...)`: the T a null value cannot show. + String token = publishesValueByType(cc.type.name) + ? providedTypeToken(cc.type.args, ctx) : null; + if (token != null) { + call += ".providedType(" + token + ")"; + } + return new Out(call, m.returnType); + } + } + diags.error(e, "E0126", "Cannot resolve named constructor " + cc.type.name + "." + cc.ctorName); + return new Out("null", TypeRef.DYNAMIC); + } + return emitCtorCall(cc.type.name, cc.type.args, cc.args, e, ctx); + } + if (e instanceof IndexGet) { + return emitIndexGet((IndexGet) e, ctx); + } + if (e instanceof Assign) { + return emitAssign((Assign) e, ctx); + } + if (e instanceof Binary) { + return emitBinary((Binary) e, ctx); + } + if (e instanceof Unary) { + Unary u = (Unary) e; + Out o = emitExpr(u.operand, null, ctx); + if (isDynamic(o.type)) { + // Java's unary operators do not apply to Object; dispatch on the value. + ctx.importClass("dart.runtime.DartRuntime"); + if (u.op.equals("!")) { + return new Out("!DartRuntime.dynBool(" + o.code + ")", TypeRef.BOOL); + } + String fn = u.op.equals("-") ? "dynNegate" : u.op.equals("~") ? "dynBitNot" : null; + if (fn != null) { + return new Out("DartRuntime." + fn + "(" + o.code + ")", TypeRef.DYNAMIC); + } + } + return new Out(u.op + paren(o.code), o.type); + } + if (e instanceof IncDec) { + IncDec id = (IncDec) e; + boolean statement = ctx.statementExpr == e; + Assign inc = new Assign().at(id.file, id.line, id.col); + inc.lhs = id.operand; + inc.op = id.increment ? "+=" : "-="; + IntLit one = new IntLit().at(id.file, id.line, id.col); + one.value = 1; + inc.rhs = one; + Expr savedStmt = ctx.statementExpr; + Assign savedPostfix = ctx.postfixAssign; + if (statement) { + ctx.statementExpr = inc; + } else if (!id.prefix) { + ctx.postfixAssign = inc; + } + try { + if (needsShort(inc) + || (id.operand instanceof Ident && isTopLevelSetter(((Ident) id.operand).name, ctx))) { + // `a?.b++` is shorted like every assignment behind a `?.`; `top++` on a + // library-level getter/setter pair writes through the setter. + return emitAssign(inc, ctx); + } + ctx.pushWriter(ctx.currentIndent()); + Out target = emitExpr(id.operand, null, ctx); + String targetLifted = ctx.popWriter(); + boolean accessor = target.code.endsWith("()") && target.code.contains(".get$"); + if ((id.operand instanceof PropertyGet + && target.code.endsWith("." + ((PropertyGet) id.operand).name + "()")) + || (accessor && !statement)) { + // `x.prop++` through a getter/setter pair (an app or stub property): the + // read is a call, and `x.prop()++` is not Java. Lowered as `x.prop += 1`, + // which writes through the setter, in its value form when the value is + // used (the old value for a postfix). The read emitted above is dropped + // with whatever it lifted, so its receiver is not evaluated twice. + ctx.pushWriter(ctx.currentIndent()); + Out write = emitAssign(inc, ctx); + String writeLifted = ctx.popWriter(); + // No setter found: emitAssign wrote the read back as an assignment + // target; keep the historical form rather than that. + if (!write.code.startsWith(target.code + " ")) { + ctx.writer().raw(writeLifted); + return write; + } + } + ctx.writer().raw(targetLifted); + if (target.code.endsWith("()") && target.code.contains(".get$")) { + String base = target.code.substring(0, target.code.lastIndexOf(".get$")); + String prop = target.code.substring(target.code.lastIndexOf(".get$") + 5, target.code.length() - 2); + String delta = id.increment ? " + 1" : " - 1"; + return new Out(base + ".set$" + prop + "(" + target.code + delta + ")", target.type); + } + if (isDynamic(target.type)) { + // `x++` on a dynamic x: Java's ++ does not apply to Object. + ctx.importClass("dart.runtime.DartRuntime"); + String write = target.code + " = DartRuntime.dynBinary(\"" + (id.increment ? "+" : "-") + + "\", " + target.code + ", 1L)"; + return new Out(id.prefix ? write + : "DartRuntime.dynPostfix(" + target.code + ", " + write + ")", target.type); + } + String op = id.increment ? "++" : "--"; + return new Out(id.prefix ? op + target.code : target.code + op, target.type); + } finally { + ctx.statementExpr = savedStmt; + ctx.postfixAssign = savedPostfix; + } + } + if (e instanceof Conditional) { + Conditional c = (Conditional) e; + Out cond = emitExpr(c.condition, TypeRef.BOOL, ctx); + // `x is T ? x.member : ...` promotes x to T in the then-branch. + List undo = applyGuardPromotions(c.condition, ctx); + Lazy la = emitLazyOperand(c.thenExpr, expected, ctx, 1); + restorePromotions(undo, ctx); + Lazy lb = emitLazyOperand(c.elseExpr, expected, ctx, 1); + Out a = la.out; + Out b = lb.out; + TypeRef t = conditionalType(a.type, b.type, expected); + boolean mixedNumeric = a.type != null && b.type != null + && ((a.type.is("int") && b.type.is("double")) || (a.type.is("double") && b.type.is("int"))) + && expected != null + && (expected.is("Object") || expected.is("num") || expected.is("dynamic")); + if (la.lifts() || lb.lifts()) { + // An arm lifted statements: they belong to that arm alone, so the + // conditional becomes an if/else assigning a temp. + String jt = mixedNumeric ? "Number" : declarableType(t, false, ctx); + Ctx.Writer w = ctx.writer(); + if (jt == null) { + // No temp can hold the value; keep the old, eager form. + w.raw(la.lifted); + w.raw(lb.lifted); + } else { + String r = ctx.newTemp(); + w.line(jt + " " + r + ";"); + w.line("if (" + cond.code + ") {"); + w.raw(la.lifted); + ctx.indent(1); + w.line(r + " = " + (mixedNumeric ? "(Number) " + paren(a.code) : coerce(a, t, ctx)) + ";"); + ctx.indent(-1); + w.line("} else {"); + w.raw(lb.lifted); + ctx.indent(1); + w.line(r + " = " + (mixedNumeric ? "(Number) " + paren(b.code) : coerce(b, t, ctx)) + ";"); + ctx.indent(-1); + w.line("}"); + return new Out(r, t); + } + } + if (mixedNumeric) { + // An int arm and a double arm STORED as num or Object: Java's ternary + // would promote the int to double even when that arm is chosen, so + // `Object v = f ? 1 : 1.5` held 1.0 and `v is int` was false. Casting + // each arm to Number keeps the selected value's own type -- boxing + // alone is not enough, since a Long/Double pair is still promoted. + // Only for a reference target: as an arithmetic operand or a double + // return, the promotion gives the same number Dart does, and Number + // arms would not compile there. + return new Out("(" + cond.code + " ? (Number) " + paren(a.code) + " : (Number) " + + paren(b.code) + ")", t); + } + return new Out("(" + cond.code + " ? " + a.code + " : " + b.code + ")", t); + } + if (e instanceof NotNullAssert) { + Out o = emitExpr(((NotNullAssert) e).operand, null, ctx); + ctx.importClass("dart.runtime.DartRuntime"); + TypeRef t = copyNonNull(o.type); + return new Out("DartRuntime.nn(" + o.code + ")", t, o.fromError); + } + if (e instanceof IsTest) { + IsTest t = (IsTest) e; + Out o = emitExpr(t.operand, null, ctx); + String subject = o.code; + if (o.type != null && !o.type.nullable + && (o.type.is("int") || o.type.is("double") || o.type.is("bool"))) { + // A primitive operand cannot be tested with instanceof; the cast boxes it, + // so `min(1, 2) is int` tests the Long it is. + subject = "((Object) " + paren(o.code) + ")"; + } + String check; + if (t.type.is("Null")) { + // `x is Null` holds only for null; it mapped to Object, which is the opposite. + check = subject + " == null"; + } else if (t.type.nullable) { + // `x is int?` holds for null too. The subject is evaluated once, inside the + // helper, so `next() is int?` does not call next() twice. + ctx.importClass("dart.runtime.DartRuntime"); + check = "DartRuntime.isOrNull(" + subject + ", " + erasedJavaType(t.type, ctx) + ".class)"; + } else { + // Erased: `x is List` cannot name its arguments in a Java instanceof. + check = subject + " instanceof " + erasedJavaType(t.type, ctx); + } + return new Out(t.negated ? "!(" + check + ")" : "(" + check + ")", TypeRef.BOOL); + } + if (e instanceof AsCast) { + return emitAsCast((AsCast) e, ctx); + } + if (e instanceof Lambda) { + return emitLambda((Lambda) e, expected, ctx); + } + diags.error(e, "E0128", "Unsupported expression in emitter: " + e.getClass().getSimpleName()); + return new Out("null", TypeRef.DYNAMIC); + } + + /** + * The Java class an {@code is}/{@code as} test checks against: the type's Java form + * with its type arguments dropped, since Java can neither test nor cast-check a + * parameterized type at runtime ({@code instanceof DartList} does not + * compile). The class itself is kept -- List<int> stays DartLongList -- because + * that is the class a value of that type is held in, and the one a promoted use casts to. + */ + private String erasedJavaType(TypeRef t, Ctx ctx) { + String jt = javaType(t, true, ctx); + int lt = jt.indexOf('<'); + return lt < 0 ? jt : jt.substring(0, lt); + } + + /** A type that is a type VARIABLE here ({@code T}): it has no class to test against. */ + private boolean isTypeVariable(TypeRef t, Ctx ctx) { + if (t == null || !t.args.isEmpty() || t.funcReturn != null || t.name == null + || t.name.indexOf('.') >= 0) { + return false; + } + String n = t.name; + if (n.equals("int") || n.equals("double") || n.equals("num") || n.equals("bool") + || n.equals("String") || n.equals("Object") || n.equals("dynamic") || n.equals("var") + || n.equals("Null") || n.equals("void") || n.equals("Function")) { + return false; + } + if (program.resolveClass(n, ctx.library()) != null || program.classesByName.containsKey(n) + || program.enums.containsKey(n) || program.typedefs.containsKey(n) + || stubs.isStubClass(n) || stubs.isStubEnum(n) || typedefSig(n) != null) { + return false; + } + String jt = javaType(t, true, ctx); + return jt.equals(n); + } + + /** + * {@code value as T}. A failed cast is Dart's TypeError, so it goes through + * DartRuntime.as, which tests with isInstance and throws one. A plain Java cast threw + * ClassCastException, which {@code on TypeError} never caught -- and on iOS threw + * nothing, because ParparVM does not check casts. {@code as T?} lets null through. + */ + private Out emitAsCast(AsCast c, Ctx ctx) { + Out o = emitExpr(c.operand, null, ctx); + String jt = javaType(c.type, true, ctx); + TypeRef target = c.type; + boolean unchecked = target.is("dynamic") || target.is("var") + || (target.is("Object") && target.nullable) + // No class to test a type variable against; Dart reifies it, Java erases it. + || isTypeVariable(target, ctx) + // An upcast to the static type it already has cannot fail. + || (o.type != null && !isDynamic(o.type) && o.type.args.isEmpty() && target.args.isEmpty() + && o.type.name != null && o.type.name.equals(target.name) + && (target.nullable || !o.type.nullable)); + if (unchecked) { + return new Out("((" + jt + ") " + o.code + ")", c.type); + } + ctx.importClass("dart.runtime.DartRuntime"); + String erased = erasedJavaType(target, ctx); + String operand = o.code; + if (o.type != null && !o.type.nullable + && (o.type.is("int") || o.type.is("double") || o.type.is("bool"))) { + operand = "(Object) " + paren(o.code); + } + String call = "DartRuntime.as(" + operand + ", " + erased + ".class, " + target.nullable + ", " + + quote(target.toString()) + ")"; + // The helper answers the erased class; a parameterized target narrows it unchecked. + return new Out(jt.equals(erased) ? call : "((" + jt + ") " + call + ")", c.type); + } + + private Out emitString(StringLit s, Ctx ctx) { + if (s.parts.size() == 1 && s.parts.get(0) instanceof String) { + return new Out(quote((String) s.parts.get(0)), TypeRef.STRING); + } + if (s.parts.isEmpty()) { + return new Out("\"\"", TypeRef.STRING); + } + ctx.importClass("dart.runtime.DartRuntime"); + if (s.parts.size() == 1) { + Out o = emitExpr((Expr) s.parts.get(0), null, ctx); + return new Out("DartRuntime.str(" + o.code + ")", TypeRef.STRING); + } + StringBuilder sb = new StringBuilder(); + boolean first = true; + boolean firstIsString = s.parts.get(0) instanceof String; + for (Object part : s.parts) { + if (!first) { + sb.append(" + "); + } + if (part instanceof String) { + sb.append(quote((String) part)); + } else { + Out o = emitExpr((Expr) part, null, ctx); + sb.append("DartRuntime.str(").append(o.code).append(')'); + } + first = false; + } + String code = sb.toString(); + if (!firstIsString) { + // DartRuntime.str returns String, so + concatenation is already string-typed + } + return new Out(code, TypeRef.STRING); + } + + private Out emitListLit(ListLit l, TypeRef expected, Ctx ctx) { + ctx.importClass("dart.core.DartList"); + TypeRef elem = l.elementType; + if (elem == null && expected != null && expected.is("List") && !expected.args.isEmpty()) { + elem = expected.arg(0); + } + boolean structured = false; + for (Expr e : l.elements) { + if (e instanceof SpreadElement || e instanceof IfElement || e instanceof ForElement) { + structured = true; + break; + } + } + if (structured) { + if (elem == null) { + // No type argument and no context: Dart infers the element type from + // the elements. Built as List instead, `x == null ? [] : + // [...x]` held a DartList where its int type promised a DartLongList, + // and the cast to it failed. + elem = structuredElementTypeQuiet(l.elements, ctx); + } + if (elem == null) { + elem = TypeRef.DYNAMIC; + } + String tmp = ctx.newTemp(); + String pk = primitiveListKind(TypeRef.of("List", elem)); + if (pk != null) { + ctx.importClass("dart.core.Dart" + pk + "List"); + ctx.writer().line("Dart" + pk + "List " + tmp + " = new Dart" + pk + "List();"); + } else { + ctx.writer().line("DartList<" + javaType(elem, true, ctx) + "> " + tmp + " = new DartList<>();"); + } + for (Expr e : l.elements) { + emitListElementInto(tmp, e, elem, ctx); + } + return new Out(tmp, TypeRef.of("List", elem)); + } + StringBuilder sb = new StringBuilder(); + List codes = new ArrayList(); + TypeRef inferred = null; + for (Expr e : l.elements) { + Out o = emitExpr(e, elem, ctx); + codes.add(elementCode(o, elem, ctx)); + if (inferred == null) { + inferred = o.type; + } + } + if (elem == null) { + elem = inferred != null ? inferred : TypeRef.DYNAMIC; + } + String pk = primitiveListKind(TypeRef.of("List", elem)); + if (pk != null) { + ctx.importClass("dart.core.Dart" + pk + "List"); + sb.append("Dart").append(pk).append("List.of").append("Long".equals(pk) ? "Longs(" : "Doubles("); + for (int i = 0; i < codes.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(codes.get(i)); + } + sb.append(')'); + return new Out(sb.toString(), TypeRef.of("List", elem)); + } + sb.append("DartList.<").append(javaType(elem, true, ctx)).append(">of("); + for (int i = 0; i < codes.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(codes.get(i)); + } + sb.append(')'); + return new Out(sb.toString(), TypeRef.of("List", elem)); + } + + /** + * The element type every element of an untyped collection literal agrees on -- + * a spread of a typed variable, a literal, a typed variable, an if-element whose + * branches agree -- or null when any of them cannot be told without emitting it. + */ + private TypeRef structuredElementTypeQuiet(List elements, Ctx ctx) { + TypeRef common = null; + for (Expr e : elements) { + TypeRef t = elementTypeQuiet(e, ctx); + if (t == null || isDynamicType(t)) { + return null; + } + if (common == null) { + common = t; + } else if (!common.toString().equals(t.toString())) { + return null; + } + } + return common; + } + + private TypeRef elementTypeQuiet(Expr e, Ctx ctx) { + if (e instanceof SpreadElement) { + TypeRef st = inferExprTypeQuiet(((SpreadElement) e).expr, ctx); + if (st != null && (st.is("List") || st.is("Set") || st.is("Iterable")) && st.args.size() == 1) { + return st.arg(0); + } + return null; + } + if (e instanceof IfElement) { + IfElement i = (IfElement) e; + TypeRef a = elementTypeQuiet(i.thenElement, ctx); + if (i.elseElement == null || a == null) { + return a; + } + TypeRef b = elementTypeQuiet(i.elseElement, ctx); + return b != null && a.toString().equals(b.toString()) ? a : null; + } + if (e instanceof IntLit) { + return TypeRef.INT; + } + if (e instanceof DoubleLit) { + return TypeRef.DOUBLE; + } + if (e instanceof BoolLit) { + return TypeRef.BOOL; + } + if (e instanceof StringLit) { + return TypeRef.STRING; + } + if (e instanceof Ident) { + return inferExprTypeQuiet(e, ctx); + } + return null; + } + + /** Lowers one collection element (plain / spread / if / for) to adds on the builder list. */ + private void emitListElementInto(String list, Expr e, TypeRef elem, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + if (e instanceof SpreadElement) { + SpreadElement s = (SpreadElement) e; + Out src = emitExpr(s.expr, null, ctx); + if (s.nullAware) { + String tmp = ctx.newTemp(); + w.line("var " + tmp + " = " + src.code + ";"); + w.line("if (" + tmp + " != null) {"); + ctx.indent(1); + w.line(list + ".addAllIterable(" + tmp + ");"); + ctx.indent(-1); + w.line("}"); + } else { + w.line(list + ".addAllIterable(" + src.code + ");"); + } + return; + } + if (e instanceof IfElement) { + IfElement i = (IfElement) e; + Out cond = emitExpr(i.condition, TypeRef.BOOL, ctx); + w.line("if (" + cond.code + ") {"); + ctx.indent(1); + emitListElementInto(list, i.thenElement, elem, ctx); + ctx.indent(-1); + if (i.elseElement != null) { + w.line("} else {"); + ctx.indent(1); + emitListElementInto(list, i.elseElement, elem, ctx); + ctx.indent(-1); + } + w.line("}"); + return; + } + if (e instanceof ForElement) { + final ForElement f = (ForElement) e; + final String list$ = list; + final TypeRef elem$ = elem; + if (isIndexedRecordFor(f.pattern, f.iterable)) { + emitIndexedFor(f.pattern, f.iterable, ctx, new Runnable() { + public void run() { + emitListElementInto(list$, f.body, elem$, ctx); + } + }); + return; + } + ctx.pushScope(); + if (f.pattern != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = iter.type != null && (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set")) + ? iter.type.arg(0) : TypeRef.DYNAMIC; + String loopVar = ctx.newTemp(); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + ctx.declare(loopVar, et); + List binds = new ArrayList(); + patternMatch(f.pattern, loopVar, et, ctx, binds); + for (String b : binds) { + w.line(b); + } + emitListElementInto(list, f.body, elem, ctx); + ctx.indent(-1); + w.line("}"); + } else if (f.varName != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = f.varType != null && !f.varType.is("var") + ? f.varType + : (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set") + ? iter.type.arg(0) : TypeRef.DYNAMIC); + String loopVar = ctx.declareShadowSafe(f.varName, et); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + emitListElementInto(list, f.body, elem, ctx); + ctx.indent(-1); + w.line("}"); + } else { + String initCode = ""; + if (f.init instanceof VarDeclStmt) { + VarDeclStmt v = (VarDeclStmt) f.init; + Out init = v.initializer != null ? emitExpr(v.initializer, v.type, ctx) : null; + TypeRef t = v.type == null || v.type.is("var") + ? (init != null ? init.type : TypeRef.DYNAMIC) : v.type; + String loopVar2 = ctx.declareShadowSafe(v.name, t); + initCode = javaType(t, false, ctx) + " " + loopVar2 + " = " + + (init != null ? coerce(init, t, ctx) : zeroValue(t)); + } else if (f.init instanceof ExprStmt) { + initCode = statementize(emitStatementExpr(((ExprStmt) f.init).expr, ctx).code); + } + emitCForLoop(initCode, f.condition, f.updates, ctx, new Runnable() { + public void run() { + emitListElementInto(list$, f.body, elem$, ctx); + } + }); + } + ctx.popScope(); + return; + } + Out o = emitExpr(e, elem, ctx); + w.line(list + ".add(" + coerce(o, elem, ctx) + ");"); + } + + private Out emitSetLit(SetLit s, TypeRef expected, Ctx ctx) { + ctx.importClass("dart.core.DartSet"); + TypeRef elem = s.elementType; + if (elem == null && expected != null && expected.is("Set") && !expected.args.isEmpty()) { + elem = expected.arg(0); + } + boolean structured = false; + for (Expr e : s.elements) { + if (e instanceof SpreadElement || e instanceof IfElement || e instanceof ForElement) { + structured = true; + break; + } + } + if (structured) { + if (elem == null) { + elem = TypeRef.DYNAMIC; + } + String tmp = ctx.newTemp(); + ctx.writer().line("DartSet<" + javaType(elem, true, ctx) + "> " + tmp + " = new DartSet<>();"); + for (Expr e : s.elements) { + emitSetElementInto(tmp, e, elem, ctx); + } + return new Out(tmp, TypeRef.of("Set", elem)); + } + List codes = new ArrayList(); + TypeRef inferred = null; + for (Expr e : s.elements) { + Out o = emitExpr(e, elem, ctx); + codes.add(elementCode(o, elem, ctx)); + if (inferred == null) { + inferred = o.type; + } + } + if (elem == null) { + elem = inferred != null ? inferred : TypeRef.DYNAMIC; + } + StringBuilder sb = new StringBuilder("DartSet.<").append(javaType(elem, true, ctx)).append(">of("); + for (int i = 0; i < codes.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(codes.get(i)); + } + sb.append(')'); + return new Out(sb.toString(), TypeRef.of("Set", elem)); + } + + /** Lowers one set-literal element (plain / spread / if / for) to adds on the builder set. */ + private void emitSetElementInto(String set, Expr e, TypeRef elem, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + if (e instanceof SpreadElement) { + SpreadElement s = (SpreadElement) e; + Out src = emitExpr(s.expr, null, ctx); + if (s.nullAware) { + String tmp = ctx.newTemp(); + w.line("var " + tmp + " = " + src.code + ";"); + w.line("if (" + tmp + " != null) {"); + ctx.indent(1); + w.line(set + ".addAllIterable(" + tmp + ");"); + ctx.indent(-1); + w.line("}"); + } else { + w.line(set + ".addAllIterable(" + src.code + ");"); + } + return; + } + if (e instanceof IfElement) { + IfElement i = (IfElement) e; + Out cond = emitExpr(i.condition, TypeRef.BOOL, ctx); + w.line("if (" + cond.code + ") {"); + ctx.indent(1); + emitSetElementInto(set, i.thenElement, elem, ctx); + ctx.indent(-1); + if (i.elseElement != null) { + w.line("} else {"); + ctx.indent(1); + emitSetElementInto(set, i.elseElement, elem, ctx); + ctx.indent(-1); + } + w.line("}"); + return; + } + if (e instanceof ForElement) { + final ForElement f = (ForElement) e; + final String set$ = set; + final TypeRef elem$ = elem; + if (isIndexedRecordFor(f.pattern, f.iterable)) { + emitIndexedFor(f.pattern, f.iterable, ctx, new Runnable() { + public void run() { + emitSetElementInto(set$, f.body, elem$, ctx); + } + }); + return; + } + ctx.pushScope(); + if (f.pattern != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = iter.type != null && (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set")) + ? iter.type.arg(0) : TypeRef.DYNAMIC; + String loopVar = ctx.newTemp(); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + ctx.declare(loopVar, et); + List binds = new ArrayList(); + patternMatch(f.pattern, loopVar, et, ctx, binds); + for (String b : binds) { + w.line(b); + } + emitSetElementInto(set, f.body, elem, ctx); + ctx.indent(-1); + w.line("}"); + } else if (f.varName != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = f.varType != null && !f.varType.is("var") + ? f.varType + : (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set") + ? iter.type.arg(0) : TypeRef.DYNAMIC); + String loopVar = ctx.declareShadowSafe(f.varName, et); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + emitSetElementInto(set, f.body, elem, ctx); + ctx.indent(-1); + w.line("}"); + } else { + String initCode = ""; + if (f.init instanceof VarDeclStmt) { + VarDeclStmt v = (VarDeclStmt) f.init; + Out init = v.initializer != null ? emitExpr(v.initializer, v.type, ctx) : null; + TypeRef t = v.type == null || v.type.is("var") + ? (init != null ? init.type : TypeRef.DYNAMIC) : v.type; + String loopVar2 = ctx.declareShadowSafe(v.name, t); + initCode = javaType(t, false, ctx) + " " + loopVar2 + " = " + + (init != null ? coerce(init, t, ctx) : zeroValue(t)); + } else if (f.init instanceof ExprStmt) { + initCode = statementize(emitStatementExpr(((ExprStmt) f.init).expr, ctx).code); + } + emitCForLoop(initCode, f.condition, f.updates, ctx, new Runnable() { + public void run() { + emitSetElementInto(set$, f.body, elem$, ctx); + } + }); + } + ctx.popScope(); + return; + } + Out o = emitExpr(e, elem, ctx); + w.line(set + ".add(" + coerce(o, elem, ctx) + ");"); + } + + private Out emitMapLit(MapLit m, TypeRef expected, Ctx ctx) { + TypeRef k = m.keyType; + TypeRef v = m.valueType; + if (k == null && expected != null && expected.is("Map") && expected.args.size() == 2) { + k = expected.arg(0); + v = expected.arg(1); + } + if (m.structured) { + // Collection if/for/spread in a map literal: build into a DartMap with conditional/looped put(). + ctx.importClass("dart.core.DartMap"); + TypeRef kt = k == null ? TypeRef.DYNAMIC : k; + TypeRef vt = v == null ? TypeRef.DYNAMIC : v; + String tmp = ctx.newTemp(); + ctx.writer().line("DartMap<" + javaType(kt, true, ctx) + ", " + javaType(vt, true, ctx) + + "> " + tmp + " = new DartMap<>();"); + for (Expr e : m.elements) { + emitMapElementInto(tmp, e, kt, vt, ctx); + } + return new Out(tmp, TypeRef.of("Map", kt, vt)); + } + TypeRef mapType = TypeRef.of("Map", + k == null ? TypeRef.DYNAMIC : k, v == null ? TypeRef.DYNAMIC : v); + // Primitive long->long path: {...} -> DartLongMap.ofLongs(k0,v0,...) (no boxing). + if (isPrimitiveLongMap(mapType)) { + ctx.importClass("dart.core.DartLongMap"); + StringBuilder sb = new StringBuilder("DartLongMap.ofLongs("); + for (int i = 0; i < m.keys.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(emitExpr(m.keys.get(i), k, ctx).code).append(", ") + .append(emitExpr(m.values.get(i), v, ctx).code); + } + sb.append(')'); + return new Out(sb.toString(), mapType); + } + ctx.importClass("dart.core.DartMap"); + List parts = new ArrayList(); + for (int i = 0; i < m.keys.size(); i++) { + Out ko = emitExpr(m.keys.get(i), k, ctx); + Out vo = emitExpr(m.values.get(i), v, ctx); + if (k == null) { + k = ko.type; + } + if (v == null) { + v = vo.type; + } + parts.add(funcCast(ko, k, ctx)); + parts.add(funcCast(vo, v, ctx)); + } + // A type witness lets `of` infer K,V from the declared/inferred entry types (the + // Object... varargs otherwise erase them to Object, breaking downstream `.idx` reads). + String witness = ""; + if (k != null && v != null && isConcreteType(k) && isConcreteType(v)) { + witness = ".<" + javaType(k, true, ctx) + ", " + javaType(v, true, ctx) + ">"; + } + StringBuilder sb = new StringBuilder("DartMap").append(witness.isEmpty() ? ".of(" : witness + "of("); + for (int i = 0; i < parts.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(parts.get(i)); + } + sb.append(')'); + return new Out(sb.toString(), TypeRef.of("Map", + k == null ? TypeRef.DYNAMIC : k, v == null ? TypeRef.DYNAMIC : v)); + } + + /** Lowers one map-literal element (entry / spread / if / for) to put()/putAll() on the builder map. */ + private void emitMapElementInto(String map, Expr e, TypeRef kt, TypeRef vt, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + if (e instanceof MapEntry) { + MapEntry me = (MapEntry) e; + Out ko = emitExpr(me.key, kt, ctx); + Out vo = emitExpr(me.value, vt, ctx); + w.line(map + ".put(" + boxIfPrimitive(ko, ctx) + ", " + boxIfPrimitive(vo, ctx) + ");"); + return; + } + if (e instanceof SpreadElement) { + SpreadElement s = (SpreadElement) e; + Out src = emitExpr(s.expr, null, ctx); + if (s.nullAware) { + String tmp = ctx.newTemp(); + w.line("var " + tmp + " = " + src.code + ";"); + w.line("if (" + tmp + " != null) {"); + ctx.indent(1); + w.line(map + ".addAll(" + tmp + ");"); + ctx.indent(-1); + w.line("}"); + } else { + w.line(map + ".addAll(" + src.code + ");"); + } + return; + } + if (e instanceof IfElement) { + IfElement i = (IfElement) e; + Out cond = emitExpr(i.condition, TypeRef.BOOL, ctx); + w.line("if (" + cond.code + ") {"); + ctx.indent(1); + emitMapElementInto(map, i.thenElement, kt, vt, ctx); + ctx.indent(-1); + if (i.elseElement != null) { + w.line("} else {"); + ctx.indent(1); + emitMapElementInto(map, i.elseElement, kt, vt, ctx); + ctx.indent(-1); + } + w.line("}"); + return; + } + if (e instanceof ForElement) { + final ForElement f = (ForElement) e; + final String map$ = map; + final TypeRef kt$ = kt; + final TypeRef vt$ = vt; + if (isIndexedRecordFor(f.pattern, f.iterable)) { + emitIndexedFor(f.pattern, f.iterable, ctx, new Runnable() { + public void run() { + emitMapElementInto(map$, f.body, kt$, vt$, ctx); + } + }); + return; + } + ctx.pushScope(); + if (f.pattern != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = iter.type != null && (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set")) + ? iter.type.arg(0) : TypeRef.DYNAMIC; + String loopVar = ctx.newTemp(); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + ctx.declare(loopVar, et); + List binds = new ArrayList(); + patternMatch(f.pattern, loopVar, et, ctx, binds); + for (String b : binds) { + w.line(b); + } + emitMapElementInto(map, f.body, kt, vt, ctx); + ctx.indent(-1); + w.line("}"); + } else if (f.varName != null) { + Out iter = emitExpr(f.iterable, null, ctx); + TypeRef et = f.varType != null && !f.varType.is("var") + ? f.varType + : (iter.type.is("List") || iter.type.is("Iterable") || iter.type.is("Set") + ? iter.type.arg(0) : TypeRef.DYNAMIC); + String loopVar = ctx.declareShadowSafe(f.varName, et); + w.line("for (" + javaType(et, true, ctx) + " " + loopVar + " : " + iter.code + ") {"); + ctx.indent(1); + emitMapElementInto(map, f.body, kt, vt, ctx); + ctx.indent(-1); + w.line("}"); + } else { + String initCode = ""; + if (f.init instanceof VarDeclStmt) { + VarDeclStmt v2 = (VarDeclStmt) f.init; + Out init = v2.initializer != null ? emitExpr(v2.initializer, v2.type, ctx) : null; + TypeRef t = v2.type == null || v2.type.is("var") + ? (init != null ? init.type : TypeRef.DYNAMIC) : v2.type; + String loopVar2 = ctx.declareShadowSafe(v2.name, t); + initCode = javaType(t, false, ctx) + " " + loopVar2 + " = " + + (init != null ? coerce(init, t, ctx) : zeroValue(t)); + } else if (f.init instanceof ExprStmt) { + initCode = statementize(emitStatementExpr(((ExprStmt) f.init).expr, ctx).code); + } + emitCForLoop(initCode, f.condition, f.updates, ctx, new Runnable() { + public void run() { + emitMapElementInto(map$, f.body, kt$, vt$, ctx); + } + }); + } + ctx.popScope(); + return; + } + diags.error(e, "E0205", "Unsupported map-literal element in emitter"); + } + + private Out emitIdent(Ident id, TypeRef expected, Ctx ctx) { + String n = id.name; + TypeRef local = ctx.lookup(n); + if (local != null) { + String jn = ctx.javaNameOf(n); + String base = ctx.isBoxed(n) ? jn + ".v" : jn; + // Flow-promoted by an `is` guard: read as the narrowed type via a cast. + TypeRef promo = ctx.promotedType(n); + if (promo != null) { + return new Out("((" + javaType(promo, true, ctx) + ") " + base + ")", promo); + } + return new Out(base, local); + } + ClassDecl cc = ctx.currentClass; + if (cc != null) { + FieldDecl f = cc.field(n); + if (f != null) { + if (cc.isMixin && !f.isStatic) { + // interface default methods reach mixin state via accessors + return new Out("this.get$" + n + "()", fieldType(f, ctx)); + } + if (isLazyStatic(f) || isLateChecked(f, cc)) { + return new Out((f.isStatic ? javaClassName(cc) : "this") + ".get$" + n + "()", fieldType(f, ctx)); + } + return new Out(f.isStatic ? javaClassName(cc) + "." + n : "this." + n, fieldType(f, ctx)); + } + MethodDecl getter = cc.getter(n); + if (getter != null) { + return new Out("this." + n + "()", getter.returnType); + } + // tear-off of an own method when a function-ish value is expected + // (a static method tears off through the class name, never `this`) + MethodDecl md = cc.method(n); + if (md != null) { + return new Out((md.isStatic ? cc.name : "this") + "::" + n, new TypeRef("Function")); + } + // field/getter inherited from a program superclass. The declared field is + // private in the superclass, so reads route through its public get$ accessor; + // a getter is a public zero-arg method. + FieldDecl inhF = findInheritedField(cc, n); + if (inhF != null && !inhF.isStatic && !inhF.name.startsWith("_")) { + return new Out("this.get$" + n + "()", fieldType(inhF, ctx)); + } + MethodDecl inhG = findInheritedGetter(cc, n); + if (inhG != null) { + return new Out("this." + n + "()", inhG.returnType); + } + MethodDecl inhM = findMethodInHierarchy(cc, n); + if (inhM != null) { + return new Out("this::" + n, new TypeRef("Function")); + } + // 'widget' inside a State subclass + if (n.equals("widget") && stateTypeArg(cc) != null) { + return new Out("this.widget()", stateTypeArg(cc)); + } + if (n.equals("context") && isStateSubclass(cc)) { + return new Out("this.context()", new TypeRef("BuildContext")); + } + if (n.equals("mounted") && isStateSubclass(cc)) { + return new Out("this.mounted()", TypeRef.BOOL); + } + // bare `runtimeType` — the receiver is the implicit `this` + if (n.equals("runtimeType")) { + return new Out("this.getClass()", new TypeRef("Type")); + } + // inside an enhanced-enum body: a bare enum-constant name resolves to the + // constant, and the implicit name()/index() intrinsics are in scope. + if (program.enums.containsKey(cc.name)) { + Ast.EnumDecl selfEnum = program.enums.get(cc.name); + if (selfEnum.hasEntry(n)) { + return new Out(cc.name + "." + n, new TypeRef(cc.name)); + } + if (n.equals("name")) { + return new Out("name()", TypeRef.STRING); + } + if (n.equals("index")) { + return new Out("ordinal()", TypeRef.INT); + } + } + // inherited stub getters + String stubSuper = nearestStubSuper(cc); + if (stubSuper != null) { + Ast.MethodDecl sg = stubs.findMethod(stubSuper, n, true); + if (sg != null) { + // Narrow a generic getter (declared `T get value`) to the concrete + // type argument the class fixes for its stub super. + TypeRef rt = inheritedStubMemberReturnType(cc, n, true); + return new Out("this." + n + "()", rt != null ? rt : sg.returnType); + } + } + } + Out top = resolveTopLevel(n, ctx); + if (top != null) { + return top; + } + diags.error(id, "E0129", "Cannot resolve identifier '" + n + + "'. Confirm the file passes `dart analyze`."); + return new Out(n, TypeRef.DYNAMIC, true); + } + + /** + * Resolves a name against whole-program top-level scope: a user or stub class / + * enum, a top-level const/var, a top-level function (as a method reference), or a + * stub-contributed top-level value. Returns null when the name is not top-level. + * Shared by bare-identifier resolution and import-prefix ({@code prefix.name}) + * member access — both denote the same global namespace in the single-package model. + */ + private Out resolveTopLevel(String n, Ctx ctx) { + return resolveTopLevel(n, null, ctx); + } + + /** + * @param prefix the import prefix the name was accessed through ({@code prefix.n}), + * or null for a bare identifier — used to disambiguate a top-level + * name declared in several libraries. + */ + private Out resolveTopLevel(String n, String prefix, Ctx ctx) { + if (program.classes.containsKey(n) || program.enums.containsKey(n) + || stubs.isStubClass(n) || stubs.isStubEnum(n) + || n.equals("Future") || n.equals("Duration") || n.equals("Iterable")) { + return new Out(n, classRef(n)); + } + if (program.topLevelVars.containsKey(n)) { + Library owner = program.resolveTopLevelVarOwner(n, ctx.library(), prefix); + FieldDecl v = program.topLevelVars.get(n); + // A lazily-initialised variable is an accessor pair, not a field — + // see isLazyTopLevel for why it cannot be a field. + String ref = Program.libClassName(owner.fileName) + + (isLazyTopLevel(v) ? ".get$" + n + "()" : "." + n); + return new Out(ref, fieldType(v, ctx)); + } + if (program.functions.containsKey(n)) { + Library owner = program.resolveFunctionOwner(n, ctx.library(), prefix); + FunctionDecl fn = program.functions.get(n); + if (fn.isGetter) { + // top-level getter access: `x` -> `OwnerLib.x()` + TypeRef rt = fn.returnType == null || fn.returnType.is("var") + ? TypeRef.DYNAMIC : fn.returnType; + return new Out(Program.libClassName(owner.fileName) + "." + n + "()", rt); + } + return new Out(Program.libClassName(owner.fileName) + "::" + n, new TypeRef("Function")); + } + // Top-level library values contributed by a stub (@JavaName maps the + // Dart top-level `pi` / `timeDilation` / `defaultTargetPlatform` to a + // fully-qualified Java static field). Both reads and writes route here. + FieldDecl stubVar = stubs.topLevelVars.get(n); + if (stubVar != null) { + return new Out(stubVar.javaName, fieldType(stubVar, ctx)); + } + return null; + } + + /** Whether {@code target} is an {@code import '...' as name} prefix, not a value. */ + private boolean isImportPrefix(Expr target, Ctx ctx) { + return target instanceof Ident + && ctx.lookup(((Ident) target).name) == null + && (ctx.currentClass == null || ctx.currentClass.field(((Ident) target).name) == null) + && program.importPrefixes.contains(((Ident) target).name) + && !program.topLevelVars.containsKey(((Ident) target).name) + && !program.classes.containsKey(((Ident) target).name) + && !program.enums.containsKey(((Ident) target).name); + } + + private FieldDecl findInheritedField(ClassDecl c, String name) { + ClassDecl s = c.superclass != null ? program.classes.get(c.superclass.name) : null; + while (s != null) { + FieldDecl f = s.field(name); + if (f != null) { + return f; + } + s = s.superclass != null ? program.classes.get(s.superclass.name) : null; + } + return null; + } + + private MethodDecl findInheritedGetter(ClassDecl c, String name) { + ClassDecl s = c.superclass != null ? program.classes.get(c.superclass.name) : null; + while (s != null) { + MethodDecl g = s.getter(name); + if (g != null) { + return g; + } + s = s.superclass != null ? program.classes.get(s.superclass.name) : null; + } + return null; + } + + /** + * {@code SomeEnum.values} — Dart exposes an enum's members as a GETTER returning a + * {@code List}, Java as a static method returning an ARRAY. + * + *

Both halves of that matter. Emitting the Dart spelling is a field access to + * something that does not exist; emitting the bare {@code values()} hands back an array + * to code that will go on to call Dart's list members on it. Wrapping restores the + * declared type, so indexing, iteration and {@code length} all work as written.

+ */ + private Out enumValues(String enumName, Ctx ctx) { + ctx.importClass("dart.core.DartList"); + return new Out("DartList.of(" + simpleEnumName(enumName, ctx) + ".values())", + TypeRef.of("List", new TypeRef(enumName))); + } + + private Out emitPropertyGet(PropertyGet pg, Ctx ctx) { + // `prefix.member` where prefix is an `import '...' as prefix` name: the member + // is a top-level const/var/class/enum/function of another user (or stub) library. + // In the whole-program single-package model that is just a global top-level lookup. + if (isImportPrefix(pg.target, ctx)) { + // deferred import: `m.loadLibrary` is a tear-off of type Future Function(). + // We compile everything ahead-of-time, so the library is always loaded; the + // tear-off is a supplier of an already-completed future. + if (pg.name.equals("loadLibrary")) { + ctx.importClass("dart.async.Future"); + return new Out("(() -> Future.value(null))", new TypeRef("Function")); + } + Out top = resolveTopLevel(pg.name, + pg.target instanceof Ident ? ((Ident) pg.target).name : null, ctx); + if (top != null) { + return top; + } + } + // `SomeEnum.values` - Dart exposes an enum's members as a GETTER, Java as a static + // METHOD. Intercepted here rather than in the class-reference path below because a + // bare `Enum.values` (a for-in subject, say) arrives as a plain property get whose + // target is a type name, not a class reference. + if (pg.name.equals("values") && pg.target instanceof Ident + && ctx.lookup(((Ident) pg.target).name) == null) { + String en = ((Ident) pg.target).name; + if (program.enums.containsKey(en) || stubs.isStubEnum(en)) { + importEnum(en, ctx); + return enumValues(en, ctx); + } + } + // Named constants on the primitive numeric types (double.infinity, double.nan, ...). + // These reach us as `.`; the type name is not a resolvable + // expression on its own, so intercept before trying to emit it as a target. + if (pg.target instanceof Ident && ctx.lookup(((Ident) pg.target).name) == null) { + Out prim = emitPrimitiveTypeConstant(((Ident) pg.target).name, pg.name); + if (prim != null) { + return prim; + } + } + Out target = emitExprRaw(pg.target, null, ctx); + if (pg.nullAware) { + // a?.b -> lift `$t = a` (materializing any guard a itself carries) and start a + // new short: the member reads on the non-null $t, and $t being null shorts the + // rest of the chain. The guard is NOT wrapped here so trailing plain selectors + // (`.c()`) fold into the same conditional. + Out mat = materializeShort(target); + String tmp = ctx.newTemp(); + ctx.writer().line("var " + tmp + " = " + mat.code + ";"); + Out member = emitMemberGet(new Out(tmp, copyNonNull(mat.type), mat.fromError), pg.name, pg, ctx); + return new Out(member.code, boxType(member.type), member.fromError, tmp); + } + Out member = emitMemberGet(target, pg.name, pg, ctx); + // a plain selector after a `?.` stays inside the short (a?.b.c) + return member.withShort(target.shortGuard); + } + + /** dart:core named constants on the primitive numeric types, e.g. {@code double.infinity}. */ + private Out emitPrimitiveTypeConstant(String type, String member) { + if (type.equals("double")) { + if (member.equals("infinity")) { + return new Out("Double.POSITIVE_INFINITY", TypeRef.DOUBLE); + } + if (member.equals("negativeInfinity")) { + return new Out("Double.NEGATIVE_INFINITY", TypeRef.DOUBLE); + } + if (member.equals("nan")) { + return new Out("Double.NaN", TypeRef.DOUBLE); + } + if (member.equals("maxFinite")) { + return new Out("Double.MAX_VALUE", TypeRef.DOUBLE); + } + if (member.equals("minPositive")) { + return new Out("Double.MIN_VALUE", TypeRef.DOUBLE); + } + } + return null; + } + + /** Property access driven by the target's static type. */ + private Out emitMemberGet(Out target, String name, Node posNode, Ctx ctx) { + TypeRef tt = target.type; + // A field/param typed with an import prefix (`intl.DateFormat`, `ui.Size`) keeps the + // prefix in its type name; strip it so member resolution sees the real stub type. + if (tt != null && tt.name != null && tt.name.indexOf('.') > 0) { + tt.name = stripImportPrefix(tt.name); + } + // Cascade suppression: the receiver already fell to `dynamic` from a reported + // diagnostic upstream; a member read on a dynamic receiver is legal Dart, so + // don't re-diagnose the same root cause on every link of the chain. + if (target.fromError && tt.is("dynamic")) { + return new Out(target.code + "." + name, TypeRef.DYNAMIC, true); + } + // Genuine `dynamic` receiver: a member read is legal Dart (dynamic dispatch, + // resolved at runtime) — e.g. `(dynamic demo) => demo.slug` or a dynamic-typed + // `platformDispatcher.platformBrightness`. Emit the access with a dynamic result. + if (tt != null && tt.is("dynamic")) { + return new Out(target.code + "." + name, TypeRef.DYNAMIC); + } + // record component accessor: `r.$1`, `r.$2`, or `r.namedField` + if (tt != null && tt.name.startsWith("Rec$")) { + return new Out(target.code + "." + name + "()", recordComponentType(tt, name)); + } + // Object protocol: `x.runtimeType` — every Dart object exposes it. Maps to the + // Java class token, which compares by identity exactly like Dart's Type equality + // (`other.runtimeType == runtimeType`). + if (name.equals("runtimeType") && tt != null && !isClassRef(tt)) { + return new Out(target.code + ".getClass()", new TypeRef("Type")); + } + // static access through a class reference + if (isClassRef(tt)) { + String cls = tt.arg(0).name; + if (program.enums.containsKey(cls) || stubs.isStubEnum(cls)) { + importEnum(cls, ctx); + if (name.equals("values")) { + return enumValues(cls, ctx); + } + return new Out(simpleEnumName(cls, ctx) + "." + name, new TypeRef(cls)); + } + if (stubs.isStubClass(cls)) { + Ast.MethodDecl g = stubs.findMethod(cls, name, true); + if (g != null && g.isStatic) { + return new Out(stubSimpleName(cls, ctx) + "." + name, g.returnType); + } + Ast.MethodDecl m = stubs.findMethod(cls, name, false); + if (m != null && m.isStatic) { + // static method tear-off — unsupported + diags.error(posNode, "E0130", "Static method tear-offs are not supported yet"); + return new Out("null", TypeRef.DYNAMIC); + } + } + ClassDecl pc = program.classes.get(cls); + if (pc != null) { + FieldDecl f = pc.field(name); + if (f != null && f.isStatic) { + return new Out(cls + (isLazyStatic(f) || isLateChecked(f, pc) ? ".get$" + name + "()" : "." + name), + fieldType(f, ctx)); + } + } + diags.error(posNode, "E0131", "Cannot resolve static member '" + name + "' on " + cls); + return new Out("null", TypeRef.DYNAMIC, true); + } + // intrinsics + if (tt.is("String")) { + ctx.importClass("dart.core.DString"); + if (name.equals("length")) { + return new Out("DString.length(" + target.code + ")", TypeRef.INT); + } + if (name.equals("isEmpty")) { + return new Out("DString.isEmpty(" + target.code + ")", TypeRef.BOOL); + } + if (name.equals("isNotEmpty")) { + return new Out("DString.isNotEmpty(" + target.code + ")", TypeRef.BOOL); + } + } + if (tt.is("List") || tt.is("Iterable") || tt.is("Set")) { + TypeRef elem = tt.arg(0); + if (name.equals("length")) { + return new Out(target.code + ".length()", TypeRef.INT); + } + if (name.equals("isEmpty")) { + return new Out(target.code + ".isEmpty()", TypeRef.BOOL); + } + if (name.equals("isNotEmpty")) { + return new Out(target.code + ".isNotEmpty()", TypeRef.BOOL); + } + if (name.equals("first")) { + return new Out(target.code + ".first()", elem); + } + if (name.equals("last")) { + return new Out(target.code + ".last()", elem); + } + if (name.equals("reversed")) { + return new Out(target.code + ".reversed()", TypeRef.of("Iterable", elem)); + } + } + if (tt.is("Map")) { + if (name.equals("length")) { + return new Out(target.code + ".length()", TypeRef.INT); + } + if (name.equals("keys")) { + return new Out(target.code + ".keys()", TypeRef.of("Iterable", tt.arg(0))); + } + if (name.equals("values")) { + return new Out(target.code + ".valuesIterable()", TypeRef.of("Iterable", tt.arg(1))); + } + if (name.equals("entries")) { + return new Out(target.code + ".entries()", + TypeRef.of("Iterable", TypeRef.of("MapEntry", tt.arg(0), tt.arg(1)))); + } + if (name.equals("isEmpty")) { + return new Out(target.code + ".isEmpty()", TypeRef.BOOL); + } + if (name.equals("isNotEmpty")) { + return new Out(target.code + ".isNotEmpty()", TypeRef.BOOL); + } + } + if (tt.is("int") || tt.is("double")) { + if (name.equals("isEven")) { + return new Out("(" + target.code + " % 2 == 0)", TypeRef.BOOL); + } + if (name.equals("isOdd")) { + return new Out("(" + target.code + " % 2 != 0)", TypeRef.BOOL); + } + } + // implicit instance members on a program enum value: .index (int), .name (String) + if (program.enums.containsKey(tt.name)) { + if (name.equals("index")) { + return new Out(target.code + ".ordinal()", TypeRef.INT); + } + if (name.equals("name")) { + return new Out(target.code + ".name()", TypeRef.STRING); + } + // enhanced-enum getter declared in the enum body + Ast.EnumDecl ed = program.enums.get(tt.name); + MethodDecl eg = ed.getter(name); + if (eg != null) { + return new Out(target.code + "." + name + "()", + eg.returnType == null || eg.returnType.is("var") ? TypeRef.DYNAMIC : eg.returnType); + } + } + // program class member + ClassDecl pc = programClass(tt.name, ctx); + if (pc != null) { + FieldDecl f = pc.field(name); + if (f != null) { + if (target.code.equals("this") && !isLateChecked(f, pc)) { + return new Out("this." + name, fieldType(f, ctx)); + } + return new Out(target.code + ".get$" + name + "()", fieldType(f, ctx)); + } + MethodDecl g = pc.getter(name); + if (g != null) { + return new Out(target.code + "." + name + "()", g.returnType); + } + Object mixF = findMixinMember(pc, name, true); + if (mixF instanceof FieldDecl) { + return new Out(target.code + ".get$" + name + "()", fieldType((FieldDecl) mixF, ctx)); + } + Object mixG = findMixinMember(pc, name, false); + if (mixG instanceof MethodDecl && ((MethodDecl) mixG).isGetter) { + return new Out(target.code + "." + name + "()", ((MethodDecl) mixG).returnType); + } + // field/getter inherited from a program superclass + FieldDecl inhF = findInheritedField(pc, name); + if (inhF != null && !inhF.name.startsWith("_")) { + return new Out(target.code + ".get$" + name + "()", fieldType(inhF, ctx)); + } + MethodDecl inhG = findInheritedGetter(pc, name); + if (inhG != null) { + return new Out(target.code + "." + name + "()", inhG.returnType); + } + // method tear-off on an instance: `obj.method` as a function value (method reference) + MethodDecl tm = pc.method(name); + if (tm == null) { + tm = findMethodInHierarchy(pc, name); + } + if (tm != null && !tm.isStatic) { + return new Out(target.code + "::" + name, new TypeRef("Function")); + } + } + // stub class member (walk supers) + if (stubs.isStubClass(tt.name) || tt.is("State")) { + Ast.MethodDecl g = stubs.findMethod(tt.name, name, true); + if (g != null) { + TypeRef rt = g.returnType; + // Narrow a generic getter (declared `V get value`) to the concrete + // type argument the receiver instantiates — e.g. + // MapEntry.value -> DisplayOption. + TypeRef sub = stubMemberReturnType(tt, name, true); + if (sub != null) { + rt = sub; + } + // State.widget returns the type argument + if (tt.is("State") && name.equals("widget") && !tt.args.isEmpty()) { + rt = tt.arg(0); + } + // GlobalKey.currentState returns the type argument (a `!` null-assertion + // at the use site strips the nullability the Dart getter declares) + if (tt.is("GlobalKey") && name.equals("currentState") && !tt.args.isEmpty()) { + rt = tt.arg(0); + } + return new Out(target.code + "." + name + "()", rt); + } + // instance-method tear-off: `controller.reverse` / `controller.forward` used as a + // callback value (e.g. `onTap: controller.reverse`). The member is a method on the + // stub type, not a field/getter, so emit a Java method reference typed as a Function + // — mirroring the own-method `this::name` tear-off. The stub already carries the + // no-arg / optional-arg overloads the target functional interface needs. + Ast.MethodDecl tearoff = stubs.findMethod(tt.name, name, false); + if (tearoff != null && !tearoff.isStatic) { + return new Out("(" + target.code + ")::" + name, new TypeRef("Function")); + } + } + // getter/field inherited by a program class from its stub superclass or a stub mixin + // (e.g. a RestorableProperty subclass reading `.value`, or a State-mixed accessor). + if (pc != null) { + String stubSuper = nearestStubSuper(pc); + if (stubSuper != null) { + Ast.MethodDecl sg = stubs.findMethod(stubSuper, name, true); + if (sg != null) { + TypeRef rt = inheritedStubMemberReturnType(pc, name, true); + return new Out(target.code + "." + name + "()", rt != null ? rt : sg.returnType); + } + } + for (TypeRef mixRef : pc.mixins) { + if (stubs.isStubClass(mixRef.name)) { + Ast.MethodDecl sg = stubs.findMethod(mixRef.name, name, true); + if (sg != null) { + return new Out(target.code + "." + name + "()", sg.returnType); + } + } + } + } + // `.mounted` (bool) — on a State receiver, or the BuildContext.mounted guard; + // neither is part of the minimal State/BuildContext stub surface. + if ((tt.is("State") || tt.is("BuildContext")) && name.equals("mounted")) { + return new Out(target.code + ".mounted()", TypeRef.BOOL); + } + ClassDecl extCls = program.findExtension(tt.name, name, true); + if (extCls != null) { + MethodDecl eg = extCls.getter(name); + return new Out(extCls.name + "." + name + "(" + target.code + ")", + eg.returnType == null || eg.returnType.is("var") ? TypeRef.DYNAMIC : eg.returnType); + } + // extension getter contributed by a stub (`extension AnimationStatusExtensions on + // AnimationStatus { bool get isAnimating; }`). The stub extension is emitted as a + // static-method class (its @JavaName); dispatch `status.isAnimating` to + // `AnimationStatusExtensions.isAnimating(status)`. + Ast.ClassDecl stubExt = stubs.findExtension(tt.name, name, true); + if (stubExt != null) { + Ast.MethodDecl eg = extensionMember(stubExt, name, true); + String extSimple = stubExtensionSimpleName(stubExt, ctx); + TypeRef rt = eg == null || eg.returnType == null || eg.returnType.is("var") + ? TypeRef.DYNAMIC : eg.returnType; + return new Out(extSimple + "." + name + "(" + target.code + ")", rt); + } + if (tt.is("Stopwatch")) { + TypeRef rt = name.equals("isRunning") ? TypeRef.BOOL + : name.equals("elapsed") ? new TypeRef("Duration") : TypeRef.INT; + return new Out(target.code + "." + name + "()", rt); + } + diags.error(posNode, "E0132", "Cannot resolve member '" + name + "' on type " + tt + + ". Confirm the file passes `dart analyze`."); + return new Out(target.code + "." + name, TypeRef.DYNAMIC, true); + } + + /** + * For a Dart {@code List}/{@code List} with a non-nullable primitive element, the + * primitive-list kind ("Long"/"Double") whose getLong/setLong/addLong methods avoid boxing; + * null for any other list (which uses the boxed {@code DartList}). + */ + private static String primitiveListKind(TypeRef tt) { + if (tt == null || !tt.is("List") || tt.args.isEmpty()) { + return null; + } + TypeRef e = tt.arg(0); + if (e == null || e.nullable) { + return null; + } + if (e.is("int")) { + return "Long"; + } + if (e.is("double")) { + return "Double"; + } + return null; + } + + /** + * True only for a non-nullable {@code Map} — targets the + * primitive {@code long}→{@code long} {@link dart.core.DartLongMap} so + * puts/gets avoid Long boxing. Other key/value combinations keep the boxed + * {@code DartMap}. + */ + /** A side-effect-free constant literal — safe to evaluate eagerly (e.g. for a ?? default). */ + private static boolean isPureLiteral(Expr e) { + return e instanceof IntLit || e instanceof DoubleLit || e instanceof BoolLit + || e instanceof NullLit || e instanceof StringLit; + } + + private static boolean isPrimitiveLongMap(TypeRef t) { + if (t == null || !t.is("Map") || t.args.size() != 2) { + return false; + } + TypeRef k = t.arg(0); + TypeRef v = t.arg(1); + return k != null && v != null && !k.nullable && !v.nullable && k.is("int") && v.is("int"); + } + + private Out emitIndexGet(IndexGet ig, Ctx ctx) { + if (chainHasNullAware(ig.target)) { + // `a?.b[i]`: a null `a` shorts the index too, which is not evaluated, and + // anything after it. The target was materialized to `(a == null ? null : + // a.b)` and indexed, which threw. Index the guarded read inside the short + // instead, and keep what the index lifts behind the guard. + Out raw = emitExprRaw(ig.target, null, ctx); + if (raw.shortGuard != null) { + ctx.pushWriter(ctx.currentIndent() + 1); + Out r = emitIndexGetOn(new Out(raw.code, copyNonNull(raw.type), raw.fromError), ig, ctx); + String lifted = ctx.popWriter(); + if (!lifted.isEmpty()) { + Out g = guardLifted(raw.shortGuard, lifted, r, ctx); + if (g != null) { + return g; + } + } + return new Out(r.code, boxType(r.type), r.fromError, raw.shortGuard); + } + return emitIndexGetOn(raw, ig, ctx); + } + return emitIndexGetOn(emitExpr(ig.target, null, ctx), ig, ctx); + } + + private Out emitIndexGetOn(Out target, IndexGet ig, Ctx ctx) { + Out idx = emitExpr(ig.index, null, ctx); + TypeRef tt = target.type; + ClassDecl opClass = program.classes.get(tt.name); + if (opClass != null) { + MethodDecl om = findMethodInHierarchy(opClass, "$index"); + if (om != null) { + return new Out(target.code + ".$index(" + idx.code + ")", + om.returnType == null || om.returnType.is("var") ? TypeRef.DYNAMIC : om.returnType); + } + } + if (tt.is("String")) { + ctx.importClass("dart.core.DString"); + return new Out("DString.idx(" + target.code + ", " + idx.code + ")", TypeRef.STRING); + } + if (tt.is("Map")) { + if (isPrimitiveLongMap(tt)) { + // Bare m[k] read: returns a nullable boxed Long (null when absent). + // The common m[k] ?? default pattern is unboxed via the ?? peephole below. + return new Out(target.code + ".idxLong(" + idx.code + ")", boxType(tt.arg(1))); + } + return new Out(target.code + ".idx(" + boxIfPrimitive(idx, ctx) + ")", boxType(tt.arg(1))); + } + String pk = primitiveListKind(tt); + if (pk != null) { + return new Out(target.code + ".get" + pk + "(" + idx.code + ")", tt.arg(0)); + } + return new Out(target.code + ".idx(" + idx.code + ")", tt.arg(0), target.fromError && tt.is("dynamic")); + } + + /** The element type of an indexable receiver (Map value / list-or-collection element). */ + private TypeRef indexElementType(TypeRef t) { + if (t == null) { + return TypeRef.DYNAMIC; + } + if (t.is("Map")) { + return t.args.size() >= 2 ? t.arg(1) : TypeRef.DYNAMIC; + } + return t.args.isEmpty() ? TypeRef.DYNAMIC : t.arg(0); + } + + /** + * The value code for a compound index-assignment {@code x[i] op= v}: {@code read op v}, + * coerced to the element type. {@code read} is the already-emitted element read. + */ + private String compoundValue(String readCode, TypeRef vt, Assign a, Ctx ctx) { + Out rhs = emitExpr(a.rhs, vt, ctx); + String baseOp = a.op.substring(0, a.op.length() - 1); + String expr; + if (isDynamic(vt) || isDynamic(rhs.type)) { + ctx.importClass("dart.runtime.DartRuntime"); + // Dart downcasts the result to the slot's type (`int x; x += d`). + return coerce(new Out("DartRuntime.dynBinary(\"" + baseOp + "\", " + readCode + ", " + + rhs.code + ")", TypeRef.DYNAMIC), vt, ctx); + } + if (vt != null && vt.is("String") && baseOp.equals("*")) { + ctx.importClass("dart.core.DString"); + return "DString.repeat(" + readCode + ", " + rhs.code + ")"; + } + if (vt != null && vt.is("num") && !baseOp.equals("/")) { + // A num slot keeps an int an int, as the binary operator does. + ctx.importClass("dart.runtime.DartRuntime"); + return "((Number) DartRuntime.dynBinary(\"" + baseOp + "\", " + readCode + ", " + rhs.code + "))"; + } + if (baseOp.equals("~/") || baseOp.equals("%")) { + ctx.importClass("dart.runtime.DartRuntime"); + String fn = baseOp.equals("~/") ? "tdiv" : "mod"; + expr = "DartRuntime." + fn + "(" + readCode + ", " + rhs.code + ")"; + } else if ((baseOp.equals("<<") || baseOp.equals(">>") || baseOp.equals(">>>")) + && !(a.rhs instanceof IntLit && ((IntLit) a.rhs).value >= 0 && ((IntLit) a.rhs).value < 64)) { + // `x <<= n` with Dart's shift-count rules, as the binary operator has. + ctx.importClass("dart.runtime.DartRuntime"); + String fn = baseOp.equals("<<") ? "shl" : baseOp.equals(">>") ? "shr" : "ushr"; + expr = "DartRuntime." + fn + "(" + readCode + ", " + rhs.code + ")"; + } else { + expr = "(" + readCode + " " + baseOp + " " + rhs.code + ")"; + } + return coerce(new Out(expr, vt), vt, ctx); + } + + /** {@code x[i] ??= v} or {@code o.p ??= v}: a null-aware assignment whose target is not a plain variable. */ + private static boolean isNullAwareMemberAssign(Assign a) { + return a.op.equals("??=") && (a.lhs instanceof IndexGet || a.lhs instanceof PropertyGet); + } + + /** + * The target of {@code x[i] ??= v} / {@code o.p ??= v} with its receiver and index + * evaluated once, into temps, so the read and the write below can both name them. + * + *

Rewriting over temps is what lets the write go through the ordinary {@code =} + * lowering -- idxSet, $indexSet, primitive lists, stub and app setters, accessors -- + * instead of emitting the read as an assignment target. The generic expansion this + * replaces produced {@code map.idx(key) = value}, which javac rejects, and evaluated + * the receiver and index up to three times.

+ */ + private Expr hoistNullAwareTarget(Assign a, Ctx ctx, List inline) { + if (a.lhs instanceof IndexGet) { + IndexGet ig = (IndexGet) a.lhs; + IndexGet copy = new IndexGet().at(a.file, a.line, a.col); + copy.target = hoistOnce(ig.target, ctx, inline); + copy.index = hoistOnce(ig.index, ctx, inline); + return copy; + } + PropertyGet pg = (PropertyGet) a.lhs; + PropertyGet copy = new PropertyGet().at(a.file, a.line, a.col); + copy.target = hoistOnce(pg.target, ctx, inline); + copy.name = pg.name; + copy.nullAware = pg.nullAware; + return copy; + } + + /** + * {@code e} evaluated into a fresh local, unless it is already {@code this} or a local. + * With {@code inline} null the evaluation is a lifted statement, which is right only + * where the enclosing statement runs unconditionally; otherwise only the declaration + * is lifted and the assignment is appended to {@code inline}, for the caller to + * sequence where the expression really is evaluated. + */ + private Expr hoistOnce(Expr e, Ctx ctx, List inline) { + if (e instanceof ThisExpr || (e instanceof Ident && ctx.lookup(((Ident) e).name) != null)) { + return e; + } + Out o = emitExpr(e, null, ctx); + if (o.type != null && isClassRef(o.type)) { + // `Config.label ??= v`: the receiver is a class, which has nothing to + // evaluate and cannot be held in a variable. + return e; + } + TypeRef t = o.type == null ? TypeRef.DYNAMIC : o.type; + String tmp = ctx.newTemp(); + if (inline == null) { + ctx.writer().line(javaType(t, false, ctx) + " " + tmp + " = " + o.code + ";"); + } else { + ctx.writer().line(javaType(t, false, ctx) + " " + tmp + ";"); + inline.add(tmp + " = " + o.code); + } + ctx.declare(tmp, t); + Ident id = new Ident().at(e.file, e.line, e.col); + id.name = tmp; + return id; + } + + /** + * The receiver of a read-modify-write through a getter/setter pair, which names it + * twice: kept as written when naming it again has no effect (a local, {@code this}, a + * field chain), otherwise evaluated once into a local ahead of the statement, as the + * compound index assignment does with its receiver. + */ + private String receiverOnce(Out tgt, Ctx ctx) { + if (isRepeatable(tgt)) { + return tgt.code; + } + String tmp = ctx.newTemp(); + TypeRef t = tgt.type == null ? TypeRef.DYNAMIC : tgt.type; + ctx.writer().line(javaType(t, false, ctx) + " " + tmp + " = " + tgt.code + ";"); + return tmp; + } + + /** A name that a bare assignment writes through a library-level {@code set name(v)}. */ + private boolean isTopLevelSetter(String nm, Ctx ctx) { + return ctx.lookup(nm) == null + && (ctx.currentClass == null || ctx.currentClass.field(nm) == null) + && program.topLevelSetters.containsKey(nm); + } + + /** A receiver naming it again has no effect on: a local, {@code this}, a field chain, a class. */ + private boolean isRepeatable(Out tgt) { + return tgt.code.matches("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*") + || (tgt.type != null && isClassRef(tgt.type)); + } + + private static Assign plainAssign(Assign a, Expr lhs) { + Assign plain = new Assign().at(a.file, a.line, a.col); + plain.lhs = lhs; + plain.op = "="; + plain.rhs = a.rhs; + return plain; + } + + /** + * {@code x[i] ??= v;} as a statement -- the common shape -- lowers to + * {@code if (x[i] == null) { x[i] = v; }}: the value is evaluated only when the slot + * is null, and the write is whatever {@code =} would emit, void setters and + * {@code operator []=} included. + */ + private void emitNullAwareAssignStatement(Assign a, Ctx ctx) { + // A statement runs unconditionally, so lifting its receiver and index ahead of + // it evaluates them exactly when Dart would. + Expr lhs = hoistNullAwareTarget(a, ctx, null); + Out read = emitExpr(lhs, null, ctx); + Ctx.Writer w = ctx.writer(); + w.line("if (" + read.code + " == null) {"); + ctx.indent(1); + String write = statementize(emitStatementExpr(plainAssign(a, lhs), ctx).code); + if (!write.isEmpty()) { + ctx.writer().line(write + ";"); + } + ctx.indent(-1); + ctx.writer().line("}"); + } + + /** + * {@code x[i] ??= v} used as a value: the current value if non-null, otherwise the + * written one. The write has to be a Java expression that yields the value, which + * DartList/DartMap's idxSet and a plain field assignment are; an {@code operator []=} + * or a setter is void in Java and is reported rather than emitted as invalid code. + */ + private Out emitNullAwareAssignValue(Assign a, Ctx ctx) { + // A value can sit where it is evaluated lazily -- a loop condition, a + // short-circuited operand -- so nothing is evaluated ahead of it: the temps are + // declared, and assigned in order inside the expression itself. + List inline = new ArrayList(); + Expr lhs = hoistNullAwareTarget(a, ctx, inline); + Out read = emitExpr(lhs, null, ctx); + TypeRef rt = read.type == null ? TypeRef.DYNAMIC : read.type; + String cur = ctx.newTemp(); + ctx.writer().line(javaType(rt, true, ctx) + " " + cur + ";"); + inline.add(cur + " = " + read.code); + ctx.pushWriter(ctx.currentIndent() + 1); + Out write = emitAssign(plainAssign(a, lhs), ctx); + String writeLifted = ctx.popWriter(); + boolean yieldsValue = write.code.startsWith(read.code + " = "); + if (lhs instanceof IndexGet) { + String receiver = emitExpr(((IndexGet) lhs).target, null, ctx).code; + yieldsValue = write.code.startsWith(receiver + ".idxSet("); + } + // A void setter or operator []= written in its value form lifts the write + // and yields a temp. + yieldsValue |= !writeLifted.isEmpty(); + if (!yieldsValue) { + diags.error(a, "E0141", "'??=' on an operator []= or a setter is supported as a statement," + + " not yet as a value. Assign first, then read the target."); + } + ctx.importClass("dart.runtime.DartRuntime"); + String chain = "true"; + for (int k = inline.size() - 1; k >= 0; k--) { + chain = "DartRuntime.seq(" + inline.get(k) + ", " + chain + ")"; + } + if (!writeLifted.isEmpty()) { + // The written value lifted statements (`m[k] ??= [...load()]`), which must + // run only when the slot is null: an if, which the enclosing operand's own + // handling (emitLazyOperand) places wherever this value is evaluated. + Ctx.Writer w = ctx.writer(); + w.line("if (!(" + chain + " && " + cur + " != null)) {"); + w.raw(writeLifted); + ctx.indent(1); + w.line(cur + " = " + write.code + ";"); + ctx.indent(-1); + w.line("}"); + return new Out(cur, rt); + } + return new Out("(" + chain + " && " + cur + " != null ? " + cur + " : (" + write.code + "))", rt); + } + + private Out emitAssign(Assign a, Ctx ctx) { + Out shorted = emitShortedAssign(a, ctx); + if (shorted != null) { + return shorted; + } + if (isNullAwareMemberAssign(a)) { + if (ctx.statementExpr == a) { + emitNullAwareAssignStatement(a, ctx); + return new Out("", TypeRef.VOID); + } + return emitNullAwareAssignValue(a, ctx); + } + // ??= on a variable: a local or field is a valid Java assignment target. + if (a.op.equals("??=")) { + Out lhs = emitExpr(a.lhs, null, ctx); + Lazy rl = emitLazyOperand(a.rhs, lhs.type, ctx, 1); + Out rhs = rl.out; + boolean accessor = lhs.code.endsWith("()") && lhs.code.contains(".get$"); + if (rl.lifts()) { + // The value lifted statements (`cache ??= [...load()]`); they run only + // when the variable is null, so the write becomes an if. + String write; + if (accessor) { + String base = lhs.code.substring(0, lhs.code.lastIndexOf(".get$")); + String prop = lhs.code.substring(lhs.code.lastIndexOf(".get$") + 5, lhs.code.length() - 2); + write = base + ".set$" + prop + "(" + coerce(rhs, lhs.type, ctx) + ")"; + } else { + write = lhs.code + " = " + rhs.code; + } + Ctx.Writer w = ctx.writer(); + w.line("if (" + lhs.code + " == null) {"); + w.raw(rl.lifted); + ctx.indent(1); + w.line(write + ";"); + ctx.indent(-1); + w.line("}"); + return new Out("(" + lhs.code + ")", lhs.type); + } + if (accessor) { + // A lazily initialised static or top-level is an accessor pair; its + // setter answers the stored value. + String base = lhs.code.substring(0, lhs.code.lastIndexOf(".get$")); + String prop = lhs.code.substring(lhs.code.lastIndexOf(".get$") + 5, lhs.code.length() - 2); + return new Out("(" + lhs.code + " == null ? " + base + ".set$" + prop + "(" + + coerce(rhs, lhs.type, ctx) + ") : " + lhs.code + ")", lhs.type); + } + return new Out("(" + lhs.code + " == null ? (" + lhs.code + " = " + rhs.code + ") : " + lhs.code + ")", + lhs.type); + } + if (a.lhs instanceof IndexGet) { + IndexGet ig = (IndexGet) a.lhs; + Out target = emitExpr(ig.target, null, ctx); + Out idx = emitExpr(ig.index, null, ctx); + boolean compound = !a.op.equals("="); + ClassDecl idxOpClass = program.classes.get(target.type.name); + boolean voidIndexSet = idxOpClass != null && findMethodInHierarchy(idxOpClass, "$indexSet") != null + && ctx.statementExpr != a; + if (compound || voidIndexSet) { + // x[i] op= v -> x[i] = x[i] op v, evaluating x and i exactly once. + String tTmp = ctx.newTemp(); + ctx.writer().line(javaType(target.type, false, ctx) + " " + tTmp + " = " + target.code + ";"); + String iTmp = ctx.newTemp(); + ctx.writer().line(javaType(idx.type, false, ctx) + " " + iTmp + " = " + idx.code + ";"); + target = new Out(tTmp, target.type); + idx = new Out(iTmp, idx.type); + } + ClassDecl opClass = program.classes.get(target.type.name); + if (opClass != null && findMethodInHierarchy(opClass, "$indexSet") != null) { + TypeRef ivt = compound ? indexElementType(target.type) : null; + MethodDecl readOp = findMethodInHierarchy(opClass, "$index"); + if (compound && readOp != null && readOp.returnType != null && !readOp.returnType.is("var")) { + // A class's own `operator []` says what it reads: the receiver type + // carries no type argument to take it from, and `grid[i] += 1` became + // dynamic arithmetic passed to an int parameter. + ivt = readOp.returnType; + } + Out rhsOut = compound ? null : emitExpr(a.rhs, null, ctx); + String rhsCode = compound + ? compoundValue(target.code + ".$index(" + idx.code + ")", ivt, a, ctx) + : rhsOut.code; + TypeRef rt = ivt != null ? ivt : compound ? TypeRef.DYNAMIC : rhsOut.type; + if (voidIndexSet) { + // `operator []=` is void in Java; as a value, the value is a temp, + // declared as the operator's value parameter. + MethodDecl ism = findMethodInHierarchy(opClass, "$indexSet"); + if (ism.params.size() == 2 && ism.params.get(1).type != null + && !isDynamicType(ism.params.get(1).type)) { + rt = ism.params.get(1).type; + } + String jt = declarableType(rt, false, ctx); + String v = ctx.newTemp(); + ctx.writer().line((jt != null ? jt : "var") + " " + v + " = " + rhsCode + ";"); + ctx.writer().line(target.code + ".$indexSet(" + idx.code + ", " + v + ");"); + return new Out(v, rt); + } + return new Out(target.code + ".$indexSet(" + idx.code + ", " + rhsCode + ")", + ivt != null ? ivt : TypeRef.DYNAMIC); + } + TypeRef vt = target.type.is("Map") ? target.type.arg(1) : target.type.arg(0); + String pk = primitiveListKind(target.type); + if (pk != null) { + String rhsCode = compound + ? compoundValue(target.code + ".get" + pk + "(" + idx.code + ")", vt, a, ctx) + : coerce(emitExpr(a.rhs, vt, ctx), vt, ctx); + return new Out(target.code + ".set" + pk + "(" + idx.code + ", " + rhsCode + ")", vt); + } + // Primitive long->long map: m[k] = v -> putLong(k, v), no boxing. + if (isPrimitiveLongMap(target.type)) { + // The key is coerced like the value: a dynamic one (an untyped forEach + // parameter, say) did not compile against putLong(long, long). + String key = coerce(idx, TypeRef.INT, ctx); + String rhsCode = compound + ? compoundValue(target.code + ".idxLong(" + key + ")", vt, a, ctx) + : coerce(emitExpr(a.rhs, vt, ctx), vt, ctx); + return new Out(target.code + ".putLong(" + key + ", " + rhsCode + ")", vt); + } + String key = target.type.is("Map") ? boxIfPrimitive(idx, ctx) : idx.code; + String rhsCode = compound + ? compoundValue(target.code + ".idx(" + key + ")", vt, a, ctx) + : coerce(emitExpr(a.rhs, vt, ctx), vt, ctx); + return new Out(target.code + ".idxSet(" + key + ", " + rhsCode + ")", vt); + } + // assignment to a stub property that declares a Dart setter: + // `x.value = v` -> the overloaded setter method `x.value(v)`. Compound forms + // (`x.value -= d`) read through the getter: `x.value(x.value() - d)`. + Out lhsOnce = null; + if (a.lhs instanceof PropertyGet) { + PropertyGet pg = (PropertyGet) a.lhs; + Out tgt = emitExpr(pg.target, null, ctx); + if (tgt.type != null && (stubs.isStubClass(tgt.type.name) || tgt.type.is("State"))) { + Ast.MethodDecl setter = stubs.findSetter(tgt.type.name, pg.name); + if (setter != null) { + TypeRef pt = setter.params.isEmpty() ? TypeRef.DYNAMIC : setter.params.get(0).type; + // Used as a value the receiver is named twice (temp, then call), so + // it is evaluated once first, ahead of the value, as Dart orders it. + String recv = a.op.equals("=") && ctx.statementExpr == a ? tgt.code : receiverOnce(tgt, ctx); + return voidSetterWrite(a, recv + "." + pg.name, + a.op.equals("=") ? null : recv + "." + pg.name + "()", pt, ctx); + } + } + // App class (or its supers) declaring `set prop(v)` — emitted as the overloaded + // instance method `prop(v)`, so `x.prop = v` becomes `x.prop(v)` (never `x.prop() = v`). + // A compound `x.prop += v` reads through the getter, `x.prop(x.prop() + v)`, as the + // stub branch above does; it fell through to `x.prop() += v`, which is not Java. + // (`??=` never gets here: it is rewritten to a plain `=` under a null test.) + if (tgt.type != null) { + ClassDecl tc = program.resolveClass(tgt.type.name, ctx.library()); + Ast.MethodDecl setter = findAppSetter(tc, pg.name); + if (setter != null) { + TypeRef pt = setter.params.isEmpty() ? TypeRef.DYNAMIC : setter.params.get(0).type; + String recv = a.op.equals("=") && ctx.statementExpr == a ? tgt.code : receiverOnce(tgt, ctx); + return voidSetterWrite(a, recv + "." + pg.name, + a.op.equals("=") ? null : recv + "." + pg.name + "()", pt, ctx); + } + } + if ((ctx.statementExpr != a || !a.op.equals("=")) && !isRepeatable(tgt)) { + // A field reached through accessors names the receiver in the read and + // in the write (`make().v += 3` called make() twice), and a value form + // evaluates the value before the call: evaluate the receiver once, first. + String recv = receiverOnce(tgt, ctx); + lhsOnce = emitMemberGet(new Out(recv, tgt.type, tgt.fromError), pg.name, pg, ctx); + } + } + // assignment to a top-level setter: `x = v` where `set x(v)` is declared at + // library scope -> the static setter method `OwnerLib.x(v)`. + if (a.lhs instanceof Ident && !a.op.equals("??=") && isTopLevelSetter(((Ident) a.lhs).name, ctx)) { + String nm = ((Ident) a.lhs).name; + { + Library owner = program.topLevelSetters.get(nm); + String call = Program.libClassName(owner.fileName) + "." + nm; + if (!a.op.equals("=")) { + // `top += 2` reads through the top-level getter and writes through the + // setter; it emitted `Lib.top() += 2`, which is not Java. + Out read = emitExpr(a.lhs, null, ctx); + return voidSetterWrite(a, call, read.code, read.type, ctx); + } + Out rhs = emitExpr(a.rhs, null, ctx); + if (ctx.statementExpr == a) { + return new Out(call + "(" + rhs.code + ")", rhs.type); + } + // The setter is void in Java: as a value, the value goes into a temp. + String jt = declarableType(rhs.type, false, ctx); + String v = ctx.newTemp(); + ctx.writer().line((jt != null ? jt : "var") + " " + v + " = " + rhs.code + ";"); + ctx.writer().line(call + "(" + v + ");"); + return new Out(v, rhs.type); + } + } + Out lhs = lhsOnce != null ? lhsOnce : emitExpr(a.lhs, null, ctx); + String lcode = lhs.code; + // setters through accessors: x.get$f() as assignment target -> x.set$f(v) + if (lcode.endsWith("()") && lcode.contains(".get$")) { + String base = lcode.substring(0, lcode.lastIndexOf(".get$")); + String prop = lcode.substring(lcode.lastIndexOf(".get$") + 5, lcode.length() - 2); + // `x op= v` through an accessor pair -- a lazily initialised static or + // top-level, or a mixin's field -- is set$x(get$x() op v). + if (ctx.statementExpr != a && ctx.postfixAssign == a) { + // `x.f++` as a value: the old value, read once. + String jt = declarableType(lhs.type, false, ctx); + String old = ctx.newTemp(); + ctx.writer().line((jt != null ? jt : "var") + " " + old + " = " + lcode + ";"); + ctx.writer().line(base + ".set$" + prop + "(" + compoundValue(old, lhs.type, a, ctx) + ");"); + return new Out(old, lhs.type); + } + String value = a.op.equals("=") + ? coerce(emitExpr(a.rhs, lhs.type, ctx), lhs.type, ctx) + : compoundValue(lcode, lhs.type, a, ctx); + if (ctx.statementExpr != a) { + // An instance field's set$ is void; as a value, the value is a temp. + String jt = declarableType(lhs.type, false, ctx); + String v = ctx.newTemp(); + ctx.writer().line((jt != null ? jt : "var") + " " + v + " = " + value + ";"); + ctx.writer().line(base + ".set$" + prop + "(" + v + ");"); + return new Out(v, lhs.type); + } + return new Out(base + ".set$" + prop + "(" + value + ")", lhs.type); + } + if (!a.op.equals("=") && (isDynamic(lhs.type) + || (lhs.type != null && lhs.type.is("num") && !a.op.equals("/=")) + || (lhs.type != null && lhs.type.is("String") && a.op.equals("*=")))) { + // `x += 1` on a dynamic x: Java's compound operators do not apply to Object. + return new Out(lcode + " = " + compoundValue(lcode, lhs.type, a, ctx), lhs.type); + } + if ((a.op.equals("<<=") || a.op.equals(">>=") || a.op.equals(">>>=")) + && !(a.rhs instanceof IntLit && ((IntLit) a.rhs).value >= 0 && ((IntLit) a.rhs).value < 64)) { + return new Out(lcode + " = " + compoundValue(lcode, lhs.type, a, ctx), lhs.type); + } + String jop = a.op.equals("~/=") ? null : a.op; + if (a.op.equals("~/=") || a.op.equals("%=")) { + ctx.importClass("dart.runtime.DartRuntime"); + Out rhs = emitExpr(a.rhs, lhs.type, ctx); + String fn = a.op.equals("~/=") ? "tdiv" : "mod"; + return new Out(lcode + " = DartRuntime." + fn + "(" + lcode + ", " + rhs.code + ")", lhs.type); + } + if (ctx.postfixAssign == a && a.rhs instanceof IntLit && (a.op.equals("+=") || a.op.equals("-="))) { + // `a?.b++` reaches here as `$t.b += 1` under the short; it yields the old value. + return new Out(lcode + (a.op.equals("+=") ? "++" : "--"), lhs.type); + } + Out rhs = emitExpr(a.rhs, lhs.type, ctx); + return new Out(lcode + " " + jop + " " + coerce(rhs, lhs.type, ctx), lhs.type); + } + + /** + * Applies the flow promotions implied by a boolean guard {@code cond} holding true: + * every {@code x is T} test (including those AND-ed together) where {@code x} is a + * simple in-scope local narrows {@code x} to {@code T}. Returns an undo list of + * {name, priorPromotion} pairs to pass to {@link #restorePromotions}. + */ + private List applyGuardPromotions(Expr cond, Ctx ctx) { + List undo = new ArrayList(); + collectPromotions(cond, ctx, undo); + return undo; + } + + private void collectPromotions(Expr cond, Ctx ctx, List undo) { + if (cond instanceof Binary && "&&".equals(((Binary) cond).op)) { + collectPromotions(((Binary) cond).left, ctx, undo); + collectPromotions(((Binary) cond).right, ctx, undo); + return; + } + if (cond instanceof IsTest) { + IsTest t = (IsTest) cond; + if (!t.negated && t.operand instanceof Ident && t.type != null + && ctx.lookup(((Ident) t.operand).name) != null) { + String name = ((Ident) t.operand).name; + TypeRef prev = ctx.pushPromotion(name, t.type); + undo.add(new Object[] {name, prev}); + } + } + } + + /** + * Promotions implied by a boolean guard {@code cond} holding FALSE — used for the right + * operand of {@code ||} (reached only when the left is false). A false {@code ||} means + * every disjunct is false, so recurse both sides; a false {@code x is! T} narrows + * {@code x} to {@code T}. + */ + private void collectNegativePromotions(Expr cond, Ctx ctx, List undo) { + if (cond instanceof Binary && "||".equals(((Binary) cond).op)) { + collectNegativePromotions(((Binary) cond).left, ctx, undo); + collectNegativePromotions(((Binary) cond).right, ctx, undo); + return; + } + if (cond instanceof IsTest) { + IsTest t = (IsTest) cond; + if (t.negated && t.operand instanceof Ident && t.type != null + && ctx.lookup(((Ident) t.operand).name) != null) { + String name = ((Ident) t.operand).name; + TypeRef prev = ctx.pushPromotion(name, t.type); + undo.add(new Object[] {name, prev}); + } + } + } + + private void restorePromotions(List undo, Ctx ctx) { + for (int i = undo.size() - 1; i >= 0; i--) { + ctx.restorePromotion((String) undo.get(i)[0], (TypeRef) undo.get(i)[1]); + } + } + + /** + * Static type of a conditional/ternary expression: the two branch types' least upper + * bound. Identical types win; a {@code null}/dynamic branch yields the other (boxed); + * otherwise the nearest common ancestor, falling back to {@code Object} (never + * {@code dynamic}, so ordinary members still resolve where the common type has them). + */ + private TypeRef conditionalType(TypeRef a, TypeRef b, TypeRef expected) { + if (a == null || a.is("dynamic") || a.is("Null")) { + return b == null ? TypeRef.DYNAMIC : boxType(b); + } + if (b == null || b.is("dynamic") || b.is("Null")) { + return boxType(a); + } + if (a.name.equals(b.name)) { + return a; + } + if (expected != null && !expected.is("var") && !expected.is("dynamic")) { + return expected; + } + TypeRef anc = commonAncestor(a.name, b.name); + return anc != null ? anc : new TypeRef("Object"); + } + + /** Nearest common ancestor class name of two types (program or stub), or null. */ + private TypeRef commonAncestor(String x, String y) { + java.util.LinkedHashSet xs = new java.util.LinkedHashSet(); + for (String c = x; c != null; c = superName(c)) { + xs.add(c); + } + for (String c = y; c != null; c = superName(c)) { + if (xs.contains(c)) { + return new TypeRef(c); + } + } + return null; + } + + /** Direct superclass name of a program or stub class, or null. */ + private String superName(String name) { + ClassDecl pc = program.classes.get(name); + if (pc != null && pc.superclass != null) { + return pc.superclass.name; + } + Ast.ClassDecl sc = stubs.classes.get(name); + if (sc != null && sc.superclass != null) { + return sc.superclass.name; + } + return null; + } + + /** + * Emits {@code e} as a statement: its value is discarded, so an assignment through + * a void setter keeps its plain call form ({@link #voidSetterWrite}). + */ + private Out emitStatementExpr(Expr e, Ctx ctx) { + Expr saved = ctx.statementExpr; + ctx.statementExpr = e; + try { + return emitExpr(e, null, ctx); + } finally { + ctx.statementExpr = saved; + } + } + + /** True when some selector in the receiver chain of {@code e} is null-aware ({@code a?.b}). */ + private static boolean chainHasNullAware(Expr e) { + while (e != null) { + if (e instanceof PropertyGet) { + if (((PropertyGet) e).nullAware) { + return true; + } + e = ((PropertyGet) e).target; + } else if (e instanceof Call) { + if (((Call) e).nullAware) { + return true; + } + e = ((Call) e).target; + } else if (e instanceof IndexGet) { + e = ((IndexGet) e).target; + } else { + return false; + } + } + return false; + } + + /** An assignment whose target sits behind a null-aware selector, which shorts it. */ + private static boolean needsShort(Assign a) { + if (a.lhs instanceof PropertyGet) { + PropertyGet pg = (PropertyGet) a.lhs; + return pg.nullAware || chainHasNullAware(pg.target); + } + return a.lhs instanceof IndexGet && chainHasNullAware(((IndexGet) a.lhs).target); + } + + /** + * {@code a?.b = v}, {@code a?.b op= v}, {@code a?.b ??= v}, {@code a?.b[i] = v}: a + * null receiver shorts the whole assignment -- neither the index nor the value is + * evaluated, and its value is null. The assignment was emitted as though the + * {@code ?.} were a plain {@code .}, and threw on the null. The write is emitted + * against a temp holding the non-null receiver, inside {@code if (guard != null)}. + * Returns null when the target is not behind a null-aware selector. + */ + private Out emitShortedAssign(Assign a, Ctx ctx) { + if (!needsShort(a)) { + return null; + } + Expr recvExpr = a.lhs instanceof PropertyGet ? ((PropertyGet) a.lhs).target : ((IndexGet) a.lhs).target; + boolean direct = a.lhs instanceof PropertyGet && ((PropertyGet) a.lhs).nullAware; + String guard; + String recvInit = null; + TypeRef recvType; + if (direct) { + Out mat = emitExpr(recvExpr, null, ctx); + guard = ctx.newTemp(); + ctx.writer().line("var " + guard + " = " + mat.code + ";"); + recvType = copyNonNull(mat.type); + } else { + Out raw = emitExprRaw(recvExpr, null, ctx); + guard = raw.shortGuard; + recvType = copyNonNull(raw.type); + recvInit = raw.code; + } + ctx.pushWriter(ctx.currentIndent() + 1); + String recv = guard; + if (recvInit != null) { + // The rest of the receiver chain, read where the guard says it may be. + recv = ctx.newTemp(); + String jt = declarableType(recvType, false, ctx); + ctx.writer().line((jt != null ? jt : "var") + " " + recv + " = " + recvInit + ";"); + } + ctx.declare(recv, recvType); + Ident id = new Ident().at(a.file, a.line, a.col); + id.name = recv; + Expr lhs; + if (a.lhs instanceof PropertyGet) { + PropertyGet copy = new PropertyGet().at(a.lhs.file, a.lhs.line, a.lhs.col); + copy.target = id; + copy.name = ((PropertyGet) a.lhs).name; + lhs = copy; + } else { + IndexGet copy = new IndexGet().at(a.lhs.file, a.lhs.line, a.lhs.col); + copy.target = id; + copy.index = ((IndexGet) a.lhs).index; + lhs = copy; + } + Assign inner = new Assign().at(a.file, a.line, a.col); + inner.lhs = lhs; + inner.op = a.op; + inner.rhs = a.rhs; + boolean statement = ctx.statementExpr == a; + Expr savedStmt = ctx.statementExpr; + Assign savedPostfix = ctx.postfixAssign; + if (statement) { + ctx.statementExpr = inner; + } + if (ctx.postfixAssign == a) { + ctx.postfixAssign = inner; + } + Out w = emitAssign(inner, ctx); + ctx.statementExpr = savedStmt; + ctx.postfixAssign = savedPostfix; + String lifted = ctx.popWriter(); + if (guard == null) { + // No short reached the receiver after all (a parenthesized chain): plain. + ctx.writer().raw(lifted); + return w; + } + if (statement || w.type == null || w.type.is("void")) { + return guardLifted(guard, lifted, new Out(w.code, TypeRef.VOID), ctx); + } + Out g = guardLifted(guard, lifted, w, ctx); + return g != null ? g : w.withShort(guard); + } + + /** + * A write through a setter, which Java declares void. As a statement it is the + * plain call; used as a value -- {@code (x.p += 3) > 0}, {@code print(x.p = 4)} -- + * the new value goes into a temp first, the setter is called with it, and the + * value is the temp; for a postfix {@code x.p++} ({@link Ctx#postfixAssign}) the + * old value is read into the temp instead. The call as a value was + * {@code 'void' type not allowed here} from javac. + * + * @param recv the receiver, already evaluated once when the write reads it too + * @param call the setter call up to its argument, e.g. {@code recv.p} + * @param read the getter read for a compound write, null for {@code =} + */ + private Out voidSetterWrite(Assign a, String call, String read, TypeRef pt, Ctx ctx) { + boolean statement = ctx.statementExpr == a; + String jt = declarableType(pt, false, ctx); + if (jt == null) { + jt = "var"; + } + Ctx.Writer w = ctx.writer(); + if (!statement && ctx.postfixAssign == a && read != null) { + String old = ctx.newTemp(); + w.line(jt + " " + old + " = " + read + ";"); + w.line(call + "(" + compoundValue(old, pt, a, ctx) + ");"); + return new Out(old, pt); + } + String value = read == null + ? coerce(emitExpr(a.rhs, pt, ctx), pt, ctx) + : compoundValue(read, pt, a, ctx); + if (statement) { + return new Out(call + "(" + value + ")", pt); + } + String v = ctx.newTemp(); + w.line(jt + " " + v + " = " + value + ";"); + w.line(call + "(" + v + ");"); + return new Out(v, pt); + } + + /** An operand emitted by {@link #emitLazyOperand}, and the statements it lifted. */ + private static final class Lazy { + final Out out; + /** Already indented; empty when the operand lifted nothing. */ + final String lifted; + + Lazy(Out out, String lifted) { + this.out = out; + this.lifted = lifted; + } + + boolean lifts() { + return !lifted.isEmpty(); + } + } + + /** + * Emits an operand Java evaluates only sometimes, or more than once -- a + * conditional's arm, the right side of {@code && || ??}, a null-short's call, a + * loop condition or update -- keeping apart whatever statements its emission + * lifts (a compound index assignment's receiver temps, a spread list's builder, + * a cascade, a named-argument constructor). Lifted into the enclosing statement + * they ran once and unconditionally: {@code while ((xs[next()] += 1) < 10)} called + * next() once, {@code false && (a[f()] += 1) > 0} still called f(), and + * {@code xs == null ? [] : [...xs]} spread a null list before the test. + * The caller must run {@code lifted} exactly where the operand is evaluated; with + * nothing lifted the operand is emitted exactly as it always was. + * + * @param depth how many blocks deeper than the current statement the caller will + * place the lifted lines, for their indentation + */ + private Lazy emitLazyOperand(Expr e, TypeRef expected, Ctx ctx, int depth) { + ctx.pushWriter(ctx.currentIndent() + depth); + Out o = emitExpr(e, expected, ctx); + return new Lazy(o, ctx.popWriter()); + } + + /** A Java type a temp can be declared with for {@code t}, or null when there is none. */ + private String declarableType(TypeRef t, boolean boxed, Ctx ctx) { + if (t == null || t.is("var") || t.is("void") || t.funcParams != null || t.is("Function")) { + return null; + } + return javaType(t, boxed, ctx); + } + + /** + * {@code called}, whose emission lifted {@code lifted}, evaluated only while + * {@code guard} is non-null: the tail of a null-short ({@code a?.add([...xs])}), + * whose arguments Dart never evaluates when {@code a} is null. Returns null -- + * after appending the lines unguarded, as before -- when no temp can hold the + * result. + */ + private Out guardLifted(String guard, String lifted, Out called, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + if (called.type == null || called.type.is("void") || called.code == null || called.code.isEmpty()) { + w.line("if (" + guard + " != null) {"); + w.raw(lifted); + String code = called.code == null ? "" : statementize(called.code); + if (!code.isEmpty()) { + ctx.indent(1); + w.line(code + ";"); + ctx.indent(-1); + } + w.line("}"); + return new Out("", TypeRef.VOID); + } + TypeRef bt = boxType(called.type); + String jt = declarableType(bt, true, ctx); + if (jt == null) { + w.raw(lifted); + return null; + } + String r = ctx.newTemp(); + w.line(jt + " " + r + " = null;"); + w.line("if (" + guard + " != null) {"); + w.raw(lifted); + ctx.indent(1); + w.line(r + " = " + called.code + ";"); + ctx.indent(-1); + w.line("}"); + return new Out(r, bt, called.fromError, guard); + } + + /** + * Writes a C-style {@code for} loop -- statement or collection element -- and runs + * {@code body} inside it. A condition or update that lifts statements (see + * {@link #emitLazyOperand}) has them run on every evaluation: the condition moves + * into the body as {@code if (!(cond)) break;}, after its lines, and lifting + * updates run at the top of every pass but the first. A {@code continue} still + * reaches both, since each sits where the loop restarts. + */ + private void emitCForLoop(String initCode, Expr condition, List updates, Ctx ctx, Runnable body) { + Ctx.Writer w = ctx.writer(); + Lazy cond = condition != null ? emitLazyOperand(condition, TypeRef.BOOL, ctx, 1) : null; + List ups = new ArrayList(); + boolean updatesLift = false; + for (Expr u : updates) { + Expr savedStmt = ctx.statementExpr; + ctx.statementExpr = u; + Lazy l = emitLazyOperand(u, null, ctx, 2); + ctx.statementExpr = savedStmt; + ups.add(l); + updatesLift |= l.lifts(); + } + boolean condLifts = cond != null && cond.lifts(); + if (!condLifts && !updatesLift) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ups.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(statementize(ups.get(i).out.code)); + } + w.line("for (" + initCode + "; " + (cond != null ? cond.out.code : "") + "; " + sb + ") {"); + ctx.indent(1); + } else { + if (updatesLift) { + String first = ctx.newTemp(); + w.line("boolean " + first + " = true;"); + w.line("for (" + initCode + "; ; ) {"); + ctx.indent(1); + w.line("if (!" + first + ") {"); + for (Lazy l : ups) { + w.raw(l.lifted); + String code = statementize(l.out.code); + if (!code.isEmpty()) { + ctx.indent(1); + w.line(code + ";"); + ctx.indent(-1); + } + } + w.line("}"); + w.line(first + " = false;"); + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ups.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(statementize(ups.get(i).out.code)); + } + w.line("for (" + initCode + "; ; " + sb + ") {"); + ctx.indent(1); + } + if (cond != null) { + w.raw(cond.lifted); + w.line("if (!(" + cond.out.code + ")) {"); + ctx.indent(1); + w.line("break;"); + ctx.indent(-1); + w.line("}"); + } + } + body.run(); + ctx.indent(-1); + w.line("}"); + } + + /** + * {@code a ?? b} whose default lifted statements: {@code tmp = a; if (tmp == null) + * { ; tmp = b; }}. {@code jt} declares {@code tmp} here, or is null when + * the caller already declared it. + */ + private Out nullCoalesceLifted(String jt, String tmp, Out left, Lazy right, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + w.line((jt != null ? jt + " " : "") + tmp + " = " + left.code + ";"); + w.line("if (" + tmp + " == null) {"); + w.raw(right.lifted); + ctx.indent(1); + w.line(tmp + " = " + right.out.code + ";"); + ctx.indent(-1); + w.line("}"); + return new Out(tmp, copyNonNull(left.type)); + } + + /** + * {@code l && r} (or {@code l || r}) whose right operand lifted statements: they + * run only when the left side does not decide the result, as Dart evaluates it. + */ + private Out shortCircuitLifted(Out l, Lazy r, boolean and, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + String tmp = ctx.newTemp(); + w.line("boolean " + tmp + " = " + logicalOperand(l, ctx) + ";"); + w.line("if (" + (and ? tmp : "!" + tmp) + ") {"); + w.raw(r.lifted); + ctx.indent(1); + w.line(tmp + " = " + logicalOperand(r.out, ctx) + ";"); + ctx.indent(-1); + w.line("}"); + return new Out(tmp, TypeRef.BOOL); + } + + private Out emitBinary(Binary b, Ctx ctx) { + if (b.op.equals("??")) { + // Peephole: (m[k] ?? literal) on a primitive Map -> getLongOr(k, literal), + // eliminating the boxed read. Only for a side-effect-free literal default, so eager + // evaluation of the default matches ??'s short-circuit semantics. + if (b.left instanceof IndexGet && isPureLiteral(b.right)) { + IndexGet ig = (IndexGet) b.left; + Out mt = emitExpr(ig.target, null, ctx); + if (isPrimitiveLongMap(mt.type)) { + Out idx = emitExpr(ig.index, null, ctx); + Out def = emitExpr(b.right, TypeRef.of("int"), ctx); + return new Out(mt.code + ".getLongOr(" + idx.code + ", " + def.code + ")", TypeRef.of("int")); + } + } + Out left = emitExpr(b.left, null, ctx); + // `a ?? b` must evaluate a once and b only when a is null, WHERE the + // expression is -- which may be a loop condition, a short-circuited operand + // or a conditional's arm. Lifting `var t = a;` into a statement ran a once + // before the enclosing statement instead: `while (next() ?? false)` tested + // one stale value forever, and `false && (f() ?? true)` still called f(). + // Only the temp's declaration is lifted now; the assignment stays inline. + // The default can lift statements of its own (`xs ?? [...fallback()]`); they + // must run only when the left side is null, so then the whole operator + // becomes `tmp = a; if (tmp == null) { ; tmp = b; }`. + Out right; + TypeRef lt = left.type; + String jt = lt == null || lt.is("var") || lt.funcParams != null ? null : javaType(lt, true, ctx); + if (b.left instanceof Ident || b.left instanceof ThisExpr) { + // A variable read has no effect and nothing to cache. + Lazy rl = emitLazyOperand(b.right, left.type, ctx, 1); + if (rl.lifts() && jt != null) { + return nullCoalesceLifted(jt, ctx.newTemp(), left, rl, ctx); + } + ctx.writer().raw(rl.lifted); + right = rl.out; + return new Out("(" + left.code + " != null ? " + left.code + " : " + right.code + ")", + copyNonNull(left.type)); + } + String tmp = ctx.newTemp(); + if (jt == null) { + // A type javac infers but this emitter cannot name: no declaration can + // hold it, so it keeps the eager form. + ctx.writer().line("var " + tmp + " = " + left.code + ";"); + right = emitExpr(b.right, left.type, ctx); + return new Out("(" + tmp + " != null ? " + tmp + " : " + right.code + ")", + copyNonNull(left.type)); + } + ctx.writer().line(jt + " " + tmp + ";"); + Lazy rl = emitLazyOperand(b.right, left.type, ctx, 1); + if (rl.lifts()) { + return nullCoalesceLifted(null, tmp, left, rl, ctx); + } + right = rl.out; + return new Out("((" + tmp + " = " + left.code + ") != null ? " + tmp + " : " + right.code + ")", + copyNonNull(left.type)); + } + if (b.op.equals("&&")) { + // `x is T && x.member`: the left `is` guard flow-promotes x to T for the right operand. + Out l = emitExpr(b.left, null, ctx); + List undo = applyGuardPromotions(b.left, ctx); + Lazy r = emitLazyOperand(b.right, null, ctx, 1); + restorePromotions(undo, ctx); + if (r.lifts()) { + return shortCircuitLifted(l, r, true, ctx); + } + return new Out(logicalOperand(l, ctx) + " && " + logicalOperand(r.out, ctx), TypeRef.BOOL); + } + if (b.op.equals("||")) { + // `x is! T || x.member`: reaching the right operand means the left was false, + // i.e. `x is T` held — flow-promote x to T for the right operand. + Out l = emitExpr(b.left, null, ctx); + List undo = new ArrayList(); + collectNegativePromotions(b.left, ctx, undo); + Lazy r = emitLazyOperand(b.right, null, ctx, 1); + restorePromotions(undo, ctx); + if (r.lifts()) { + return shortCircuitLifted(l, r, false, ctx); + } + return new Out(logicalOperand(l, ctx) + " || " + logicalOperand(r.out, ctx), TypeRef.BOOL); + } + Out l = emitExpr(b.left, null, ctx); + Out r = emitExpr(b.right, null, ctx); + boolean numeric = isNumeric(l.type) && isNumeric(r.type); + // user-defined operators on program classes + ClassDecl opClass = program.classes.get(l.type.name); + if (opClass != null && !b.op.equals("==") && !b.op.equals("!=") + && !b.op.equals("&&") && !b.op.equals("||") && !b.op.equals("??")) { + String mangled = com.codename1.dart.transpiler.parser.AstBuilder.mangleOperator(b.op); + MethodDecl om = mangled != null ? findMethodInHierarchy(opClass, mangled) : null; + if (om != null) { + return new Out(l.code + "." + mangled + "(" + r.code + ")", + om.returnType == null || om.returnType.is("var") ? TypeRef.DYNAMIC : om.returnType); + } + } + // user-defined operators on stub value types (e.g. Offset + Offset, Radius * t) + if (opClass == null && l.type != null && stubs.isStubClass(l.type.name) + && !b.op.equals("==") && !b.op.equals("!=") + && !b.op.equals("&&") && !b.op.equals("||") && !b.op.equals("??")) { + String mangled = com.codename1.dart.transpiler.parser.AstBuilder.mangleOperator(b.op); + Ast.MethodDecl om = mangled != null ? stubs.findMethod(l.type.name, mangled, false) : null; + if (om != null) { + return new Out(l.code + "." + mangled + "(" + paren(r.code) + ")", + om.returnType == null || om.returnType.is("var") ? TypeRef.DYNAMIC : om.returnType); + } + } + if (b.op.equals("==") || b.op.equals("!=")) { + // Java's primitive == on an int and a double widens the int, and that is Dart's + // semantics too, not an approximation of them: measured on the Dart 3.9 VM, + // 9007199254740993 == 9007199254740992.0 is TRUE and the matching > is false. + // Only compareTo compares an int and a double exactly. + if (numeric || (l.type.is("bool") && r.type.is("bool"))) { + return new Out(paren(l.code) + " " + b.op + " " + paren(r.code), TypeRef.BOOL); + } + ctx.importClass("dart.runtime.DartRuntime"); + String eq = "DartRuntime.eq(" + l.code + ", " + boxIfPrimitive(r, ctx) + ")"; + return new Out(b.op.equals("==") ? eq : "!" + eq, TypeRef.BOOL); + } + // A dynamic operand: Dart chooses the operator from the run-time value, and none + // of the typed lowerings below apply -- the fallback emitted `Object + long`, which + // javac rejects. A statically-String `+` keeps its concatenation. + if ((isDynamic(l.type) || isDynamic(r.type)) + && !(b.op.equals("+") && l.type.is("String") && r.type.is("String"))) { + ctx.importClass("dart.runtime.DartRuntime"); + if (b.op.equals("<") || b.op.equals(">") || b.op.equals("<=") || b.op.equals(">=")) { + return new Out("DartRuntime.dynCompare(\"" + b.op + "\", " + l.code + ", " + r.code + ")", + TypeRef.BOOL); + } + return new Out("DartRuntime.dynBinary(\"" + b.op + "\", " + l.code + ", " + r.code + ")", + TypeRef.DYNAMIC); + } + if (b.op.equals("~/")) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.tdiv(" + l.code + ", " + r.code + ")", TypeRef.INT); + } + if (b.op.equals("%")) { + if (numeric) { + ctx.importClass("dart.runtime.DartRuntime"); + TypeRef t = l.type.is("double") || r.type.is("double") ? TypeRef.DOUBLE : TypeRef.INT; + return new Out("DartRuntime.mod(" + l.code + ", " + r.code + ")", t); + } + } + if (b.op.equals("/") && numeric) { + if (l.type.is("int") && r.type.is("int")) { + return new Out("((double) " + paren(l.code) + ") / " + paren(r.code), TypeRef.DOUBLE); + } + return new Out(paren(l.code) + " / " + paren(r.code), TypeRef.DOUBLE); + } + // Dart's String repetition, `'ab' * 3`. Only the dynamic path handled it, so the + // typed form reached the fallback and emitted a Java string multiplication. + if (b.op.equals("*") && l.type != null && l.type.is("String") && r.type != null && r.type.is("int")) { + ctx.importClass("dart.core.DString"); + return new Out("DString.repeat(" + l.code + ", " + r.code + ")", TypeRef.STRING); + } + // Dart's `List + List` concatenation -> a new DartList. + if (b.op.equals("+") && l.type != null && l.type.is("List") && r.type != null && r.type.is("List")) { + ctx.importClass("dart.core.DartList"); + return new Out("DartList.concat(" + l.code + ", " + r.code + ")", l.type); + } + if (b.op.equals("+") && (l.type.is("String") || r.type.is("String"))) { + // Keep string-concatenation chains flat so javac fuses them into ONE StringBuilder. A left + // operand that is itself a `+` concatenation needs no parens (same precedence, left-assoc); + // wrapping it (as paren() would) forces a separate builder + intermediate String per link, + // which is pure GC churn. Non-concat left operands keep their precedence parens. + String left = (b.left instanceof Binary && "+".equals(((Binary) b.left).op)) ? l.code : paren(l.code); + return new Out(left + " + " + paren(r.code), TypeRef.STRING); + } + // `num` (Java Number) arithmetic/comparison: unbox the num operand(s) to double so Java's + // numeric operators apply (Dart's `num` is the int|double supertype; a Number reference + // cannot be used with +, -, *, / directly). + if ((l.type != null && l.type.is("num")) || (r.type != null && r.type.is("num"))) { + boolean lok = l.type != null && (isNumeric(l.type) || l.type.is("num")); + boolean rok = r.type != null && (isNumeric(r.type) || r.type.is("num")); + boolean arith = b.op.equals("+") || b.op.equals("-") || b.op.equals("*") || b.op.equals("/"); + boolean cmp = b.op.equals("<") || b.op.equals(">") || b.op.equals("<=") || b.op.equals(">="); + if (lok && rok && (arith || cmp)) { + // Dispatched on the VALUES, as Dart does: an int held in a num stays an + // int (num n = 1; n + 1 is the int 2) and compares exactly above 2^53. + // Converting every num to double lost both. Only / always yields a + // double, so it keeps the double path. + ctx.importClass("dart.runtime.DartRuntime"); + if (cmp) { + return new Out("DartRuntime.dynCompare(\"" + b.op + "\", " + l.code + ", " + r.code + ")", + TypeRef.BOOL); + } + if (!b.op.equals("/")) { + return new Out("((Number) DartRuntime.dynBinary(\"" + b.op + "\", " + l.code + ", " + + r.code + "))", new TypeRef("num")); + } + String lc = l.type.is("num") ? "((Number) " + paren(l.code) + ").doubleValue()" : l.code; + String rc = r.type.is("num") ? "((Number) " + paren(r.code) + ").doubleValue()" : r.code; + return new Out(paren(lc) + " " + b.op + " " + paren(rc), TypeRef.DOUBLE); + } + } + // Integer shifts with Dart's count rules; a constant count of 0..63 is where + // Java's operator already agrees, and keeps it. + if ((b.op.equals("<<") || b.op.equals(">>") || b.op.equals(">>>")) + && l.type != null && l.type.is("int") && r.type != null && r.type.is("int") + && !(b.right instanceof IntLit && ((IntLit) b.right).value >= 0 && ((IntLit) b.right).value < 64)) { + ctx.importClass("dart.runtime.DartRuntime"); + String fn = b.op.equals("<<") ? "shl" : b.op.equals(">>") ? "shr" : "ushr"; + return new Out("DartRuntime." + fn + "(" + l.code + ", " + r.code + ")", TypeRef.INT); + } + // Relational operators on enum operands compare by declaration order (Dart enum + // semantics). Java enums expose that order as ordinal(). + if ((b.op.equals("<") || b.op.equals(">") || b.op.equals("<=") || b.op.equals(">=")) + && l.type != null + && (program.enums.containsKey(l.type.name) || stubs.isStubEnum(l.type.name))) { + return new Out(paren(l.code) + ".ordinal() " + b.op + " " + paren(r.code) + ".ordinal()", + TypeRef.BOOL); + } + TypeRef t; + if (b.op.equals("<") || b.op.equals(">") || b.op.equals("<=") || b.op.equals(">=")) { + t = TypeRef.BOOL; + } else if (b.op.equals("&&") || b.op.equals("||")) { + t = TypeRef.BOOL; + } else if (numeric) { + t = l.type.is("double") || r.type.is("double") ? TypeRef.DOUBLE : TypeRef.INT; + } else { + t = l.type; + } + return new Out(paren(l.code) + " " + b.op + " " + paren(r.code), t); + } + + /** + * The catch clauses of one try, as ONE {@code catch (Throwable)} whose body tests the + * Dart clauses in order and rethrows what none of them takes. Separate Java catch + * clauses cannot express Dart's rules: `on Error` and `on Exception` are tests, not + * Java types (this runtime's Dart errors are RuntimeExceptions), a thrown value that is + * not an exception -- `throw 'boom'`, `throw token` -- arrives carried in a DartThrown + * and must be matched by ITS type and bound as itself, and a filtered clause that + * rethrew from its own Java catch skipped every clause after it. + */ + private void emitCatchChain(List catches, Ctx ctx) { + Ctx.Writer w = ctx.writer(); + ctx.importClass("dart.runtime.DartRuntime"); + String caught = ctx.newTemp(); + w.line("} catch (Throwable " + caught + ") {"); + ctx.indent(1); + boolean first = true; + boolean catchAll = false; + for (CatchClause cc : catches) { + TypeRef on = cc.onType; + String test; + String bindType; + String bindValue; + TypeRef varType; + if (on == null || on.is("Object") || on.is("dynamic")) { + test = null; + bindType = "Object"; + bindValue = "DartRuntime.caught(" + caught + ")"; + varType = TypeRef.DYNAMIC; + } else if (on.is("Error") && on.args.isEmpty()) { + test = "DartRuntime.isDartError(" + caught + ")"; + bindType = "Object"; + bindValue = caught; + varType = TypeRef.DYNAMIC; + } else if (on.is("Exception") && on.args.isEmpty()) { + test = "DartRuntime.isDartException(" + caught + ")"; + bindType = "Object"; + bindValue = caught; + varType = TypeRef.DYNAMIC; + } else if (isThrowableType(on)) { + String jt; + String coreError = CORE_ERRORS.get(on.name); + if (coreError != null) { + ctx.importClass(coreError); + jt = coreError.substring(coreError.lastIndexOf('.') + 1); + } else { + jt = javaType(on, true, ctx); + } + test = caught + " instanceof " + jt; + bindType = jt; + bindValue = "(" + jt + ") " + caught; + varType = on; + if (on.is("ConcurrentModificationError")) { + // Maps and sets iterate with Java's iterators, which report a change + // during iteration as java.util.ConcurrentModificationException; it is + // the same Dart error, so the clause takes both. + // Bound as the Dart error in both cases, so `e is ConcurrentModificationError` + // holds and e has that type's API. + test = "(" + test + " || " + caught + " instanceof java.util.ConcurrentModificationException)"; + bindValue = "DartRuntime.asConcurrentModificationError(" + caught + ")"; + } + } else { + // A thrown value that is not an exception: matched by its own type. + ctx.importClass("dart.core.DartThrown"); + String jt = javaType(on, true, ctx); + String value = "((DartThrown) " + caught + ").value()"; + test = caught + " instanceof DartThrown && " + value + " instanceof " + jt; + bindType = jt; + bindValue = "(" + jt + ") " + value; + varType = on; + } + if (test == null) { + w.line(first ? "{" : "} else {"); + catchAll = true; + } else { + w.line((first ? "if (" : "} else if (") + test + ") {"); + } + first = false; + ctx.indent(1); + ctx.pushScope(); + String var = ctx.declareShadowSafe(cc.exceptionVar != null ? cc.exceptionVar : "$e", varType); + w.line(bindType + " " + var + " = " + bindValue + ";"); + if (cc.stackVar != null) { + // stack traces are not modeled; bind the name for compilation + String stackJn = ctx.declareShadowSafe(cc.stackVar, TypeRef.DYNAMIC); + w.line("Object " + stackJn + " = null;"); + } + emitStatements(cc.body, ctx); + ctx.popScope(); + ctx.indent(-1); + if (catchAll) { + break; // a catch-all takes everything; later clauses are unreachable + } + } + if (!catchAll) { + w.line("} else {"); + w.line(" throw DartRuntime.rethrow(" + caught + ");"); + } + w.line("}"); + ctx.indent(-1); + } + + /** + * Whether values of {@code t} are Java throwables here: the dart:core errors, the + * framework's own error types, and application classes whose superclass chain reaches + * one. Anything else a Dart program throws travels in a DartThrown. + */ + private boolean isThrowableType(TypeRef t) { + java.util.Set seen = new java.util.HashSet(); + String n = t.name; + while (n != null && seen.add(n)) { + if (CORE_ERRORS.containsKey(n) || n.equals("Error") || n.equals("Exception") + || n.endsWith("Error") && stubs.isStubClass(n) + || n.endsWith("Exception") && stubs.isStubClass(n)) { + return true; + } + ClassDecl c = program.classes.get(n); + if (c == null || c.superclass == null) { + return false; + } + n = c.superclass.name; + } + return false; + } + + /** + * An operand of && or ||: a dynamic one is checked to be a bool at run time, as Dart + * does. Emitted as is it was `Object && boolean`, which javac rejects. Java's && and + * || still short-circuit, so the right operand is evaluated only when Dart would. + */ + private String logicalOperand(Out o, Ctx ctx) { + if (o.type == null || isDynamicType(o.type) || (o.type.is("bool") && o.type.nullable)) { + ctx.importClass("dart.runtime.DartRuntime"); + return "DartRuntime.dynBool(" + o.code + ")"; + } + return paren(o.code); + } + + /** + * A class mixing in ChangeNotifier holds its own listener list. The runtime's default + * kept them in a shared map keyed weakly by the notifier, but a listener closing over + * the notifier is held strongly there and leads back to the key, so a model with such a + * listener could never be collected unless someone called dispose. + */ + private void emitChangeNotifierState(TypeRef mixin, StringBuilder body) { + Ast.ClassDecl sc = stubs.classes.get(mixin.name); + if (sc == null || !"com.codename1.flutter.foundation.ChangeNotifier".equals(sc.javaName)) { + return; + } + body.append(" private final java.util.List changeNotifierListeners =\n") + .append(" new java.util.ArrayList();\n\n") + .append(" @Override\n") + .append(" public java.util.List changeNotifierListeners$() {\n") + .append(" return changeNotifierListeners;\n }\n\n"); + } + + /** dart:core error constructors -> dart-runtime classes. */ + private static final Map CORE_ERRORS = new LinkedHashMap(); + + static { + CORE_ERRORS.put("Exception", "dart.core.DartException"); + CORE_ERRORS.put("StateError", "dart.core.StateError"); + CORE_ERRORS.put("ArgumentError", "dart.core.ArgumentError"); + CORE_ERRORS.put("FormatException", "dart.core.FormatException"); + CORE_ERRORS.put("UnsupportedError", "dart.core.UnsupportedError"); + CORE_ERRORS.put("UnimplementedError", "dart.core.UnimplementedError"); + CORE_ERRORS.put("RangeError", "dart.core.RangeError"); + CORE_ERRORS.put("ConcurrentModificationError", "dart.core.ConcurrentModificationError"); + CORE_ERRORS.put("TypeError", "dart.core.TypeError"); + } + + /** + * {@code State} lifecycle methods that the framework overrides but that are not part + * of the minimal {@code State} stub surface (initState/dispose/setState/build are). They + * resolve as inherited {@code void} calls on any {@code State}-typed receiver (typically a + * {@code super.()} call), so a subclass can chain {@code super}. + */ + private static final java.util.Set STATE_LIFECYCLE = new java.util.HashSet(java.util.Arrays.asList( + "didChangeDependencies", "didUpdateWidget", "deactivate", "activate", "reassemble")); + + /** Known function typedefs: name -> [param types..., return type]. */ + private static final Map TYPEDEFS = new LinkedHashMap(); + + static { + TYPEDEFS.put("VoidCallback", new TypeRef[] {TypeRef.VOID}); + // (T value) -> void, the standard Flutter value-change callback + TYPEDEFS.put("ValueChanged", new TypeRef[] {TypeRef.DYNAMIC, TypeRef.VOID}); + // (T value) -> void + TYPEDEFS.put("ValueSetter", new TypeRef[] {TypeRef.DYNAMIC, TypeRef.VOID}); + // () -> T + TYPEDEFS.put("ValueGetter", new TypeRef[] {TypeRef.DYNAMIC}); + // () -> void, the tap-gesture callback + TYPEDEFS.put("GestureTapCallback", new TypeRef[] {TypeRef.VOID}); + TYPEDEFS.put("WidgetBuilder", new TypeRef[] {new TypeRef("BuildContext"), new TypeRef("Widget")}); + TYPEDEFS.put("IndexedWidgetBuilder", new TypeRef[] {new TypeRef("BuildContext"), TypeRef.INT, new TypeRef("Widget")}); + // (BuildContext, BoxConstraints) -> Widget, for LayoutBuilder + TYPEDEFS.put("LayoutWidgetBuilder", new TypeRef[] {new TypeRef("BuildContext"), new TypeRef("BoxConstraints"), new TypeRef("Widget")}); + // (BuildContext) -> List>, erased to Object return, for PopupMenuButton + TYPEDEFS.put("PopupMenuItemBuilder", new TypeRef[] {new TypeRef("BuildContext"), TypeRef.DYNAMIC}); + // value-change callbacks (transpiler-internal typedef names used in stubs) + TYPEDEFS.put("StringCallback", new TypeRef[] {TypeRef.STRING, TypeRef.VOID}); + TYPEDEFS.put("BoolCallback", new TypeRef[] {TypeRef.BOOL, TypeRef.VOID}); + TYPEDEFS.put("DoubleCallback", new TypeRef[] {TypeRef.DOUBLE, TypeRef.VOID}); + TYPEDEFS.put("IntCallback", new TypeRef[] {TypeRef.INT, TypeRef.VOID}); + TYPEDEFS.put("DynamicCallback", new TypeRef[] {TypeRef.DYNAMIC, TypeRef.VOID}); + // (int index) -> E, for List.generate's element generator + TYPEDEFS.put("IndexedGenerator", new TypeRef[] {TypeRef.INT, TypeRef.DYNAMIC}); + // (NavigatorState, Object?) -> String, for RestorableRouteFuture.onPresent + TYPEDEFS.put("RoutePresentationCallback", new TypeRef[] {new TypeRef("NavigatorState"), TypeRef.DYNAMIC, TypeRef.STRING}); + // (BuildContext, Widget?) -> Widget, for AnimatedBuilder.builder + TYPEDEFS.put("TransitionBuilder", new TypeRef[] {new TypeRef("BuildContext"), new TypeRef("Widget"), new TypeRef("Widget")}); + // (AnimationStatus) -> void, for Animation.addStatusListener + TYPEDEFS.put("AnimationStatusListener", new TypeRef[] {new TypeRef("AnimationStatus"), TypeRef.VOID}); + // (DateTime) -> void, for CupertinoDatePicker.onDateTimeChanged + TYPEDEFS.put("DateTimeCallback", new TypeRef[] {new TypeRef("DateTime"), TypeRef.VOID}); + // (Duration) -> void, for CupertinoTimerPicker.onTimerDurationChanged + TYPEDEFS.put("DurationCallback", new TypeRef[] {new TypeRef("Duration"), TypeRef.VOID}); + // (Set) -> Color, for MaterialStateProperty/WidgetStateProperty.resolveWith + TYPEDEFS.put("MaterialPropertyResolver", + new TypeRef[] {TypeRef.of("Set", new TypeRef("MaterialState")), new TypeRef("Color")}); + } + + /** + * The function-type signature ({@code [paramTypes..., returnType]}) of a typedef, + * whether a built-in ({@link #TYPEDEFS}) or a user-declared function-type alias, or + * {@code null} if {@code name} is not a (function-type) typedef. + */ + /** + * Fills a parameterized typedef signature's {@code dynamic} placeholders with the supplied + * type arguments in order (built-in typedefs like ValueChanged use {@code dynamic} for their + * type parameter). Returns a fresh array; the shared TYPEDEFS entries are never mutated. + */ + private TypeRef[] substituteTypedefTypeArgs(TypeRef[] sig, List args) { + if (sig == null || args == null || args.isEmpty()) { + return sig; + } + TypeRef[] out = new TypeRef[sig.length]; + int ai = 0; + for (int i = 0; i < sig.length; i++) { + if (sig[i] != null && sig[i].is("dynamic") && ai < args.size()) { + out[i] = args.get(ai++); + } else { + out[i] = sig[i]; + } + } + return out; + } + + private TypeRef[] typedefSig(String name) { + Ast.TypedefDecl td = program.typedefs.get(name); + if (td != null && td.returnType != null) { + TypeRef[] sig = new TypeRef[td.paramTypes.size() + 1]; + for (int i = 0; i < td.paramTypes.size(); i++) { + sig[i] = td.paramTypes.get(i); + } + sig[td.paramTypes.size()] = td.returnType; + return sig; + } + return TYPEDEFS.get(name); + } + + /** Whether {@code name} is a function-type typedef (built-in or user-declared). */ + private boolean isFunctionTypedef(String name) { + return typedefSig(name) != null; + } + + /** Whether a value of this type is directly invocable (a bare {@code Function} or a function typedef). */ + private boolean isFunctionValued(TypeRef t) { + return t != null && (t.is("Function") || typedefSig(t.name) != null); + } + + /** The result type produced by invoking a function-valued {@link TypeRef} (VOID or DYNAMIC when unknown). */ + private TypeRef funcResultType(TypeRef t) { + TypeRef[] sig = t == null ? null : typedefSig(t.name); + if (sig != null) { + TypeRef r = sig[sig.length - 1]; + return r.is("void") ? TypeRef.VOID : r; + } + return TypeRef.DYNAMIC; + } + + /** Dart identifiers that are Java reserved words; escaped with a trailing underscore. */ + private static final java.util.Set JAVA_KEYWORDS = new java.util.HashSet(java.util.Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", + "volatile", "while", "true", "false", "null")); + + /** + * The Java method name for a Dart member/named-parameter identifier: a + * Dart name that collides with a Java reserved word (e.g. {@code package}) + * is escaped with a trailing underscore. Hand-written runtime setters must + * use the same escaped name. + */ + static String javaMethodName(String dartName) { + return JAVA_KEYWORDS.contains(dartName) ? dartName + "_" : dartName; + } + + /** True when the identifier is one or more underscores and nothing else. */ + private static boolean isAllUnderscores(String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) != '_') { + return false; + } + } + return true; + } + + /** + * The legal Java identifier for an arbitrary Dart name emitted as a Java + * name. Folds in the reserved-word escape ({@link #javaMethodName}) and + * additionally rewrites Dart's all-underscore wildcard/placeholder names + * ({@code _}, {@code __}, ...) — a bare {@code _} is a reserved keyword in + * Java 9+ — by appending a trailing underscore ({@code _}->{@code __}, + * {@code __}->{@code ___}), which is always legal. Deterministic: the same + * Dart name always maps to the same Java name so declarations and + * references stay consistent. + */ + static String javaIdent(String dartName) { + if (isAllUnderscores(dartName)) { + return dartName + "_"; + } + return javaMethodName(dartName); + } + + /** Whether the most recently emitted lambda had a {@code void}-typed expression body. */ + private boolean lastLambdaVoid; + + private Out emitLambda(Lambda l, TypeRef expected, Ctx ctx) { + lastLambdaVoid = false; + // typedef-typed target position gives untyped lambda params real types + TypeRef[] sigTypes = expected != null ? typedefSig(expected.name) : null; + // An inline function type target (`Widget Function(BuildContext, T, Widget?)`, + // e.g. a generic stub builder whose element type was just substituted) likewise + // supplies concrete param types: flatten funcParams + funcReturn into a sig. + if (sigTypes == null && expected != null && expected.funcParams != null) { + sigTypes = new TypeRef[expected.funcParams.size() + 1]; + for (int i = 0; i < expected.funcParams.size(); i++) { + sigTypes[i] = expected.funcParams.get(i); + } + sigTypes[sigTypes.length - 1] = + expected.funcReturn != null ? expected.funcReturn : TypeRef.DYNAMIC; + } + boolean outerAsync = ctx.inAsyncBody; + TypeRef outerReturn = ctx.methodReturnType; + boolean outerNarrowInt = ctx.narrowReturnToInt; + ctx.inAsyncBody = false; + // Thread the lambda's SAM return type so `return`/switch-expression arms inside the body + // resolve against it (e.g. an onGenerateRoute builder whose switch arms are Route values). + TypeRef lambdaReturn = null; + if (sigTypes != null && sigTypes.length > 0) { + TypeRef r = sigTypes[sigTypes.length - 1]; + if (r != null && !r.is("void") && !r.is("dynamic")) { + lambdaReturn = r; + } + } + ctx.methodReturnType = lambdaReturn; + ctx.narrowReturnToInt = false; + // A break/continue cannot target a loop/switch outside the lambda body. + List savedBreaks = new ArrayList(ctx.breakTargets); + ctx.breakTargets.clear(); + ctx.pushScope(); + StringBuilder sig = new StringBuilder("("); + for (int i = 0; i < l.params.size(); i++) { + Param p = l.params.get(i); + TypeRef pt = p.type == null || p.type.is("var") ? TypeRef.DYNAMIC : p.type; + if (pt.is("dynamic") && sigTypes != null && i < sigTypes.length - 1) { + pt = sigTypes[i]; + } + if (i > 0) { + sig.append(", "); + } + sig.append(ctx.declareShadowSafe(p.name, pt)); + } + sig.append(')'); + String head = sig.toString(); + String code; + if (l.body != null) { + Ctx.Writer w = ctx.pushWriter(ctx.currentIndent() + 1); + emitStatements(l.body, ctx); + String body = ctx.popWriter(); + code = head + " -> {\n" + body + indentStr(ctx.currentIndent()) + "}"; + } else { + Ctx.Writer w = ctx.pushWriter(ctx.currentIndent() + 1); + // Emit the arrow body against the lambda's SAM return type so a switch-expression / + // conditional body unifies its arms to that type (e.g. an onGenerateRoute arrow whose + // switch arms are Route values). + // A void callback's arrow body is a statement: `() => model.count = 5` keeps + // the plain setter call rather than a value form a void lambda cannot return. + Expr savedStmt = ctx.statementExpr; + if (lambdaReturn == null || lambdaReturn.is("void")) { + ctx.statementExpr = l.exprBody; + } + Out o = emitExpr(l.exprBody, lambdaReturn, ctx); + ctx.statementExpr = savedStmt; + if (lambdaReturn != null && o.type != null && o.type.is("num") + && (lambdaReturn.is("double") || lambdaReturn.is("int"))) { + // num arithmetic is a Number; a lambda typed to return a double or int + // (fold's combiner, a double-returning builder) needs that primitive. + o = new Out(coerce(o, lambdaReturn, ctx), lambdaReturn); + } + String lifted = ctx.popWriter(); + lastLambdaVoid = o.type != null && o.type.is("void"); + if (lifted.isEmpty()) { + code = head + " -> " + o.code; + } else if (o.type.is("void") || o.type.is("Null")) { + code = head + " -> {\n" + lifted + indentStr(ctx.currentIndent() + 1) + + statementize(o.code) + ";\n" + indentStr(ctx.currentIndent()) + "}"; + } else { + code = head + " -> {\n" + lifted + indentStr(ctx.currentIndent() + 1) + + "return " + o.code + ";\n" + indentStr(ctx.currentIndent()) + "}"; + } + } + ctx.popScope(); + ctx.breakTargets.clear(); + ctx.breakTargets.addAll(savedBreaks); + ctx.inAsyncBody = outerAsync; + ctx.methodReturnType = outerReturn; + ctx.narrowReturnToInt = outerNarrowInt; + return new Out(code, new TypeRef("Function")); + } + + // ================================================================== + // Calls + // ================================================================== + + private Out emitCall(Call c, TypeRef expected, Ctx ctx) { + // closure value invocation: f(...) where f is a local of Function type + if (c.name == null && c.target != null) { + Out target = emitExpr(c.target, null, ctx); + return new Out(target.code + ".call(" + plainArgs(c.args, ctx) + ")", TypeRef.DYNAMIC); + } + if (c.target == null) { + return emitBareCall(c, ctx); + } + // deferred import: `m.loadLibrary()` — AOT, so the library is already loaded; + // hand back an already-completed future. + if (c.name != null && c.name.equals("loadLibrary") && isImportPrefix(c.target, ctx)) { + ctx.importClass("dart.async.Future"); + return new Out("Future.value(null)", TypeRef.of("Future", TypeRef.DYNAMIC)); + } + // `prefix.fn(...)` / `prefix.Type(...)` through an `import '...' as prefix` name: + // the invoked function or constructor lives in another user (or stub) library and + // is globally addressable, so dispatch it as a bare (unqualified) call. + if (c.name != null && isImportPrefix(c.target, ctx) + && (program.functions.containsKey(c.name) || program.classes.containsKey(c.name) + || stubs.isStubClass(c.name) || stubs.functions.containsKey(c.name))) { + Call bare = new Call(); + bare.file = c.file; + bare.line = c.line; + bare.col = c.col; + bare.name = c.name; + bare.args = c.args; + return emitBareCall(bare, ctx); + } + // Static factory methods on the primitive numeric types — `double.parse(s)`, + // `double.tryParse(s)`, `int.parse(s)`, `int.tryParse(s)`. The type name is not a + // resolvable value expression, so intercept before emitting it as a target. + if (c.name != null && c.target instanceof Ident + && ctx.lookup(((Ident) c.target).name) == null + && (ctx.currentClass == null || ctx.currentClass.field(((Ident) c.target).name) == null)) { + Out prim = emitPrimitiveStaticCall(((Ident) c.target).name, c, ctx); + if (prim != null) { + return prim; + } + } + Out target = emitExprRaw(c.target, null, ctx); + if (c.nullAware) { + // a?.m() - the SAME shorting a?.b gets, and it was missing here: only the + // function-valued `call` case honoured the flag, so every other `?.m()` emitted + // an unguarded invocation. The gallery's `_timeDilationTimer?.cancel()` is one, + // and toggling slow motion threw a NullPointerException on the very first use + // because that timer is null until something has been dilated. + // + // Lift the receiver into a temp and start a short: the call runs on the + // non-null temp, and the temp being null shorts this and any trailing + // selectors, exactly as Dart specifies. + Out mat = materializeShort(target); + String tmp = ctx.newTemp(); + ctx.writer().line("var " + tmp + " = " + mat.code + ";"); + // The arguments are evaluated only when the receiver is non-null, so any + // statements they lift (`a?.addAll([...xs])`) go inside the guard. + ctx.pushWriter(ctx.currentIndent() + 1); + Out called = emitMethodCallOn( + new Out(tmp, copyNonNull(mat.type), mat.fromError), c, ctx); + String lifted = ctx.popWriter(); + if (!lifted.isEmpty()) { + Out guarded = guardLifted(tmp, lifted, called, ctx); + if (guarded != null) { + return guarded; + } + } + if (called.code == null || called.code.isEmpty()) { + // The callee emitted its own guarded statement (the function-valued `call` + // path does this) - there is no expression left to short. + return called; + } + if (called.type != null && called.type.is("void")) { + // A void call cannot be the value of a ternary. In statement position the + // guard is an `if`, which is also what the result is used for: nothing. + ctx.writer().line("if (" + tmp + " != null) { " + called.code + "; }"); + return new Out("", TypeRef.VOID); + } + return new Out(called.code, boxType(called.type), called.fromError, tmp); + } + if (target.shortGuard != null) { + // a plain method call after a `?.` stays inside the short (a?.b.c()), and so + // do any statements its arguments lift + ctx.pushWriter(ctx.currentIndent() + 1); + Out result = emitMethodCallOn(target, c, ctx); + String lifted = ctx.popWriter(); + if (!lifted.isEmpty()) { + Out guarded = guardLifted(target.shortGuard, lifted, result, ctx); + if (guarded != null) { + return guarded; + } + } + return result.withShort(target.shortGuard); + } + return emitMethodCallOn(target, c, ctx); + } + + /** + * Static numeric parse factories on {@code int} / {@code double}, whose receiver is a + * bare type name rather than a value. Returns null for any other name. + * + *

Both go through the runtime's DString helpers. tryParse used to become the same + * Long.parseLong as parse, so malformed input -- exactly what a tryParse guard is + * written for -- threw NumberFormatException instead of answering null; and parse + * threw Java's exception rather than Dart's FormatException, which an + * {@code on FormatException} clause does not catch.

+ */ + private Out emitPrimitiveStaticCall(String typeName, Call c, Ctx ctx) { + boolean isInt = typeName.equals("int"); + boolean isDouble = typeName.equals("double"); + if ((!isInt && !isDouble) || c.args.positional.isEmpty()) { + return null; + } + if (!c.name.equals("parse") && !c.name.equals("tryParse")) { + return null; + } + String arg = emitExpr(c.args.positional.get(0), TypeRef.STRING, ctx).code; + boolean nullable = c.name.equals("tryParse"); + ctx.importClass("dart.core.DString"); + TypeRef t = new TypeRef(isInt ? "int" : "double"); + t.nullable = nullable; + String fn = (nullable ? "tryParse" : "parse") + (isInt ? "Int" : "Double"); + if (isInt) { + // `radix:` is the one named argument int.parse takes; it was dropped, so + // int.parse('ff', radix: 16) failed as a decimal number. + for (NamedArg na : c.args.named) { + if (na.name.equals("radix")) { + arg += ", " + emitExpr(na.value, TypeRef.INT, ctx).code; + } + } + } + return new Out("DString." + fn + "(" + arg + ")", t); + } + + private Out emitBareCall(Call c, Ctx ctx) { + String n = c.name; + // dart:core print + if (n.equals("print")) { + ctx.importClass("dart.runtime.DartRuntime"); + Expr arg = c.args.positional.isEmpty() ? null : c.args.positional.get(0); + Out o = arg == null ? new Out("\"\"", TypeRef.STRING) : emitExpr(arg, null, ctx); + return new Out("DartRuntime.print(" + o.code + ")", TypeRef.VOID); + } + // local closure variable + TypeRef local = ctx.lookup(n); + if (local != null) { + // A function-typed local (bare `Function` or a function typedef such as + // `LibraryLoader = Future Function()`) invocation yields the typedef's + // result type, so a chained `loader().then(...)` sees a real Future receiver. + TypeRef ret = isFunctionValued(local) ? funcResultType(local) : TypeRef.DYNAMIC; + return new Out(n + ".call(" + plainArgs(c.args, ctx) + ")", ret); + } + // inside an extension body, bare calls probe the receiver first + if (ctx.extensionSelfType != null) { + Out self = new Out("$self", ctx.extensionSelfType); + Out probe = intrinsicCall(self, c, ctx); + if (probe != null) { + return probe; + } + Ast.MethodDecl sm = stubs.findMethod(ctx.extensionSelfType.name, n, false); + if (sm != null) { + return new Out("$self." + n + "(" + stubMethodArgs(sm, c.args, ctx) + ")", sm.returnType); + } + ClassDecl extCls = program.findExtension(ctx.extensionSelfType.name, n, false); + if (extCls != null) { + MethodDecl em = extCls.method(n); + return new Out(extCls.name + "." + n + "($self" + + (c.args.positional.isEmpty() && c.args.named.isEmpty() ? "" : ", " + + methodArgs(em.params, c.args, ctx)) + ")", + em.returnType == null || em.returnType.is("var") ? TypeRef.DYNAMIC : em.returnType); + } + } + // Stopwatch() — dart:core intrinsic (no-arg monotonic timer) + if (n.equals("Stopwatch") && c.args.positional.isEmpty() && c.args.named.isEmpty()) { + ctx.importClass("dart.core.Stopwatch"); + return new Out("new Stopwatch()", new TypeRef("Stopwatch")); + } + // Duration(seconds: 2, ...) — dart:core intrinsic with canonical named order + if (n.equals("Duration")) { + return emitDurationOf(c.args, ctx); + } + // dart:core exception constructors + String coreError = CORE_ERRORS.get(n); + if (coreError != null) { + ctx.importClass(coreError); + String simple = coreError.substring(coreError.lastIndexOf('.') + 1); + String msg = c.args.positional.isEmpty() ? "\"\"" + : emitExpr(c.args.positional.get(0), TypeRef.STRING, ctx).code; + return new Out("new " + simple + "(" + msg + ")", new TypeRef("Exception")); + } + // constructor of program class + ClassDecl pc = program.classes.get(n); + if (pc != null) { + return emitCtorCall(n, c.typeArgs, c.args, c, ctx); + } + // constructor of stub class + if (stubs.isStubClass(n)) { + return emitCtorCall(n, c.typeArgs, c.args, c, ctx); + } + // method of current class / inherited stub method + ClassDecl cc = ctx.currentClass; + if (cc != null) { + MethodDecl m = cc.method(n); + if (m != null) { + String recv = m.isStatic ? cc.name : "this"; + return new Out(recv + "." + n + "(" + + methodArgs(m.params, c.args, ctx) + ")", + m.returnType == null || m.returnType.is("var") ? TypeRef.DYNAMIC : m.returnType); + } + String stubSuper = nearestStubSuper(cc); + if (stubSuper != null) { + Ast.MethodDecl sm = stubs.findMethod(stubSuper, n, false); + if (sm != null) { + return new Out("this." + n + "(" + stubMethodArgs(sm, c.args, ctx) + ")", sm.returnType); + } + } + // members contributed by an applied stub mixin (e.g. RestorationMixin's + // registerForRestoration): resolved as an inherited default method. + for (TypeRef mixRef : cc.mixins) { + if (stubs.isStubClass(mixRef.name)) { + Ast.MethodDecl sm = stubs.findMethod(mixRef.name, n, false); + if (sm != null) { + return new Out("this." + n + "(" + stubMethodArgs(sm, c.args, ctx) + ")", sm.returnType); + } + } + } + // bare invocation of an own (or inherited) function-typed field: + // `onChanged(v)` where onChanged is a `void Function(...)` field. + FieldDecl ff = cc.field(n); + if (ff == null) { + ff = findInheritedField(cc, n); + } + if (ff != null && isFunctionValued(ff.type)) { + Out fieldRead = emitMemberGet(new Out(ff.isStatic ? cc.name : "this", + new TypeRef(cc.name)), n, c, ctx); + return new Out(fieldRead.code + ".call(" + plainArgs(c.args, ctx) + ")", + funcResultType(ff.type)); + } + } + // top-level function (user code) + FunctionDecl fn = program.functions.get(n); + if (fn != null) { + Library owner = program.resolveFunctionOwner(n, ctx.library(), null); + // A private top-level function name can be declared in several libraries (Dart + // privacy is library-scoped); use the RESOLVED owner's declaration so the parameter + // list matches (e.g. crane's 2-arg `_customIconTheme` vs shrine's 1-arg one). + FunctionDecl ownerFn = functionInLibrary(owner, n); + if (ownerFn != null) { + fn = ownerFn; + } + String cls = Program.libClassName(owner.fileName); + String jn = n.equals("main") ? "main$" : n; + return new Out(cls + "." + jn + "(" + methodArgs(fn.params, c.args, ctx) + ")", + fn.returnType == null || fn.returnType.is("var") ? TypeRef.DYNAMIC : fn.returnType); + } + // dart:core identical(a, b): unresolved before, so any use failed the build. + if (n.equals("identical") && c.args.positional.size() == 2 && !program.functions.containsKey(n)) { + ctx.importClass("dart.runtime.DartRuntime"); + Out a = emitExpr(c.args.positional.get(0), null, ctx); + Out b = emitExpr(c.args.positional.get(1), null, ctx); + // Object parameters: a primitive long/double/bool argument autoboxes. + return new Out("DartRuntime.identical(" + a.code + ", " + b.code + ")", TypeRef.BOOL); + } + // stub top-level function (e.g. runApp) + Ast.FunctionDecl sf = stubs.functions.get(n); + if (sf != null && sf.javaName != null) { + int dot = sf.javaName.lastIndexOf('.'); + String cls = sf.javaName.substring(0, dot); + String method = sf.javaName.substring(dot + 1); + Out numeric = mathNumCall(cls, method, c, ctx); + if (numeric != null) { + return numeric; + } + ctx.importClass(cls); + String simple = cls.substring(cls.lastIndexOf('.') + 1); + return new Out(simple + "." + method + "(" + methodArgs(sf.params, c.args, ctx) + ")", + sf.returnType); + } + diags.error(c, "E0135", "Cannot resolve function or constructor '" + n + + "'. Confirm the file passes `dart analyze`, or the API may be unsupported in M1."); + return new Out("null", TypeRef.DYNAMIC, true); + } + + private Out emitMethodCallOn(Out target, Call c, Ctx ctx) { + TypeRef tt = target.type; + // A receiver typed with an import prefix (`intl.DateFormat`, `ui.Size`) keeps the + // prefix in its type name; strip it so method resolution sees the real stub type. + if (tt != null && tt.name != null && tt.name.indexOf('.') > 0) { + tt.name = stripImportPrefix(tt.name); + } + String n = c.name; + // Cascade suppression: the receiver's type already fell to `dynamic` because a + // diagnostic was reported for it upstream (unresolved identifier/member/etc.). + // Accessing a method on a dynamic receiver is legal Dart (dynamic dispatch), so + // re-diagnosing here would just spam the same root cause down the whole chain. + if (target.fromError && tt.is("dynamic")) { + return new Out(target.code + "." + n + "(" + plainArgs(c.args, ctx) + ")", + TypeRef.DYNAMIC, true); + } + // static method on a class reference + if (isClassRef(tt)) { + String cls = tt.arg(0).name; + // Dart's `Object.hash(a, b, ...)` -> java.util.Objects.hash(Object...). + if (cls.equals("Object") && n.equals("hashAll") && c.args.positional.size() == 1) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.hashAll(" + emitExpr(c.args.positional.get(0), null, ctx).code + ")", + TypeRef.INT); + } + if (cls.equals("Object") && n.equals("hash")) { + // Through DartRuntime.hash, which hashes numbers as Dart's == compares + // them; java.util.Objects.hash gave 1 and 1.0 different hashes. + ctx.importClass("dart.runtime.DartRuntime"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < c.args.positional.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("(Object) ").append(paren(emitExpr(c.args.positional.get(i), null, ctx).code)); + } + return new Out("DartRuntime.hash(" + sb + ")", TypeRef.INT); + } + // Dart's `Comparable.compare(a, b)` -> DartComparable.compare (delegates to compareTo). + if (cls.equals("Comparable") && n.equals("compare")) { + ctx.importClass("dart.core.DartComparable"); + String a = emitExpr(c.args.positional.get(0), null, ctx).code; + String b = emitExpr(c.args.positional.get(1), null, ctx).code; + return new Out("DartComparable.compare(" + a + ", " + b + ")", TypeRef.INT); + } + // Iterable.generate(count, [generator]) with no type arguments parses as a + // static call on the name Iterable, which resolved to nothing; the typed form + // (Iterable.generate) is a constructor call handled with the collections. + if (cls.equals("Iterable") && n.equals("generate") && !c.args.positional.isEmpty()) { + ctx.importClass("dart.core.DartIterable"); + String count = emitExpr(c.args.positional.get(0), TypeRef.INT, ctx).code; + String gen = c.args.positional.size() > 1 + ? ", " + emitExpr(c.args.positional.get(1), new TypeRef("IndexedGenerator"), ctx).code : ""; + return new Out("DartIterable.generate(" + count + gen + ")", + TypeRef.of("Iterable", TypeRef.DYNAMIC)); + } + if (cls.equals("Future")) { + ctx.importClass("dart.async.Future"); + if (n.equals("delayed")) { + String dur = emitExpr(c.args.positional.get(0), new TypeRef("Duration"), ctx).code; + String comp = c.args.positional.size() > 1 + ? emitExpr(c.args.positional.get(1), null, ctx).code : null; + return new Out("Future.delayed(" + dur + (comp != null ? ", " + comp : "") + ")", + TypeRef.of("Future", TypeRef.DYNAMIC)); + } + if (n.equals("value")) { + Out v = c.args.positional.isEmpty() ? new Out("null", TypeRef.NULL) + : emitExpr(c.args.positional.get(0), null, ctx); + return new Out("Future.value(" + boxIfPrimitive(v, ctx) + ")", + TypeRef.of("Future", v.type)); + } + if (n.equals("wait")) { + Out l = emitExpr(c.args.positional.get(0), null, ctx); + // eagerError: and cleanUp: were dropped, so an eager wait still waited + // for every input and a cleanUp callback never ran. + String eager = null; + String cleanUp = null; + for (NamedArg na : c.args.named) { + if (na.name.equals("eagerError")) { + eager = emitExpr(na.value, TypeRef.BOOL, ctx).code; + } else if (na.name.equals("cleanUp")) { + cleanUp = pinnedHandler(na.value, emitExpr(na.value, null, ctx), true, + "dart.runtime.Funcs.VoidFunc1", "dart.runtime.Funcs.VoidFunc1"); + } + } + String args = eager == null && cleanUp == null ? l.code + : l.code + ", " + (eager == null ? "false" : eager) + ", " + + (cleanUp == null ? "(dart.runtime.Funcs.VoidFunc1) null" : cleanUp); + return new Out("Future.wait(" + args + ")", + TypeRef.of("Future", TypeRef.of("List", TypeRef.DYNAMIC))); + } + diags.error(c, "E0304", "Unsupported Future member: " + n); + return new Out("null", TypeRef.DYNAMIC); + } + if (stubs.isStubClass(cls)) { + Ast.MethodDecl m = stubs.findMethod(cls, n, false); + if (m != null && m.isStatic) { + Ast.ClassDecl sc = stubs.classes.get(cls); + Out numeric = sc == null ? null : mathNumCall(sc.javaName, n, c, ctx); + if (numeric != null) { + return numeric; + } + return stubCallOut(m, c, stubSimpleName(cls, ctx) + "." + n, ctx); + } + } + ClassDecl pc = program.classes.get(cls); + if (pc != null) { + MethodDecl m = pc.method(n); + if (m != null && m.isStatic) { + return new Out(cls + "." + n + "(" + methodArgs(m.params, c.args, ctx) + ")", + m.returnType == null ? TypeRef.DYNAMIC : m.returnType); + } + CtorDecl named = pc.namedCtor(n); + if (named != null) { + // named (or named factory) constructor -> static factory + return new Out(cls + "." + n + "(" + canonicalArgs(named, c.args, ctx) + ")", + new TypeRef(cls)); + } + diags.error(c, "E0136", "Cannot resolve static member or constructor '" + cls + "." + n + "'"); + return new Out("null", TypeRef.DYNAMIC, true); + } + diags.error(c, "E0136", "Cannot resolve static method '" + n + "' on " + cls); + return new Out("null", TypeRef.DYNAMIC, true); + } + // intrinsics + Out intrinsic = intrinsicCall(target, c, ctx); + if (intrinsic != null) { + return intrinsic; + } + // enhanced-enum instance method: `category.displayTitle(loc)` + Ast.EnumDecl ed = program.enums.get(tt.name); + if (ed != null) { + MethodDecl em = ed.method(n); + if (em != null) { + return new Out(target.code + "." + n + "(" + methodArgs(em.params, c.args, ctx) + ")", + em.returnType == null || em.returnType.is("var") ? TypeRef.DYNAMIC : em.returnType); + } + } + // program class instance method + ClassDecl pc = programClass(tt.name, ctx); + if (pc != null) { + MethodDecl m = pc.method(n); + if (m == null) { + m = findMethodInHierarchy(pc, n); + } + if (m == null) { + Object mixM = findMixinMember(pc, n, false); + if (mixM instanceof MethodDecl && !((MethodDecl) mixM).isGetter) { + m = (MethodDecl) mixM; + } + } + if (m != null) { + return new Out(target.code + "." + n + "(" + methodArgs(m.params, c.args, ctx) + ")", + m.returnType == null || m.returnType.is("var") ? TypeRef.DYNAMIC : m.returnType); + } + // method inherited from the program class's stub superclass or a stub mixin + // (e.g. a RestorableProperty subclass calling the inherited `dispose()`). + String stubSuper = nearestStubSuper(pc); + if (stubSuper != null) { + Ast.MethodDecl sm = stubs.findMethod(stubSuper, n, false); + if (sm != null) { + return stubCallOut(sm, c, target.code + "." + n, ctx); + } + } + for (TypeRef mixRef : pc.mixins) { + if (stubs.isStubClass(mixRef.name)) { + Ast.MethodDecl sm = stubs.findMethod(mixRef.name, n, false); + if (sm != null) { + return stubCallOut(sm, c, target.code + "." + n, ctx); + } + } + } + // invocation of a function-typed field: `obj.onTap(args)` where onTap is a + // `void Function(...)`/callback field — read the field then invoke its SAM. + FieldDecl ff = pc.field(n); + if (ff == null) { + ff = findInheritedField(pc, n); + } + if (ff != null && isFunctionValued(ff.type)) { + Out fieldRead = emitMemberGet(target, n, c, ctx); + return new Out(fieldRead.code + ".call(" + plainArgs(c.args, ctx) + ")", + funcResultType(ff.type)); + } + } + // SAM invocation on a function-typed value: `f.call(args)` / `f?.call(args)` + if (n.equals("call") && isFunctionValued(tt)) { + TypeRef ret = funcResultType(tt); + if (c.nullAware) { + String tmp = ctx.newTemp(); + ctx.writer().line("var " + tmp + " = " + target.code + ";"); + // Arguments run only for a non-null callback; what they lift goes + // inside the guard. + ctx.pushWriter(ctx.currentIndent() + 1); + String args = plainArgs(c.args, ctx); + String lifted = ctx.popWriter(); + // A void (or untyped-`Function`, whose void return was erased) callback is + // fire-and-forget in statement position: guard with an `if` so a void SAM + // isn't illegally used as a ternary value. + if (ret.is("void") || ret.is("dynamic")) { + if (!lifted.isEmpty()) { + return guardLifted(tmp, lifted, new Out(tmp + ".call(" + args + ")", TypeRef.VOID), ctx); + } + ctx.writer().line("if (" + tmp + " != null) { " + tmp + ".call(" + + args + "); }"); + return new Out("", TypeRef.VOID); + } + if (!lifted.isEmpty()) { + Out guarded = guardLifted(tmp, lifted, new Out(tmp + ".call(" + args + ")", ret), ctx); + if (guarded != null) { + return materializeShort(guarded); + } + } + return new Out("(" + tmp + " == null ? null : " + tmp + ".call(" + + args + "))", boxType(ret)); + } + return new Out(target.code + ".call(" + plainArgs(c.args, ctx) + ")", ret); + } + // stub instance method + String stubName = stubs.isStubClass(tt.name) ? tt.name : null; + if (stubName != null || tt.is("State")) { + Ast.MethodDecl m = stubs.findMethod(tt.name, n, false); + if (m != null) { + // Narrow a generic method result (declared `T evaluate(...)`) to the + // concrete type argument the receiver instantiates. + TypeRef sub = stubMemberReturnType(tt, n, false); + return stubCallOut(m, c, target.code + "." + n, ctx, sub); + } + } + // State lifecycle overrides not present on the minimal State stub surface + // (typically a `super.()` chain); resolve as inherited void calls. + if (tt.is("State") && STATE_LIFECYCLE.contains(n)) { + return new Out(target.code + "." + n + "(" + plainArgs(c.args, ctx) + ")", TypeRef.VOID); + } + // extension methods + ClassDecl extCls = program.findExtension(tt.name, n, false); + if (extCls != null) { + MethodDecl em = extCls.method(n); + String rest = methodArgs(em.params, c.args, ctx); + return new Out(extCls.name + "." + n + "(" + target.code + + (rest.isEmpty() ? "" : ", " + rest) + ")", + em.returnType == null || em.returnType.is("var") ? TypeRef.DYNAMIC : em.returnType); + } + if (tt.is("Stopwatch")) { + // start / stop / reset are void no-arg controls + return new Out(target.code + "." + n + "()", TypeRef.VOID); + } + // `.then(cb)` on a void receiver: some controller actions the runtime models as void + // return Flutter's synchronously-completing TickerFuture (e.g. + // `controller.reverse().then(...)`). Everything is AOT-synchronous, so run the receiver + // for its effect, then continue on an already-completed future. + if (n.equals("then") && tt.is("void") && !c.args.positional.isEmpty()) { + ctx.importClass("dart.async.Future"); + if (target.code != null && !target.code.isEmpty()) { + ctx.writer().line(statementize(target.code) + ";"); + } + Out cb = emitExpr(c.args.positional.get(0), null, ctx); + return new Out("Future.value(null).then(" + cb.code + ")", + TypeRef.of("Future", TypeRef.DYNAMIC)); + } + // Object protocol + if (n.equals("toString") && c.args.positional.isEmpty()) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.str(" + target.code + ")", TypeRef.STRING); + } + diags.error(c, "E0137", "Cannot resolve method '" + n + "' on type " + tt + + ". Confirm the file passes `dart analyze`, or the API may be unsupported in M1."); + return new Out(target.code + "." + n + "(" + plainArgs(c.args, ctx) + ")", TypeRef.DYNAMIC, true); + } + + /** Core-type method table (String / List / Map / int / double). */ + private Out intrinsicCall(Out target, Call c, Ctx ctx) { + TypeRef tt = target.type; + String n = c.name; + List pos = c.args.positional; + if (tt.is("String")) { + ctx.importClass("dart.core.DString"); + if (n.equals("substring")) { + String args = target.code; + for (Expr e : pos) { + args += ", " + emitExpr(e, TypeRef.INT, ctx).code; + } + return new Out("DString.substring(" + args + ")", TypeRef.STRING); + } + if (n.equals("contains")) { + // contains(pattern, startIndex): the start was dropped, so the search + // always began at 0. + String start = pos.size() > 1 ? ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code : ""; + return new Out("DString.contains(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + start + ")", TypeRef.BOOL); + } + if (n.equals("split")) { + return new Out("DString.split(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.of("List", TypeRef.STRING)); + } + if (n.equals("indexOf")) { + String args = target.code; + for (Expr e : pos) { + args += ", " + emitExpr(e, null, ctx).code; + } + return new Out("DString.indexOf(" + args + ")", TypeRef.INT); + } + if (n.equals("lastIndexOf")) { + // Unlowered, it reached Java's String.lastIndexOf: an int result, no + // RegExp pattern, and a long start that did not compile. + String args = target.code + ", " + emitExpr(pos.get(0), null, ctx).code; + if (pos.size() > 1) { + args += ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code; + } + return new Out("DString.lastIndexOf(" + args + ")", TypeRef.INT); + } + if (n.equals("replaceFirst")) { + // Unresolved before: DString's overloads were never reached. + String start = pos.size() > 2 ? ", " + emitExpr(pos.get(2), TypeRef.INT, ctx).code : ""; + return new Out("DString.replaceFirst(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + ", " + + emitExpr(pos.get(1), null, ctx).code + start + ")", TypeRef.STRING); + } + if (n.equals("replaceAll")) { + return new Out("DString.replaceAll(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + ", " + + emitExpr(pos.get(1), null, ctx).code + ")", TypeRef.STRING); + } + if (n.equals("codeUnitAt")) { + return new Out("DString.codeUnitAt(" + target.code + ", " + + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", TypeRef.INT); + } + if (n.equals("padLeft") || n.equals("padRight")) { + String args = target.code; + for (Expr e : pos) { + args += ", " + emitExpr(e, null, ctx).code; + } + return new Out("DString." + n + "(" + args + ")", TypeRef.STRING); + } + if (n.equals("compareTo")) { + return new Out("DString.compareTo(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.INT); + } + if (n.equals("toUpperCase") || n.equals("toLowerCase")) { + // Through the runtime, not Java's String methods: those follow the + // device's default locale, so under Turkish 'i'.toUpperCase() was a + // dotted capital and case-folded identifiers varied by device. Dart's + // are locale independent. + return new Out("DString." + n + "(" + target.code + ")", TypeRef.STRING); + } + if (n.equals("trim")) { + // Dart's whitespace set, not Java's <= U+0020. + return new Out("DString.trim(" + target.code + ")", TypeRef.STRING); + } + if (n.equals("startsWith") && pos.size() > 1) { + // startsWith(pattern, index): the index was dropped, so + // 'abc'.startsWith('b', 1) answered false. + return new Out("DString.startsWith(" + target.code + ", " + + emitExpr(pos.get(0), null, ctx).code + ", " + + emitExpr(pos.get(1), TypeRef.INT, ctx).code + ")", TypeRef.BOOL); + } + if (n.equals("startsWith") || n.equals("endsWith")) { + return new Out(target.code + "." + n + "(" + + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.BOOL); + } + if (n.equals("toString")) { + return new Out(target.code, TypeRef.STRING); + } + } + if ((tt.is("int") || tt.is("double") || tt.is("num")) && n.equals("compareTo") && pos.size() == 1) { + // num.compareTo across representations: an int against a double compares + // exactly, -0.0 orders below 0, NaN above everything. Unresolved before. + ctx.importClass("dart.core.DartComparable"); + Out other = emitExpr(pos.get(0), null, ctx); + return new Out("DartComparable.compare(" + boxNumber(new Out(target.code, tt)) + ", " + + boxNumber(other) + ")", TypeRef.INT); + } + if (tt.is("int")) { + if (n.equals("toString")) { + return new Out("Long.toString(" + target.code + ")", TypeRef.STRING); + } + if (n.equals("toStringAsFixed")) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.toStringAsFixed(" + paren(target.code) + ", " + + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", TypeRef.STRING); + } + if (n.equals("toRadixString")) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.toRadixString(" + target.code + ", " + + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", TypeRef.STRING); + } + if (n.equals("toDouble")) { + return new Out("((double) " + paren(target.code) + ")", TypeRef.DOUBLE); + } + if (n.equals("toInt")) { + return new Out(paren(target.code), TypeRef.INT); + } + if (n.equals("abs")) { + return new Out("Math.abs(" + target.code + ")", TypeRef.INT); + } + if (n.equals("clamp")) { + // ArgumentError for lower > upper, as Dart; nested min/max answered a value. + ctx.importClass("dart.runtime.DartRuntime"); + String lo = emitExpr(pos.get(0), TypeRef.INT, ctx).code; + String hi = emitExpr(pos.get(1), TypeRef.INT, ctx).code; + return new Out("DartRuntime.clamp(" + target.code + ", " + lo + ", " + hi + ")", TypeRef.INT); + } + } + if (tt.is("double")) { + ctx.importClass("dart.runtime.DartRuntime"); + if (n.equals("toString")) { + return new Out("DartRuntime.doubleStr(" + target.code + ")", TypeRef.STRING); + } + if (n.equals("toStringAsFixed")) { + return new Out("DartRuntime.toStringAsFixed(" + paren(target.code) + ", " + + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", TypeRef.STRING); + } + // toInt, floor and ceil refuse NaN and the infinities with UnsupportedError, + // as Dart does; a Java cast made NaN 0 and saturated the infinities. + if (n.equals("toInt") || n.equals("truncate")) { + return new Out("DartRuntime.toInt(" + target.code + ")", TypeRef.INT); + } + if (n.equals("toDouble")) { + return new Out("((double) " + paren(target.code) + ")", TypeRef.DOUBLE); + } + if (n.equals("floor")) { + return new Out("DartRuntime.floor(" + target.code + ")", TypeRef.INT); + } + if (n.equals("ceil")) { + return new Out("DartRuntime.ceil(" + target.code + ")", TypeRef.INT); + } + if (n.equals("round")) { + // Half away from zero, as Dart rounds; Math.round rounds -1.5 to -1. + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.round(" + target.code + ")", TypeRef.INT); + } + if (n.equals("floorToDouble")) { + return new Out("Math.floor(" + target.code + ")", TypeRef.DOUBLE); + } + if (n.equals("ceilToDouble")) { + return new Out("Math.ceil(" + target.code + ")", TypeRef.DOUBLE); + } + if (n.equals("roundToDouble")) { + ctx.importClass("dart.runtime.DartRuntime"); + return new Out("DartRuntime.roundToDouble(" + target.code + ")", TypeRef.DOUBLE); + } + if (n.equals("abs")) { + return new Out("Math.abs(" + target.code + ")", TypeRef.DOUBLE); + } + if (n.equals("clamp")) { + String lo = emitExpr(pos.get(0), TypeRef.DOUBLE, ctx).code; + String hi = emitExpr(pos.get(1), TypeRef.DOUBLE, ctx).code; + return new Out("DartRuntime.clamp(((double) " + paren(target.code) + "), " + lo + ", " + + hi + ")", TypeRef.DOUBLE); + } + } + if (tt.is("List") || tt.is("Iterable") || tt.is("Set")) { + TypeRef elem = tt.arg(0); + if (n.equals("add")) { + Out v = emitExpr(pos.get(0), elem, ctx); + String pk = primitiveListKind(tt); + if (pk != null) { + return new Out(target.code + ".add" + pk + "(" + coerce(v, elem, ctx) + ")", TypeRef.VOID); + } + // Set.add answers whether the element was new, as Dart's does; List.add + // is void. Typing both void made `var added = set.add(x)` declare a + // void local. + return new Out(target.code + ".add(" + boxIfPrimitive(v, ctx) + ")", + tt.is("Set") ? TypeRef.BOOL : TypeRef.VOID); + } + if (n.equals("addAll")) { + return new Out(target.code + ".addAllIterable(" + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.VOID); + } + if (n.equals("insert")) { + return new Out(target.code + ".insert(" + emitExpr(pos.get(0), TypeRef.INT, ctx).code + + ", " + emitExpr(pos.get(1), elem, ctx).code + ")", TypeRef.VOID); + } + if (n.equals("removeAt")) { + return new Out(target.code + ".removeAt(" + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", elem); + } + if (n.equals("remove")) { + Out v = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".removeValue(" + boxIfPrimitive(v, ctx) + ")", TypeRef.BOOL); + } + if (n.equals("contains")) { + Out v = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".contains(" + boxIfPrimitive(v, ctx) + ")", TypeRef.BOOL); + } + if (n.equals("indexOf")) { + Out v = emitExpr(pos.get(0), null, ctx); + // indexOf(element, start): the start was dropped, so a scan from an + // offset began again at 0. + String start = pos.size() > 1 ? ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code : ""; + return new Out(target.code + ".indexOfDart(" + boxIfPrimitive(v, ctx) + start + ")", TypeRef.INT); + } + if (n.equals("lastIndexOf")) { + // Was unlowered: it reached Java's lastIndexOf, which compares with the + // wrappers' equals (1 never matched 1.0) and has no start at all. + Out v = emitExpr(pos.get(0), null, ctx); + String start = pos.size() > 1 ? ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code : ""; + return new Out(target.code + ".lastIndexOfDart(" + boxIfPrimitive(v, ctx) + start + ")", TypeRef.INT); + } + if (n.equals("join")) { + String sep = pos.isEmpty() ? "\"\"" : emitExpr(pos.get(0), null, ctx).code; + return new Out(target.code + ".join(" + sep + ")", TypeRef.STRING); + } + if (n.equals("map")) { + Out f = emitExpr(pos.get(0), elementLambda(elem, TypeRef.DYNAMIC), ctx); + return new Out(target.code + ".map(" + f.code + ")", TypeRef.of("Iterable", TypeRef.DYNAMIC)); + } + if (n.equals("where")) { + Out f = emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx); + return new Out(target.code + ".where(" + f.code + ")", TypeRef.of("Iterable", elem)); + } + if (n.equals("forEach")) { + Out f = emitExpr(pos.get(0), elementLambda(elem, TypeRef.VOID), ctx); + return new Out(target.code + ".forEachDart(" + f.code + ")", TypeRef.VOID); + } + if (n.equals("toList")) { + TypeRef listType = TypeRef.of("List", elem); + // toList(growable: false) used to ignore the flag and hand back a growable list. + String growable = null; + for (NamedArg na : c.args.named) { + if (na.name.equals("growable")) { + growable = emitExpr(na.value, TypeRef.BOOL, ctx).code; + } + } + // A Dart List/List variable has Java type Dart{Long,Double}List, but + // the runtime toList() returns a boxed DartList. Re-wrap into the primitive + // list so the value matches its declared/target type. + String pk = primitiveListKind(listType); + if (pk != null) { + ctx.importClass("dart.core.Dart" + pk + "List"); + return new Out("Dart" + pk + "List.from" + pk + "s(" + target.code + ".toList()" + + (growable != null ? ", " + growable : "") + ")", listType); + } + return new Out(target.code + ".toList(" + (growable != null ? growable : "") + ")", listType); + } + if (n.equals("sublist")) { + String args = ""; + for (Expr e : pos) { + args += (args.isEmpty() ? "" : ", ") + + coerce(emitExpr(e, TypeRef.INT, ctx), TypeRef.INT, ctx); + } + return new Out(target.code + ".sublist(" + args + ")", tt); + } + if (n.equals("clear")) { + return new Out(target.code + ".clear()", TypeRef.VOID); + } + if (n.equals("any") || n.equals("every")) { + Out f = emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx); + return new Out(target.code + "." + n + "(" + f.code + ")", TypeRef.BOOL); + } + if (n.equals("fold")) { + Out init = emitExpr(pos.get(0), null, ctx); + // The combiner is (R, E) -> R with R the seed's type, so its result is + // coerced to R: `fold(0.0, (num sum, int e) => sum + e)` computes a num + // and must hand back the double the seed made R. + TypeRef seed = init.type != null && !init.type.is("dynamic") && !init.type.is("var") + ? init.type : null; + TypeRef combineType = null; + if (seed != null) { + combineType = new TypeRef("Function"); + combineType.funcParams = new ArrayList(); + combineType.funcParams.add(seed); + combineType.funcParams.add(elem != null ? elem : TypeRef.DYNAMIC); + combineType.funcReturn = seed; + } + Out combine = emitExpr(pos.get(1), combineType, ctx); + // The generic result R is inferred from the (boxed) seed; unbox it + // back to a primitive when the seed is numeric so it flows straight + // into arithmetic / a primitive-typed return. + TypeRef r = init.type != null && (init.type.is("int") || init.type.is("double")) + ? init.type : TypeRef.DYNAMIC; + String call = target.code + ".fold(" + boxIfPrimitive(init, ctx) + ", " + combine.code + ")"; + return new Out(unboxPrimitiveResult(call, r), r); + } + if (n.equals("firstWhere")) { + Out test = emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx); + String orElse = "null"; + for (NamedArg na : c.args.named) { + if (na.name.equals("orElse")) { + orElse = emitExpr(na.value, null, ctx).code; + } + } + String call = target.code + ".firstWhere(" + test.code + ", " + orElse + ")"; + return new Out(unboxPrimitiveResult(call, elem), elem); + } + if (n.equals("elementAt")) { + String call = target.code + ".elementAt(" + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")"; + return new Out(unboxPrimitiveResult(call, elem), elem); + } + if (n.equals("sort")) { + if (pos.isEmpty()) { + return new Out(target.code + ".sortDefault()", TypeRef.VOID); + } + // DartList inherits java.util.List.sort(Comparator) too, so a bare comparator + // lambda is ambiguous. Pin it to DartList's Func2 overload. + ctx.importClass("dart.runtime.Funcs"); + String et = elem != null ? javaType(boxType(elem), true, ctx) : "Object"; + String cmp = emitExpr(pos.get(0), null, ctx).code; + return new Out(target.code + ".sort((Funcs.Func2<" + et + ", " + et + ", Long>) " + + paren(cmp) + ")", TypeRef.VOID); + } + if (n.equals("indexWhere")) { + String args = emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx).code; + if (pos.size() > 1) { + args += ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code; + } + return new Out(target.code + ".indexWhere(" + args + ")", TypeRef.INT); + } + if (n.equals("lastIndexWhere")) { + String start = pos.size() > 1 ? ", " + emitExpr(pos.get(1), TypeRef.INT, ctx).code : ""; + return new Out(target.code + ".lastIndexWhere(" + + emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx).code + start + ")", + TypeRef.INT); + } + if (n.equals("removeWhere")) { + return new Out(target.code + ".removeWhere(" + + emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx).code + ")", + TypeRef.VOID); + } + if (n.equals("retainWhere")) { + return new Out(target.code + ".retainWhere(" + + emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx).code + ")", + TypeRef.VOID); + } + if (n.equals("lastWhere")) { + Out test = emitExpr(pos.get(0), elementLambda(elem, TypeRef.BOOL), ctx); + String orElse = "null"; + for (NamedArg na : c.args.named) { + if (na.name.equals("orElse")) { + orElse = emitExpr(na.value, null, ctx).code; + } + } + String call = target.code + ".lastWhere(" + test.code + ", " + orElse + ")"; + return new Out(unboxPrimitiveResult(call, elem), elem); + } + if (n.equals("singleWhere")) { + Out test = emitExpr(pos.get(0), null, ctx); + String orElse = "null"; + for (NamedArg na : c.args.named) { + if (na.name.equals("orElse")) { + orElse = emitExpr(na.value, null, ctx).code; + } + } + String call = target.code + ".singleWhere(" + test.code + ", " + orElse + ")"; + return new Out(unboxPrimitiveResult(call, elem), elem); + } + if (n.equals("reduce")) { + String call = target.code + ".reduce(" + emitExpr(pos.get(0), null, ctx).code + ")"; + return new Out(unboxPrimitiveResult(call, elem), elem); + } + if (n.equals("expand")) { + return new Out(target.code + ".expand(" + emitExpr(pos.get(0), null, ctx).code + ")", + TypeRef.of("Iterable", TypeRef.DYNAMIC)); + } + if (n.equals("followedBy")) { + return new Out(target.code + ".followedBy(" + emitExpr(pos.get(0), null, ctx).code + ")", + TypeRef.of("Iterable", elem)); + } + if (n.equals("take") || n.equals("skip")) { + return new Out(target.code + "." + n + "(" + emitExpr(pos.get(0), TypeRef.INT, ctx).code + ")", + TypeRef.of("Iterable", elem)); + } + if (n.equals("getRange")) { + String args = emitExpr(pos.get(0), TypeRef.INT, ctx).code + ", " + + emitExpr(pos.get(1), TypeRef.INT, ctx).code; + return new Out(target.code + ".getRange(" + args + ")", TypeRef.of("Iterable", elem)); + } + if (n.equals("asMap")) { + // The runtime answers a boxed live view (DartMap), never the + // primitive DartLongMap, so a List's view is typed Map: + // the nullable value keeps isPrimitiveLongMap from claiming it, and + // matches what the view's [] returns in Dart anyway. + TypeRef viewValue = elem; + if (elem != null && elem.is("int") && !elem.nullable) { + viewValue = TypeRef.of("int"); + viewValue.nullable = true; + } + return new Out(target.code + ".asMap()", TypeRef.of("Map", TypeRef.INT, viewValue)); + } + if (n.equals("toSet")) { + return new Out(target.code + ".toSet()", TypeRef.of("Set", elem)); + } + if (tt.is("Set") && n.equals("difference")) { + return new Out(target.code + ".difference(" + emitExpr(pos.get(0), null, ctx).code + ")", tt); + } + if (tt.is("Set") && n.equals("intersection")) { + return new Out(target.code + ".intersection(" + emitExpr(pos.get(0), null, ctx).code + ")", tt); + } + if (tt.is("Set") && n.equals("union")) { + return new Out(target.code + ".union(" + emitExpr(pos.get(0), null, ctx).code + ")", tt); + } + if (tt.is("Set") && n.equals("containsAll")) { + return new Out(target.code + ".containsAll(" + emitExpr(pos.get(0), null, ctx).code + ")", + TypeRef.BOOL); + } + } + if (tt.is("Map")) { + if (n.equals("containsKey")) { + Out v = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".containsKey(" + boxIfPrimitive(v, ctx) + ")", TypeRef.BOOL); + } + if (n.equals("containsValue")) { + // Boxed through Object so a primitive argument picks containsValue(Object), + // which compares with Dart's ==; unresolved before. + Out v = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".containsValue((Object) " + paren(v.code) + ")", TypeRef.BOOL); + } + if (n.equals("remove")) { + Out v = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".removeDart(" + boxIfPrimitive(v, ctx) + ")", boxType(tt.arg(1))); + } + if (n.equals("forEach")) { + Out f = emitExpr(pos.get(0), null, ctx); + return new Out(target.code + ".forEachDart(" + f.code + ")", TypeRef.VOID); + } + if (n.equals("putIfAbsent")) { + Out k = emitExpr(pos.get(0), null, ctx); + Out f = emitExpr(pos.get(1), null, ctx); + return new Out(target.code + ".putIfAbsentDart(" + boxIfPrimitive(k, ctx) + ", " + f.code + ")", + boxType(tt.arg(1))); + } + if (n.equals("addAll")) { + return new Out(target.code + ".addAll(" + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.VOID); + } + if (n.equals("addEntries")) { + return new Out(target.code + ".addEntries(" + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.VOID); + } + if (n.equals("removeWhere")) { + return new Out(target.code + ".removeWhere(" + emitExpr(pos.get(0), null, ctx).code + ")", TypeRef.VOID); + } + if (n.equals("update")) { + Out k = emitExpr(pos.get(0), null, ctx); + Out upd = emitExpr(pos.get(1), null, ctx); + String ifAbsent = "null"; + for (NamedArg na : c.args.named) { + if (na.name.equals("ifAbsent")) { + ifAbsent = emitExpr(na.value, null, ctx).code; + } + } + return new Out(target.code + ".update(" + boxIfPrimitive(k, ctx) + ", " + upd.code + + ", " + ifAbsent + ")", boxType(tt.arg(1))); + } + if (n.equals("clear")) { + return new Out(target.code + ".clear()", TypeRef.VOID); + } + } + if (tt.is("Future")) { + ctx.importClass("dart.async.Future"); + if (n.equals("then")) { + Out cb = emitExpr(pos.get(0), null, ctx); + // A lambda fits BOTH then overloads (Func1 / VoidFunc1) when it is void- or + // throw-bodied, which javac reports as ambiguous; pin those to VoidFunc1. + String cbCode = pinnedHandler(pos.get(0), cb, false, "dart.runtime.Funcs.Func1", + "dart.runtime.Funcs.VoidFunc1"); + // then(onValue, onError: handler): the handler was dropped, so a failure + // skipped it and the chain stayed failed. + for (NamedArg na : c.args.named) { + if (na.name.equals("onError")) { + String handler = pinnedHandler(na.value, emitExpr(na.value, null, ctx), true, + "dart.runtime.Funcs.Func1", "dart.runtime.Funcs.VoidFunc1"); + return new Out(target.code + ".then(" + cbCode + ", (Object) " + paren(handler) + ")", + TypeRef.of("Future", TypeRef.DYNAMIC)); + } + } + return new Out(target.code + ".then(" + cbCode + ")", TypeRef.of("Future", TypeRef.DYNAMIC)); + } + if (n.equals("catchError")) { + Out cb = emitExpr(pos.get(0), null, ctx); + String cbCode = pinnedHandler(pos.get(0), cb, false, "dart.runtime.Funcs.Func1", + "dart.runtime.Funcs.VoidFunc1"); + boolean valueHandler = pos.get(0) instanceof Lambda + && (((Lambda) pos.get(0)).exprBody != null ? !lastLambdaVoid : lambdaReturnsValue(pos.get(0))); + // `test` is a NAMED parameter in Dart; reading it positionally meant it was + // always null, so the handler caught failures its predicate rejected. The + // runtime takes it as an Object, so a lambda is given its predicate type. + String test = "null"; + Expr testExpr = pos.size() > 1 ? pos.get(1) : null; + for (NamedArg na : c.args.named) { + if (na.name.equals("test")) { + testExpr = na.value; + } + } + if (testExpr != null) { + test = pinnedHandler(testExpr, emitExpr(testExpr, null, ctx), true, + "dart.runtime.Funcs.Func1", "dart.runtime.Funcs.Func1"); + } + // A handler that returns a value recovers with it, so the runtime answers + // a Future: typed that way, an await of it coerces to the slot. + return new Out(target.code + ".catchError(" + cbCode + ", " + test + ")", + valueHandler ? TypeRef.of("Future", TypeRef.DYNAMIC) : tt); + } + if (n.equals("whenComplete")) { + // A cleanup that can produce a value -- `() => asyncCleanup()`, or a tear-off + // of a function returning one -- goes to whenCompleteFuture, which waits for + // a returned Future and surfaces its failure. The void form discarded it. + Expr action = pos.get(0); + Out cb = emitExpr(action, null, ctx); + boolean value; + if (action instanceof Lambda) { + value = ((Lambda) action).exprBody != null ? !lastLambdaVoid : lambdaReturnsValue(action); + } else { + // A tear-off is typed a bare Function, so its declared return is read + // from the method or function it names; unresolved stays on the void form. + TypeRef ret = cb.type != null ? cb.type.funcReturn : null; + if (ret == null) { + ret = tearOffReturn(action, ctx); + } + value = ret != null && !ret.is("void"); + } + if (value) { + String fn = action instanceof Lambda + ? "(dart.runtime.Funcs.Func0) " + paren(cb.code) : cb.code; + return new Out(target.code + ".whenCompleteFuture(" + fn + ")", tt); + } + return new Out(target.code + ".whenComplete(" + cb.code + ")", tt); + } + } + return null; + } + + // ------------------------------------------------------------------ + // Constructor calls + // ------------------------------------------------------------------ + + /** + * Wraps a boxed generic-collection result (e.g. {@code Iterable.fold}/{@code firstWhere}) + * in an unboxing cast when the Dart element/result type is a primitive {@code int}/{@code double}, + * so the value can flow into primitive arithmetic and primitive-typed returns. The intermediate + * {@code (Long)}/{@code (Double)} cast keeps it valid even when the static type is erased to Object. + */ + private static String unboxPrimitiveResult(String code, TypeRef t) { + if (t != null && t.is("int")) { + return "((long)(Long) (" + code + "))"; + } + if (t != null && t.is("double")) { + return "((double)(Double) (" + code + "))"; + } + return code; + } + + /** + * Emits a Dart {@code List} factory constructor ({@code generate}, {@code filled} + * or {@code from}). Non-nullable {@code int}/{@code double} element types route to the + * primitive {@link dart.core.DartLongList}/{@link dart.core.DartDoubleList} so the + * result stays index/add consistent with how {@code List}/{@code List} + * variables are typed elsewhere; everything else uses the boxed {@link dart.core.DartList}. + */ + private Out emitListFactory(CtorCall cc, Ctx ctx) { + TypeRef listType = cc.type; + TypeRef elem = listType.args.isEmpty() ? TypeRef.DYNAMIC : listType.arg(0); + String pk = primitiveListKind(listType); + String cls; + if (pk != null) { + ctx.importClass("dart.core.Dart" + pk + "List"); + cls = "Dart" + pk + "List"; + } else { + ctx.importClass("dart.core.DartList"); + cls = "DartList"; + } + // Explicit type witness only for the generic boxed list; primitive lists are raw. + String witness = pk == null ? ".<" + javaType(elem, true, ctx) + ">" : "."; + // The primitive lists name generate/from with a *Longs/*Doubles suffix (their + // unsuffixed forms would erasure-clash with the inherited DartList statics); + // filled keeps its name since its primitive signature has a distinct erasure. + String sfx = pk == null ? "" : ("Long".equals(pk) ? "Longs" : "Doubles"); + List pos = cc.args.positional; + String growable = null; + for (NamedArg na : cc.args.named) { + if (na.name.equals("growable")) { + growable = emitExpr(na.value, TypeRef.BOOL, ctx).code; + } + } + if (cc.ctorName.equals("unmodifiable")) { + // A read-only copy; unresolved before, so any use failed the build. + String src = emitExpr(pos.get(0), null, ctx).code; + return new Out(cls + witness + (pk == null ? "unmodifiable" : "unmodifiable" + sfx) + "(" + src + ")", + listType); + } + if (cc.ctorName.equals("from")) { + String src = emitExpr(pos.get(0), null, ctx).code; + // Each element is checked against a concrete element type, as Dart does + // (the primitive lists check their own); dynamic, Object and type variables + // accept anything and keep the plain copy. + if (pk == null && elem != null && isConcreteType(elem) && !isDynamicType(elem)) { + String jt = javaType(copyNonNull(elem), true, ctx); + int lt = jt.indexOf('<'); + String raw = lt >= 0 ? jt.substring(0, lt) : jt; + return new Out(cls + witness + "fromChecked(" + src + ", " + raw + ".class, " + elem.nullable + + ", " + (growable != null ? growable : "true") + ")", listType); + } + // growable: false was parsed above and then dropped for from(). + return new Out(cls + witness + "from" + sfx + "(" + src + + (growable != null ? ", " + growable : "") + ")", listType); + } + String len = emitExpr(pos.get(0), TypeRef.INT, ctx).code; + if (cc.ctorName.equals("filled")) { + Out fillOut = emitExpr(pos.get(1), elem, ctx); + String fill = pk == null ? boxIfPrimitive(fillOut, ctx) : coerce(fillOut, elem, ctx); + String args = len + ", " + fill + (growable != null ? ", " + growable : ""); + return new Out(cls + witness + "filled(" + args + ")", listType); + } + // generate: the generator's index parameter is typed via the IndexedGenerator typedef + String gen = emitExpr(pos.get(1), new TypeRef("IndexedGenerator"), ctx).code; + String args = len + ", " + gen + (growable != null ? ", " + growable : ""); + return new Out(cls + witness + "generate" + sfx + "(" + args + ")", listType); + } + + /** + * dart:core Map/Set/Iterable named factory constructors, routed to the existing + * statics on {@code DartMap}/{@code DartSet}/{@code DartIterable}. Returns null + * when {@code cc} is not one of these core collection factories so the caller can + * fall through to program-class / stub resolution. + * + *

Semantics match dart:core: {@code Map.of}/{@code Map.from} shallow-copy the + * source map, {@code Map.fromIterable} applies optional {@code key}/{@code value} + * transforms (element identity when a transform is absent), {@code Map.fromEntries} + * copies key/value pairs, {@code Set.of}/{@code Set.from} copy an iterable, and + * {@code Iterable.generate(count, [generator])} builds a lazy index sequence. + */ + /** + * dart:async {@code Future} named constructors used in ctor position + * ({@code Future.delayed} / {@code Future.value} / {@code Future.error}), + * routed to the {@link dart.async.Future} statics. Returns null for an + * unrecognized name so the caller can fall through to the E0126 diagnostic. + */ + private Out emitFutureNamedCtor(CtorCall cc, Ctx ctx) { + ctx.importClass("dart.async.Future"); + String ctor = cc.ctorName; + List pos = cc.args.positional; + if (ctor.equals("delayed")) { + String dur = emitExpr(pos.get(0), new TypeRef("Duration"), ctx).code; + String comp = pos.size() > 1 ? emitExpr(pos.get(1), null, ctx).code : null; + return new Out("Future.delayed(" + dur + (comp != null ? ", " + comp : "") + ")", + TypeRef.of("Future", TypeRef.DYNAMIC)); + } + if (ctor.equals("value")) { + Out v = pos.isEmpty() ? new Out("null", TypeRef.NULL) : emitExpr(pos.get(0), null, ctx); + return new Out("Future.value(" + boxIfPrimitive(v, ctx) + ")", + TypeRef.of("Future", v.type)); + } + if (ctor.equals("error")) { + return new Out("Future.error(" + emitExpr(pos.get(0), null, ctx).code + ")", + TypeRef.of("Future", TypeRef.DYNAMIC)); + } + return null; + } + + private Out emitCoreCollectionFactory(CtorCall cc, Ctx ctx) { + String name = cc.type.name; + String ctor = cc.ctorName; + List pos = cc.args.positional; + if (name.equals("Map")) { + ctx.importClass("dart.core.DartMap"); + TypeRef kt = cc.type.args.isEmpty() ? TypeRef.DYNAMIC : cc.type.arg(0); + TypeRef vt = cc.type.args.size() < 2 ? TypeRef.DYNAMIC : cc.type.arg(1); + String witness = ".<" + javaType(kt, true, ctx) + ", " + javaType(vt, true, ctx) + ">"; + if (ctor.equals("of") || ctor.equals("from")) { + String src = emitExpr(pos.get(0), null, ctx).code; + if (isPrimitiveLongMap(cc.type)) { + ctx.importClass("dart.core.DartLongMap"); + return new Out("DartLongMap.from(" + src + ")", cc.type); + } + return new Out("DartMap" + witness + "from(" + src + ")", cc.type); + } + if (ctor.equals("identity")) { + return new Out("DartMap" + witness + "identity()", cc.type); + } + if (ctor.equals("fromEntries")) { + String src = emitExpr(pos.get(0), null, ctx).code; + return new Out("DartMap" + witness + "fromEntries(" + src + ")", cc.type); + } + if (ctor.equals("fromIterable")) { + Out iterO = emitExpr(pos.get(0), null, ctx); + String iter = iterO.code; + // The key/value transforms take one element of the iterable; type their lambda + // param from the element type so member access on it resolves. + TypeRef elem = iterO.type != null && !iterO.type.args.isEmpty() + ? iterO.type.arg(0) : TypeRef.DYNAMIC; + TypeRef keyFn = inlineFuncType(elem, kt); + TypeRef valFn = inlineFuncType(elem, vt); + String key = "null"; + String val = "null"; + for (NamedArg na : cc.args.named) { + if (na.name.equals("key")) { + key = emitExpr(na.value, keyFn, ctx).code; + } else if (na.name.equals("value")) { + val = emitExpr(na.value, valFn, ctx).code; + } + } + // fromIterable is generic in ; emit all three explicitly (a null key/value + // otherwise leaves K/V uninferable). + String fiWitness = ".<" + javaType(elem, true, ctx) + ", " + javaType(kt, true, ctx) + + ", " + javaType(vt, true, ctx) + ">"; + return new Out("DartMap" + fiWitness + "fromIterable(" + iter + ", " + key + ", " + val + ")", cc.type); + } + return null; + } + if (name.equals("Set")) { + ctx.importClass("dart.core.DartSet"); + TypeRef et = cc.type.args.isEmpty() ? TypeRef.DYNAMIC : cc.type.arg(0); + String witness = ".<" + javaType(et, true, ctx) + ">"; + if (ctor.equals("identity")) { + return new Out("DartSet" + witness + "identity()", cc.type); + } + if (ctor.equals("of") || ctor.equals("from")) { + String src = emitExpr(pos.get(0), null, ctx).code; + return new Out("DartSet" + witness + "from(" + src + ")", cc.type); + } + return null; + } + if (name.equals("Iterable")) { + if (ctor.equals("generate")) { + ctx.importClass("dart.core.DartIterable"); + TypeRef et = cc.type.args.isEmpty() ? TypeRef.DYNAMIC : cc.type.arg(0); + String witness = ".<" + javaType(et, true, ctx) + ">"; + String count = emitExpr(pos.get(0), TypeRef.INT, ctx).code; + if (pos.size() > 1) { + String gen = emitExpr(pos.get(1), new TypeRef("IndexedGenerator"), ctx).code; + return new Out("DartIterable" + witness + "generate(" + count + ", " + gen + ")", cc.type); + } + return new Out("DartIterable" + witness + "generate(" + count + ")", cc.type); + } + return null; + } + return null; + } + + /** Duration(days:..,hours:..,..) -> Duration.of(...) with canonical named order. */ + private Out emitDurationOf(Args args, Ctx ctx) { + ctx.importClass("dart.core.Duration"); + String[] names = {"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}; + StringBuilder sb = new StringBuilder("Duration.of("); + for (int i = 0; i < names.length; i++) { + if (i > 0) { + sb.append(", "); + } + Expr match = null; + for (NamedArg na : args.named) { + if (na.name.equals(names[i])) { + match = na.value; + break; + } + } + sb.append(match == null ? "0L" : emitExpr(match, TypeRef.INT, ctx).code); + } + sb.append(')'); + return new Out(sb.toString(), new TypeRef("Duration")); + } + + private Out emitCtorCall(String className, Args args, Node posNode, Ctx ctx) { + return emitCtorCall(className, java.util.Collections.emptyList(), args, posNode, ctx); + } + + /** + * Widgets that look a model up BY TYPE, and so need the Dart type argument at runtime. + * + *

Java erases {@code Selector}, so the runtime cannot recover {@code A} and used + * to ask the provider chain for the nearest value of ANY type. With more than one model + * in scope that is right only by luck: Reply has its localizations and its EmailStore + * above the same Selector, took the localizations, and failed — visibly as a cast error + * on the desktop, and on iOS as a wrong object that flowed on until an unrelated switch + * matched nothing and the page came up blank.

+ */ + private static boolean readsProvidedValueByType(String className) { + return className.equals("Selector") || className.equals("Consumer"); + } + + /** + * Emits {@code tmp.providedType(A.class)} for those widgets. + * + *

Only for a plain class: a generic or dynamic argument has no class literal, and the + * runtime's Object default (nearest provider) remains — no worse than before.

+ */ + private void emitProvidedTypeToken(String tmp, String className, List typeArgs, + Ctx ctx) { + if (!readsProvidedValueByType(className) && !publishesValueByType(className)) { + return; + } + String token = providedTypeToken(typeArgs, ctx); + if (token != null) { + ctx.writer().line(tmp + ".providedType(" + token + ");"); + } + } + + /** + * Providers that PUBLISH by type. Given their {@code T}, one holding null still + * matches a lookup for T, so the reader subscribes and rebuilds when the value + * arrives; without it the lookup went past the provider as if it were not there. + */ + private static boolean publishesValueByType(String className) { + return className.equals("Provider") || className.equals("ChangeNotifierProvider"); + } + + /** {@code A.class} for the first type argument when it is a plain class, else null. */ + private String providedTypeToken(List typeArgs, Ctx ctx) { + if (typeArgs == null || typeArgs.isEmpty() || typeArgs.get(0) == null) { + return null; + } + TypeRef a = typeArgs.get(0); + if (isTypeVariable(a, ctx)) { + return null; + } + String javaName = javaType(a, true, ctx); + if (javaName == null || javaName.indexOf('<') >= 0 || javaName.equals("Object")) { + return null; + } + return javaName + ".class"; + } + + private Out emitCtorCall(String className, List typeArgs, Args args, Node posNode, Ctx ctx) { + // dart:core intrinsics whose Java stub has no matching named-arg constructor: + // route to the canonical factory rather than the generic allocate-then-setters path. + if (className.equals("Duration")) { + return emitDurationOf(args, ctx); + } + if (className.equals("Stopwatch") && args.positional.isEmpty() && args.named.isEmpty()) { + ctx.importClass("dart.core.Stopwatch"); + return new Out("new Stopwatch()", new TypeRef("Stopwatch")); + } + // Resolve against the referencing library's imports (not the flat simple-name map), + // so a name shared by several files (`Backdrop` in pages/ vs studies/shrine/) binds to + // the one this library actually imports. + ClassDecl pc = program.resolveClass(className, ctx.library()); + // An app class may share a name with a stub type (new_gallery's routes.dart `Path` + // vs dart:ui `Path`). Prefer the app class only when it is actually visible to the + // current library (declared or imported); otherwise fall through to the stub. + if (pc != null && stubs.isStubClass(className) && !classVisibleFrom(pc, ctx.library())) { + pc = null; + } + if (pc != null) { + String jn = javaClassName(pc); + CtorDecl ct = pc.defaultCtor(); + if (ct != null && ct.isFactory) { + return new Out(jn + ".$create(" + canonicalArgs(ct, pc, args, ctx) + ")", + new TypeRef(className)); + } + // A generic program class constructed raw (`new Foo(...)`) erases the generics on ALL + // its members — a constructor param typed Func1 becomes raw + // Func1, so a lambda argument gets Object params. A diamond keeps the signatures. + String diamond = pc.typeParams.isEmpty() ? "" : "<>"; + return new Out("new " + jn + diamond + "(" + canonicalArgs(ct, pc, args, ctx) + ")", + new TypeRef(className)); + } + Ast.ClassDecl sc = stubs.classes.get(className); + if (sc == null) { + diags.error(posNode, "E0135", "Cannot resolve constructor '" + className + "'"); + return new Out("null", TypeRef.DYNAMIC, true); + } + String simple = stubSimpleName(className, ctx); + // A generic stub class constructed raw (`new Foo()`) erases the generics on ALL its + // instance members — including builder/callback setters whose SAM types don't even + // mention the type variable — so a lambda passed to such a setter gets Object params. + // Emit an explicit type witness (diamond) to keep those signatures intact, filling any + // missing Dart type argument with Object. For the provider/scoped_model builders we + // also thread the argument into the setter param types so the lambda BODY resolves the + // model's members (the deferred "constructor-type-argument threading", scoped here). + Map typeSubst = null; + String diamond = ""; + List resultArgs = new ArrayList(); + if (!sc.typeParams.isEmpty()) { + if (!typeArgs.isEmpty()) { + typeSubst = new LinkedHashMap(); + } + StringBuilder d = new StringBuilder("<"); + for (int i = 0; i < sc.typeParams.size(); i++) { + TypeRef arg = i < typeArgs.size() ? typeArgs.get(i) : null; + if (typeSubst != null && arg != null) { + typeSubst.put(sc.typeParams.get(i), arg); + } + if (i > 0) { + d.append(", "); + } + d.append(arg != null ? javaType(arg, true, ctx) : "Object"); + resultArgs.add(arg != null ? arg : TypeRef.DYNAMIC); + } + d.append('>'); + diamond = d.toString(); + } + // The instance type carries the (diamond-filled) type arguments so a later assignment into + // a differently-parameterized target can bridge the Dart-covariance/Java-invariance gap. + TypeRef stubResultType = resultArgs.isEmpty() + ? new TypeRef(className) + : TypeRef.of(className, resultArgs.toArray(new TypeRef[0])); + Ast.CtorDecl ct = sc.defaultCtor(); + // positional args -> Java constructor arguments + StringBuilder posArgs = new StringBuilder(); + List positionalParams = new ArrayList(); + List namedParams = new ArrayList(); + if (ct != null) { + for (Ast.Param p : ct.params) { + if (p.named) { + namedParams.add(p); + } else { + positionalParams.add(p); + } + } + } + for (int i = 0; i < args.positional.size(); i++) { + TypeRef pt = i < positionalParams.size() ? positionalParams.get(i).type : null; + // Substitute the class's type parameters (e.g. AlwaysStoppedAnimation's T) + // so a numeric literal argument coerces to the instantiated element type. + if (typeSubst != null && pt != null) { + pt = substituteTypeParams(pt, typeSubst); + } + Out o = emitExpr(args.positional.get(i), pt, ctx); + if (i > 0) { + posArgs.append(", "); + } + posArgs.append(coerce(o, pt, ctx)); + } + // Fill omitted optional positional params (Dart's `[int month, int day, ...]`) so the + // call matches the Java constructor, which takes them all (e.g. DateTime(year) needs + // month/day/... supplied). Use the declared default, else the type's zero value. + for (int i = args.positional.size(); i < positionalParams.size(); i++) { + Ast.Param p = positionalParams.get(i); + if (i > 0) { + posArgs.append(", "); + } + if (p.defaultValue != null) { + posArgs.append(coerce(emitExpr(p.defaultValue, p.type, ctx), p.type, ctx)); + } else { + posArgs.append(zeroValue(p.type)); + } + } + if (args.named.isEmpty()) { + return new Out("new " + simple + diamond + "(" + posArgs + ")", stubResultType); + } + // allocate-then-setters (ANF) + String tmp = ctx.newTemp(); + ctx.writer().line("var " + tmp + " = new " + simple + diamond + "(" + posArgs + ");"); + emitProvidedTypeToken(tmp, className, typeArgs, ctx); + for (NamedArg na : args.named) { + TypeRef pt = null; + for (Ast.Param p : namedParams) { + if (p.name.equals(na.name)) { + pt = p.type; + break; + } + } + if (pt == null) { + // inherited named param (e.g. key) — look up stub super chain + Ast.ClassDecl cur = sc; + outer: + while (cur != null && cur.superclass != null) { + cur = stubs.classes.get(cur.superclass.name); + if (cur == null) { + break; + } + Ast.CtorDecl sct = cur.defaultCtor(); + if (sct != null) { + for (Ast.Param p : sct.params) { + if (p.named && p.name.equals(na.name)) { + pt = p.type; + break outer; + } + } + } + } + } + if (typeSubst != null && pt != null) { + pt = substituteTypeParams(pt, typeSubst); + } + Out v = emitExpr(na.value, pt, ctx); + ctx.writer().line(tmp + "." + javaMethodName(na.name) + "(" + coerce(v, pt, ctx) + ");"); + } + return new Out(tmp, stubResultType); + } + + /** Finds a Dart {@code set name(v)} declared on an app class or its app superclasses. */ + private Ast.MethodDecl findAppSetter(ClassDecl c, String name) { + while (c != null) { + for (Ast.MethodDecl m : c.methods) { + if (m.isSetter && m.name.equals(name)) { + return m; + } + } + c = c.superclass != null ? program.classes.get(c.superclass.name) : null; + } + return null; + } + + /** Whether an app class is declared in, or imported by, the given library. */ + private boolean classVisibleFrom(ClassDecl pc, Library from) { + if (from == null || pc.ownerLibrary == null) { + return true; // no library context: keep the historical (program-preferred) behavior + } + if (pc.ownerLibrary == from) { + return true; + } + for (String uri : from.imports) { + if (program.resolveImportedLibrary(from, uri) == pc.ownerLibrary) { + return true; + } + } + return false; + } + + /** Recursively replaces type-parameter names (e.g. {@code T}, {@code A}) with concrete args. */ + private TypeRef substituteTypeParams(TypeRef t, Map subst) { + if (t == null) { + return null; + } + if (t.funcParams == null && subst.containsKey(t.name) && t.args.isEmpty()) { + return subst.get(t.name); + } + TypeRef out = new TypeRef(t.name); + out.nullable = t.nullable; + for (TypeRef a : t.args) { + out.args.add(substituteTypeParams(a, subst)); + } + if (t.funcParams != null) { + out.funcParams = new ArrayList(); + for (TypeRef p : t.funcParams) { + out.funcParams.add(substituteTypeParams(p, subst)); + } + out.funcReturn = substituteTypeParams(t.funcReturn, subst); + } + return out; + } + + /** Program-class calls use canonical positional order with defaults inlined. */ + private String canonicalArgs(CtorDecl ct, Args args, Ctx ctx) { + return canonicalArgs(ct, null, args, ctx); + } + + private String canonicalArgs(CtorDecl ct, ClassDecl owner, Args args, Ctx ctx) { + StringBuilder sb = new StringBuilder(); + if (ct == null) { + for (int i = 0; i < args.positional.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(emitExpr(args.positional.get(i), null, ctx).code); + } + return sb.toString(); + } + String reordered = sourceOrderedArgs(ct, owner, args, ctx); + if (reordered != null) { + return reordered; + } + int posIdx = 0; + boolean first = true; + for (Param p : ct.params) { + if (!first) { + sb.append(", "); + } + first = false; + TypeRef pt = paramType(owner, p, ctx); + if (!p.named) { + if (posIdx < args.positional.size()) { + Out o = emitExpr(args.positional.get(posIdx++), pt, ctx); + sb.append(coerce(o, pt, ctx)); + } else if (p.defaultValue != null) { + sb.append(coerce(emitExpr(p.defaultValue, pt, ctx), pt, ctx)); + } else { + sb.append(zeroValue(pt)); + } + } else { + NamedArg match = null; + for (NamedArg na : args.named) { + if (na.name.equals(p.name)) { + match = na; + break; + } + } + if (match != null) { + Out o = emitExpr(match.value, pt, ctx); + sb.append(coerce(o, pt, ctx)); + } else if (p.defaultValue != null) { + sb.append(coerce(emitExpr(p.defaultValue, pt, ctx), pt, ctx)); + } else { + sb.append(zeroValue(pt)); + } + } + } + reportUnknownNamedArgs(ct, owner, args); + return sb.toString(); + } + + /** Set while emitting a super(...) initializer, where no statement may come first. */ + private boolean inSuperInitializer; + + /** + * Whether evaluating {@code e} can have an effect another argument could observe: + * an assignment, an increment, an await, a cascade, any call -- a function, a + * method on this object or on a value ({@code counter.next()}), a static method, a + * constructor that is not {@code const} -- a getter on a value, and an index + * operator. Any of those runs code a user class can give effects, and nothing here + * knows which ones are pure: {@code Pair(b: counter.next(), a: counter.next())} + * gave a and b each other's values when calls on a value were assumed pure. + * + *

A {@code const} constructor, a read through a class name + * ({@code ReplyColors.white50}, {@code Axis.vertical}), literals and operators + * over them are free. Treating every call as an effect costs little: sequencing + * only happens for a program class's constructor or method whose order-sensitive + * arguments are written out of parameter order, and then costs one temp per + * argument -- a fraction of a percent of the gallery's compiled classes.

+ */ + private static boolean hasEffect(Expr e) { + if (e == null || e instanceof IntLit || e instanceof DoubleLit || e instanceof BoolLit + || e instanceof NullLit || e instanceof Ident || e instanceof ThisExpr + || e instanceof SuperExpr || e instanceof Lambda) { + return false; + } + if (e instanceof Assign || e instanceof IncDec || e instanceof AwaitExpr || e instanceof Cascade) { + return true; + } + if (e instanceof StringLit) { + for (Object part : ((StringLit) e).parts) { + if (part instanceof Expr && hasEffect((Expr) part)) { + return true; + } + } + return false; + } + if (e instanceof PropertyGet) { + Expr target = ((PropertyGet) e).target; + if (isClassReference(target)) { + return false; + } + return true; + } + if (e instanceof IndexGet) { + // operator [] is a method call, and a user class can give it effects. + return true; + } + if (e instanceof ParenExpr) { + return hasEffect(((ParenExpr) e).inner); + } + if (e instanceof NotNullAssert) { + return hasEffect(((NotNullAssert) e).operand); + } + if (e instanceof Unary) { + return hasEffect(((Unary) e).operand); + } + if (e instanceof Binary) { + return hasEffect(((Binary) e).left) || hasEffect(((Binary) e).right); + } + if (e instanceof Conditional) { + Conditional c = (Conditional) e; + return hasEffect(c.condition) || hasEffect(c.thenExpr) || hasEffect(c.elseExpr); + } + if (e instanceof CtorCall) { + CtorCall cc = (CtorCall) e; + return !cc.isConst || argsHaveEffect(cc.args); + } + if (e instanceof Call) { + return true; + } + if (e instanceof ListLit) { + for (Expr x : ((ListLit) e).elements) { + if (hasEffect(x)) { + return true; + } + } + return false; + } + // Anything else is assumed to be able to: sequencing an argument that did + // not need it costs a temp, while skipping one that did reorders effects. + return true; + } + + /** A bare class name used as a receiver: {@code ReplyColors.white50}, {@code Axis.vertical}. */ + private static boolean isClassReference(Expr e) { + if (!(e instanceof Ident)) { + return false; + } + String n = ((Ident) e).name; + return n != null && !n.isEmpty() && Character.isUpperCase(n.charAt(0)); + } + + private static boolean argsHaveEffect(Args args) { + for (Expr x : args.positional) { + if (hasEffect(x)) { + return true; + } + } + for (NamedArg na : args.named) { + if (hasEffect(na.value)) { + return true; + } + } + return false; + } + + /** + * An argument whose position relative to an effect matters: one that has an + * effect, or one that reads a variable, which an effect can change -- + * {@code f(b: x, a: log())} must read x before log() runs. Constants, reads through + * a class ({@code ReplyColors.white50}) and closures are left in place. + */ + private static boolean orderSensitive(Expr e) { + return hasEffect(e) || readsVariable(e); + } + + /** + * Whether {@code e} reads a variable anywhere in it, so that an effect evaluated + * before it could change its value: {@code f(b: n + 1, a: bump())} must read n + * before bump() runs, exactly as {@code f(b: n, a: bump())} must. A class name + * used as a receiver is not a variable, and a closure reads nothing until it runs. + */ + private static boolean readsVariable(Expr e) { + if (e == null || e instanceof Lambda) { + return false; + } + if (e instanceof Ident) { + return !isClassReference(e); + } + if (e instanceof StringLit) { + for (Object part : ((StringLit) e).parts) { + if (part instanceof Expr && readsVariable((Expr) part)) { + return true; + } + } + return false; + } + if (e instanceof ParenExpr) { + return readsVariable(((ParenExpr) e).inner); + } + if (e instanceof NotNullAssert) { + return readsVariable(((NotNullAssert) e).operand); + } + if (e instanceof Unary) { + return readsVariable(((Unary) e).operand); + } + if (e instanceof Binary) { + return readsVariable(((Binary) e).left) || readsVariable(((Binary) e).right); + } + if (e instanceof Conditional) { + Conditional c = (Conditional) e; + return readsVariable(c.condition) || readsVariable(c.thenExpr) || readsVariable(c.elseExpr); + } + if (e instanceof ListLit) { + for (Expr x : ((ListLit) e).elements) { + if (readsVariable(x)) { + return true; + } + } + return false; + } + if (e instanceof CtorCall) { + Args args = ((CtorCall) e).args; + for (Expr x : args.positional) { + if (readsVariable(x)) { + return true; + } + } + for (NamedArg na : args.named) { + if (readsVariable(na.value)) { + return true; + } + } + return false; + } + return false; + } + + private static boolean sourceBefore(Expr a, Expr b) { + return a.line < b.line || (a.line == b.line && a.col < b.col); + } + + /** + * The argument list of a program-class call whose arguments must be evaluated in a + * different order from the parameters they fill, or null when the plain + * canonical-order emission is already faithful. + * + *

Dart evaluates arguments left to right as written, and named arguments may be + * written in any order: {@code f(b: log('b'), a: log('a'))} logs b first. Emitting + * them in the callee's parameter order ran whichever parameter is declared first. + * That only matters when an argument has an effect ({@link #hasEffect}) and the + * written order of the {@linkplain #orderSensitive order-sensitive} arguments + * differs from their parameter order; then each of those is assigned to a temp, in + * source order, inside + * the first slot that holds one: {@code f((DartRuntime.seq($t1 = log('b'), + * DartRuntime.seq($t2 = log('a'), true)) ? $t2 : $t2), $t1)}. Java evaluates + * arguments left to right, and a conditional over the same temp keeps that slot's + * exact type. Only the temps' DECLARATIONS are lifted into statements; lifting the + * evaluation would run an argument even on a branch not taken, as in + * {@code x == null ? null : Foo(b: x.bar(), a: 1)}.

+ * + *

Not applied inside a super(...) initializer, where no declaration may precede + * the call, nor to an argument whose type a temp cannot name (a function value, or + * one javac infers); those stay in place.

+ */ + private String sourceOrderedArgs(CtorDecl ct, ClassDecl owner, Args args, Ctx ctx) { + if (args.named.isEmpty() || inSuperInitializer) { + return null; + } + // Which parameter slot each call-site argument fills. + Map slotOf = new java.util.IdentityHashMap(); + int posIdx = 0; + boolean anyEffect = false; + for (int i = 0; i < ct.params.size(); i++) { + Param p = ct.params.get(i); + Expr supplied = null; + if (!p.named) { + if (posIdx < args.positional.size()) { + supplied = args.positional.get(posIdx++); + } + } else { + for (NamedArg na : args.named) { + if (na.name.equals(p.name)) { + supplied = na.value; + break; + } + } + } + if (supplied != null) { + slotOf.put(supplied, Integer.valueOf(i)); + anyEffect |= hasEffect(supplied); + } + } + if (!anyEffect) { + return null; + } + List ordered = new ArrayList(); + for (Expr e : slotOf.keySet()) { + if (orderSensitive(e)) { + ordered.add(e); + } + } + java.util.Collections.sort(ordered, new java.util.Comparator() { + @Override + public int compare(Expr a, Expr b) { + return sourceBefore(a, b) ? -1 : sourceBefore(b, a) ? 1 : 0; + } + }); + boolean inOrder = true; + for (int i = 1; i < ordered.size(); i++) { + if (slotOf.get(ordered.get(i - 1)).intValue() > slotOf.get(ordered.get(i)).intValue()) { + inOrder = false; + break; + } + } + if (inOrder) { + return null; + } + String[] slotCode = new String[ct.params.size()]; + List assigns = new ArrayList(); + int anchor = -1; + String anchorTemp = null; + TypeRef anchorType = null; + for (Expr e : ordered) { + int slot = slotOf.get(e).intValue(); + TypeRef pt = paramType(owner, ct.params.get(slot), ctx); + Out o = emitExpr(e, pt, ctx); + TypeRef at = o.type; + String jt = at == null || at.is("var") || at.funcParams != null || at.is("Function") + ? null : javaType(at, false, ctx); + if (jt == null || (jt.equals("Object") && !isDynamicType(pt))) { + // No temp can hold this value with the type the slot needs; it stays put. + slotCode[slot] = coerce(o, pt, ctx); + continue; + } + String tmp = ctx.newTemp(); + ctx.writer().line(jt + " " + tmp + ";"); + assigns.add(tmp + " = " + o.code); + slotCode[slot] = coerce(new Out(tmp, at, o.fromError), pt, ctx); + if (anchor < 0 || slot < anchor) { + anchor = slot; + anchorTemp = tmp; + anchorType = at; + } + } + if (anchor < 0) { + return null; + } + ctx.importClass("dart.runtime.DartRuntime"); + String chain = "true"; + for (int k = assigns.size() - 1; k >= 0; k--) { + chain = "DartRuntime.seq(" + assigns.get(k) + ", " + chain + ")"; + } + slotCode[anchor] = coerce(new Out("(" + chain + " ? " + anchorTemp + " : " + anchorTemp + ")", + anchorType), paramType(owner, ct.params.get(anchor), ctx), ctx); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ct.params.size(); i++) { + Param p = ct.params.get(i); + String code = slotCode[i]; + if (code == null) { + TypeRef pt = paramType(owner, p, ctx); + Expr supplied = null; + for (Map.Entry en : slotOf.entrySet()) { + if (en.getValue().intValue() == i) { + supplied = en.getKey(); + } + } + if (supplied != null) { + code = coerce(emitExpr(supplied, pt, ctx), pt, ctx); + } else if (p.defaultValue != null) { + code = coerce(emitExpr(p.defaultValue, pt, ctx), pt, ctx); + } else { + code = zeroValue(pt); + } + } + if (i > 0) { + sb.append(", "); + } + sb.append(code); + } + reportUnknownNamedArgs(ct, owner, args); + return sb.toString(); + } + + /** + * Reports every named argument the callee does not declare. + * + *

Canonical expansion walks the PARAMETERS and looks up an argument for each, so an + * argument nobody declared is simply never read — it vanishes with no diagnostic and no + * runtime complaint. That is how {@code ListView.builder(shrinkWrap: true, ...)} came to + * be built without shrink-wrapping: the value was written, transpiled, and dropped.

+ * + *

Silently ignoring what the source asked for is the worst failure mode available + * here, because the app looks like it works. Reporting turns each one into a line of a + * to-do list instead: either the runtime grows the parameter, or the gap is a known one.

+ */ + private void reportUnknownNamedArgs(CtorDecl ct, ClassDecl owner, Args args) { + if (ct == null || args == null || args.named == null || args.named.isEmpty()) { + return; + } + for (NamedArg na : args.named) { + boolean declared = false; + for (Param p : ct.params) { + // By NAME alone, deliberately. A parameter written `required T name` inside + // the braces is not always flagged named by the parser, and requiring the + // flag reported arguments that are in fact declared and passed correctly - + // a false positive is fatal for a diagnostic that is meant to become an error. + if (na.name.equals(p.name)) { + declared = true; + break; + } + } + if (!declared) { + String callee = owner != null ? owner.name : ""; + if (ct.name != null) { + callee = callee + "." + ct.name; + } + // An ERROR, not a warning. Dart itself rejects an undeclared named + // argument, so accepting one is our divergence from the language, and the + // way we diverged was the worst available: the value was dropped and the + // app looked like it worked. Failing the build is what Flutter does. + diags.error(na.value, "E0140", + callee + " does not declare named argument '" + na.name + + "'; add it to the runtime API and its Dart stub"); + } + } + } + + /** Program method calls: positional plus named-in-declared-order. */ + private String methodArgs(List params, Args args, Ctx ctx) { + CtorDecl fake = new CtorDecl(); + fake.params = params; + return canonicalArgs(fake, args, ctx); + } + + /** Stub method calls: canonical positional per stub declaration order. */ + private String stubMethodArgs(Ast.MethodDecl m, Args args, Ctx ctx) { + CtorDecl fake = new CtorDecl(); + fake.params = m.params; + return canonicalArgs(fake, args, ctx); + } + + /** + * Emits a stub method call, recovering the Dart generic {@code } witness + * the emitter otherwise drops. When the stub method returns one of its own + * type parameters (a return type that resolves to no known class/enum/core + * type, e.g. {@code Provider.of} or + * {@code context.dependOnInheritedWidgetOfExactType}) and the call site + * supplies a type argument, that argument is: + *
    + *
  • passed to the Java method as a trailing {@code T.class} token, so + * the runtime can dispatch on the requested type; the Java runtime + * method must therefore accept a trailing {@code Class} parameter;
  • + *
  • used as the call's static type and applied as a Java cast so member + * access on the result type-checks.
  • + *
+ * Methods that return a concrete type are emitted unchanged. + */ + /** + * dart:math's {@code min}, {@code max} and {@code pow} take and return {@code num}: + * two ints give an int, and pow of an int by a non-negative int is an int. The stubs + * declare them over double, which turned {@code min(1, 2)} into 1.0 and failed + * {@code is int}. Two ints use the runtime's long overloads (pow by a non-negative + * literal uses powInt); a double against anything but an int keeps the double form; + * an int against a double, or two values typed only num or dynamic, go through the + * Number versions, which answer one of the arguments unconverted. Null when the + * call is not one of these. + */ + private Out mathNumCall(String javaClass, String method, Call c, Ctx ctx) { + if (!"dart.math.DartMath".equals(javaClass) || c.args.positional.size() != 2 + || !c.args.named.isEmpty() + || !(method.equals("min") || method.equals("max") || method.equals("pow"))) { + return null; + } + ctx.importClass("dart.math.DartMath"); + Expr ex = c.args.positional.get(0); + Expr ey = c.args.positional.get(1); + Out x = emitExpr(ex, null, ctx); + Out y = emitExpr(ey, null, ctx); + boolean xi = x.type != null && x.type.is("int") && !x.type.nullable; + boolean yi = y.type != null && y.type.is("int") && !y.type.nullable; + boolean xd = x.type != null && x.type.is("double") && !x.type.nullable; + boolean yd = y.type != null && y.type.is("double") && !y.type.nullable; + if (xi && yi) { + if (!method.equals("pow")) { + return new Out("DartMath." + method + "(" + x.code + ", " + y.code + ")", TypeRef.INT); + } + if (ey instanceof IntLit && ((IntLit) ey).value >= 0) { + return new Out("DartMath.powInt(" + x.code + ", " + y.code + ")", TypeRef.INT); + } + } + // pow with a double on either side is always a double in Dart; min and max + // answer one of their arguments, so an int beside a double can come back. + if ((xd && !yi) || (yd && !xi) || (method.equals("pow") && (xd || yd))) { + // A double against another double -- or against a value this emitter only + // types as num or dynamic, which in the gallery is a double whose type was + // lost in arithmetic -- keeps the double form the stubs declare, so it still + // fits the double slots it flows into. + return new Out("DartMath." + method + "(" + coerce(x, TypeRef.DOUBLE, ctx) + ", " + + coerce(y, TypeRef.DOUBLE, ctx) + ")", TypeRef.DOUBLE); + } + // Boxed explicitly: a primitive long reaches a Number parameter only through boxing. + return new Out("DartMath." + method + "Num(" + boxNumber(x) + ", " + boxNumber(y) + ")", + TypeRef.of("num")); + } + + private String boxNumber(Out o) { + if (o.type != null && o.type.is("int") && !o.type.nullable) { + return "Long.valueOf(" + o.code + ")"; + } + if (o.type != null && o.type.is("double") && !o.type.nullable) { + return "Double.valueOf(" + o.code + ")"; + } + return "(Number) " + paren(o.code); + } + + private Out stubCallOut(Ast.MethodDecl m, Call c, String callee, Ctx ctx) { + return stubCallOut(m, c, callee, ctx, null); + } + + /** + * As {@link #stubCallOut(Ast.MethodDecl, Call, String, Ctx)} but with an already + * type-argument-substituted return type ({@code substReturn}, e.g. + * {@code ColorTween.evaluate(...)} narrowed from {@code T} to {@code Color}). The + * explicit-{@code }-witness path still keys off the raw declared return type. + */ + private Out stubCallOut(Ast.MethodDecl m, Call c, String callee, Ctx ctx, TypeRef substReturn) { + String args = stubMethodArgs(m, c.args, ctx); + TypeRef rt = m.returnType; + if (rt != null && !isConcreteType(rt)) { + // The stub method returns one of its own type parameters, so the runtime + // method takes a trailing Class witness. Recover T from the explicit + // at the call site, or — when it was inferred and dropped (the common + // `X.of(context) => context.dependOnInheritedWidgetOfExactType()` shape) — + // from the enclosing method's return type. + TypeRef sub = null; + if (!c.typeArgs.isEmpty()) { + sub = c.typeArgs.get(0); + } else if (INFERRED_WITNESS_METHODS.contains(m.name) + && ctx.methodReturnType != null && isConcreteType(ctx.methodReturnType) + && !ctx.methodReturnType.is("void") && !ctx.methodReturnType.is("dynamic")) { + // A BuildContext ancestor lookup (`X.of(context) => + // context.dependOnInheritedWidgetOfExactType()`) whose was inferred + // from and dropped by the enclosing return type. Recover it from there. + sub = ctx.methodReturnType; + } + if (sub != null) { + // The Java runtime method's Class parameter lets javac infer the + // return type, so no cast is needed (and a leading cast '(' would + // trip statementize into wrapping a void setter call). + String token = javaType(sub, true, ctx) + ".class"; + String all = args.isEmpty() ? token : args + ", " + token; + return new Out(callee + "(" + all + ")", sub); + } + } + return new Out(callee + "(" + args + ")", substReturn != null ? substReturn : rt); + } + + /** + * BuildContext generic ancestor-lookup methods whose {@code } is routinely inferred + * from the enclosing {@code static X? of(context) => ...} return type rather than written + * explicitly. For these, when no explicit type argument is present, the witness is recovered + * from {@code ctx.methodReturnType}. + */ + private static final java.util.Set INFERRED_WITNESS_METHODS = new java.util.HashSet( + java.util.Arrays.asList("dependOnInheritedWidgetOfExactType", + "findAncestorWidgetOfExactType", "findAncestorStateOfType", + "findAncestorRenderObjectOfType", "getInheritedWidgetOfExactType")); + + private static final java.util.Set CONCRETE_CORE = new java.util.HashSet( + java.util.Arrays.asList("int", "double", "bool", "String", "void", "num", + "dynamic", "var", "Object", "Null", "List", "Map", "Set", "Iterable", + "Future", "FutureOr", "Duration", "Stopwatch", "Function")); + + /** + * Whether a type reference names a resolvable concrete type (stub, program + * or core) rather than an unbound generic type parameter. + */ + private boolean isConcreteType(TypeRef t) { + if (t == null) { + return false; + } + String n = t.name; + return stubs.isStubClass(n) || stubs.isStubEnum(n) + || program.classes.containsKey(n) || program.enums.containsKey(n) + || TYPEDEFS.containsKey(n) || program.typedefs.containsKey(n) + || CONCRETE_CORE.contains(n); + } + + /** + * Whether {@code sub} is (transitively) a subtype of {@code sup} across the program and stub + * class hierarchies. Used to decide when a generic cross-type assignment needs an erasing cast. + */ + private boolean isSubtypeName(String sub, String sup) { + if (sub == null || sup == null) { + return false; + } + java.util.Set seen = new java.util.HashSet(); + String cur = sub; + while (cur != null && seen.add(cur)) { + if (cur.equals(sup)) { + return true; + } + TypeRef next = null; + ClassDecl pc = program.classes.get(cur); + if (pc != null) { + next = pc.superclass; + } else { + Ast.ClassDecl sc = stubs.classes.get(cur); + if (sc != null) { + next = sc.superclass; + } + } + cur = next != null ? next.name : null; + } + return false; + } + + /** + * Whether {@code value} already inherits exactly the instantiation {@code target} names, in + * which case Java accepts the assignment as-is and an erasing cast would be pure noise. + */ + private boolean inheritsSameInstantiation(TypeRef value, TypeRef target) { + TypeRef inherited = supertypeInstantiation(value.name, value.args, target.name); + return inherited != null + && copyNonNull(inherited).toString().equals(copyNonNull(target).toString()); + } + + /** + * The instantiation of {@code sup} that {@code sub} already inherits, or null when the + * superclass chain does not reach {@code sup}. From {@code class _MyHomePageState extends + * State} the instantiation of {@code State} seen from {@code _MyHomePageState} is + * {@code State} — which is why assigning one to the other needs no cast. + * + *

Walks the same chain as {@link #isSubtypeName}, but substitutes each class's type + * parameters with the arguments carried down from the previous link.

+ */ + private TypeRef supertypeInstantiation(String sub, List args, String sup) { + java.util.Set seen = new java.util.HashSet(); + String cur = sub; + List curArgs = args; + while (cur != null && seen.add(cur)) { + if (cur.equals(sup)) { + TypeRef t = new TypeRef(cur); + if (curArgs != null) { + t.args.addAll(curArgs); + } + return t; + } + ClassDecl decl = program.classes.get(cur); + List params; + TypeRef next; + if (decl != null) { + params = decl.typeParams; + next = decl.superclass; + } else { + Ast.ClassDecl sc = stubs.classes.get(cur); + if (sc == null) { + return null; + } + params = sc.typeParams; + next = sc.superclass; + } + if (next == null) { + return null; + } + curArgs = substituteTypeParams(next.args, params, curArgs); + cur = next.name; + } + return null; + } + + /** {@code types} with each occurrence of {@code params[i]} replaced by {@code args[i]}. */ + private List substituteTypeParams(List types, List params, List args) { + List out = new ArrayList(); + for (TypeRef t : types) { + out.add(substituteTypeParam(t, params, args)); + } + return out; + } + + private TypeRef substituteTypeParam(TypeRef t, List params, List args) { + if (t == null) { + return null; + } + if (params != null && args != null) { + int i = params.indexOf(t.name); + if (i >= 0 && i < args.size()) { + return args.get(i); + } + } + if (t.args.isEmpty()) { + return t; + } + TypeRef copy = new TypeRef(t.name); + copy.nullable = t.nullable; + copy.args.addAll(substituteTypeParams(t.args, params, args)); + return copy; + } + + /** The top-level function named {@code n} declared in library {@code lib}, or null. */ + private FunctionDecl functionInLibrary(Library lib, String n) { + if (lib == null) { + return null; + } + for (FunctionDecl f : lib.functions) { + if (f.name.equals(n)) { + return f; + } + } + return null; + } + + /** + * The expected type of a collection method's per-element lambda -- map, where, + * forEach, any/every, the *Where searches -- so an untyped parameter gets the + * element type. Passing nothing left `(e) => e.key` with a dynamic `e`: the member + * read became a raw field access and the arithmetic a dynamic dispatch. Null (no + * expectation) when the element type itself is unknown. + */ + private TypeRef elementLambda(TypeRef elem, TypeRef ret) { + if (elem == null || elem.is("dynamic") || elem.is("var")) { + return null; + } + return inlineFuncType(elem, ret); + } + + /** + * Whether a callback can produce a value: an arrow lambda whose body is not void, or + * a block lambda that returns a value somewhere outside a nested function. False + * for anything that is not a lambda literal. + */ + /** + * The declared return type of a bare-name tear-off -- an own or inherited method, + * or a top-level function -- or null when the expression is not one of those. + */ + private TypeRef tearOffReturn(Expr e, Ctx ctx) { + if (!(e instanceof Ident)) { + return null; + } + String n = ((Ident) e).name; + if (ctx.lookup(n) != null) { + return null; // a local shadows any method or function of that name + } + if (ctx.currentClass != null) { + MethodDecl md = findMethodInHierarchy(ctx.currentClass, n); + if (md != null) { + return declaredReturn(md.returnType); + } + } + FunctionDecl fn = program.functions.get(n); + return fn != null && !fn.isGetter ? declaredReturn(fn.returnType) : null; + } + + /** An omitted or inferred return type is emitted as Object, so it produces a value. */ + private static TypeRef declaredReturn(TypeRef t) { + return t == null || t.is("var") ? TypeRef.DYNAMIC : t; + } + + private boolean lambdaReturnsValue(Expr e) { + if (!(e instanceof Lambda)) { + return false; + } + Lambda l = (Lambda) e; + if (l.exprBody != null) { + return true; // decided by the caller from lastLambdaVoid + } + return returnsValue(l.body); + } + + private static boolean returnsValue(Stmt s) { + if (s == null) { + return false; + } + if (s instanceof ReturnStmt) { + return ((ReturnStmt) s).value != null; + } + if (s instanceof Block) { + for (Stmt x : ((Block) s).statements) { + if (returnsValue(x)) { + return true; + } + } + return false; + } + if (s instanceof IfStmt) { + return returnsValue(((IfStmt) s).thenStmt) || returnsValue(((IfStmt) s).elseStmt); + } + if (s instanceof WhileStmt) { + return returnsValue(((WhileStmt) s).body); + } + if (s instanceof ForStmt) { + return returnsValue(((ForStmt) s).body); + } + if (s instanceof ForInStmt) { + return returnsValue(((ForInStmt) s).body); + } + if (s instanceof TryStmt) { + TryStmt t = (TryStmt) s; + if (returnsValue(t.tryBlock) || returnsValue(t.finallyBlock)) { + return true; + } + for (CatchClause c : t.catches) { + if (returnsValue(c.body)) { + return true; + } + } + return false; + } + if (s instanceof SwitchStmt) { + for (SwitchCase c : ((SwitchStmt) s).cases) { + for (Stmt x : c.body) { + if (returnsValue(x)) { + return true; + } + } + } + } + return false; // a nested function's returns are its own + } + + /** + * A callback lambda cast to the functional type its body fits. A lambda has no type + * of its own, so it cannot go where the parameter is Object; and a void- or + * throw-bodied one fits both the value and the void overload of then/catchError, + * which javac reports as ambiguous. {@code forceCast} casts a value-returning lambda + * too (an Object parameter); otherwise only a void one is cast and javac resolves the + * rest as it always has. Anything that is not a lambda passes as is. + */ + private String pinnedHandler(Expr src, Out o, boolean forceCast, String asValue, String asVoid) { + if (!(src instanceof Lambda)) { + return o.code; + } + boolean value = ((Lambda) src).exprBody != null ? !lastLambdaVoid : lambdaReturnsValue(src); + if (value && !forceCast) { + return o.code; + } + return "(" + (value ? asValue : asVoid) + ") " + paren(o.code); + } + + /** An inline single-parameter function type {@code (param) -> ret}, for typing a lambda arg. */ + private TypeRef inlineFuncType(TypeRef param, TypeRef ret) { + TypeRef t = new TypeRef("Function"); + t.funcParams = new ArrayList(); + t.funcParams.add(param != null ? param : TypeRef.DYNAMIC); + t.funcReturn = ret != null ? ret : TypeRef.DYNAMIC; + return t; + } + + /** {@link #isConcreteType} extended recursively through all type arguments. */ + private boolean isFullyConcrete(TypeRef t) { + if (!isConcreteType(t)) { + return false; + } + for (TypeRef a : t.args) { + if (!isFullyConcrete(a)) { + return false; + } + } + return true; + } + + private String plainArgs(Args args, Ctx ctx) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < args.positional.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(emitExpr(args.positional.get(i), null, ctx).code); + } + return sb.toString(); + } + + // ================================================================== + // Types & helpers + // ================================================================== + + private TypeRef fieldType(FieldDecl f, Ctx ctx) { + if (f.type != null && !f.type.is("var")) { + return f.type; + } + if (f.initializer != null) { + // cheap literal-driven inference + if (f.initializer instanceof IntLit) { + return TypeRef.INT; + } + if (f.initializer instanceof DoubleLit) { + return TypeRef.DOUBLE; + } + if (f.initializer instanceof BoolLit) { + return TypeRef.BOOL; + } + if (f.initializer instanceof StringLit) { + return TypeRef.STRING; + } + // A collection literal written with its type arguments names its own type: + // `final log = [];` was dynamic, and `log.add(...)` did not resolve. + if (f.initializer instanceof ListLit && ((ListLit) f.initializer).elementType != null) { + return TypeRef.of("List", ((ListLit) f.initializer).elementType); + } + if (f.initializer instanceof SetLit && ((SetLit) f.initializer).elementType != null) { + return TypeRef.of("Set", ((SetLit) f.initializer).elementType); + } + if (f.initializer instanceof MapLit && ((MapLit) f.initializer).keyType != null + && ((MapLit) f.initializer).valueType != null) { + MapLit m = (MapLit) f.initializer; + return TypeRef.of("Map", m.keyType, m.valueType); + } + } + return TypeRef.DYNAMIC; + } + + private TypeRef typeOfField(ClassDecl c, String name, Ctx ctx) { + FieldDecl f = c.field(name); + return f == null ? TypeRef.DYNAMIC : fieldType(f, ctx); + } + + /** Type of a constructor parameter, resolving this./super. against fields. */ + private TypeRef paramType(ClassDecl c, Param p, Ctx ctx) { + // A `this.x`/`super.x` initializing formal often carries no written type (parsed as + // `dynamic`); fall through to resolve it from the field / super constructor instead of + // returning the erased `dynamic` early. + if (p.type != null && !p.type.is("var") && !p.type.is("dynamic")) { + return p.type; + } + if (p.isThis && c != null) { + FieldDecl f = c.field(p.name); + if (f != null) { + return fieldType(f, ctx); + } + } + if (p.isSuper && c != null && c.superclass != null) { + // super.x forwards to the super constructor's parameter named x; resolve its + // type against the program (app) super chain first, then the stub super chain. + ClassDecl progCur = program.classes.get(c.superclass.name); + while (progCur != null) { + Ast.CtorDecl sct = progCur.defaultCtor(); + if (sct != null) { + for (Ast.Param sp : sct.params) { + if (sp.name.equals(p.name)) { + TypeRef rt = paramType(progCur, sp, ctx); + if (rt != null && !rt.is("dynamic")) { + return rt; + } + } + } + } + // the super param may correspond directly to an inherited field + FieldDecl sf = progCur.field(p.name); + if (sf != null) { + return fieldType(sf, ctx); + } + progCur = progCur.superclass != null ? program.classes.get(progCur.superclass.name) : null; + } + // look up the named param type on the stub super chain + Ast.ClassDecl cur = stubs.classes.get(c.superclass.name); + while (cur != null) { + Ast.CtorDecl sct = cur.defaultCtor(); + if (sct != null) { + for (Ast.Param sp : sct.params) { + if (sp.name.equals(p.name)) { + return sp.type; + } + } + } + cur = cur.superclass != null ? stubs.classes.get(cur.superclass.name) : null; + } + } + return TypeRef.DYNAMIC; + } + + /** Maps a Dart type to Java source. boxed=true forces reference types. */ + private String javaType(TypeRef t, boolean boxed, Ctx ctx) { + if (t == null || t.is("var") || t.is("dynamic") || t.is("Object") || t.is("Null")) { + return "Object"; + } + // A type written with an import prefix (`intl.DateFormat`, `ui.Size`) carries the + // prefix in its name; drop it so the base name resolves against a stub/program type. + if (t.name != null && t.name.indexOf('.') > 0) { + String stripped = stripImportPrefix(t.name); + if (!stripped.equals(t.name)) { + t.name = stripped; + } + } + boolean box = boxed || t.nullable; + if (t.is("int")) { + return box ? "Long" : "long"; + } + if (t.is("double")) { + return box ? "Double" : "double"; + } + if (t.is("bool")) { + return box ? "Boolean" : "boolean"; + } + if (t.is("String")) { + return "String"; + } + if (t.is("void")) { + // `void` as a type argument (e.g. Dart Route) must box to Void; + // Java has no `void` type argument. + return box ? "Void" : "void"; + } + if (t.is("num")) { + return "Number"; + } + if (t.is("List")) { + String pk = primitiveListKind(t); + if (pk != null) { + ctx.importClass("dart.core.Dart" + pk + "List"); + return "Dart" + pk + "List"; + } + ctx.importClass("dart.core.DartList"); + return "DartList<" + javaType(t.arg(0), true, ctx) + ">"; + } + if (t.is("Map")) { + if (isPrimitiveLongMap(t)) { + ctx.importClass("dart.core.DartLongMap"); + return "DartLongMap"; + } + ctx.importClass("dart.core.DartMap"); + return "DartMap<" + javaType(t.arg(0), true, ctx) + ", " + javaType(t.arg(1), true, ctx) + ">"; + } + if (t.is("Set")) { + ctx.importClass("dart.core.DartSet"); + return "DartSet<" + javaType(t.arg(0), true, ctx) + ">"; + } + if (t.is("Iterable")) { + ctx.importClass("dart.core.DartIterable"); + return "DartIterable<" + javaType(t.arg(0), true, ctx) + ">"; + } + if (t.is("Future") || t.is("FutureOr")) { + ctx.importClass("dart.async.Future"); + TypeRef a = t.args.isEmpty() ? TypeRef.DYNAMIC : t.arg(0); + if (a.is("void") || a.is("Null")) { + a = TypeRef.DYNAMIC; + } + return "Future<" + javaType(a, true, ctx) + ">"; + } + if (t.is("Duration")) { + ctx.importClass("dart.core.Duration"); + return "Duration"; + } + if (t.is("Stopwatch")) { + ctx.importClass("dart.core.Stopwatch"); + return "Stopwatch"; + } + // user typedef that plainly aliases another type: resolve through to the target + Ast.TypedefDecl userTd = program.typedefs.get(t.name); + if (userTd != null && userTd.aliased != null) { + return javaType(userTd.aliased, boxed, ctx); + } + if (typedefSig(t.name) != null) { + ctx.importClass("dart.runtime.Funcs"); + // Substitute the typedef's type arguments (e.g. ValueChanged => the `dynamic` + // placeholder in `void Function(dynamic)` becomes `int`), so a callback of a + // parameterized typedef renders VoidFunc1 rather than VoidFunc1. + TypeRef[] sig = substituteTypedefTypeArgs(typedefSig(t.name), t.args); + int arity = sig.length - 1; + TypeRef ret = sig[arity]; + if (ret.is("void")) { + if (arity == 0) { + return "Funcs.VoidFunc0"; + } + StringBuilder sb = new StringBuilder("Funcs.VoidFunc").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(sig[i], true, ctx)); + } + return sb.append('>').toString(); + } + StringBuilder sb = new StringBuilder("Funcs.Func").append(arity).append('<'); + for (int i = 0; i < arity; i++) { + sb.append(javaType(sig[i], true, ctx)).append(", "); + } + sb.append(javaType(ret, true, ctx)); + return sb.append('>').toString(); + } + if (t.is("Function")) { + // An inline function type (`void Function(int)`) carries its parsed signature; + // render the matching Funcs.* SAM so callbacks accept lambdas (not Object). + if (t.funcReturn != null) { + return funcSamTypeFromRefs(t.funcParams, t.funcReturn, ctx); + } + return "Object"; + } + // app class (preferred when visible) — a disambiguated name resolves a collision with + // another app class or a stub type of the same simple name. + String appName = resolveAppClassName(t.name, ctx.library(), ctx); + if (appName != null) { + return appName + (t.args.isEmpty() ? "" : genericSuffix(t, ctx)); + } + // stub class or enum + Ast.ClassDecl sc = stubs.classes.get(t.name); + if (sc != null && sc.javaName != null) { + ctx.importClass(sc.javaName); + String simple = sc.javaName.substring(sc.javaName.lastIndexOf('.') + 1); + if (!t.args.isEmpty()) { + StringBuilder sb = new StringBuilder(simple).append('<'); + for (int i = 0; i < t.args.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(t.args.get(i), true, ctx)); + } + return sb.append('>').toString(); + } + return simple; + } + Ast.EnumDecl se = stubs.enums.get(t.name); + if (se != null && se.javaName != null) { + ctx.importClass(se.javaName); + return se.javaName.substring(se.javaName.lastIndexOf('.') + 1); + } + // program class / enum / type parameter — same package + return t.name + (t.args.isEmpty() ? "" : genericSuffix(t, ctx)); + } + + private String genericSuffix(TypeRef t, Ctx ctx) { + StringBuilder sb = new StringBuilder("<"); + for (int i = 0; i < t.args.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(javaType(t.args.get(i), true, ctx)); + } + return sb.append('>').toString(); + } + + /** + * The Java class name for an app class. When its simple name collides — with another + * app class of the same name in a different library (new_gallery has several + * {@code _FrontLayer} / {@code HomePage} / {@code Backdrop}) or with a stub type (the + * routes.dart {@code Path} vs dart:ui {@code Path}) — it is disambiguated with a + * library-path prefix. Non-colliding names are returned unchanged to minimise churn. + */ + private String javaClassName(ClassDecl c) { + if (c == null) { + return null; + } + boolean collides = stubs.isStubClass(c.name) || stubs.isStubEnum(c.name); + List byName = program.classesByName.get(c.name); + if (byName != null && byName.size() > 1) { + collides = true; + } + if (!collides || c.ownerLibrary == null) { + return c.name; + } + return libPathPrefix(c.ownerLibrary) + c.name; + } + + /** Camel-cased library path (dir + basename, no extension) used to disambiguate class names. */ + private String libPathPrefix(Library lib) { + String base = lib.fileName.replace('\\', '/'); + if (base.endsWith(".dart")) { + base = base.substring(0, base.length() - 5); + } + StringBuilder sb = new StringBuilder(); + boolean up = true; + for (int i = 0; i < base.length(); i++) { + char ch = base.charAt(i); + if (ch == '_' || ch == '-' || ch == '.' || ch == '/') { + up = true; + } else { + sb.append(up ? Character.toUpperCase(ch) : ch); + up = false; + } + } + return sb.toString(); + } + + /** + * Resolves a user type name to its (possibly disambiguated) Java class name, or null when + * no app class of that name is visible from {@code from} (so the caller falls back to a stub). + */ + private String resolveAppClassName(String name, Library from, Ctx ctx) { + ClassDecl c = program.resolveClass(name, from); + if (c == null) { + return null; + } + // When the name also denotes a stub type, only prefer the app class if it is actually + // visible (declared/imported) from the referencing library. + if ((stubs.isStubClass(name) || stubs.isStubEnum(name)) && !classVisibleFrom(c, from)) { + return null; + } + return javaClassName(c); + } + + private String stubSimpleName(String dartName, Ctx ctx) { + Ast.ClassDecl sc = stubs.classes.get(dartName); + if (sc != null && sc.javaName != null) { + ctx.importClass(sc.javaName); + return sc.javaName.substring(sc.javaName.lastIndexOf('.') + 1); + } + return dartName; + } + + /** The getter/method declaration named {@code name} on an extension declaration, or null. */ + private Ast.MethodDecl extensionMember(Ast.ClassDecl ext, String name, boolean getter) { + for (Ast.MethodDecl m : ext.methods) { + if (m.name.equals(name) && m.isGetter == getter && !m.isSetter) { + return m; + } + } + return null; + } + + /** Simple Java class name hosting a stub extension's static members; imports its @JavaName. */ + private String stubExtensionSimpleName(Ast.ClassDecl ext, Ctx ctx) { + if (ext.javaName != null) { + ctx.importClass(ext.javaName); + return ext.javaName.substring(ext.javaName.lastIndexOf('.') + 1); + } + return ext.name; + } + + private void importEnum(String dartName, Ctx ctx) { + Ast.EnumDecl se = stubs.enums.get(dartName); + if (se != null && se.javaName != null) { + ctx.importClass(se.javaName); + } + } + + private String simpleEnumName(String dartName, Ctx ctx) { + Ast.EnumDecl se = stubs.enums.get(dartName); + if (se != null && se.javaName != null) { + return se.javaName.substring(se.javaName.lastIndexOf('.') + 1); + } + return dartName; + } + + /** + * Strips a leading import-prefix segment from a type name — {@code intl.DateFormat} + * → {@code DateFormat}, {@code ui.Size} → {@code Size} — when the segment + * before the first dot is a known {@code import '...' as prefix} name (tracked + * program-wide). In the single-package whole-program model the prefix is redundant once + * the type is resolved, so the base name resolves against {@code program}/{@link StubRegistry}. + * Returns the name unchanged when there is no such prefix. + */ + private String stripImportPrefix(String name) { + if (name != null) { + int dot = name.indexOf('.'); + if (dot > 0 && program.importPrefixes.contains(name.substring(0, dot))) { + return name.substring(dot + 1); + } + } + return name; + } + + /** Pseudo-type marking a reference to a class itself (for static access). */ + private TypeRef classRef(String className) { + TypeRef t = new TypeRef("$class"); + t.args.add(new TypeRef(className)); + return t; + } + + /** + * A user class by simple name, resolved with same-library preference. Two libraries + * may declare a class of the same name in the single-package model (e.g. the gallery's + * {@code Backdrop} in pages/ and studies/crane/, or a private {@code _FrontLayer} in + * two studies); the plain {@code program.classes} map keeps only one, so a + * {@code widget.} read from within a State resolves against the wrong sibling. + * Prefer the declaration in the library that owns the code currently being emitted. + */ + private ClassDecl programClass(String name, Ctx ctx) { + Ast.Library lib = ctx != null && ctx.currentClass != null + ? ctx.currentClass.ownerLibrary : null; + return program.resolveClass(name, lib); + } + + private boolean isClassRef(TypeRef t) { + return t != null && t.is("$class"); + } + + /** + * Dart's {@code dynamic}: an operand whose operators are chosen at run time. Narrower + * than {@link #isDynamicType} on purpose -- a {@code var} local is declared with Java's + * {@code var} and keeps its inferred primitive type, so {@code i + 1} on it already + * compiles and must stay a Java operator. + */ + private static boolean isDynamic(TypeRef t) { + return t != null && t.is("dynamic"); + } + + private boolean isNumeric(TypeRef t) { + return t != null && (t.is("int") || t.is("double")); + } + + private boolean containsDynamic(TypeRef t) { + if (t == null || t.is("dynamic")) { + return true; + } + for (TypeRef a : t.args) { + if (containsDynamic(a)) { + return true; + } + } + return false; + } + + private TypeRef boxType(TypeRef t) { + if (t == null) { + return TypeRef.DYNAMIC; + } + TypeRef c = TypeRef.of(t.name, t.args.toArray(new TypeRef[0])); + c.nullable = true; + return c; + } + + private TypeRef copyNonNull(TypeRef t) { + if (t == null) { + return TypeRef.DYNAMIC; + } + TypeRef c = TypeRef.of(t.name, t.args.toArray(new TypeRef[0])); + c.nullable = false; + return c; + } + + /** Whether a Dart type erases to Java Object (untyped/dynamic value). */ + private boolean isDynamicType(TypeRef t) { + return t == null || t.is("var") || t.is("dynamic") || t.is("Object"); + } + + /** + * Casts a bare lambda/function value to its target SAM type. Needed when the value flows + * into an {@code Object...} varargs slot (e.g. DartMap.of / DartList.of), where a bare + * lambda has no functional target and javac reports "Object is not a functional interface". + */ + private String funcCast(Out o, TypeRef target, Ctx ctx) { + if (target != null && isFunctionValued(target)) { + return "(" + javaType(target, true, ctx) + ") " + paren(o.code); + } + return o.code; + } + + /** Whether a parameterized type has a Dart-`dynamic` (Java Object) type argument. */ + private boolean hasDynamicArg(TypeRef t) { + if (t == null || t.args == null) { + return false; + } + for (TypeRef a : t.args) { + if (isDynamicType(a)) { + return true; + } + } + return false; + } + + /** + * Element/value coercion for collection literals: applies a functional-target cast for + * callback elements, and a raw-type bridge when the element type is a `G<dynamic>` + * (rendered `G<Object>`) but the concrete element is `G<X>` — which Java will not + * convert to `G<Object>`. The unchecked raw cast mirrors Dart's `dynamic` covariance. + */ + private String elementCode(Out o, TypeRef elem, Ctx ctx) { + if (elem != null && isFunctionValued(elem)) { + return funcCast(o, elem, ctx); + } + if (elem != null && hasDynamicArg(elem) && o.type != null && o.type.name != null + && o.type.name.equals(elem.name) && !o.type.args.isEmpty() + && !o.type.toString().equals(elem.toString())) { + String raw = javaType(new TypeRef(elem.name), false, ctx); + return "(" + raw + ") " + paren(o.code); + } + return coerce(o, elem, ctx); + } + + private String coerce(Out o, TypeRef target, Ctx ctx) { + if (target == null) { + return o.code; + } + // A Dart type literal (a bare class name used as a value, e.g. `GalleryLocalizations` + // passed as a `Type` argument) becomes a Java class literal. + if (o.type != null && isClassRef(o.type) && !isClassRef(target)) { + return o.code + ".class"; + } + // A Dart `double` value reaching a Dart `int` target only happens through stub + // imprecision (e.g. math.max/min typed to return double): Dart itself forbids the + // implicit narrowing, so the value is known int-valued — truncate to match. + if (target.is("int") && !target.nullable && o.type != null && o.type.is("double")) { + return "(long) " + paren(o.code); + } + if (target.is("double") && o.type.is("int")) { + if (o.code.endsWith("L")) { + String digits = o.code.substring(0, o.code.length() - 1); + try { + Long.parseLong(digits); + return digits + ".0"; + } catch (NumberFormatException ignore) { + // fall through + } + } + return "((double) " + paren(o.code) + ")"; + } + // Unbox an Object/dynamic value -- or a num, which is a Java Number -- flowing into + // a Java primitive numeric target (e.g. an untyped lambda param assigned to a + // `long`/`double` setter, or num arithmetic passed on as a double). + if (!target.nullable && (isDynamicType(o.type) || (o.type != null && o.type.is("num")))) { + if (target.is("int")) { + return "((Number) " + paren(o.code) + ").longValue()"; + } + if (target.is("double")) { + return "((Number) " + paren(o.code) + ").doubleValue()"; + } + } + // A Dart List/Set flowing into an Iterable target: the runtime List/Set is not a + // DartIterable, so bridge with asIterable(). + if (target.is("Iterable") && o.type != null && (o.type.is("List") || o.type.is("Set"))) { + return paren(o.code) + ".asIterable()"; + } + // A concrete generic target whose value is a proper subtype with different type arguments + // (e.g. MaterialPageRoute -> Route): the subtyping holds but the type + // argument mismatch makes Java reject it, so erase through Object. + if (target.name != null && o.type != null && o.type.name != null + && !target.name.equals(o.type.name) + && !target.args.isEmpty() + && isFullyConcrete(target) + && isSubtypeName(o.type.name, target.name) + && !inheritsSameInstantiation(o.type, target)) { + return "(" + javaType(copyNonNull(target), false, ctx) + ") (Object) " + paren(o.code); + } + // Covariant generic assignment: Dart lists/maps/futures are covariant in their type + // arguments, but Java generics are invariant. When value and target are the SAME generic + // type with differing type arguments (e.g. DartList -> DartList, + // Future> -> Future), bridge with a RAW cast. A cast between two + // distinct concrete parameterizations is illegal ("inconvertible types") in Java, so we + // erase through the raw type — matching Dart's covariance (unchecked at runtime). + if (target.name != null && o.type != null && o.type.name != null + && target.name.equals(o.type.name) + && !target.args.isEmpty() && !o.type.args.isEmpty() + && !isDynamicType(target) + && isFullyConcrete(target) + && !target.toString().equals(o.type.toString())) { + // Erase through Object so javac accepts the cross-parameterization cast (a direct cast + // between two distinct concrete parameterizations is "inconvertible types"). Only when + // the target is fully concrete — a type-variable arg (e.g. DartList) is resolved by + // Java's own inference and must not be pinned by a cast. + return "(" + javaType(copyNonNull(target), false, ctx) + ") (Object) " + paren(o.code); + } + return o.code; + } + + private String boxIfPrimitive(Out o, Ctx ctx) { + // Java autoboxing covers long/double/boolean → Long/Double/Boolean + return o.code; + } + + private String zeroValue(TypeRef t) { + if (t == null) { + return "null"; + } + if (t.nullable) { + return "null"; + } + if (t.is("int")) { + return "0L"; + } + if (t.is("double")) { + return "0.0"; + } + if (t.is("bool")) { + return "false"; + } + return "null"; + } + + /** Java holder class for a boxed captured local of the given type. */ + private String refHolder(TypeRef t, Ctx ctx) { + if (t.is("int") && !t.nullable) { + ctx.importClass("dart.runtime.RefLong"); + return "RefLong"; + } + if (t.is("double") && !t.nullable) { + ctx.importClass("dart.runtime.RefDouble"); + return "RefDouble"; + } + if (t.is("bool") && !t.nullable) { + ctx.importClass("dart.runtime.RefBool"); + return "RefBool"; + } + ctx.importClass("dart.runtime.Ref"); + return "Ref<" + javaType(t, true, ctx) + ">"; + } + + private String paren(String code) { + // parenthesize composite expressions to preserve precedence + if (code.matches("[A-Za-z0-9_$.()\\[\\]\"]+") || code.startsWith("(")) { + return code; + } + return "(" + code + ")"; + } + + /** Turns an expression emission into a valid Java statement expression. */ + private String statementize(String code) { + if (code.matches("[A-Za-z_$][A-Za-z0-9_$]*")) { + // a bare temp/identifier (e.g. a cascade's receiver) — no-op statement + return ""; + } + if (code.startsWith("(") && code.endsWith(")")) { + // ternaries etc. are not valid statements; assign to a discard temp + return "var $unused" + (unusedCounter++) + " = " + code; + } + return code; + } + + private int unusedCounter; + + private boolean isStateSubclass(ClassDecl c) { + return c.superclass != null && c.superclass.is("State"); + } + + private TypeRef stateTypeArg(ClassDecl c) { + if (isStateSubclass(c) && !c.superclass.args.isEmpty()) { + return c.superclass.arg(0); + } + return null; + } + + /** Finds a mixin-contributed member (field or method) for a program class hierarchy. */ + private Object findMixinMember(ClassDecl c, String name, boolean wantField) { + while (c != null) { + for (TypeRef mixRef : c.mixins) { + ClassDecl mx = program.classes.get(mixRef.name); + if (mx == null) { + continue; + } + if (wantField) { + FieldDecl f = mx.field(name); + if (f != null) { + return f; + } + } else { + MethodDecl m = mx.method(name); + if (m != null) { + return m; + } + MethodDecl g = mx.getter(name); + if (g != null) { + return g; + } + } + } + c = c.superclass != null ? program.classes.get(c.superclass.name) : null; + } + return null; + } + + /** Finds a method walking the program-class superclass chain. */ + private MethodDecl findMethodInHierarchy(ClassDecl c, String name) { + while (c != null) { + MethodDecl m = c.method(name); + if (m != null) { + return m; + } + c = c.superclass != null ? program.classes.get(c.superclass.name) : null; + } + return null; + } + + /** Nearest superclass of a program class that is a stub class. */ + private String nearestStubSuper(ClassDecl c) { + TypeRef sup = c.superclass; + while (sup != null) { + if (stubs.isStubClass(sup.name)) { + return sup.name; + } + ClassDecl pc = program.classes.get(sup.name); + sup = pc != null ? pc.superclass : null; + } + return null; + } + + private Ast.ClassDecl stubClassOf(TypeRef t) { + return t == null ? null : stubs.classes.get(t.name); + } + + /** + * Substitutes type variables in {@code t} using {@code subst}, preserving + * nullability. A bare type variable (no type arguments of its own) maps + * directly; otherwise the substitution recurses into the type arguments. + */ + private TypeRef substTypeVars(TypeRef t, java.util.Map subst) { + if (t == null || subst.isEmpty()) { + return t; + } + if (t.args.isEmpty()) { + TypeRef mapped = subst.get(t.name); + if (mapped == null) { + return t; + } + if (t.nullable && !mapped.nullable) { + TypeRef nn = new TypeRef(mapped.name); + nn.args.addAll(mapped.args); + nn.nullable = true; + return nn; + } + return mapped; + } + java.util.List newArgs = new java.util.ArrayList(); + boolean changed = false; + for (TypeRef a : t.args) { + TypeRef na = substTypeVars(a, subst); + newArgs.add(na); + if (na != a) { + changed = true; + } + } + if (!changed) { + return t; + } + TypeRef nt = new TypeRef(t.name); + nt.args.addAll(newArgs); + nt.nullable = t.nullable; + return nt; + } + + /** Maps a class's declared type-parameter names to concrete type arguments. */ + private static java.util.Map paramMap(java.util.List params, + java.util.List args) { + java.util.Map m = new java.util.HashMap(); + if (params != null) { + for (int i = 0; i < params.size() && i < args.size(); i++) { + m.put(params.get(i), args.get(i)); + } + } + return m; + } + + /** + * The concrete (type-argument-substituted) declared return type of a getter or + * method {@code member} on a stub receiver {@code receiver}. Walks the stub + * superclass chain composing the substitution from the receiver's own + * instantiation, so e.g. {@code MapEntry.value} resolves + * to {@code DisplayOption}, {@code FormFieldState.value} to + * {@code String?}, and {@code ColorTween.evaluate(...)} (via {@code Tween} + * / {@code Animatable}) to {@code Color}. Returns {@code null} when the member + * is not found on the stub chain (leaving the caller's default type in place). + */ + private TypeRef stubMemberReturnType(TypeRef receiver, String member, boolean getter) { + if (receiver == null) { + return null; + } + Ast.ClassDecl c = stubs.classes.get(receiver.name); + java.util.Map subst = + paramMap(c == null ? null : c.typeParams, receiver.args); + while (c != null) { + for (Ast.MethodDecl m : c.methods) { + if (m.name.equals(member) && m.isGetter == getter && !m.isSetter) { + return substTypeVars(m.returnType, subst); + } + } + TypeRef sup = c.superclass; + if (sup == null) { + break; + } + Ast.ClassDecl sd = stubs.classes.get(sup.name); + java.util.List superArgs = new java.util.ArrayList(); + for (TypeRef a : sup.args) { + superArgs.add(substTypeVars(a, subst)); + } + subst = paramMap(sd == null ? null : sd.typeParams, superArgs); + c = sd; + } + return null; + } + + /** + * The concrete declared return type of a getter/method {@code member} inherited by + * a program class {@code pc} from a generic stub superclass whose type argument the + * program class fixes — e.g. {@code class _RestorableEmailState extends + * RestorableListenable} reading the inherited {@code value} getter + * resolves the getter's {@code T} to {@code EmailStore}. Walks the program then stub + * superclass chain composing the substitution. Returns {@code null} when the member + * is not found on a stub ancestor. + */ + private TypeRef inheritedStubMemberReturnType(ClassDecl pc, String member, boolean getter) { + java.util.Map subst = new java.util.HashMap(); + TypeRef sup = pc.superclass; + while (sup != null) { + java.util.List superArgs = new java.util.ArrayList(); + for (TypeRef a : sup.args) { + superArgs.add(substTypeVars(a, subst)); + } + ClassDecl superProg = program.classes.get(sup.name); + Ast.ClassDecl superStub = stubs.classes.get(sup.name); + java.util.List params = superProg != null ? superProg.typeParams + : superStub != null ? superStub.typeParams + : java.util.Collections.emptyList(); + java.util.Map newSubst = paramMap(params, superArgs); + if (superStub != null) { + for (Ast.MethodDecl m : superStub.methods) { + if (m.name.equals(member) && m.isGetter == getter && !m.isSetter) { + return substTypeVars(m.returnType, newSubst); + } + } + } + subst = newSubst; + sup = superProg != null ? superProg.superclass + : superStub != null ? superStub.superclass : null; + } + return null; + } + + private String quote(String s) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.append('"').toString(); + } + + private String indentStr(int level) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < level; i++) { + sb.append(" "); + } + return sb.toString(); + } + + // ================================================================== + // Emission context + // ================================================================== + + private final class Ctx { + final ClassDecl currentClass; + /** The library whose top-level (lib-class) body is being emitted; set only when currentClass is null. */ + Library currentLibrary; + final Map imports = new TreeMap(); + final List> scopes = new ArrayList>(); + final List writers = new ArrayList(); + final java.util.Set boxedLocals = new HashSet(); + private final java.util.Set boxedActive = new HashSet(); + TypeRef methodReturnType; + TypeRef extensionSelfType; + boolean inAsyncBody; + /** Inside a hashCode/compareTo body: narrow each `return` value to Java int. */ + boolean narrowReturnToInt; + String syncStarList; // non-null inside a sync* body: the result-list temp + /** + * The expression being emitted as a statement, whose value nobody reads (see + * {@link #emitStatementExpr}); an assignment through a void setter only needs a + * value form when it is not this. + */ + Expr statementExpr; + /** An {@code x.p += 1} standing for {@code x.p++} used as a value: it yields the OLD value. */ + Assign postfixAssign; + TypeRef syncStarElem; // element type yielded by the enclosing sync* body + private int tempCounter; + + void markBoxed(String name) { + boxedActive.add(name); + } + + boolean isBoxed(String name) { + return boxedActive.contains(name); + } + + private final List cascadeTargets = new ArrayList(); + + void pushCascadeTarget(Out t) { + cascadeTargets.add(t); + } + + void popCascadeTarget() { + cascadeTargets.remove(cascadeTargets.size() - 1); + } + + Out cascadeTarget() { + return cascadeTargets.isEmpty() ? new Out("null", TypeRef.DYNAMIC) + : cascadeTargets.get(cascadeTargets.size() - 1); + } + + // Break targets: a Dart `switch` is lowered to a labeled Java block (not a + // Java switch), so a `break` targeting it needs the label; a `break` inside a + // loop stays bare. Entries: the switch's label, or null for a loop. + private final List breakTargets = new ArrayList(); + + void pushBreakTarget(String label) { + breakTargets.add(label); + } + + void popBreakTarget() { + breakTargets.remove(breakTargets.size() - 1); + } + + /** The label a bare {@code break} must carry (a switch block), or null when a loop. */ + String currentBreakLabel() { + return breakTargets.isEmpty() ? null : breakTargets.get(breakTargets.size() - 1); + } + + Ctx(ClassDecl currentClass) { + this.currentClass = currentClass; + } + + /** The library this context is emitting within (class owner, or the lib class itself). */ + Library library() { + if (currentClass != null) { + return currentClass.ownerLibrary; + } + return currentLibrary; + } + + void importClass(String fqcn) { + String simple = fqcn.substring(fqcn.lastIndexOf('.') + 1); + String existing = imports.get(simple); + if (existing == null) { + imports.put(simple, fqcn); + } + } + + final List> renameScopes = new ArrayList>(); + private int shadowCounter; + + void pushScope() { + scopes.add(new LinkedHashMap()); + renameScopes.add(new LinkedHashMap()); + } + + void popScope() { + scopes.remove(scopes.size() - 1); + renameScopes.remove(renameScopes.size() - 1); + } + + void declare(String name, TypeRef type) { + if (!scopes.isEmpty()) { + scopes.get(scopes.size() - 1).put(name, type); + } + } + + /** + * Declares a local, renaming when the Dart name would illegally + * shadow an enclosing Java local/param. Returns the Java name. + */ + String declareShadowSafe(String name, TypeRef type) { + String javaName = javaIdent(name); + if (lookup(name) != null) { + javaName = javaName + "$" + (shadowCounter++); + } + declare(name, type); + if (!javaName.equals(name) && !renameScopes.isEmpty()) { + renameScopes.get(renameScopes.size() - 1).put(name, javaName); + } + return javaName; + } + + String javaNameOf(String name) { + for (int i = renameScopes.size() - 1; i >= 0; i--) { + if (scopes.get(i).containsKey(name)) { + String renamed = renameScopes.get(i).get(name); + return renamed != null ? renamed : name; + } + } + return name; + } + + TypeRef lookup(String name) { + for (int i = scopes.size() - 1; i >= 0; i--) { + TypeRef t = scopes.get(i).get(name); + if (t != null) { + return t; + } + } + return null; + } + + /** + * Flow-based type promotions from {@code x is T} guards, e.g. inside + * {@code x is T && x.member} or an {@code if (x is T) { x.member }} then-branch. + * Maps a promoted local's Dart name to the narrowed type; reads emit a cast. + */ + private final Map promotions = new java.util.HashMap(); + + TypeRef promotedType(String name) { + return promotions.get(name); + } + + /** Applies a promotion, returning the prior value (possibly null) for later restore. */ + TypeRef pushPromotion(String name, TypeRef type) { + TypeRef prev = promotions.get(name); + promotions.put(name, type); + return prev; + } + + void restorePromotion(String name, TypeRef prev) { + if (prev == null) { + promotions.remove(name); + } else { + promotions.put(name, prev); + } + } + + String newTemp() { + return "$t" + (tempCounter++); + } + + Writer pushWriter(int indent) { + Writer w = new Writer(indent); + writers.add(w); + return w; + } + + String popWriter() { + Writer w = writers.remove(writers.size() - 1); + return w.sb.toString(); + } + + Writer writer() { + return writers.get(writers.size() - 1); + } + + int currentIndent() { + return writers.isEmpty() ? 1 : writer().indent; + } + + void indent(int delta) { + writer().indent += delta; + } + + final class Writer { + final StringBuilder sb = new StringBuilder(); + int indent; + + Writer(int indent) { + this.indent = indent; + } + + void line(String s) { + sb.append(indentStr(indent)).append(s).append('\n'); + } + + /** Appends lines another writer already indented (see {@link #emitLazyOperand}). */ + void raw(String lines) { + sb.append(lines); + } + } + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java new file mode 100644 index 00000000000..2c0c7af06ac --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/AstBuilder.java @@ -0,0 +1,2469 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.parser; + +import com.codename1.dart.transpiler.api.Diagnostics; +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.ast.Ast.*; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; + +import java.util.List; + +/** + * Converts the ANTLR Dart parse tree into the transpiler's own AST + * ({@link Ast}). This is the ONLY class that touches generated parser + * contexts — grammar upgrades are absorbed here. + * + *

Constructs outside the supported subset produce a source-positioned + * diagnostic instead of silently wrong output.

+ */ +public final class AstBuilder { + + private final Diagnostics diags; + private String file; + + public AstBuilder(Diagnostics diags) { + this.diags = diags; + } + + // ------------------------------------------------------------------ + // Entry points + // ------------------------------------------------------------------ + + public Library parse(String fileName, String source) { + this.file = fileName; + Dart2Lexer lexer = new Dart2Lexer(CharStreams.fromString(source)); + Dart2Parser parser = new Dart2Parser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + lexer.removeErrorListeners(); + BaseErrorListener listener = new BaseErrorListener() { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, + int charPositionInLine, String msg, RecognitionException e) { + diags.error(fileName, line, charPositionInLine, "E0001", "Syntax error: " + msg); + } + }; + parser.addErrorListener(listener); + lexer.addErrorListener(listener); + Dart2Parser.CompilationUnitContext unit = parser.compilationUnit(); + Library lib = new Library(); + lib.fileName = fileName; + pos(lib, unit); + if (unit.libraryDeclaration() != null) { + buildLibrary(unit.libraryDeclaration(), lib); + } + return lib; + } + + /** Parses a lone expression (used for string-interpolation fragments). */ + Expr parseExprFragment(String source, int line, int col) { + Dart2Lexer lexer = new Dart2Lexer(CharStreams.fromString(source)); + Dart2Parser parser = new Dart2Parser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + lexer.removeErrorListeners(); + final String f = file; + BaseErrorListener listener = new BaseErrorListener() { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int l, + int c, String msg, RecognitionException e) { + diags.error(f, line, col, "E0002", "Syntax error in string interpolation: " + msg); + } + }; + parser.addErrorListener(listener); + lexer.addErrorListener(listener); + return buildExpr(parser.expr()); + } + + // ------------------------------------------------------------------ + // Declarations + // ------------------------------------------------------------------ + + private void buildLibrary(Dart2Parser.LibraryDeclarationContext ctx, Library lib) { + for (Dart2Parser.ImportOrExportContext ie : ctx.importOrExport()) { + if (ie.libraryImport() != null) { + Dart2Parser.ImportSpecificationContext spec = ie.libraryImport().importSpecification(); + String uri = spec.configurableUri().getText(); + lib.imports.add(stripQuotes(uri)); + // `import '...' as prefix;` — record the prefix so prefixed member + // access (prefix.topLevelConst) resolves against the whole program. + if (spec.identifier() != null) { + lib.importPrefixes.add(spec.identifier().getText()); + lib.prefixImports.put(spec.identifier().getText(), stripQuotes(uri)); + } + } + } + List decls = ctx.topLevelDeclaration(); + List metas = ctx.metadata(); + for (int i = 0; i < decls.size(); i++) { + buildTopLevel(decls.get(i), metas.size() > i ? metas.get(i) : null, lib); + } + } + + private void buildTopLevel(Dart2Parser.TopLevelDeclarationContext ctx, + Dart2Parser.MetadataContext meta, Library lib) { + String javaName = annotationArg(meta, "JavaName"); + if (ctx.classDeclaration() != null) { + ClassDecl cd = buildClass(ctx.classDeclaration()); + if (cd != null) { + cd.javaName = javaName; + lib.classes.add(cd); + } + } else if (ctx.extensionDeclaration() != null) { + Dart2Parser.ExtensionDeclarationContext ext = ctx.extensionDeclaration(); + ClassDecl cd = new ClassDecl(); + pos(cd, ext); + cd.name = ext.identifier() != null ? ext.identifier().getText() + : "Ext$" + Integer.toHexString(ext.getStart().getStartIndex()); + cd.javaName = javaName; + cd.extensionOn = buildType(ext.type()); + java.util.List members = ext.classMemberDeclaration(); + java.util.List metas = ext.metadata(); + for (int i = 0; i < members.size(); i++) { + buildMember(members.get(i), metas.size() > i ? metas.get(i) : null, cd); + } + lib.classes.add(cd); + } else if (ctx.mixinDeclaration() != null) { + Dart2Parser.MixinDeclarationContext mx = ctx.mixinDeclaration(); + ClassDecl cd = new ClassDecl(); + pos(cd, mx); + cd.name = mx.typeIdentifier().getText(); + cd.isMixin = true; + cd.javaName = javaName; + cd.javaName = javaName; + java.util.List members = mx.classMemberDeclaration(); + java.util.List metas = mx.metadata(); + for (int i = 0; i < members.size(); i++) { + buildMember(members.get(i), metas.size() > i ? metas.get(i) : null, cd); + } + lib.classes.add(cd); + } else if (ctx.enumType() != null) { + EnumDecl ed = buildEnum(ctx.enumType()); + ed.javaName = javaName; + lib.enums.add(ed); + } else if (ctx.functionSignature() != null && ctx.EXTERNAL_() != null) { + FunctionDecl fn = new FunctionDecl(); + pos(fn, ctx); + buildFunctionSignature(ctx.functionSignature(), fn); + fn.isExternal = true; + fn.javaName = javaName; + lib.functions.add(fn); + } else if (ctx.functionSignature() != null && ctx.functionBody() != null) { + FunctionDecl fn = new FunctionDecl(); + pos(fn, ctx); + buildFunctionSignature(ctx.functionSignature(), fn); + fn.javaName = javaName; + buildFunctionBodyInto(ctx.functionBody(), fn); + lib.functions.add(fn); + } else if (ctx.initializedIdentifierList() != null || ctx.staticFinalDeclarationList() != null) { + // top-level variables become statics on the library class + TypeRef type = TypeRef.VAR; + if (ctx.type() != null) { + type = buildType(ctx.type()); + } else if (ctx.varOrType() != null && ctx.varOrType().type() != null) { + type = buildType(ctx.varOrType().type()); + } + boolean isFinal = ctx.FINAL_() != null || ctx.CONST_() != null; + if (ctx.initializedIdentifierList() != null) { + for (Dart2Parser.InitializedIdentifierContext ii : ctx.initializedIdentifierList().initializedIdentifier()) { + FieldDecl f = new FieldDecl(); + pos(f, ii); + f.name = ii.identifier().getText(); + f.type = type; + f.isFinal = isFinal; + f.isStatic = true; + f.javaName = javaName; + if (ii.expr() != null) { + f.initializer = buildExpr(ii.expr()); + } + lib.topLevelVars.add(f); + } + } else { + for (Dart2Parser.StaticFinalDeclarationContext sf : ctx.staticFinalDeclarationList().staticFinalDeclaration()) { + FieldDecl f = new FieldDecl(); + pos(f, sf); + f.name = sf.identifier().getText(); + f.type = type; + f.isFinal = true; + f.isStatic = true; + f.javaName = javaName; + f.initializer = buildExpr(sf.expr()); + lib.topLevelVars.add(f); + } + } + } else if (ctx.typeAlias() != null) { + TypedefDecl td = buildTypedef(ctx.typeAlias()); + if (td != null) { + lib.typedefs.add(td); + } + } else if (ctx.getterSignature() != null && ctx.functionBody() != null) { + // top-level getter: `T get x => ...;` -> a zero-arg static method on the library class + FunctionDecl fn = new FunctionDecl(); + pos(fn, ctx); + fn.name = ctx.getterSignature().identifier().getText(); + fn.returnType = ctx.getterSignature().type() != null + ? buildType(ctx.getterSignature().type()) : TypeRef.DYNAMIC; + fn.isGetter = true; + fn.javaName = javaName; + buildFunctionBodyInto(ctx.functionBody(), fn); + lib.functions.add(fn); + } else if (ctx.setterSignature() != null && ctx.functionBody() != null) { + // top-level setter: `set x(v) { ... }` -> a static void method on the library class + FunctionDecl fn = new FunctionDecl(); + pos(fn, ctx); + fn.name = ctx.setterSignature().identifier().getText(); + fn.returnType = TypeRef.VOID; + fn.isSetter = true; + fn.javaName = javaName; + buildParams(ctx.setterSignature().formalParameterList(), fn.params); + buildFunctionBodyInto(ctx.functionBody(), fn); + lib.functions.add(fn); + } else { + unsupported(ctx, "E0103", "Unsupported top-level declaration: " + snippet(ctx)); + } + } + + private TypedefDecl buildTypedef(Dart2Parser.TypeAliasContext ctx) { + TypedefDecl td = new TypedefDecl(); + pos(td, ctx); + if (ctx.functionTypeAlias() != null) { + // old-style: typedef ReturnType Name(params); + Dart2Parser.FunctionTypeAliasContext fta = ctx.functionTypeAlias(); + td.name = fta.functionPrefix().identifier().getText(); + td.returnType = fta.functionPrefix().type() != null + ? buildType(fta.functionPrefix().type()) : TypeRef.DYNAMIC; + td.paramTypes = new java.util.ArrayList(); + if (fta.formalParameterPart() != null + && fta.formalParameterPart().formalParameterList() != null) { + java.util.List ps = new java.util.ArrayList(); + buildParams(fta.formalParameterPart().formalParameterList(), ps); + for (Param p : ps) { + td.paramTypes.add(p.type == null ? TypeRef.DYNAMIC : p.type); + } + } + return td; + } + // new-style: typedef Name = ; + td.name = ctx.typeIdentifier().getText(); + if (ctx.typeParameters() != null) { + for (Dart2Parser.TypeParameterContext tp : ctx.typeParameters().typeParameter()) { + td.typeParams.add(tp.identifier().getText()); + } + } + Dart2Parser.TypeContext type = ctx.type(); + if (type != null && type.functionType() != null) { + fillFunctionTypedef(td, type.functionType()); + } else if (type != null) { + td.aliased = buildType(type); + } else { + td.aliased = TypeRef.DYNAMIC; + } + return td; + } + + /** Extracts the (paramTypes, returnType) signature of a {@code Ret Function(A, B)} type. */ + private void fillFunctionTypedef(TypedefDecl td, Dart2Parser.FunctionTypeContext ft) { + td.returnType = ft.typeNotFunction() != null + ? buildTypeNotFunction(ft.typeNotFunction()) : TypeRef.DYNAMIC; + td.paramTypes = new java.util.ArrayList(); + Dart2Parser.FunctionTypeTailsContext tails = ft.functionTypeTails(); + if (tails == null || tails.functionTypeTail() == null) { + return; + } + Dart2Parser.ParameterTypeListContext ptl = tails.functionTypeTail().parameterTypeList(); + if (ptl == null || ptl.normalParameterTypes() == null) { + return; + } + for (Dart2Parser.NormalParameterTypeContext npt + : ptl.normalParameterTypes().normalParameterType()) { + if (npt.type() != null) { + td.paramTypes.add(buildType(npt.type())); + } else if (npt.typedIdentifier() != null && npt.typedIdentifier().type() != null) { + td.paramTypes.add(buildType(npt.typedIdentifier().type())); + } else { + td.paramTypes.add(TypeRef.DYNAMIC); + } + } + } + + private ClassDecl buildClass(Dart2Parser.ClassDeclarationContext ctx) { + if (ctx.mixinApplicationClass() != null) { + unsupported(ctx, "E0104", "Mixin application classes are not supported yet (M4)"); + return null; + } + ClassDecl cd = new ClassDecl(); + pos(cd, ctx); + cd.name = ctx.typeIdentifier().getText(); + // NB: classModifiers is a (X)* rule, so each accessor returns a LIST — + // `!= null` would be true even when the modifier is absent. + Dart2Parser.ClassModifiersContext mods = ctx.classModifiers(); + cd.isAbstract = mods != null && !mods.ABSTRACT_().isEmpty(); + cd.isSealed = mods != null && !mods.SEALED_().isEmpty(); + if (cd.isSealed) { + // a sealed class is implicitly abstract in Dart + cd.isAbstract = true; + } + if (ctx.typeParameters() != null) { + for (Dart2Parser.TypeParameterContext tp : ctx.typeParameters().typeParameter()) { + cd.typeParams.add(tp.identifier().getText()); + } + } + if (ctx.superclass() != null) { + if (ctx.superclass().mixins() != null) { + for (Dart2Parser.TypeNotVoidContext t : ctx.superclass().mixins().typeNotVoidList().typeNotVoid()) { + cd.mixins.add(buildTypeNotVoid(t)); + } + } + if (ctx.superclass().typeNotVoid() != null) { + cd.superclass = buildTypeNotVoid(ctx.superclass().typeNotVoid()); + } + } + if (ctx.interfaces() != null) { + for (Dart2Parser.TypeNotVoidContext t : ctx.interfaces().typeNotVoidList().typeNotVoid()) { + cd.interfaces.add(buildTypeNotVoid(t)); + } + } + List members = ctx.classMemberDeclaration(); + List metas = ctx.metadata(); + for (int i = 0; i < members.size(); i++) { + buildMember(members.get(i), metas.size() > i ? metas.get(i) : null, cd); + } + return cd; + } + + private EnumDecl buildEnum(Dart2Parser.EnumTypeContext ctx) { + EnumDecl ed = new EnumDecl(); + pos(ed, ctx); + ed.name = ctx.identifier().getText(); + for (Dart2Parser.EnumEntryContext e : ctx.enumEntry()) { + // Dart 2.17 enhanced enums: the constant may carry constructor arguments + // (`material('...')`) or a named ctor (`x.named(...)`); we keep only the + // constant's own name. The first identifier is the constant. + ed.entries.add(e.identifier(0).getText()); + } + // Enhanced-enum body: methods / getters, fields and constructors declared after the + // trailing `;`. These share the class-member grammar, so build them into a throwaway + // ClassDecl and lift the parsed members onto the enum. + List members = ctx.classMemberDeclaration(); + if (members != null && !members.isEmpty()) { + List metas = ctx.metadata(); + ClassDecl holder = new ClassDecl(); + holder.name = ed.name; + for (int i = 0; i < members.size(); i++) { + buildMember(members.get(i), metas.size() > i ? metas.get(i) : null, holder); + } + ed.fields.addAll(holder.fields); + ed.methods.addAll(holder.methods); + ed.ctors.addAll(holder.ctors); + } + return ed; + } + + private void buildMember(Dart2Parser.ClassMemberDeclarationContext ctx, + Dart2Parser.MetadataContext meta, ClassDecl cd) { + boolean override = hasAnnotation(meta, "override"); + if (ctx.methodSignature() != null) { + buildMethodWithBody(ctx.methodSignature(), ctx.functionBody(), override, cd); + return; + } + Dart2Parser.DeclarationContext d = ctx.declaration(); + if (d == null) { + return; + } + if (d.constructorSignature() != null || d.constantConstructorSignature() != null) { + // constructor without body (ends in ';') + CtorDecl ctor = new CtorDecl(); + pos(ctor, d); + Dart2Parser.ConstructorSignatureContext sig; + if (d.constantConstructorSignature() != null) { + ctor.isConst = true; + sig = null; + buildCtorName(d.constantConstructorSignature().constructorName(), ctor, cd); + buildParams(d.constantConstructorSignature().formalParameterList(), ctor.params); + } else { + sig = d.constructorSignature(); + buildCtorName(sig.constructorName(), ctor, cd); + buildParams(sig.formalParameterList(), ctor.params); + } + if (d.initializers() != null) { + buildInitializers(d.initializers(), ctor); + } + if (d.redirection() != null) { + unsupported(d, "E0106", "Redirecting constructors are not supported yet (M2)"); + } + cd.ctors.add(ctor); + return; + } + if (d.redirectingFactoryConstructorSignature() != null) { + unsupported(d, "E0106", "Redirecting factory constructors are not supported yet"); + return; + } + if (d.factoryConstructorSignature() != null) { + // external/abstract factory (no body) — only meaningful in stubs + CtorDecl ctor = new CtorDecl(); + pos(ctor, d); + ctor.isFactory = true; + buildCtorName(d.factoryConstructorSignature().constructorName(), ctor, cd); + buildParams(d.factoryConstructorSignature().formalParameterList(), ctor.params); + cd.ctors.add(ctor); + return; + } + if (d.operatorSignature() != null) { + // abstract or external operator overload (e.g. `external Offset operator +(Offset o);`) + Dart2Parser.OperatorSignatureContext op = d.operatorSignature(); + String mangled = mangleOperator(op.operator().getText()); + if (mangled == null) { + unsupported(d, "E0109", "Unsupported operator overload: " + op.operator().getText()); + return; + } + MethodDecl m = new MethodDecl(); + pos(m, d); + m.isAbstract = true; + m.isOverride = override; + m.name = mangled; + m.returnType = op.type() != null ? buildType(op.type()) : TypeRef.VAR; + buildParams(op.formalParameterList(), m.params); + cd.methods.add(m); + return; + } + if (d.functionSignature() != null || d.getterSignature() != null || d.setterSignature() != null) { + // abstract or external member + MethodDecl m = new MethodDecl(); + pos(m, d); + m.isAbstract = true; + m.isStatic = d.STATIC_() != null; + m.isOverride = override; + if (d.functionSignature() != null) { + FunctionDecl tmp = new FunctionDecl(); + buildFunctionSignature(d.functionSignature(), tmp); + // Grammar ambiguity: a body-less constructor matches + // functionSignature first. `Vec(this.x, this.y);` arrives as + // name==class with no return type; `Vec.unit(...);` as + // returnType==class. Reclassify as constructors. + if (!m.isStatic && tmp.name.equals(cd.name) + && (tmp.returnType == null || tmp.returnType.is("var"))) { + CtorDecl ctor = new CtorDecl(); + pos(ctor, d); + ctor.params = tmp.params; + if (d.initializers() != null) { + buildInitializers(d.initializers(), ctor); + } + cd.ctors.add(ctor); + return; + } + if (!m.isStatic && tmp.returnType != null && tmp.returnType.is(cd.name) + && d.EXTERNAL_() == null) { + CtorDecl ctor = new CtorDecl(); + pos(ctor, d); + ctor.name = tmp.name; + ctor.params = tmp.params; + if (d.initializers() != null) { + buildInitializers(d.initializers(), ctor); + } + cd.ctors.add(ctor); + return; + } + m.name = tmp.name; + m.returnType = tmp.returnType; + m.params = tmp.params; + m.typeParams = tmp.typeParams; + } else if (d.getterSignature() != null) { + m.isGetter = true; + m.name = d.getterSignature().identifier().getText(); + m.returnType = d.getterSignature().type() != null ? buildType(d.getterSignature().type()) : TypeRef.DYNAMIC; + } else { + m.isSetter = true; + m.name = d.setterSignature().identifier().getText(); + m.returnType = TypeRef.VOID; + buildParams(d.setterSignature().formalParameterList(), m.params); + } + cd.methods.add(m); + return; + } + // field declarations + if (d.initializedIdentifierList() != null || d.staticFinalDeclarationList() != null) { + boolean isStatic = d.STATIC_() != null; + boolean isFinal = d.FINAL_() != null; + boolean isConst = d.CONST_() != null; + boolean isLate = d.LATE_() != null; + TypeRef type = TypeRef.VAR; + if (d.type() != null) { + type = buildType(d.type()); + } else if (d.varOrType() != null && d.varOrType().type() != null) { + type = buildType(d.varOrType().type()); + } + if (d.initializedIdentifierList() != null) { + for (Dart2Parser.InitializedIdentifierContext ii : d.initializedIdentifierList().initializedIdentifier()) { + FieldDecl f = new FieldDecl(); + pos(f, ii); + f.name = ii.identifier().getText(); + f.type = type; + f.isFinal = isFinal; + f.isConst = isConst; + f.isStatic = isStatic; + f.isLate = isLate; + if (ii.expr() != null) { + f.initializer = buildExpr(ii.expr()); + } + cd.fields.add(f); + } + } else { + for (Dart2Parser.StaticFinalDeclarationContext sf : d.staticFinalDeclarationList().staticFinalDeclaration()) { + FieldDecl f = new FieldDecl(); + pos(f, sf); + f.name = sf.identifier().getText(); + f.type = type; + f.isFinal = true; + f.isConst = isConst; + f.isStatic = isStatic; + f.initializer = buildExpr(sf.expr()); + cd.fields.add(f); + } + } + return; + } + unsupported(d, "E0108", "Unsupported class member: " + snippet(d)); + } + + private void buildCtorName(Dart2Parser.ConstructorNameContext name, CtorDecl ctor, ClassDecl cd) { + // constructorName : typeIdentifier (D identifier)? + if (name.identifier() != null) { + ctor.name = name.identifier().getText(); + } + } + + private void buildMethodWithBody(Dart2Parser.MethodSignatureContext sig, + Dart2Parser.FunctionBodyContext body, + boolean override, ClassDecl cd) { + if (sig.constructorSignature() != null) { + CtorDecl ctor = new CtorDecl(); + pos(ctor, sig); + buildCtorName(sig.constructorSignature().constructorName(), ctor, cd); + buildParams(sig.constructorSignature().formalParameterList(), ctor.params); + if (sig.initializers() != null) { + buildInitializers(sig.initializers(), ctor); + } + ctor.body = buildBodyBlock(body); + cd.ctors.add(ctor); + return; + } + if (sig.factoryConstructorSignature() != null) { + CtorDecl ctor = new CtorDecl(); + pos(ctor, sig); + ctor.isFactory = true; + buildCtorName(sig.factoryConstructorSignature().constructorName(), ctor, cd); + buildParams(sig.factoryConstructorSignature().formalParameterList(), ctor.params); + ctor.body = buildBodyBlock(body); + cd.ctors.add(ctor); + return; + } + if (sig.operatorSignature() != null) { + Dart2Parser.OperatorSignatureContext op = sig.operatorSignature(); + String mangled = mangleOperator(op.operator().getText()); + if (mangled == null) { + unsupported(sig, "E0109", "Unsupported operator overload: " + op.operator().getText()); + return; + } + MethodDecl m = new MethodDecl(); + pos(m, sig); + m.name = mangled; + m.isOverride = override; + m.returnType = op.type() != null ? buildType(op.type()) : TypeRef.VAR; + buildParams(op.formalParameterList(), m.params); + buildFunctionBodyIntoMethod(body, m); + cd.methods.add(m); + return; + } + MethodDecl m = new MethodDecl(); + pos(m, sig); + m.isStatic = sig.STATIC_() != null; + m.isOverride = override; + if (sig.functionSignature() != null) { + FunctionDecl tmp = new FunctionDecl(); + buildFunctionSignature(sig.functionSignature(), tmp); + m.name = tmp.name; + m.returnType = tmp.returnType; + m.params = tmp.params; + m.typeParams = tmp.typeParams; + } else if (sig.getterSignature() != null) { + m.isGetter = true; + m.name = sig.getterSignature().identifier().getText(); + m.returnType = sig.getterSignature().type() != null ? buildType(sig.getterSignature().type()) : TypeRef.VAR; + } else if (sig.setterSignature() != null) { + m.isSetter = true; + m.name = sig.setterSignature().identifier().getText(); + m.returnType = TypeRef.VOID; + buildParams(sig.setterSignature().formalParameterList(), m.params); + } + buildFunctionBodyIntoMethod(body, m); + cd.methods.add(m); + } + + private void buildInitializers(Dart2Parser.InitializersContext inits, CtorDecl ctor) { + for (Dart2Parser.InitializerListEntryContext e : inits.initializerListEntry()) { + if (e.fieldInitializer() != null) { + FieldInit fi = new FieldInit(); + pos(fi, e); + fi.field = e.fieldInitializer().identifier().getText(); + Dart2Parser.InitializerExpressionContext ie = e.fieldInitializer().initializerExpression(); + if (ie.conditionalExpression() != null) { + fi.value = buildConditional(ie.conditionalExpression()); + } else { + unsupported(ie, "E0110", "Cascades in initializer lists are not supported"); + } + ctor.fieldInits.add(fi); + } else if (e.SUPER_() != null) { + SuperInit si = new SuperInit(); + pos(si, e); + if (e.identifier() != null) { + si.namedCtor = e.identifier().getText(); + } + buildArgs(e.arguments(), si.args); + ctor.superInit = si; + } else if (e.assertion() != null) { + // assert(...) is a debug-only runtime check with no bearing on transpiled + // semantics; drop it silently (production Dart strips asserts too). + continue; + } else { + unsupported(e, "E0111", "Unsupported initializer-list entry: " + snippet(e)); + } + } + } + + private void buildFunctionSignature(Dart2Parser.FunctionSignatureContext sig, FunctionDecl fn) { + fn.returnType = sig.type() != null ? buildType(sig.type()) : TypeRef.VAR; + fn.name = sig.identifier().getText(); + if (sig.formalParameterPart().typeParameters() != null) { + for (Dart2Parser.TypeParameterContext tp + : sig.formalParameterPart().typeParameters().typeParameter()) { + fn.typeParams.add(tp.identifier().getText()); + } + } + buildParams(sig.formalParameterPart().formalParameterList(), fn.params); + } + + private void buildFunctionBodyInto(Dart2Parser.FunctionBodyContext body, FunctionDecl fn) { + if (body.SYNC_() != null && body.ST() != null) { + fn.isSyncStar = true; + } else { + fn.isAsync = checkBodyModifiers(body); + } + if (body.block() != null) { + fn.body = buildBlock(body.block()); + } else if (body.expr() != null) { + fn.exprBody = buildExpr(body.expr()); + } + } + + private void buildFunctionBodyIntoMethod(Dart2Parser.FunctionBodyContext body, MethodDecl m) { + if (body.SYNC_() != null && body.ST() != null) { + // sync* generator: lowered by the emitter to a list-collecting body. + m.isSyncStar = true; + } else { + m.isAsync = checkBodyModifiers(body); + } + if (body.block() != null) { + m.body = buildBlock(body.block()); + } else if (body.expr() != null) { + m.exprBody = buildExpr(body.expr()); + } + } + + /** Returns true when the body is async (plain `async`, not a generator). */ + private boolean checkBodyModifiers(Dart2Parser.FunctionBodyContext body) { + if (body.ST() != null) { + unsupported(body, "E0303", "async* generator bodies are not supported yet (M5)"); + } + if (body.NATIVE_() != null) { + unsupported(body, "E0113", "native bodies are not supported"); + } + return body.ASYNC_() != null; + } + + private void buildParams(Dart2Parser.FormalParameterListContext list, List out) { + if (list == null) { + return; + } + if (list.normalFormalParameters() != null) { + for (Dart2Parser.NormalFormalParameterContext p : list.normalFormalParameters().normalFormalParameter()) { + Param param = buildNormalParam(p.normalFormalParameterNoMetadata()); + if (param != null) { + out.add(param); + } + } + } + if (list.optionalOrNamedFormalParameters() != null) { + Dart2Parser.OptionalOrNamedFormalParametersContext opt = list.optionalOrNamedFormalParameters(); + if (opt.namedFormalParameters() != null) { + for (Dart2Parser.DefaultNamedParameterContext dn : opt.namedFormalParameters().defaultNamedParameter()) { + Param param = buildNormalParam(dn.normalFormalParameterNoMetadata()); + if (param == null) { + continue; + } + param.named = true; + param.required = dn.REQUIRED_() != null; + if (dn.expr() != null) { + param.defaultValue = buildExpr(dn.expr()); + } + out.add(param); + } + } else if (opt.optionalPositionalFormalParameters() != null) { + for (Dart2Parser.DefaultFormalParameterContext dp + : opt.optionalPositionalFormalParameters().defaultFormalParameter()) { + Param param = buildNormalParam(dp.normalFormalParameter().normalFormalParameterNoMetadata()); + if (param == null) { + continue; + } + if (dp.expr() != null) { + param.defaultValue = buildExpr(dp.expr()); + } + out.add(param); + } + } + } + } + + private Param buildNormalParam(Dart2Parser.NormalFormalParameterNoMetadataContext ctx) { + Param p = new Param(); + pos(p, ctx); + if (ctx.fieldFormalParameter() != null) { + Dart2Parser.FieldFormalParameterContext f = ctx.fieldFormalParameter(); + p.isThis = true; + p.name = f.identifier().getText(); + p.type = f.finalConstVarOrType() != null ? buildFinalConstVarOrType(f.finalConstVarOrType()) : TypeRef.VAR; + return p; + } + if (ctx.superFormalParameter() != null) { + Dart2Parser.SuperFormalParameterContext f = ctx.superFormalParameter(); + p.isSuper = true; + p.name = f.identifier().getText(); + p.type = f.finalConstVarOrType() != null ? buildFinalConstVarOrType(f.finalConstVarOrType()) : TypeRef.VAR; + return p; + } + if (ctx.simpleFormalParameter() != null) { + Dart2Parser.SimpleFormalParameterContext s = ctx.simpleFormalParameter(); + if (s.declaredIdentifier() != null) { + p.name = s.declaredIdentifier().identifier().getText(); + p.type = buildFinalConstVarOrType(s.declaredIdentifier().finalConstVarOrType()); + } else { + p.name = s.identifier().getText(); + p.type = TypeRef.VAR; + } + return p; + } + if (ctx.functionFormalParameter() != null) { + unsupported(ctx, "E0114", "Function-typed parameter syntax is not supported yet; use a typedef-style type"); + return null; + } + return null; + } + + private TypeRef buildFinalConstVarOrType(Dart2Parser.FinalConstVarOrTypeContext ctx) { + if (ctx.type() != null) { + return buildType(ctx.type()); + } + if (ctx.varOrType() != null && ctx.varOrType().type() != null) { + return buildType(ctx.varOrType().type()); + } + return TypeRef.VAR; + } + + // ------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------ + + private TypeRef buildType(Dart2Parser.TypeContext ctx) { + if (ctx.functionType() != null) { + // Inline function type (e.g. `void Function(int)`): preserve the signature so + // codegen can render a real Funcs.* SAM instead of falling back to Object. + TypeRef t = new TypeRef("Function"); + pos(t, ctx); + t.nullable = ctx.QU() != null; + fillFunctionSignature(t, ctx.functionType()); + return t; + } + return buildTypeNotFunction(ctx.typeNotFunction()); + } + + /** Captures the (paramTypes, returnType) of an inline {@code Ret Function(A, B)} type onto a TypeRef. */ + private void fillFunctionSignature(TypeRef t, Dart2Parser.FunctionTypeContext ft) { + t.funcReturn = ft.typeNotFunction() != null + ? buildTypeNotFunction(ft.typeNotFunction()) : TypeRef.DYNAMIC; + t.funcParams = new java.util.ArrayList(); + Dart2Parser.FunctionTypeTailsContext tails = ft.functionTypeTails(); + if (tails == null || tails.functionTypeTail() == null) { + return; + } + Dart2Parser.ParameterTypeListContext ptl = tails.functionTypeTail().parameterTypeList(); + if (ptl == null || ptl.normalParameterTypes() == null) { + return; + } + for (Dart2Parser.NormalParameterTypeContext npt + : ptl.normalParameterTypes().normalParameterType()) { + if (npt.type() != null) { + t.funcParams.add(buildType(npt.type())); + } else if (npt.typedIdentifier() != null && npt.typedIdentifier().type() != null) { + t.funcParams.add(buildType(npt.typedIdentifier().type())); + } else { + t.funcParams.add(TypeRef.DYNAMIC); + } + } + } + + private TypeRef buildTypeNotFunction(Dart2Parser.TypeNotFunctionContext ctx) { + if (ctx.VOID_() != null) { + return TypeRef.VOID; + } + return buildTypeNotVoidNotFunction(ctx.typeNotVoidNotFunction()); + } + + private TypeRef buildTypeNotVoid(Dart2Parser.TypeNotVoidContext ctx) { + if (ctx.functionType() != null) { + TypeRef t = new TypeRef("Function"); + pos(t, ctx); + fillFunctionSignature(t, ctx.functionType()); + return t; + } + return buildTypeNotVoidNotFunction(ctx.typeNotVoidNotFunction()); + } + + private TypeRef buildTypeNotVoidNotFunction(Dart2Parser.TypeNotVoidNotFunctionContext ctx) { + if (ctx.typeName() == null) { + TypeRef t = new TypeRef("Function"); + pos(t, ctx); + return t; + } + TypeRef t = new TypeRef(ctx.typeName().getText()); + pos(t, ctx); + if (ctx.typeArguments() != null) { + for (Dart2Parser.TypeContext a : ctx.typeArguments().typeList().type()) { + t.args.add(buildType(a)); + } + } + t.nullable = ctx.QU() != null; + return t; + } + + // ------------------------------------------------------------------ + // Statements + // ------------------------------------------------------------------ + + private Block buildBodyBlock(Dart2Parser.FunctionBodyContext body) { + if (body == null) { + return null; + } + checkBodyModifiers(body); + if (body.block() != null) { + return buildBlock(body.block()); + } + if (body.expr() != null) { + Block b = new Block(); + pos(b, body); + ExprStmt es = new ExprStmt(); + pos(es, body); + es.expr = buildExpr(body.expr()); + b.statements.add(es); + return b; + } + return null; + } + + private Block buildBlock(Dart2Parser.BlockContext ctx) { + Block b = new Block(); + pos(b, ctx); + if (ctx.statements() != null) { + for (Dart2Parser.StatementContext s : ctx.statements().statement()) { + Stmt st = buildStatement(s); + if (st != null) { + b.statements.add(st); + } + } + } + return b; + } + + private Stmt buildStatement(Dart2Parser.StatementContext ctx) { + if (ctx == null) { + return null; + } + Dart2Parser.NonLabelledStatementContext s = ctx.nonLabelledStatement(); + if (s == null) { + // parser error-recovery can leave an empty statement node; a preceding + // syntax (E0001) diagnostic already flags the real cause. + return null; + } + if (s.block() != null) { + return buildBlock(s.block()); + } + if (s.localVariableDeclaration() != null) { + return buildLocalVar(s.localVariableDeclaration()); + } + if (s.expressionStatement() != null) { + if (s.expressionStatement().expr() == null) { + return null; + } + ExprStmt es = new ExprStmt(); + pos(es, s); + es.expr = buildExpr(s.expressionStatement().expr()); + return es; + } + if (s.yieldStatement() != null) { + YieldStmt y = new YieldStmt(); + pos(y, s); + y.value = buildExpr(s.yieldStatement().expr()); + return y; + } + if (s.yieldEachStatement() != null) { + YieldStmt y = new YieldStmt(); + pos(y, s); + y.star = true; + y.value = buildExpr(s.yieldEachStatement().expr()); + return y; + } + if (s.returnStatement() != null) { + ReturnStmt r = new ReturnStmt(); + pos(r, s); + if (s.returnStatement().expr() != null) { + r.value = buildExpr(s.returnStatement().expr()); + } + return r; + } + if (s.ifStatement() != null) { + IfStmt i = new IfStmt(); + pos(i, s); + i.condition = buildExpr(s.ifStatement().expr()); + // Dart 3 if-case: `if (expr case pattern [when guard]) ...` + if (s.ifStatement().guardedPattern() != null) { + Dart2Parser.GuardedPatternContext gp = s.ifStatement().guardedPattern(); + i.casePattern = buildPattern(gp.pattern()); + if (gp.WHEN_() != null && gp.expr() != null) { + i.caseGuard = buildExpr(gp.expr()); + } + } + i.thenStmt = buildStatement(s.ifStatement().statement(0)); + if (s.ifStatement().statement().size() > 1) { + i.elseStmt = buildStatement(s.ifStatement().statement(1)); + } + return i; + } + if (s.whileStatement() != null) { + WhileStmt w = new WhileStmt(); + pos(w, s); + w.condition = buildExpr(s.whileStatement().expr()); + w.body = buildStatement(s.whileStatement().statement()); + return w; + } + if (s.forStatement() != null) { + return buildFor(s.forStatement()); + } + if (s.tryStatement() != null) { + return buildTry(s.tryStatement()); + } + if (s.breakStatement() != null) { + BreakStmt b = new BreakStmt(); + pos(b, s); + return b; + } + if (s.continueStatement() != null) { + ContinueStmt c = new ContinueStmt(); + pos(c, s); + return c; + } + if (s.switchStatement() != null) { + return buildSwitch(s.switchStatement()); + } + if (s.assertStatement() != null) { + // assert(...) is a debug-only runtime check with no bearing on transpiled + // semantics; drop it (production Dart strips asserts too), mirroring the + // initializer-list assert case. Blocks skip null statements. + return null; + } + if (s.localFunctionDeclaration() != null) { + return buildLocalFunction(s.localFunctionDeclaration()); + } + unsupported(s, "E0115", "Unsupported statement: " + snippet(s)); + return null; + } + + /** A nested function declaration: `Ret name(params) { ... }` inside a body. */ + private Stmt buildLocalFunction(Dart2Parser.LocalFunctionDeclarationContext ctx) { + LocalFunc lf = new LocalFunc(); + pos(lf, ctx); + Dart2Parser.FunctionSignatureContext sig = ctx.functionSignature(); + lf.returnType = sig.type() != null ? buildType(sig.type()) : TypeRef.VAR; + lf.name = sig.identifier().getText(); + buildParams(sig.formalParameterPart().formalParameterList(), lf.params); + Dart2Parser.FunctionBodyContext body = ctx.functionBody(); + lf.isAsync = checkBodyModifiers(body); + if (body.block() != null) { + lf.body = buildBlock(body.block()); + } else if (body.expr() != null) { + lf.exprBody = buildExpr(body.expr()); + } + return lf; + } + + // ------------------------------------------------------------------ + // Dart 3: switch statements / expressions and patterns + // ------------------------------------------------------------------ + + private Stmt buildSwitch(Dart2Parser.SwitchStatementContext ctx) { + SwitchStmt sw = new SwitchStmt(); + pos(sw, ctx); + sw.subject = buildExpr(ctx.expr()); + for (Dart2Parser.SwitchCaseContext cc : ctx.switchCase()) { + SwitchCase c = new SwitchCase(); + pos(c, cc); + c.pattern = buildPattern(cc.guardedPattern().pattern()); + if (cc.guardedPattern().WHEN_() != null && cc.guardedPattern().expr() != null) { + c.guard = buildExpr(cc.guardedPattern().expr()); + } + addStatements(cc.statements(), c.body); + sw.cases.add(c); + } + if (ctx.defaultCase() != null) { + SwitchCase c = new SwitchCase(); + pos(c, ctx.defaultCase()); + c.isDefault = true; + addStatements(ctx.defaultCase().statements(), c.body); + sw.cases.add(c); + } + return sw; + } + + private void addStatements(Dart2Parser.StatementsContext ctx, List out) { + if (ctx == null) { + return; + } + for (Dart2Parser.StatementContext st : ctx.statement()) { + Stmt s = buildStatement(st); + if (s != null) { + out.add(s); + } + } + } + + private Expr buildSwitchExpr(Dart2Parser.SwitchExpressionContext ctx) { + SwitchExpr sw = new SwitchExpr(); + pos(sw, ctx); + sw.subject = buildExpr(ctx.expr()); + for (Dart2Parser.SwitchExpressionCaseContext ec : ctx.switchExpressionCase()) { + SwitchExprCase c = new SwitchExprCase(); + pos(c, ec); + c.pattern = buildPattern(ec.guardedPattern().pattern()); + if (ec.guardedPattern().WHEN_() != null && ec.guardedPattern().expr() != null) { + c.guard = buildExpr(ec.guardedPattern().expr()); + } + c.value = buildExpr(ec.expr()); + if (c.guard == null && c.pattern instanceof VariablePattern + && ((VariablePattern) c.pattern).wildcard) { + c.isDefault = true; + } + sw.cases.add(c); + } + return sw; + } + + private Pattern buildPattern(Dart2Parser.PatternContext ctx) { + return buildOrPattern(ctx.logicalOrPattern()); + } + + private Pattern buildOrPattern(Dart2Parser.LogicalOrPatternContext ctx) { + List ands = ctx.logicalAndPattern(); + if (ands.size() == 1) { + return buildAndPattern(ands.get(0)); + } + OrPattern or = new OrPattern(); + pos(or, ctx); + for (Dart2Parser.LogicalAndPatternContext a : ands) { + or.alternatives.add(buildAndPattern(a)); + } + return or; + } + + private Pattern buildAndPattern(Dart2Parser.LogicalAndPatternContext ctx) { + List rels = ctx.relationalPattern(); + if (rels.size() == 1) { + return buildRelationalPattern(rels.get(0)); + } + AndPattern and = new AndPattern(); + pos(and, ctx); + for (Dart2Parser.RelationalPatternContext r : rels) { + and.parts.add(buildRelationalPattern(r)); + } + return and; + } + + private Pattern buildRelationalPattern(Dart2Parser.RelationalPatternContext ctx) { + if (ctx.unaryPattern() != null) { + return buildUnaryPattern(ctx.unaryPattern()); + } + RelationalPattern r = new RelationalPattern(); + pos(r, ctx); + if (ctx.EE() != null) { + r.op = "=="; + } else if (ctx.NE() != null) { + r.op = "!="; + } else if (ctx.LTE() != null) { + r.op = "<="; + } else if (ctx.GT() != null && ctx.EQ() != null) { + r.op = ">="; + } else if (ctx.LT() != null) { + r.op = "<"; + } else { + r.op = ">"; + } + r.operand = buildBitwiseOr(ctx.bitwiseOrExpression()); + return r; + } + + private Pattern buildUnaryPattern(Dart2Parser.UnaryPatternContext ctx) { + Pattern p = buildPrimaryPattern(ctx.primaryPattern()); + if (ctx.AS_() != null && ctx.type() != null) { + CastPattern c = new CastPattern(); + pos(c, ctx); + c.inner = p; + c.type = buildType(ctx.type()); + return c; + } + return p; + } + + private Pattern buildPrimaryPattern(Dart2Parser.PrimaryPatternContext ctx) { + if (ctx.constantPattern() != null) { + ConstantPattern c = new ConstantPattern(); + pos(c, ctx); + Dart2Parser.ConstantPatternContext cp = ctx.constantPattern(); + c.value = parseExprFragment(cp.getText(), cp.getStart().getLine(), + cp.getStart().getCharPositionInLine()); + return c; + } + if (ctx.objectPattern() != null) { + return buildObjectPattern(ctx.objectPattern()); + } + if (ctx.recordPattern() != null) { + return buildRecordPattern(ctx.recordPattern()); + } + if (ctx.listPattern() != null) { + return buildListPattern(ctx.listPattern()); + } + if (ctx.variablePattern() != null) { + return buildVariablePattern(ctx.variablePattern()); + } + if (ctx.pattern() != null) { + return buildPattern(ctx.pattern()); + } + unsupported(ctx, "E0430", "Unsupported pattern: " + snippet(ctx)); + return null; + } + + private Pattern buildVariablePattern(Dart2Parser.VariablePatternContext ctx) { + VariablePattern v = new VariablePattern(); + pos(v, ctx); + v.name = ctx.identifier().getText(); + if ("_".equals(v.name)) { + v.wildcard = true; + } + if (ctx.type() != null) { + v.type = buildType(ctx.type()); + } + return v; + } + + private Pattern buildObjectPattern(Dart2Parser.ObjectPatternContext ctx) { + ObjectPattern o = new ObjectPattern(); + pos(o, ctx); + o.type = new TypeRef(ctx.typeName().getText()); + if (ctx.typeArguments() != null) { + for (Dart2Parser.TypeContext t : ctx.typeArguments().typeList().type()) { + o.type.args.add(buildType(t)); + } + } + for (Dart2Parser.PatternFieldContext pf : ctx.patternField()) { + o.fields.add(buildPatternField(pf)); + } + return o; + } + + private Pattern buildRecordPattern(Dart2Parser.RecordPatternContext ctx) { + RecordPattern r = new RecordPattern(); + pos(r, ctx); + for (Dart2Parser.PatternFieldContext pf : ctx.patternField()) { + r.fields.add(buildPatternField(pf)); + } + return r; + } + + private Pattern buildListPattern(Dart2Parser.ListPatternContext ctx) { + ListPattern l = new ListPattern(); + pos(l, ctx); + for (Dart2Parser.PatternContext p : ctx.pattern()) { + l.elements.add(buildPattern(p)); + } + return l; + } + + private Expr buildRecordLit(Dart2Parser.RecordLiteralContext ctx) { + RecordLit r = new RecordLit(); + pos(r, ctx); + // 2nd grammar alt: a leading `identifier : expr` named field + if (ctx.identifier() != null && ctx.expr() != null) { + RecordField f = new RecordField(); + pos(f, ctx); + f.name = ctx.identifier().getText(); + f.value = buildExpr(ctx.expr()); + r.fields.add(f); + } + for (Dart2Parser.RecordFieldContext rf : ctx.recordField()) { + RecordField f = new RecordField(); + pos(f, rf); + if (rf.identifier() != null) { + f.name = rf.identifier().getText(); + } + f.value = buildExpr(rf.expr()); + r.fields.add(f); + } + return r; + } + + private PatternField buildPatternField(Dart2Parser.PatternFieldContext ctx) { + PatternField f = new PatternField(); + pos(f, ctx); + if (ctx.identifier() != null) { + f.name = ctx.identifier().getText(); + } + f.pattern = buildPattern(ctx.pattern()); + // `Circle(:var radius)` shorthand — a colon with no name defaults to the bound variable's + // name. This must NOT fire for positional record fields like `(var x, var y)`, which have no + // colon and stay positional. + if (f.name == null && ctx.CO() != null && f.pattern instanceof VariablePattern) { + f.name = ((VariablePattern) f.pattern).name; + } + return f; + } + + private Stmt buildTry(Dart2Parser.TryStatementContext ctx) { + TryStmt t = new TryStmt(); + pos(t, ctx); + t.tryBlock = buildBlock(ctx.block()); + if (ctx.onPart() != null) { + for (Dart2Parser.OnPartContext op : ctx.onPart()) { + CatchClause cc = new CatchClause(); + pos(cc, op); + if (op.typeNotVoid() != null) { + cc.onType = buildTypeNotVoid(op.typeNotVoid()); + } + if (op.catchPart() != null) { + cc.exceptionVar = op.catchPart().identifier(0).getText(); + if (op.catchPart().identifier().size() > 1) { + cc.stackVar = op.catchPart().identifier(1).getText(); + } + } + cc.body = buildBlock(op.block()); + t.catches.add(cc); + } + } + if (ctx.finallyPart() != null) { + t.finallyBlock = buildBlock(ctx.finallyPart().block()); + } + return t; + } + + private Stmt buildLocalVar(Dart2Parser.LocalVariableDeclarationContext ctx) { + Dart2Parser.InitializedVariableDeclarationContext iv = ctx.initializedVariableDeclaration(); + Dart2Parser.DeclaredIdentifierContext di = iv.declaredIdentifier(); + // `await x;` on a bare variable is ambiguous in the grammar with a declaration + // of a variable x of type `await`, and the parser takes the declaration. Dart + // does not: await is reserved in an async body, and no type can be named it. + // Read as a declaration it was emitted as `await x = null;`, which does not + // compile, so an `await someFuture;` statement broke the whole build. + Dart2Parser.FinalConstVarOrTypeContext fct = di.finalConstVarOrType(); + if (iv.expr() == null && iv.initializedIdentifier().isEmpty() + && fct.FINAL_() == null && fct.CONST_() == null && fct.LATE_() == null + && "await".equals(fct.getText())) { + Ident operand = new Ident(); + pos(operand, di.identifier()); + operand.name = di.identifier().getText(); + AwaitExpr aw = new AwaitExpr(); + pos(aw, ctx); + aw.operand = operand; + ExprStmt es = new ExprStmt(); + pos(es, ctx); + es.expr = aw; + return es; + } + VarDeclStmt v = new VarDeclStmt(); + pos(v, ctx); + v.name = di.identifier().getText(); + v.type = buildFinalConstVarOrType(di.finalConstVarOrType()); + v.isFinal = di.finalConstVarOrType().FINAL_() != null || di.finalConstVarOrType().CONST_() != null; + if (iv.expr() != null) { + v.initializer = buildExpr(iv.expr()); + } + if (iv.initializedIdentifier().isEmpty()) { + return v; + } + // int a = 1, b = 2; — group of sibling declarations sharing the type + VarDeclGroup group = new VarDeclGroup(); + pos(group, ctx); + group.decls.add(v); + for (Dart2Parser.InitializedIdentifierContext ii : iv.initializedIdentifier()) { + VarDeclStmt extra = new VarDeclStmt(); + pos(extra, ii); + extra.name = ii.identifier().getText(); + extra.type = v.type; + extra.isFinal = v.isFinal; + if (ii.expr() != null) { + extra.initializer = buildExpr(ii.expr()); + } + group.decls.add(extra); + } + return group; + } + + private Stmt buildFor(Dart2Parser.ForStatementContext ctx) { + if (ctx.AWAIT_() != null) { + unsupported(ctx, "E0302", "await for is not supported yet (M3)"); + return null; + } + Dart2Parser.ForLoopPartsContext parts = ctx.forLoopParts(); + if (parts.IN_() != null) { + ForInStmt fi = new ForInStmt(); + pos(fi, ctx); + if (parts.pattern() != null) { + // Dart 3 pattern for-in: `for (final (a, b) in xs)` / `for (var [x] in xs)` + fi.pattern = buildPattern(parts.pattern()); + fi.varType = TypeRef.VAR; + } else if (parts.declaredIdentifier() != null) { + fi.varName = parts.declaredIdentifier().identifier().getText(); + fi.varType = buildFinalConstVarOrType(parts.declaredIdentifier().finalConstVarOrType()); + } else { + fi.varName = parts.identifier().getText(); + fi.varType = TypeRef.VAR; + } + fi.iterable = buildExpr(parts.expr()); + fi.body = buildStatement(ctx.statement()); + return fi; + } + ForStmt f = new ForStmt(); + pos(f, ctx); + Dart2Parser.ForInitializerStatementContext init = parts.forInitializerStatement(); + if (init != null) { + if (init.localVariableDeclaration() != null) { + f.init = buildLocalVar(init.localVariableDeclaration()); + } else if (init.expr() != null) { + ExprStmt es = new ExprStmt(); + pos(es, init); + es.expr = buildExpr(init.expr()); + f.init = es; + } + } + if (parts.expr() != null) { + f.condition = buildExpr(parts.expr()); + } + if (parts.expressionList() != null) { + for (Dart2Parser.ExprContext e : parts.expressionList().expr()) { + f.updates.add(buildExpr(e)); + } + } + f.body = buildStatement(ctx.statement()); + return f; + } + + // ------------------------------------------------------------------ + // Expressions + // ------------------------------------------------------------------ + + private Expr buildExpr(Dart2Parser.ExprContext ctx) { + if (ctx == null) { + return errExpr(null); + } + if (ctx.assignableExpression() != null && ctx.assignmentOperator() != null) { + Assign a = new Assign(); + pos(a, ctx); + a.lhs = buildAssignable(ctx.assignableExpression()); + a.op = ctx.assignmentOperator().getText(); + a.rhs = buildExpr(ctx.expr()); + return arrowBodyAssign(a); + } + if (ctx.conditionalExpression() != null) { + return buildConditional(ctx.conditionalExpression()); + } + if (ctx.cascade() != null) { + return buildCascade(ctx.cascade()); + } + if (ctx.throwExpression() != null) { + ThrowExpr t = new ThrowExpr(); + pos(t, ctx); + t.value = buildExpr(ctx.throwExpression().expr()); + return t; + } + return errExpr(ctx); + } + + /** + * a..b(x)..c = y — flattens the left-recursive cascade chain; each + * section becomes an Expr tree rooted at a CascadeTarget marker that + * the emitter substitutes with the once-evaluated receiver temp. + */ + private Expr buildCascade(Dart2Parser.CascadeContext ctx) { + // walk down the left recursion collecting sections in source order + java.util.ArrayList sections = + new java.util.ArrayList(); + Dart2Parser.CascadeContext cur = ctx; + while (cur.cascade() != null) { + sections.add(0, cur.cascadeSection()); + cur = cur.cascade(); + } + sections.add(0, cur.cascadeSection()); + Cascade cas = new Cascade(); + pos(cas, ctx); + cas.nullAware = cur.QUDD() != null; + cas.target = buildConditional(cur.conditionalExpression()); + for (Dart2Parser.CascadeSectionContext s : sections) { + cas.sections.add(buildCascadeSection(s)); + } + return cas; + } + + private Expr buildCascadeSection(Dart2Parser.CascadeSectionContext ctx) { + CascadeTarget marker = new CascadeTarget(); + pos(marker, ctx); + Expr base; + Dart2Parser.CascadeSelectorContext sel = ctx.cascadeSelector(); + if (sel.identifier() != null) { + PropertyGet pg = new PropertyGet(); + pos(pg, sel); + pg.target = marker; + pg.name = sel.identifier().getText(); + base = pg; + } else { + IndexGet ig = new IndexGet(); + pos(ig, sel); + ig.target = marker; + ig.index = buildExpr(sel.expr()); + base = ig; + } + Dart2Parser.CascadeSectionTailContext tail = ctx.cascadeSectionTail(); + if (tail.selector() != null) { + for (Dart2Parser.SelectorContext s : tail.selector()) { + base = applySelector(base, s); + } + } + if (tail.assignableSelector() != null) { + base = applyAssignableSelector(base, tail.assignableSelector()); + } + if (tail.cascadeAssignment() != null) { + Assign a = new Assign(); + pos(a, tail); + a.lhs = base; + a.op = tail.cascadeAssignment().assignmentOperator().getText(); + a.rhs = buildExprWithoutCascade(tail.cascadeAssignment().expressionWithoutCascade()); + return a; + } + return base; + } + + /** + * {@code () => o.p = v} is a closure whose body assigns, but the grammar's + * {@code expr} alternatives are ambiguous there and ANTLR takes the first: an + * assignment whose target is {@code (() => o).p}. Re-associates that shape -- a + * selector chain rooted at an unparenthesized arrow closure -- into the closure + * with {@code o.p = v} as its body, which is the only reading Dart has. + */ + private static Expr arrowBodyAssign(Assign a) { + Expr holder = null; + Expr e = a.lhs; + while (true) { + Expr next; + if (e instanceof PropertyGet) { + next = ((PropertyGet) e).target; + } else if (e instanceof IndexGet) { + next = ((IndexGet) e).target; + } else if (e instanceof Call) { + next = ((Call) e).target; + } else { + break; + } + if (next == null) { + return a; + } + holder = e; + e = next; + } + if (holder == null || !(e instanceof Lambda) || ((Lambda) e).exprBody == null) { + return a; + } + Lambda l = (Lambda) e; + if (holder instanceof PropertyGet) { + ((PropertyGet) holder).target = l.exprBody; + } else if (holder instanceof IndexGet) { + ((IndexGet) holder).target = l.exprBody; + } else { + ((Call) holder).target = l.exprBody; + } + l.exprBody = arrowBodyAssign(a); + return l; + } + + private Expr buildExprWithoutCascade(Dart2Parser.ExpressionWithoutCascadeContext ctx) { + if (ctx.assignableExpression() != null && ctx.assignmentOperator() != null) { + Assign a = new Assign(); + pos(a, ctx); + a.lhs = buildAssignable(ctx.assignableExpression()); + a.op = ctx.assignmentOperator().getText(); + a.rhs = buildExprWithoutCascade(ctx.expressionWithoutCascade()); + return arrowBodyAssign(a); + } + if (ctx.conditionalExpression() != null) { + return buildConditional(ctx.conditionalExpression()); + } + unsupported(ctx, "E0118", "throw expressions are not supported yet (M2)"); + return errExpr(ctx); + } + + private Expr buildAssignable(Dart2Parser.AssignableExpressionContext ctx) { + if (ctx.identifier() != null) { + Ident id = new Ident(); + pos(id, ctx); + id.name = ctx.identifier().getText(); + return id; + } + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0119", "Assignment through 'super' is not supported"); + return errExpr(ctx); + } + // primary assignableSelectorPart : selector* assignableSelector + Expr base = buildPrimary(ctx.primary()); + Dart2Parser.AssignableSelectorPartContext part = ctx.assignableSelectorPart(); + for (Dart2Parser.SelectorContext s : part.selector()) { + base = applySelector(base, s); + } + return applyAssignableSelector(base, part.assignableSelector()); + } + + private Expr buildConditional(Dart2Parser.ConditionalExpressionContext ctx) { + Expr cond = buildIfNull(ctx.ifNullExpression()); + if (ctx.expressionWithoutCascade() != null && !ctx.expressionWithoutCascade().isEmpty()) { + Conditional c = new Conditional(); + pos(c, ctx); + c.condition = cond; + c.thenExpr = buildExprWithoutCascade(ctx.expressionWithoutCascade(0)); + c.elseExpr = buildExprWithoutCascade(ctx.expressionWithoutCascade(1)); + return c; + } + return cond; + } + + private Expr buildIfNull(Dart2Parser.IfNullExpressionContext ctx) { + Expr left = buildLogicalOr(ctx.logicalOrExpression(0)); + for (int i = 1; i < ctx.logicalOrExpression().size(); i++) { + Binary b = new Binary(); + pos(b, ctx); + b.left = left; + b.op = "??"; + b.right = buildLogicalOr(ctx.logicalOrExpression(i)); + left = b; + } + return left; + } + + private Expr buildLogicalOr(Dart2Parser.LogicalOrExpressionContext ctx) { + Expr left = buildLogicalAnd(ctx.logicalAndExpression(0)); + for (int i = 1; i < ctx.logicalAndExpression().size(); i++) { + left = binary(ctx, left, "||", buildLogicalAnd(ctx.logicalAndExpression(i))); + } + return left; + } + + private Expr buildLogicalAnd(Dart2Parser.LogicalAndExpressionContext ctx) { + Expr left = buildEquality(ctx.equalityExpression(0)); + for (int i = 1; i < ctx.equalityExpression().size(); i++) { + left = binary(ctx, left, "&&", buildEquality(ctx.equalityExpression(i))); + } + return left; + } + + private Expr buildEquality(Dart2Parser.EqualityExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super == is not supported"); + return errExpr(ctx); + } + Expr left = buildRelational(ctx.relationalExpression(0)); + if (ctx.equalityOperator() != null) { + left = binary(ctx, left, ctx.equalityOperator().getText(), + buildRelational(ctx.relationalExpression(1))); + } + return left; + } + + private Expr buildRelational(Dart2Parser.RelationalExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super relational ops are not supported"); + return errExpr(ctx); + } + Expr left = buildBitwiseOr(ctx.bitwiseOrExpression(0)); + if (ctx.typeTest() != null) { + IsTest t = new IsTest(); + pos(t, ctx); + t.operand = left; + t.negated = ctx.typeTest().isOperator().NOT() != null; + t.type = buildTypeNotVoid(ctx.typeTest().typeNotVoid()); + return t; + } + if (ctx.typeCast() != null) { + AsCast c = new AsCast(); + pos(c, ctx); + c.operand = left; + c.type = buildTypeNotVoid(ctx.typeCast().typeNotVoid()); + return c; + } + if (ctx.relationalOperator() != null) { + left = binary(ctx, left, ctx.relationalOperator().getText(), + buildBitwiseOr(ctx.bitwiseOrExpression(1))); + } + return left; + } + + private Expr buildBitwiseOr(Dart2Parser.BitwiseOrExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super bitwise ops are not supported"); + return errExpr(ctx); + } + Expr left = buildBitwiseXor(ctx.bitwiseXorExpression(0)); + for (int i = 1; i < ctx.bitwiseXorExpression().size(); i++) { + left = binary(ctx, left, "|", buildBitwiseXor(ctx.bitwiseXorExpression(i))); + } + return left; + } + + private Expr buildBitwiseXor(Dart2Parser.BitwiseXorExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super bitwise ops are not supported"); + return errExpr(ctx); + } + Expr left = buildBitwiseAnd(ctx.bitwiseAndExpression(0)); + for (int i = 1; i < ctx.bitwiseAndExpression().size(); i++) { + left = binary(ctx, left, "^", buildBitwiseAnd(ctx.bitwiseAndExpression(i))); + } + return left; + } + + private Expr buildBitwiseAnd(Dart2Parser.BitwiseAndExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super bitwise ops are not supported"); + return errExpr(ctx); + } + Expr left = buildShift(ctx.shiftExpression(0)); + for (int i = 1; i < ctx.shiftExpression().size(); i++) { + left = binary(ctx, left, "&", buildShift(ctx.shiftExpression(i))); + } + return left; + } + + private Expr buildShift(Dart2Parser.ShiftExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super shift ops are not supported"); + return errExpr(ctx); + } + Expr left = buildAdditive(ctx.additiveExpression(0)); + for (int i = 1; i < ctx.additiveExpression().size(); i++) { + left = binary(ctx, left, ctx.shiftOperator(i - 1).getText(), + buildAdditive(ctx.additiveExpression(i))); + } + return left; + } + + private Expr buildAdditive(Dart2Parser.AdditiveExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super arithmetic is not supported"); + return errExpr(ctx); + } + Expr left = buildMultiplicative(ctx.multiplicativeExpression(0)); + for (int i = 1; i < ctx.multiplicativeExpression().size(); i++) { + left = binary(ctx, left, ctx.additiveOperator(i - 1).getText(), + buildMultiplicative(ctx.multiplicativeExpression(i))); + } + return left; + } + + private Expr buildMultiplicative(Dart2Parser.MultiplicativeExpressionContext ctx) { + if (ctx.SUPER_() != null) { + unsupported(ctx, "E0120", "super arithmetic is not supported"); + return errExpr(ctx); + } + Expr left = buildUnary(ctx.unaryExpression(0)); + for (int i = 1; i < ctx.unaryExpression().size(); i++) { + left = binary(ctx, left, ctx.multiplicativeOperator(i - 1).getText(), + buildUnary(ctx.unaryExpression(i))); + } + return left; + } + + private Expr buildUnary(Dart2Parser.UnaryExpressionContext ctx) { + if (ctx.prefixOperator() != null) { + Unary u = new Unary(); + pos(u, ctx); + u.op = ctx.prefixOperator().getText(); + u.operand = buildUnary(ctx.unaryExpression()); + return u; + } + if (ctx.awaitExpression() != null) { + AwaitExpr a = new AwaitExpr(); + pos(a, ctx); + a.operand = buildUnary(ctx.awaitExpression().unaryExpression()); + return a; + } + if (ctx.incrementOperator() != null && ctx.assignableExpression() != null) { + IncDec id = new IncDec(); + pos(id, ctx); + id.prefix = true; + id.increment = ctx.incrementOperator().getText().equals("++"); + id.operand = buildAssignable(ctx.assignableExpression()); + return id; + } + if (ctx.postfixExpression() != null) { + return buildPostfix(ctx.postfixExpression()); + } + unsupported(ctx, "E0121", "Unsupported unary expression: " + snippet(ctx)); + return errExpr(ctx); + } + + private Expr buildPostfix(Dart2Parser.PostfixExpressionContext ctx) { + if (ctx.assignableExpression() != null && ctx.postfixOperator() != null) { + IncDec id = new IncDec(); + pos(id, ctx); + id.prefix = false; + id.increment = ctx.postfixOperator().getText().equals("++"); + id.operand = buildAssignable(ctx.assignableExpression()); + return id; + } + Expr base = buildPrimary(ctx.primary()); + for (Dart2Parser.SelectorContext s : ctx.selector()) { + base = applySelector(base, s); + } + return base; + } + + /** Applies one selector (call / property / index / null-assert) to a base expression. */ + private Expr applySelector(Expr base, Dart2Parser.SelectorContext s) { + if (s.NOT() != null) { + NotNullAssert n = new NotNullAssert(); + pos(n, s); + n.operand = base; + return n; + } + if (s.argumentPart() != null) { + // call on the base: fold `Ident(args)` and `x.name(args)` into Call nodes + Call call = new Call(); + pos(call, s); + if (s.argumentPart().typeArguments() != null) { + for (Dart2Parser.TypeContext t : s.argumentPart().typeArguments().typeList().type()) { + call.typeArgs.add(buildType(t)); + } + } + buildArgs(s.argumentPart().arguments(), call.args); + if (base instanceof Ident) { + call.name = ((Ident) base).name; + } else if (base instanceof PropertyGet) { + PropertyGet pg = (PropertyGet) base; + call.target = pg.target; + call.name = pg.name; + call.nullAware = pg.nullAware; + } else { + // calling an arbitrary expression value (closure invoke) + call.target = base; + call.name = null; + } + return call; + } + return applyAssignableSelector(base, s.assignableSelector()); + } + + private Expr applyUnconditional(Expr base, Dart2Parser.UnconditionalAssignableSelectorContext u) { + if (u.identifier() != null) { + PropertyGet pg = new PropertyGet(); + pos(pg, u); + pg.target = base; + pg.name = u.identifier().getText(); + return pg; + } + IndexGet ig = new IndexGet(); + pos(ig, u); + ig.target = base; + ig.index = buildExpr(u.expr()); + return ig; + } + + private Expr applyAssignableSelector(Expr base, Dart2Parser.AssignableSelectorContext sel) { + if (sel.unconditionalAssignableSelector() != null) { + return applyUnconditional(base, sel.unconditionalAssignableSelector()); + } + if (sel.QUD() != null) { + PropertyGet pg = new PropertyGet(); + pos(pg, sel); + pg.target = base; + pg.name = sel.identifier().getText(); + pg.nullAware = true; + return pg; + } + // QU OB expr CB — null-aware index + IndexGet ig = new IndexGet(); + pos(ig, sel); + ig.target = base; + ig.index = buildExpr(sel.expr()); + return ig; + } + + private void buildArgs(Dart2Parser.ArgumentsContext ctx, Args out) { + if (ctx == null || ctx.argumentList() == null) { + return; + } + Dart2Parser.ArgumentListContext list = ctx.argumentList(); + if (list.expressionList() != null) { + for (Dart2Parser.ExprContext e : list.expressionList().expr()) { + out.positional.add(buildExpr(e)); + } + } + for (Dart2Parser.NamedArgumentContext n : list.namedArgument()) { + NamedArg na = new NamedArg(); + na.name = n.label().identifier().getText(); + na.value = buildExpr(n.expr()); + out.named.add(na); + } + } + + private Expr buildPrimary(Dart2Parser.PrimaryContext ctx) { + if (ctx == null) { + // parser error-recovery can hand us a null primary; a preceding syntax + // (E0001) diagnostic already flags the real cause, so degrade gracefully. + return errExpr(null); + } + if (ctx.thisExpression() != null) { + ThisExpr t = new ThisExpr(); + pos(t, ctx); + return t; + } + if (ctx.SUPER_() != null) { + SuperExpr sup = new SuperExpr(); + pos(sup, ctx); + if (ctx.unconditionalAssignableSelector() != null) { + return applyUnconditional(sup, ctx.unconditionalAssignableSelector()); + } + unsupported(ctx, "E0122", "super(...) calls outside initializer lists are not supported"); + return errExpr(ctx); + } + if (ctx.functionExpression() != null) { + return buildLambda(ctx.functionExpression()); + } + if (ctx.literal() != null) { + return buildLiteral(ctx.literal()); + } + if (ctx.identifier() != null) { + Ident id = new Ident(); + pos(id, ctx); + id.name = ctx.identifier().getText(); + return id; + } + if (ctx.newExpression() != null) { + return buildCtorFromDesignation(ctx.newExpression().constructorDesignation(), + ctx.newExpression().arguments(), false, ctx); + } + if (ctx.constObjectExpression() != null) { + return buildCtorFromDesignation(ctx.constObjectExpression().constructorDesignation(), + ctx.constObjectExpression().arguments(), true, ctx); + } + if (ctx.constructorInvocation() != null) { + Dart2Parser.ConstructorInvocationContext ci = ctx.constructorInvocation(); + CtorCall cc = new CtorCall(); + pos(cc, ctx); + cc.type = new TypeRef(ci.typeName().getText()); + for (Dart2Parser.TypeContext t : ci.typeArguments().typeList().type()) { + cc.type.args.add(buildType(t)); + } + cc.ctorName = ci.identifier().getText(); + buildArgs(ci.arguments(), cc.args); + return cc; + } + if (ctx.switchExpression() != null) { + return buildSwitchExpr(ctx.switchExpression()); + } + if (ctx.recordLiteral() != null) { + return buildRecordLit(ctx.recordLiteral()); + } + if (ctx.expr() != null) { + ParenExpr p = new ParenExpr(); + pos(p, ctx); + p.inner = buildExpr(ctx.expr()); + return p; + } + unsupported(ctx, "E0123", "Unsupported primary expression: " + snippet(ctx)); + return errExpr(ctx); + } + + private Expr buildCtorFromDesignation(Dart2Parser.ConstructorDesignationContext d, + Dart2Parser.ArgumentsContext args, boolean isConst, + ParserRuleContext posCtx) { + CtorCall cc = new CtorCall(); + pos(cc, posCtx); + cc.isConst = isConst; + if (d.typeIdentifier() != null) { + cc.type = new TypeRef(d.typeIdentifier().getText()); + } else if (d.qualifiedName() != null) { + // X.named — type X, named ctor + String text = d.qualifiedName().getText(); + int dot = text.indexOf('.'); + cc.type = new TypeRef(text.substring(0, dot)); + cc.ctorName = text.substring(dot + 1); + } else { + cc.type = new TypeRef(d.typeName().getText()); + if (d.typeArguments() != null) { + for (Dart2Parser.TypeContext t : d.typeArguments().typeList().type()) { + cc.type.args.add(buildType(t)); + } + } + if (d.identifier() != null) { + cc.ctorName = d.identifier().getText(); + } + } + buildArgs(args, cc.args); + return cc; + } + + private Expr buildLambda(Dart2Parser.FunctionExpressionContext ctx) { + Lambda l = new Lambda(); + pos(l, ctx); + if (ctx.formalParameterPart().typeParameters() != null) { + unsupported(ctx, "E0112", "Generic closures are not supported"); + } + buildParams(ctx.formalParameterPart().formalParameterList(), l.params); + Dart2Parser.FunctionExpressionBodyContext body = ctx.functionExpressionBody(); + if (body.SYNC_() != null || body.ST() != null) { + unsupported(body, "E0303", "Generator closures are not supported"); + } + l.isAsync = body.ASYNC_() != null; + if (body.block() != null) { + l.body = buildBlock(body.block()); + } else if (body.expr() != null) { + l.exprBody = buildExpr(body.expr()); + } + return l; + } + + // ------------------------------------------------------------------ + // Literals + // ------------------------------------------------------------------ + + private Expr buildLiteral(Dart2Parser.LiteralContext ctx) { + if (ctx.nullLiteral() != null) { + NullLit n = new NullLit(); + pos(n, ctx); + return n; + } + if (ctx.booleanLiteral() != null) { + BoolLit b = new BoolLit(); + pos(b, ctx); + b.value = ctx.booleanLiteral().TRUE_() != null; + return b; + } + if (ctx.numericLiteral() != null) { + String text = ctx.numericLiteral().getText(); + if (ctx.numericLiteral().HEX_NUMBER() != null) { + IntLit i = new IntLit(); + pos(i, ctx); + // Dart hex ints are 64-bit wrapping; parse as unsigned when needed + String hex = text.substring(2); + i.value = Long.parseUnsignedLong(hex, 16); + return i; + } + if (text.contains(".") || text.contains("e") || text.contains("E")) { + DoubleLit d = new DoubleLit(); + pos(d, ctx); + d.value = Double.parseDouble(text); + return d; + } + IntLit i = new IntLit(); + pos(i, ctx); + i.value = Long.parseLong(text); + return i; + } + if (ctx.stringLiteral() != null) { + return buildString(ctx.stringLiteral()); + } + if (ctx.listLiteral() != null) { + return buildListLiteral(ctx.listLiteral()); + } + if (ctx.setOrMapLiteral() != null) { + return buildSetOrMapLiteral(ctx.setOrMapLiteral()); + } + unsupported(ctx, "E0124", "Unsupported literal: " + snippet(ctx)); + return errExpr(ctx); + } + + private Expr buildListLiteral(Dart2Parser.ListLiteralContext ctx) { + ListLit l = new ListLit(); + pos(l, ctx); + l.isConst = ctx.CONST_() != null; + if (ctx.typeArguments() != null) { + l.elementType = buildType(ctx.typeArguments().typeList().type(0)); + } + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + Expr el = buildElement(e); + if (el != null) { + l.elements.add(el); + } + } + } + return l; + } + + /** One collection-literal element: plain expression, spread, if or for. */ + private Expr buildElement(Dart2Parser.ElementContext e) { + if (e.expressionElement() != null) { + return buildExpr(e.expressionElement().expr()); + } + if (e.spreadElement() != null) { + SpreadElement s = new SpreadElement(); + pos(s, e); + s.nullAware = e.spreadElement().DDDQ() != null; + s.expr = buildExpr(e.spreadElement().expr()); + return s; + } + if (e.ifElement() != null) { + IfElement i = new IfElement(); + pos(i, e); + i.condition = buildExpr(e.ifElement().expr()); + i.thenElement = buildElement(e.ifElement().element(0)); + if (e.ifElement().element().size() > 1) { + i.elseElement = buildElement(e.ifElement().element(1)); + } + return i; + } + if (e.forElement() != null) { + Dart2Parser.ForElementContext f = e.forElement(); + if (f.AWAIT_() != null) { + unsupported(f, "E0302", "await for elements are not supported yet (M3)"); + return null; + } + ForElement fe = new ForElement(); + pos(fe, e); + Dart2Parser.ForLoopPartsContext parts = f.forLoopParts(); + if (parts.IN_() != null) { + if (parts.pattern() != null) { + // Dart 3 pattern for-in element: `for (final (i, x) in xs.indexed) ...` + fe.pattern = buildPattern(parts.pattern()); + fe.varType = TypeRef.VAR; + } else if (parts.declaredIdentifier() != null) { + fe.varName = parts.declaredIdentifier().identifier().getText(); + fe.varType = buildFinalConstVarOrType(parts.declaredIdentifier().finalConstVarOrType()); + } else { + fe.varName = parts.identifier().getText(); + fe.varType = TypeRef.VAR; + } + fe.iterable = buildExpr(parts.expr()); + } else { + Dart2Parser.ForInitializerStatementContext init = parts.forInitializerStatement(); + if (init != null) { + if (init.localVariableDeclaration() != null) { + fe.init = buildLocalVar(init.localVariableDeclaration()); + } else if (init.expr() != null) { + ExprStmt es = new ExprStmt(); + pos(es, init); + es.expr = buildExpr(init.expr()); + fe.init = es; + } + } + if (parts.expr() != null) { + fe.condition = buildExpr(parts.expr()); + } + if (parts.expressionList() != null) { + for (Dart2Parser.ExprContext u : parts.expressionList().expr()) { + fe.updates.add(buildExpr(u)); + } + } + } + fe.body = buildElement(f.element()); + return fe; + } + if (e.mapElement() != null) { + // A key:value entry — valid when this element is (transitively) inside a map literal. + MapEntry me = new MapEntry(); + pos(me, e); + me.key = buildExpr(e.mapElement().expr(0)); + me.value = buildExpr(e.mapElement().expr(1)); + return me; + } + return null; + } + + private Expr buildSetOrMapLiteral(Dart2Parser.SetOrMapLiteralContext ctx) { + // Set vs map disambiguation (matching Dart): explicit {...} or {...} decides; + // otherwise a top-level `k: v` entry means a map, and any other non-empty body is a set. + List typeArgs = ctx.typeArguments() != null + ? ctx.typeArguments().typeList().type() : null; + boolean hasMapEntry = false; + boolean hasAnyElement = ctx.elements() != null && !ctx.elements().element().isEmpty(); + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + if (e.mapElement() != null) { + hasMapEntry = true; + break; + } + } + } + boolean isSet; + if (typeArgs != null) { + isSet = typeArgs.size() == 1; + } else { + isSet = hasAnyElement && !hasMapEntry; + } + if (isSet) { + SetLit s = new SetLit(); + pos(s, ctx); + s.isConst = ctx.CONST_() != null; + if (typeArgs != null && typeArgs.size() == 1) { + s.elementType = buildType(typeArgs.get(0)); + } + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + Expr el = buildElement(e); + if (el != null) { + s.elements.add(el); + } + } + } + return s; + } + MapLit m = new MapLit(); + pos(m, ctx); + m.isConst = ctx.CONST_() != null; + if (typeArgs != null && typeArgs.size() >= 2) { + m.keyType = buildType(typeArgs.get(0)); + m.valueType = buildType(typeArgs.get(1)); + } + boolean structured = false; + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + if (e.ifElement() != null || e.forElement() != null || e.spreadElement() != null) { + structured = true; + break; + } + } + } + if (structured) { + // Collection if/for/spread in a map literal: keep an ordered element list and let the + // emitter lower it to a DartMap builder (conditional / looped put()). + m.structured = true; + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + Expr el = buildElement(e); + if (el != null) { + m.elements.add(el); + } + } + } + return m; + } + if (ctx.elements() != null) { + for (Dart2Parser.ElementContext e : ctx.elements().element()) { + if (e.mapElement() != null) { + m.keys.add(buildExpr(e.mapElement().expr(0))); + m.values.add(buildExpr(e.mapElement().expr(1))); + } else { + unsupported(e, "E0204", "Map entries are only supported directly inside map literals"); + } + } + } + return m; + } + + /** + * Parses a Dart string token (with quotes) into literal/interpolation + * parts. Handles ', ", r-prefixed raw strings and \ escapes. + */ + private Expr buildString(Dart2Parser.StringLiteralContext ctx) { + StringLit lit = new StringLit(); + pos(lit, ctx); + // adjacent string literals concatenate + for (int i = 0; i < ctx.getChildCount(); i++) { + String token = ctx.getChild(i).getText(); + parseStringToken(token, lit, ctx); + } + return lit; + } + + private void parseStringToken(String token, StringLit lit, ParserRuleContext ctx) { + boolean raw = token.startsWith("r"); + String body = raw ? token.substring(1) : token; + if (body.length() >= 6 && (body.startsWith("'''") || body.startsWith("\"\"\""))) { + body = body.substring(3, body.length() - 3); + if (raw) { + lit.parts.add(body); + return; + } + } else { + body = body.substring(1, body.length() - 1); + if (raw) { + lit.parts.add(body); + return; + } + } + StringBuilder cur = new StringBuilder(); + int i = 0; + int n = body.length(); + while (i < n) { + char c = body.charAt(i); + if (c == '\\' && i + 1 < n) { + char e = body.charAt(i + 1); + switch (e) { + case 'n': cur.append('\n'); break; + case 't': cur.append('\t'); break; + case 'r': cur.append('\r'); break; + case 'b': cur.append('\b'); break; + case 'f': cur.append('\f'); break; + case '\\': cur.append('\\'); break; + case '\'': cur.append('\''); break; + case '"': cur.append('"'); break; + case '$': cur.append('$'); break; + case 'u': { + if (i + 2 < n && body.charAt(i + 2) == '{') { + int close = body.indexOf('}', i + 3); + int cp = Integer.parseInt(body.substring(i + 3, close), 16); + cur.appendCodePoint(cp); + i = close - 1; + } else { + int cp = Integer.parseInt(body.substring(i + 2, i + 6), 16); + cur.appendCodePoint(cp); + i += 4; + } + break; + } + default: cur.append(e); break; + } + i += 2; + continue; + } + if (c == '$') { + if (i + 1 < n && body.charAt(i + 1) == '{') { + // ${expr} — find matching close brace (no nested strings-with-braces in M1) + int depth = 1; + int j = i + 2; + while (j < n && depth > 0) { + char cj = body.charAt(j); + if (cj == '{') { + depth++; + } else if (cj == '}') { + depth--; + } + j++; + } + if (cur.length() > 0) { + lit.parts.add(cur.toString()); + cur.setLength(0); + } + String frag = body.substring(i + 2, j - 1); + lit.parts.add(parseExprFragment(frag, ctx.getStart().getLine(), + ctx.getStart().getCharPositionInLine())); + i = j; + continue; + } + // $identifier + int j = i + 1; + while (j < n && (Character.isLetterOrDigit(body.charAt(j)) || body.charAt(j) == '_')) { + j++; + } + if (j > i + 1) { + if (cur.length() > 0) { + lit.parts.add(cur.toString()); + cur.setLength(0); + } + Ident id = new Ident(); + id.at(file, ctx.getStart().getLine(), ctx.getStart().getCharPositionInLine()); + id.name = body.substring(i + 1, j); + lit.parts.add(id); + i = j; + continue; + } + } + cur.append(c); + i++; + } + if (cur.length() > 0) { + lit.parts.add(cur.toString()); + } + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private Binary binary(ParserRuleContext ctx, Expr left, String op, Expr right) { + Binary b = new Binary(); + pos(b, ctx); + b.left = left; + b.op = op; + b.right = right; + return b; + } + + private String annotationArg(Dart2Parser.MetadataContext meta, String name) { + if (meta == null) { + return null; + } + for (Dart2Parser.MetadatumContext m : meta.metadatum()) { + if (m.getText().startsWith(name + "(")) { + return annotationStringArg(m); + } + } + return null; + } + + private boolean hasAnnotation(Dart2Parser.MetadataContext meta, String name) { + if (meta == null) { + return false; + } + for (Dart2Parser.MetadatumContext m : meta.metadatum()) { + if (m.getText().equals(name) || m.getText().startsWith(name + "(")) { + return true; + } + } + return false; + } + + /** Extracts the single string argument of an annotation like @JavaName('x'). */ + public static String annotationStringArg(Dart2Parser.MetadatumContext m) { + String text = m.getText(); + int open = text.indexOf('('); + if (open < 0) { + return null; + } + String arg = text.substring(open + 1, text.length() - 1).trim(); + if (arg.length() >= 2 && (arg.charAt(0) == '\'' || arg.charAt(0) == '"')) { + return arg.substring(1, arg.length() - 1); + } + return null; + } + + private void pos(Node node, ParserRuleContext ctx) { + node.file = file; + if (ctx != null) { + Token t = ctx.getStart(); + node.line = t.getLine(); + node.col = t.getCharPositionInLine(); + } + } + + private void unsupported(ParserRuleContext ctx, String code, String message) { + diags.error(file, ctx == null ? 0 : ctx.getStart().getLine(), + ctx == null ? 0 : ctx.getStart().getCharPositionInLine(), code, message); + } + + private Expr errExpr(ParserRuleContext ctx) { + NullLit n = new NullLit(); + if (ctx != null) { + pos(n, ctx); + } + return n; + } + + /** Dart operator token → mangled Java method name (per the plan's table). */ + public static String mangleOperator(String op) { + if (op.equals("+")) { + return "$plus"; + } + if (op.equals("-")) { + return "$minus"; + } + if (op.equals("*")) { + return "$times"; + } + if (op.equals("/")) { + return "$div"; + } + if (op.equals("~/")) { + return "$tdiv"; + } + if (op.equals("%")) { + return "$mod"; + } + if (op.equals("[]")) { + return "$index"; + } + if (op.equals("[]=")) { + return "$indexSet"; + } + if (op.equals("<")) { + return "$lt"; + } + if (op.equals(">")) { + return "$gt"; + } + if (op.equals("<=")) { + return "$le"; + } + if (op.equals(">=")) { + return "$ge"; + } + if (op.equals("==")) { + return "$eq"; + } + if (op.equals("~")) { + return "$bitNot"; + } + if (op.equals("&")) { + return "$bitAnd"; + } + if (op.equals("|")) { + return "$bitOr"; + } + if (op.equals("^")) { + return "$bitXor"; + } + if (op.equals("<<")) { + return "$shl"; + } + if (op.equals(">>")) { + return "$shr"; + } + return null; + } + + private String stripQuotes(String uri) { + if (uri.length() >= 2 && (uri.charAt(0) == '\'' || uri.charAt(0) == '"')) { + return uri.substring(1, uri.length() - 1); + } + return uri; + } + + private String snippet(ParserRuleContext ctx) { + String t = ctx.getText(); + return t.length() > 40 ? t.substring(0, 40) + "…" : t; + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/Dart2LexerBase.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/Dart2LexerBase.java new file mode 100644 index 00000000000..25f5313d301 --- /dev/null +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/parser/Dart2LexerBase.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.parser; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Lexer; + +/** + * Base class required by the vendored Dart2Lexer grammar; hosts the single + * semantic predicate the lexer uses for string-interpolation lexing. + */ +public abstract class Dart2LexerBase extends Lexer { + protected Dart2LexerBase(CharStream input) { + super(input); + } + + protected boolean CheckNotOpenBrace() { + return _input.LA(1) != '{'; + } +} diff --git a/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/dart_collection.dart b/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/dart_collection.dart new file mode 100644 index 00000000000..67b07f872da --- /dev/null +++ b/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/dart_collection.dart @@ -0,0 +1,69 @@ +// Built-in transpiler stubs for the dart:collection mixins. These are always +// registered (independent of the runtime stub classpath) so that classes such +// as `class Board extends Object with IterableMixin` resolve their mixin +// instead of raising E0402. Each maps to a Java interface with default methods +// (its @JavaName); the applying class supplies the abstract members it requires +// (e.g. `iterator` for IterableMixin), and every other member resolves as an +// inherited default. The Java interfaces live in the dart-runtime module. + +// Abstract base of the Iterable protocol: the class provides `iterator`, and the +// mixin contributes forEach / map / where / length / ... as default methods. +@JavaName('dart.collection.IterableMixin') +mixin IterableMixin { + Iterator get iterator; + int get length; + bool get isEmpty; + bool get isNotEmpty; + E get first; + E get last; + E get single; + bool contains(Object? element); + void forEach(void Function(E element) action); + E elementAt(int index); + bool any(bool Function(E element) test); + bool every(bool Function(E element) test); + String join([String separator = '']); +} + +// List protocol: adds indexed access on top of the Iterable surface. +@JavaName('dart.collection.ListMixin') +mixin ListMixin { + int get length; + set length(int newLength); + E operator [](int index); + void operator []=(int index, E value); + Iterator get iterator; + bool get isEmpty; + bool get isNotEmpty; + void add(E element); + bool contains(Object? element); + void forEach(void Function(E element) action); +} + +// Map protocol. +@JavaName('dart.collection.MapMixin') +mixin MapMixin { + V? operator [](Object? key); + void operator []=(K key, V value); + Iterable get keys; + int get length; + bool get isEmpty; + bool get isNotEmpty; + bool containsKey(Object? key); + void forEach(void Function(K key, V value) action); + V? remove(Object? key); + void clear(); +} + +// Set protocol. +@JavaName('dart.collection.SetMixin') +mixin SetMixin { + Iterator get iterator; + int get length; + bool get isEmpty; + bool get isNotEmpty; + bool contains(Object? element); + bool add(E value); + bool remove(Object? value); + void forEach(void Function(E element) action); +} diff --git a/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/flutter_material.dart b/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/flutter_material.dart new file mode 100644 index 00000000000..f0f705f10a7 --- /dev/null +++ b/maven/dart-transpiler/src/main/resources/com/codename1/dart/stubs/flutter_material.dart @@ -0,0 +1,475 @@ +// Codename One Flutter runtime API stubs (M1). +// +// These signature-only declarations tell the Dart transpiler how the +// hand-written Java runtime (codenameone-flutter-runtime) looks from Dart: +// which classes exist, their Java names, and — crucially — parameter shapes. +// +// Conventions the emitter applies to stub classes: +// - positional constructor parameters -> Java constructor arguments +// - named constructor parameters -> void setter methods of the same name +// - instance getters -> no-arg method calls (name()) +// - static getters -> static field access (Name.field) +// - named parameters of methods -> canonical positional order as declared +// - the VoidCallback type -> dart.runtime.Funcs.VoidFunc0 +// +// M1 scope only. This file is parsed with the transpiler's own Dart parser. + +// --- entry points ----------------------------------------------------- + +@JavaName('com.codename1.flutter.FlutterUI.runApp') +external void runApp(Widget app); + +// --- framework core --------------------------------------------------- + +@JavaName('com.codename1.flutter.Key') +abstract class Key {} + +@JavaName('com.codename1.flutter.ValueKey') +class ValueKey extends Key { + external ValueKey(Object value); +} + +@JavaName('com.codename1.flutter.BuildContext') +abstract class BuildContext {} + +@JavaName('com.codename1.flutter.Widget') +abstract class Widget { + external Widget({Key? key}); +} + +@JavaName('com.codename1.flutter.StatelessWidget') +abstract class StatelessWidget extends Widget { + external StatelessWidget({Key? key}); + Widget build(BuildContext context); +} + +@JavaName('com.codename1.flutter.StatefulWidget') +abstract class StatefulWidget extends Widget { + external StatefulWidget({Key? key}); + State createState(); +} + +@JavaName('com.codename1.flutter.State') +abstract class State { + external T get widget; + external BuildContext get context; + external void setState(VoidCallback fn); + external void initState(); + external void dispose(); + Widget build(BuildContext context); +} + +// --- value types ------------------------------------------------------ + +@JavaName('com.codename1.flutter.Color') +class Color { + external Color(int value); +} + +@JavaName('com.codename1.flutter.Colors') +abstract class Colors { + external static Color get deepPurple; + external static Color get blue; + external static Color get red; + external static Color get green; + external static Color get orange; + external static Color get purple; + external static Color get white; + external static Color get black; + external static Color get grey; + external static Color get transparent; +} + +@JavaName('com.codename1.flutter.EdgeInsets') +class EdgeInsets { + external static EdgeInsets all(double value); + external static EdgeInsets only({double left, double top, double right, double bottom}); + external static EdgeInsets symmetric({double horizontal, double vertical}); +} + +@JavaName('com.codename1.flutter.MainAxisAlignment') +enum MainAxisAlignment { start, end, center, spaceBetween, spaceAround, spaceEvenly } + +@JavaName('com.codename1.flutter.CrossAxisAlignment') +enum CrossAxisAlignment { start, end, center, stretch } + +@JavaName('com.codename1.flutter.MainAxisSize') +enum MainAxisSize { min, max } + +@JavaName('com.codename1.flutter.TextAlign') +enum TextAlign { left, right, center, start, end } + +@JavaName('com.codename1.flutter.FontWeight') +abstract class FontWeight { + external static FontWeight get w100; + external static FontWeight get w200; + external static FontWeight get w300; + external static FontWeight get w400; + external static FontWeight get w500; + external static FontWeight get w600; + external static FontWeight get w700; + external static FontWeight get w800; + external static FontWeight get w900; + external static FontWeight get normal; + external static FontWeight get bold; +} + +@JavaName('com.codename1.flutter.TextStyle') +class TextStyle { + external TextStyle({double? fontSize, FontWeight? fontWeight, Color? color, String? fontFamily, double? letterSpacing, double? height}); +} + +@JavaName('com.codename1.flutter.IconData') +class IconData {} + +@JavaName('com.codename1.flutter.Icons') +abstract class Icons { + external static IconData get add; + external static IconData get remove; + external static IconData get menu; + external static IconData get home; + external static IconData get settings; + external static IconData get search; + external static IconData get arrow_back; + external static IconData get arrow_forward; + external static IconData get close; + external static IconData get check; + external static IconData get edit; + external static IconData get delete; + external static IconData get favorite; + external static IconData get share; + external static IconData get more_vert; +} + +// --- basic widgets ---------------------------------------------------- + +@JavaName('com.codename1.flutter.widgets.Text') +class Text extends Widget { + external Text(String data, {Key? key, TextStyle? style, TextAlign? textAlign}); +} + +@JavaName('com.codename1.flutter.widgets.Icon') +class Icon extends Widget { + external Icon(IconData icon, {Key? key, double? size, Color? color}); +} + +@JavaName('com.codename1.flutter.widgets.Column') +class Column extends Widget { + external Column({Key? key, MainAxisAlignment? mainAxisAlignment, CrossAxisAlignment? crossAxisAlignment, MainAxisSize? mainAxisSize, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Row') +class Row extends Widget { + external Row({Key? key, MainAxisAlignment? mainAxisAlignment, CrossAxisAlignment? crossAxisAlignment, MainAxisSize? mainAxisSize, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Center') +class Center extends Widget { + external Center({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Padding') +class Padding extends Widget { + external Padding({Key? key, EdgeInsets padding, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.SizedBox') +class SizedBox extends Widget { + external SizedBox({Key? key, double? width, double? height, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Expanded') +class Expanded extends Widget { + external Expanded({Key? key, int flex, Widget child}); +} + +// --- material --------------------------------------------------------- + +@JavaName('com.codename1.flutter.material.MaterialApp') +class MaterialApp extends Widget { + external MaterialApp({Key? key, String? title, ThemeData? theme, ThemeData? darkTheme, ThemeMode? themeMode, Widget? home}); +} + +@JavaName('com.codename1.flutter.material.Scaffold') +class Scaffold extends Widget { + external Scaffold({Key? key, Widget? appBar, Widget? body, Widget? floatingActionButton, Widget? drawer, Widget? bottomNavigationBar}); +} + +@JavaName('com.codename1.flutter.material.AppBar') +class AppBar extends Widget { + external AppBar({Key? key, Widget? title, Color? backgroundColor, bool? centerTitle}); +} + +@JavaName('com.codename1.flutter.material.FloatingActionButton') +class FloatingActionButton extends Widget { + external FloatingActionButton({Key? key, VoidCallback? onPressed, String? tooltip, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.ThemeData') +class ThemeData { + external ThemeData({ColorScheme? colorScheme, bool? useMaterial3, Brightness? brightness}); + external ColorScheme get colorScheme; + external TextTheme get textTheme; +} + +@JavaName('com.codename1.flutter.material.ColorScheme') +class ColorScheme { + external static ColorScheme fromSeed({Color seedColor, Brightness? brightness}); + external Color get primary; + external Color get inversePrimary; + external Color get onPrimary; + external Color get surface; + external Color get onSurface; + external Color get secondary; +} + +@JavaName('com.codename1.flutter.material.TextTheme') +class TextTheme { + external TextStyle get headlineMedium; + external TextStyle get bodyMedium; + external TextStyle get titleLarge; +} + +@JavaName('com.codename1.flutter.material.Theme') +abstract class Theme { + external static ThemeData of(BuildContext context); +} + +// --- M2 additions ------------------------------------------------------- + +@JavaName('com.codename1.flutter.BoxFit') +enum BoxFit { fill, contain, cover, fitWidth, fitHeight, none } + +@JavaName('com.codename1.flutter.Alignment') +abstract class Alignment { + external static Alignment get topLeft; + external static Alignment get topCenter; + external static Alignment get topRight; + external static Alignment get centerLeft; + external static Alignment get center; + external static Alignment get centerRight; + external static Alignment get bottomLeft; + external static Alignment get bottomCenter; + external static Alignment get bottomRight; +} + +@JavaName('com.codename1.flutter.widgets.ListView') +class ListView extends Widget { + external ListView({Key? key, List children, EdgeInsets? padding, bool? shrinkWrap}); + external static ListView builder({Key? key, int? itemCount, IndexedWidgetBuilder itemBuilder, EdgeInsets? padding, bool? shrinkWrap, Object? physics, Object? scrollDirection, Object? controller, String? restorationId, bool? primary, double? itemExtent, bool? reverse}); +} + +@JavaName('com.codename1.flutter.widgets.GridView') +class GridView extends Widget { + external static GridView count({Key? key, int crossAxisCount, double? childAspectRatio, double? mainAxisSpacing, double? crossAxisSpacing, EdgeInsets? padding, List children}); +} + +@JavaName('com.codename1.flutter.widgets.SingleChildScrollView') +class SingleChildScrollView extends Widget { + external SingleChildScrollView({Key? key, EdgeInsets? padding, Axis? scrollDirection, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Image') +class Image extends Widget { + external static Image asset(String name, {Key? key, double? width, double? height, BoxFit? fit, String? package, bool? excludeFromSemantics, bool? gaplessPlayback, int? cacheWidth, int? cacheHeight, Color? color, Object? colorBlendMode, Object? alignment, Object? semanticLabel}); + external static Image network(String src, {Key? key, double? width, double? height, BoxFit? fit}); +} + +@JavaName('com.codename1.flutter.widgets.Stack') +class Stack extends Widget { + external Stack({Key? key, Alignment? alignment, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Positioned') +class Positioned extends Widget { + external Positioned({Key? key, double? left, double? top, double? right, double? bottom, double? width, double? height, Widget child}); +} + +@JavaName('com.codename1.flutter.widgets.Align') +class Align extends Widget { + external Align({Key? key, Alignment? alignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.ConstrainedBox') +class ConstrainedBox extends Widget { + external ConstrainedBox({Key? key, BoxConstraints constraints, Widget? child}); +} + +@JavaName('com.codename1.flutter.rendering.BoxConstraints') +class BoxConstraints { + external BoxConstraints({double? minWidth, double? maxWidth, double? minHeight, double? maxHeight}); +} + +@JavaName('com.codename1.flutter.material.Card') +class Card extends Widget { + external Card({Key? key, Color? color, double? elevation, EdgeInsets? margin, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.Divider') +class Divider extends Widget { + external Divider({Key? key, double? height, double? thickness, Color? color}); +} + +@JavaName('com.codename1.flutter.material.ElevatedButton') +class ElevatedButton extends Widget { + external ElevatedButton({Key? key, VoidCallback? onPressed, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.TextButton') +class TextButton extends Widget { + external TextButton({Key? key, VoidCallback? onPressed, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.OutlinedButton') +class OutlinedButton extends Widget { + external OutlinedButton({Key? key, VoidCallback? onPressed, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.IconButton') +class IconButton extends Widget { + external IconButton({Key? key, VoidCallback? onPressed, Widget? icon, double? iconSize, Color? color}); +} + +@JavaName('com.codename1.flutter.widgets.GestureDetector') +class GestureDetector extends Widget { + external GestureDetector({Key? key, VoidCallback? onTap, VoidCallback? onLongPress, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.InkWell') +class InkWell extends Widget { + external InkWell({Key? key, VoidCallback? onTap, VoidCallback? onLongPress, Widget? child}); +} + +// --- M3 additions ------------------------------------------------------- +// Callback typedefs below (StringCallback etc.) are transpiler-internal +// names mapped to dart.runtime.Funcs SAMs; they type untyped lambda params. + +@JavaName('com.codename1.flutter.material.TextEditingController') +class TextEditingController { + external TextEditingController({String? text}); + external String get text; + external void setText(String value); + external void addListener(VoidCallback listener); + external void clear(); +} + +@JavaName('com.codename1.flutter.material.InputDecoration') +class InputDecoration { + external InputDecoration({String? labelText, String? hintText}); +} + +@JavaName('com.codename1.flutter.material.TextField') +class TextField extends Widget { + external TextField({Key? key, TextEditingController? controller, InputDecoration? decoration, bool? obscureText, bool? enabled, StringCallback? onChanged, StringCallback? onSubmitted}); +} + +@JavaName('com.codename1.flutter.material.Checkbox') +class Checkbox extends Widget { + external Checkbox({Key? key, bool value, BoolCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Radio') +class Radio extends Widget { + external Radio({Key? key, Object value, Object? groupValue, DynamicCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Switch') +class Switch extends Widget { + external Switch({Key? key, bool value, BoolCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Slider') +class Slider extends Widget { + external Slider({Key? key, double value, double? min, double? max, int? divisions, DoubleCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.navigation.MaterialPageRoute') +class MaterialPageRoute { + external MaterialPageRoute({WidgetBuilder builder}); +} + +@JavaName('com.codename1.flutter.navigation.Navigator') +abstract class Navigator { + external static void push(BuildContext context, MaterialPageRoute route); + external static void pop(BuildContext context); +} + +@JavaName('com.codename1.flutter.material.Dialogs.showDialog') +external void showDialog({BuildContext context, WidgetBuilder builder}); + +@JavaName('com.codename1.flutter.material.AlertDialog') +class AlertDialog extends Widget { + external AlertDialog({Key? key, Widget? title, Widget? content, List? actions}); +} + +@JavaName('com.codename1.flutter.material.SnackBar') +class SnackBar extends Widget { + external SnackBar({Key? key, Widget content, Duration? duration}); +} + +@JavaName('com.codename1.flutter.material.ScaffoldMessenger') +abstract class ScaffoldMessenger { + external static ScaffoldMessengerState of(BuildContext context); +} + +@JavaName('com.codename1.flutter.material.ScaffoldMessengerState') +abstract class ScaffoldMessengerState { + external void showSnackBar(SnackBar snackBar); +} + +@JavaName('com.codename1.flutter.material.Drawer') +class Drawer extends Widget { + external Drawer({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.BottomNavigationBarItem') +class BottomNavigationBarItem { + external BottomNavigationBarItem({Widget? icon, String? label}); +} + +@JavaName('com.codename1.flutter.material.BottomNavigationBar') +class BottomNavigationBar extends Widget { + external BottomNavigationBar({Key? key, List items, int? currentIndex, IntCallback? onTap}); +} + +@JavaName('com.codename1.flutter.material.ListTile') +class ListTile extends Widget { + external ListTile({Key? key, Widget? leading, Widget? title, Widget? subtitle, Widget? trailing, VoidCallback? onTap}); +} + +// --- M4 additions ------------------------------------------------------- + +@JavaName('com.codename1.flutter.ThemeMode') +enum ThemeMode { system, light, dark } + +@JavaName('com.codename1.flutter.Brightness') +enum Brightness { light, dark } + +@JavaName('com.codename1.flutter.MediaQuery') +abstract class MediaQuery { + external static MediaQueryData of(BuildContext context); +} + +@JavaName('com.codename1.flutter.MediaQueryData') +abstract class MediaQueryData { + external Size get size; + external double get devicePixelRatio; + external Brightness get platformBrightness; +} + +@JavaName('com.codename1.flutter.rendering.Size') +class Size { + external Size(double width, double height); + external double get width; + external double get height; +} + +@JavaName('com.codename1.flutter.widgets.RichText') +class RichText extends Widget { + external RichText({Key? key, TextSpan text, TextAlign? textAlign}); +} + +@JavaName('com.codename1.flutter.widgets.TextSpan') +class TextSpan { + external TextSpan({String? text, TextStyle? style, List? children}); +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/AssetRootExclusionTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/AssetRootExclusionTest.java new file mode 100644 index 00000000000..90e412fbcad --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/AssetRootExclusionTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.api.DartTranspiler; +import com.codename1.dart.transpiler.api.GeneratedFile; +import com.codename1.dart.transpiler.api.TranspileRequest; +import com.codename1.dart.transpiler.api.TranspileResult; +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Only the asset root beside the sources is excluded from the parse -- not +/// every directory that happens to be called assets. +public class AssetRootExclusionTest { + + @TempDir + File tmp; + + @Test + public void aSourceDirectoryNamedAssetsIsStillSource() throws Exception { + File src = new File(tmp, "src"); + TestSupport.write(new File(src, "lib/assets/generated.dart"), "int generated() => 7;\n"); + TestSupport.write(new File(src, "main.dart"), + "import 'lib/assets/generated.dart';\n\nint twice() => generated() * 2;\n"); + // The real asset root: not Dart, and never parsed as such. + TestSupport.write(new File(src, "assets/notes.dart"), "this is an asset, not a program\n"); + // The package asset root is bundled the same way, so its .dart is data too. + TestSupport.write(new File(src, "packages/lib_x/snippet.dart"), "also data, not a program\n"); + // ...while a packages directory below the root is ordinary source. + TestSupport.write(new File(src, "lib/packages/helper.dart"), "int helper() => 1;\n"); + + TranspileResult r = new DartTranspiler().transpile(new TranspileRequest() + .sourceRoot(src).outputDir(new File(tmp, "out")) + .stateFile(new File(tmp, "state.txt")).packageName("com.example.assets")); + + assertFalse(r.hasErrors(), () -> "diagnostics: " + r.errors()); + boolean found = false; + for (GeneratedFile f : r.getGeneratedFiles()) { + found |= f.content.contains("generated()"); + } + assertTrue(found, "lib/assets/generated.dart was transpiled"); + boolean helper = false; + for (GeneratedFile f : r.getGeneratedFiles()) { + helper |= f.content.contains("helper()"); + } + assertTrue(helper, "lib/packages/helper.dart was transpiled"); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CounterTranspileTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CounterTranspileTest.java new file mode 100644 index 00000000000..baf5b527453 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CounterTranspileTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.analyze.Program; +import com.codename1.dart.transpiler.analyze.StubRegistry; +import com.codename1.dart.transpiler.api.Diagnostic; +import com.codename1.dart.transpiler.api.Diagnostics; +import com.codename1.dart.transpiler.api.GeneratedFile; +import com.codename1.dart.transpiler.ast.Ast; +import com.codename1.dart.transpiler.codegen.JavaEmitter; +import com.codename1.dart.transpiler.parser.AstBuilder; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CounterTranspileTest { + + private String readResource(String name) throws Exception { + InputStream in = getClass().getResourceAsStream(name); + java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = in.read(chunk)) > 0) { + buf.write(chunk, 0, n); + } + return new String(buf.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + public void counterAppTranspilesWithoutErrors() throws Exception { + Diagnostics diags = new Diagnostics(); + AstBuilder builder = new AstBuilder(diags); + Program program = new Program(); + program.add(builder.parse("main.dart", readResource("/fixtures/counter_main.dart"))); + + StubRegistry stubs = StubRegistry.loadEmbedded(diags); + JavaEmitter emitter = new JavaEmitter(program, stubs, diags, "com.codename1.generated.flutter"); + List files = emitter.emit(); + + StringBuilder all = new StringBuilder(); + for (GeneratedFile f : files) { + all.append("// ===== ").append(f.relativePath).append(" =====\n").append(f.content).append('\n'); + } + System.out.println(all); + for (Diagnostic d : diags.asList()) { + System.out.println("DIAG: " + d); + } + assertTrue(!diags.hasErrors(), "diagnostics: " + diags.asList()); + assertEquals(5, files.size(), "MyApp, MyHomePage, _MyHomePageState, MainLib, FlutterRegistry"); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CrossLibraryResolutionTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CrossLibraryResolutionTest.java new file mode 100644 index 00000000000..19dd7a8250c --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/CrossLibraryResolutionTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Cross-library symbol resolution exercised by the real new_gallery app: + * top-level route consts read through an {@code import '...' as prefix} name, + * and members (fields/getters) inherited from a program superclass declared in + * another library (the generated GalleryLocalizations pattern). + */ +public class CrossLibraryResolutionTest { + + @Test + public void importPrefixResolvesTopLevelConstsAndFunctions() { + String routes = + "const String homeRoute = '/home';\n" + + "const String loginRoute = '/login';\n" + + "String describeRoute(String r) => r;\n"; + String app = + "import 'routes.dart' as routes;\n" + + "class App {\n" + + " String start() => routes.homeRoute;\n" + + " String other() => routes.loginRoute;\n" + + " String desc() => routes.describeRoute(routes.homeRoute);\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"routes.dart", routes}, + {"app.dart", app}, + }); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + String out = generated(r, "App"); + assertTrue(out.contains("RoutesLib.homeRoute"), out); + assertTrue(out.contains("RoutesLib.loginRoute"), out); + assertTrue(out.contains("RoutesLib.describeRoute("), out); + } + + @Test + public void inheritedProgramSuperclassFieldAndGetterResolve() { + // GalleryLocalizations-style: a base class in one library declares a field and a + // getter; a subclass in another library reads them by bare name and via a receiver. + String base = + "class Base {\n" + + " final String localeName;\n" + + " Base(this.localeName);\n" + + " String get tag => 'x';\n" + + "}\n"; + String derived = + "import 'base.dart';\n" + + "class Derived extends Base {\n" + + " Derived(String l) : super(l);\n" + + " String describe() => localeName + tag;\n" + + "}\n" + + "String viaReceiver(Derived d) => d.localeName + d.tag;\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"base.dart", base}, + {"derived.dart", derived}, + }); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + String out = generated(r, "Derived"); + assertTrue(out.contains("this.get$localeName()"), out); + assertTrue(out.contains("this.tag()"), out); + } + + @Test + public void enhancedEnumParsesAndSiblingClassResolvesCrossFile() { + // demos.dart shape: an enhanced enum (members after `;`) sits beside `class Demos`. + // Before, the enhanced-enum syntax failed to parse and took the whole library down, + // so `Demos` (and its static methods) went unresolved everywhere it was imported. + String demos = + "enum GalleryDemoCategory {\n" + + " study,\n material,\n cupertino,\n other;\n\n" + + " String? displayTitle(String l) {\n" + + " switch (this) {\n" + + " case material:\n return name;\n" + + " default:\n return null;\n }\n }\n}\n\n" + + "class Demos {\n" + + " static List materialDemos() => ['a', 'b'];\n" + + "}\n"; + String home = + "import 'demos.dart';\n" + + "class Home {\n" + + " List all() => Demos.materialDemos();\n" + + " GalleryDemoCategory cat() => GalleryDemoCategory.material;\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"demos.dart", demos}, + {"home.dart", home}, + }); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + String out = generated(r, "Home"); + assertTrue(out.contains("Demos.materialDemos()"), out); + assertTrue(out.contains("GalleryDemoCategory.material"), out); + } + + private static String generated(TestSupport.Result r, String simpleName) { + for (com.codename1.dart.transpiler.api.GeneratedFile f : r.files) { + if (f.relativePath.endsWith(simpleName + ".java")) { + return f.content; + } + } + return ""; + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/Dart3SyntaxParseTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/Dart3SyntaxParseTest.java new file mode 100644 index 00000000000..d903c41d529 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/Dart3SyntaxParseTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.parser.Dart2Lexer; +import com.codename1.dart.transpiler.parser.Dart2Parser; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The vendored grammar is spec-Dart-2.15; Dart 3 syntax (class modifiers, + * patterns, switch expressions, records) is a CN1 extension. These parse-level + * tests pin that the extension keeps accepting Dart 3 — and that the soft + * keywords it introduces (base/sealed/when) still work as identifiers. + */ +public class Dart3SyntaxParseTest { + + private void parses(String source) { + Dart2Lexer lexer = new Dart2Lexer(CharStreams.fromString(source)); + Dart2Parser parser = new Dart2Parser(new CommonTokenStream(lexer)); + final List errors = new ArrayList(); + BaseErrorListener listener = new BaseErrorListener() { + @Override + public void syntaxError(Recognizer r, Object sym, int line, int col, + String msg, RecognitionException e) { + errors.add(line + ":" + col + " " + msg); + } + }; + parser.removeErrorListeners(); + lexer.removeErrorListeners(); + parser.addErrorListener(listener); + lexer.addErrorListener(listener); + parser.compilationUnit(); + assertTrue(errors.isEmpty(), "syntax errors: " + errors + "\nin:\n" + source); + } + + // ------------------------------------------------------------------ + // Class modifiers + // ------------------------------------------------------------------ + + @Test + public void sealedClassHierarchy() { + parses("sealed class Shape {}\n" + + "final class Circle extends Shape { final double r; Circle(this.r); }\n" + + "final class Square extends Shape { final double side; Square(this.side); }\n"); + } + + @Test + public void otherClassModifiers() { + parses("base class A {}\n" + + "interface class B {}\n" + + "abstract base class C {}\n" + + "final class D {}\n"); + } + + // ------------------------------------------------------------------ + // Switch expressions + patterns + // ------------------------------------------------------------------ + + @Test + public void switchExpressionWithObjectPatterns() { + parses("sealed class Shape {}\n" + + "double area(Shape s) => switch (s) {\n" + + " Circle(r: var r) => 3.14 * r * r,\n" + + " Square(side: var x) => x * x,\n" + + "};\n"); + } + + @Test + public void switchExpressionWithConstantAndWildcard() { + parses("String name(int i) => switch (i) {\n" + + " 0 => 'zero',\n" + + " 1 => 'one',\n" + + " _ => 'many',\n" + + "};\n"); + } + + @Test + public void switchExpressionWithGuard() { + parses("String size(int i) => switch (i) {\n" + + " int n when n > 100 => 'big',\n" + + " _ => 'small',\n" + + "};\n"); + } + + @Test + public void switchStatementWithPatternsAndGuards() { + parses("void f(Object o) {\n" + + " switch (o) {\n" + + " case int n when n > 0:\n" + + " print('pos');\n" + + " case String s:\n" + + " print(s);\n" + + " default:\n" + + " print('other');\n" + + " }\n" + + "}\n"); + } + + @Test + public void relationalAndLogicalPatterns() { + parses("String f(int i) => switch (i) {\n" + + " < 0 => 'neg',\n" + + " == 0 => 'zero',\n" + + " _ => 'pos',\n" + + "};\n"); + } + + // ------------------------------------------------------------------ + // Records + // ------------------------------------------------------------------ + + @Test + public void positionalRecordLiteral() { + parses("void f() {\n" + + " var pair = (1, 'a');\n" + + " print(pair);\n" + + "}\n"); + } + + @Test + public void namedRecordLiteral() { + parses("void f() {\n" + + " var p = (x: 1, y: 2);\n" + + " print(p);\n" + + "}\n"); + } + + @Test + public void parenthesizedExpressionStillParsesAsSuch() { + // the record grammar must not swallow ordinary parentheses + parses("void f() {\n" + + " var x = (1 + 2) * 3;\n" + + " print(x);\n" + + "}\n"); + } + + // ------------------------------------------------------------------ + // Soft keywords stay usable as identifiers + // ------------------------------------------------------------------ + + @Test + public void softKeywordsRemainIdentifiers() { + parses("void f() {\n" + + " var base = 1;\n" + + " var sealed = 2;\n" + + " var when = 3;\n" + + " print(base + sealed + when);\n" + + "}\n"); + } + + // ------------------------------------------------------------------ + // Dart 3 if-case statements + // ------------------------------------------------------------------ + + @Test + public void ifCaseStatement() { + parses("void f(Object n) {\n" + + " if (n case int x) {\n" + + " print(x);\n" + + " }\n" + + "}\n"); + } + + @Test + public void ifCaseWithGuardAndElse() { + parses("void f(Object n) {\n" + + " if (n case int x when x > 0) {\n" + + " print(x);\n" + + " } else {\n" + + " print('no');\n" + + " }\n" + + "}\n"); + } + + @Test + public void ifCaseObjectPatternDestructure() { + parses("class N { final int depth; N(this.depth); }\n" + + "void f(Object v) {\n" + + " if (v case N(depth: 0)) {\n" + + " print('zero');\n" + + " }\n" + + "}\n"); + } + + // ------------------------------------------------------------------ + // Dart 3 pattern for-in (record destructuring) + typed set collection-for + // ------------------------------------------------------------------ + + @Test + public void patternForInRecordDestructure() { + parses("void f(List xs) {\n" + + " for (final (int i, String s) in xs.indexed) {\n" + + " print('$i:$s');\n" + + " }\n" + + "}\n"); + } + + @Test + public void typedSetLiteralWithCollectionForAndIf() { + parses("Set f(List xs) {\n" + + " return {\n" + + " for (final (int i, String s) in xs.indexed)\n" + + " if (s.isNotEmpty) i,\n" + + " };\n" + + "}\n"); + } + + @Test + public void mapLiteralWithCollectionFor() { + parses("Map f(List xs) {\n" + + " return {\n" + + " for (final int x in xs) x: 'v',\n" + + " };\n" + + "}\n"); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IncrementalCacheKeyTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IncrementalCacheKeyTest.java new file mode 100644 index 00000000000..648cf031304 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IncrementalCacheKeyTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.api.DartTranspiler; +import com.codename1.dart.transpiler.api.TranspileRequest; +import com.codename1.dart.transpiler.api.TranspileResult; +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The incremental skip reuses output only when nothing that shaped it changed. +public class IncrementalCacheKeyTest { + + @TempDir + File tmp; + + private TranspileResult run(File src, File out, File state, String pkg) { + return new DartTranspiler().transpile(new TranspileRequest() + .sourceRoot(src).outputDir(out).stateFile(state).packageName(pkg)); + } + + @Test + public void unchangedInputsSkipAndAChangedPackageDoesNot() throws Exception { + File src = new File(tmp, "src"); + src.mkdirs(); + TestSupport.write(new File(src, "a.dart"), "int answer() => 42;\n"); + File out = new File(tmp, "out"); + File state = new File(tmp, "state.txt"); + + TranspileResult first = run(src, out, state, "com.example.one"); + assertFalse(first.isUpToDate(), "the first run transpiles"); + assertTrue(run(src, out, state, "com.example.one").isUpToDate(), + "nothing changed, so the second run is skipped"); + + // Only the package changes. The generated sources name their package, so + // reusing them would leave them all in the old one. + assertFalse(run(src, out, state, "com.example.two").isUpToDate(), + "a changed output package must regenerate"); + } + + @Test + public void changingThePackageRemovesThePreviousPackagesSources() throws Exception { + File src = new File(tmp, "src2"); + src.mkdirs(); + TestSupport.write(new File(src, "a.dart"), "int answer() => 42;\n"); + File out = new File(tmp, "out2"); + File state = new File(tmp, "state2.txt"); + run(src, out, state, "com.example.one"); + File oldDir = new File(out, "com/example/one"); + assertTrue(oldDir.listFiles() != null && oldDir.listFiles().length > 0, "the first run generated sources"); + run(src, out, state, "com.example.two"); + File[] left = oldDir.listFiles(); + assertTrue(left == null || left.length == 0, + "the old package's sources must not stay on the compile root beside the new ones"); + assertTrue(new File(out, "com/example/two").listFiles().length > 0); + } + + @Test + public void aFailedWriteLeavesNoStateToSkipTheNextBuild() throws Exception { + File src = new File(tmp, "src3"); + src.mkdirs(); + File dart = new File(src, "a.dart"); + TestSupport.write(dart, "int answer() => 42;\n"); + File out = new File(tmp, "out3"); + File state = new File(tmp, "state3.txt"); + run(src, out, state, "com.example.three"); + assertTrue(state.isFile(), "a successful run records its inputs"); + + // Every generated file's path becomes a directory, so writing it fails. + File pkg = new File(out, "com/example/three"); + for (File f : pkg.listFiles()) { + assertTrue(f.delete() && f.mkdir(), "could not block " + f); + } + TestSupport.write(dart, "int answer() => 43;\n"); + TranspileResult failed = run(src, out, state, "com.example.three"); + assertTrue(failed.hasErrors(), "the blocked writes are reported"); + assertFalse(state.exists(), "the digest of inputs whose output was not written is not recorded"); + + // Back to the inputs of the earlier, successful build: its state is gone, so + // this build writes again instead of trusting output that no longer matches. + TestSupport.write(dart, "int answer() => 42;\n"); + assertFalse(run(src, out, state, "com.example.three").isUpToDate()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IterableMixinTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IterableMixinTest.java new file mode 100644 index 00000000000..b5a6289628f --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/IterableMixinTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The dart:collection mixins are registered as transpiler built-in stubs, so a class that applies + * {@code with IterableMixin} resolves the mixin (no E0402) and inherited mixin members resolve. + */ +public class IterableMixinTest { + + @Test + public void iterableMixinIsRecognized() { + String src = + "class BoardPoint {}\n" + + "class Board extends Object with IterableMixin {\n" + + " final List _points = [];\n" + + " @override\n" + + " Iterator get iterator => _points.iterator;\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"board.dart", src}}); + String diagnostics = r.diags.asList().toString(); + assertFalse(diagnostics.contains("E0402"), "unexpected unknown-mixin error: " + diagnostics); + } + + @Test + public void inheritedMixinMemberResolvesInBareCall() { + // forEach is contributed by IterableMixin as an inherited default; a bare call must resolve. + String src = + "class BoardPoint {}\n" + + "class Board extends Object with IterableMixin {\n" + + " final List _points = [];\n" + + " @override\n" + + " Iterator get iterator => _points.iterator;\n" + + " void dump() {\n" + + " forEach((p) => print(p));\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"board.dart", src}}); + String diagnostics = r.diags.asList().toString(); + assertFalse(diagnostics.contains("E0402"), "unknown-mixin error: " + diagnostics); + assertTrue(!diagnostics.contains("E0137") || !diagnostics.contains("forEach"), + "forEach should resolve via the mixin: " + diagnostics); + } + + @Test + public void inheritedMixinMemberResolvesOnReceiver() { + // board.forEach(...) — a call on a typed receiver must resolve through the applied mixin. + String src = + "class BoardPoint {}\n" + + "class Board extends Object with IterableMixin {\n" + + " final List _points = [];\n" + + " @override\n" + + " Iterator get iterator => _points.iterator;\n" + + "}\n" + + "void paint(Board board) {\n" + + " board.forEach((p) => print(p));\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"board.dart", src}}); + String diagnostics = r.diags.asList().toString(); + assertTrue(!diagnostics.contains("E0137") || !diagnostics.contains("forEach"), + "forEach on a Board receiver should resolve via the mixin: " + diagnostics); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M2DemoTranspileTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M2DemoTranspileTest.java new file mode 100644 index 00000000000..77f3c5136fd --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M2DemoTranspileTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class M2DemoTranspileTest { + + @Test + public void m2DemoTranspilesWithoutErrors() throws Exception { + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"main.dart", TestSupport.read(new File("src/test/resources/fixtures/m2_demo.dart"))} + }); + for (com.codename1.dart.transpiler.api.GeneratedFile f : r.files) { + if (f.relativePath.equals("_DemoPageState.java")) { + System.out.println(f.content); + } + } + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M3DemoTranspileTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M3DemoTranspileTest.java new file mode 100644 index 00000000000..898f7a93ae7 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M3DemoTranspileTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class M3DemoTranspileTest { + + @Test + public void m3DemoTranspilesWithoutErrors() throws Exception { + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"main.dart", TestSupport.read(new File("src/test/resources/fixtures/m3_demo.dart"))} + }); + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M4DemoTranspileTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M4DemoTranspileTest.java new file mode 100644 index 00000000000..d3a4b8a975d --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/M4DemoTranspileTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class M4DemoTranspileTest { + + @Test + public void m4DemoTranspilesWithoutErrors() throws Exception { + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"main.dart", TestSupport.read(new File("src/test/resources/fixtures/m4_demo.dart"))} + }); + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ParserSmokeTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ParserSmokeTest.java new file mode 100644 index 00000000000..0c13110f3f3 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ParserSmokeTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.parser.Dart2Lexer; +import com.codename1.dart.transpiler.parser.Dart2Parser; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ParserSmokeTest { + + @Test + public void counterAppParsesWithoutSyntaxErrors() throws Exception { + InputStream in = getClass().getResourceAsStream("/fixtures/counter_main.dart"); + assertNotNull(in); + Dart2Lexer lexer = new Dart2Lexer(CharStreams.fromStream(in)); + Dart2Parser parser = new Dart2Parser(new CommonTokenStream(lexer)); + List errors = new ArrayList<>(); + parser.removeErrorListeners(); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, + int charPositionInLine, String msg, RecognitionException e) { + errors.add(line + ":" + charPositionInLine + " " + msg); + } + }); + Dart2Parser.CompilationUnitContext unit = parser.compilationUnit(); + assertNotNull(unit); + assertTrue(errors.isEmpty(), "syntax errors: " + errors); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ProviderTypeTokenTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ProviderTypeTokenTest.java new file mode 100644 index 00000000000..636c12d9bb7 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/ProviderTypeTokenTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.api.GeneratedFile; +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * {@code Provider.value(...)} carries its T to the runtime. Java erases it, and a + * provider holding null cannot show its type through its value, so without the token a + * {@code Provider} that had not loaded yet was skipped by every lookup. + */ +public class ProviderTypeTokenTest { + + @Test + public void providerValueCarriesItsTypeArgument() { + File runtime = TestSupport.findJar("codenameone-flutter-runtime"); + assumeTrue(runtime != null, "flutter runtime jar not built"); + String src = + "import 'package:flutter/material.dart';\n" + + "import 'package:provider/provider.dart';\n" + + "class Store {}\n" + + "Widget wrap(Store? s, Widget child) {\n" + + " return Provider.value(value: s, child: child);\n" + + "}\n" + + "void main() {}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}, + Collections.singletonList(runtime)); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + StringBuilder all = new StringBuilder(); + for (GeneratedFile f : r.files) { + all.append(f.content); + } + assertTrue(all.toString().contains(".providedType(Store.class)"), all.toString()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerFinalResolutionTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerFinalResolutionTest.java new file mode 100644 index 00000000000..34333c12a66 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerFinalResolutionTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Regression coverage for the resolution-level gaps closed on the final new_gallery + * transpile drive: single-package class-name collisions resolved with same-library + * preference ({@code widget.}), enhanced-enum instance methods, {@code is!}/{@code ||} + * flow promotion, dynamic member access, function-typedef call result types, primitive + * numeric {@code parse}/{@code tryParse}, and {@code Object.runtimeType}. + */ +public class TranspilerFinalResolutionTest { + + /** + * Two libraries each declare a {@code Backdrop} StatefulWidget with disjoint fields; a + * State's {@code widget.} must resolve against the same-library widget, not the + * last one registered globally. + */ + @Test + public void widgetFieldResolvesAgainstSameLibraryWidget() { + String pages = + "import 'package:flutter/material.dart';\n" + + "class Backdrop extends StatefulWidget {\n" + + " const Backdrop({super.key, required this.isDesktop, this.settingsPage});\n" + + " final bool isDesktop;\n" + + " final Widget? settingsPage;\n" + + " @override\n" + + " State createState() => _BackdropState();\n" + + "}\n" + + "class _BackdropState extends State {\n" + + " @override\n" + + " Widget build(BuildContext context) =>\n" + + " widget.isDesktop ? (widget.settingsPage ?? const Text('x')) : const Text('y');\n" + + "}\n"; + String crane = + "import 'package:flutter/material.dart';\n" + + "class Backdrop extends StatefulWidget {\n" + + " const Backdrop({super.key, required this.backLayerItems});\n" + + " final List backLayerItems;\n" + + " @override\n" + + " State createState() => _CraneState();\n" + + "}\n" + + "class _CraneState extends State {\n" + + " @override\n" + + " Widget build(BuildContext context) => Text(widget.backLayerItems.length.toString());\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"pages/backdrop.dart", pages}, {"studies/crane/backdrop.dart", crane}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void enhancedEnumInstanceMethod() { + String src = + "enum GalleryDemoCategory {\n" + + " study, material, cupertino, other;\n" + + " @override\n" + + " String toString() => name.toUpperCase();\n" + + " String? displayTitle(String fallback) => switch (this) {\n" + + " study => null,\n" + + " material || cupertino => toString(),\n" + + " other => fallback,\n" + + " };\n" + + "}\n" + + "class User {\n" + + " String? label(GalleryDemoCategory c) => c.displayTitle('ref');\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + /** `x is! T || x.member` flow-promotes x to T in the right operand. */ + @Test + public void isNotOrPromotesRightOperand() { + String src = + "import 'package:flutter/material.dart';\n" + + "class _Painter extends CustomPainter {\n" + + " _Painter({required this.time});\n" + + " final double time;\n" + + " @override\n" + + " void paint(Canvas canvas, Size size) {}\n" + + " @override\n" + + " bool shouldRepaint(CustomPainter oldDelegate) =>\n" + + " oldDelegate is! _Painter || oldDelegate.time != time;\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void dynamicMemberAccessAndTypedefCallAndParseAndRuntimeType() { + String src = + "typedef LibraryLoader = Future Function();\n" + + "class Demos {\n" + + " static Future preload(LibraryLoader loader) =>\n" + + " loader().then((dynamic _) { print('done'); });\n" + + " static String? slugOf(dynamic demo) => demo.slug as String?;\n" + + " static double? parse(String v) => double.tryParse(v);\n" + + " static int? parseI(String v) => int.parse(v);\n" + + "}\n" + + "class BoardPoint {\n" + + " final int q;\n" + + " BoardPoint(this.q);\n" + + " @override\n" + + " bool operator ==(Object other) =>\n" + + " other.runtimeType == runtimeType && other is BoardPoint && other.q == q;\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerRemainTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerRemainTest.java new file mode 100644 index 00000000000..b09052422d0 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TranspilerRemainTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Regression coverage for the transpiler-level gaps closed for the new_gallery + * drive: nested local functions, function-value {@code .call()} invocation, + * function-typed field invocation, and instance method tear-offs. + */ +public class TranspilerRemainTest { + + @Test + public void nestedLocalFunctionCalledAndTornOff() { + // A local function called directly, and one passed by name (tear-off). + String src = + "class C {\n" + + " int compute(int seed) {\n" + + " int square(int x) {\n" + + " return x * x;\n" + + " }\n" + + " void log(int v) {\n" + + " print(v);\n" + + " }\n" + + " log(square(seed));\n" + + " return square(seed) + 1;\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void nestedLocalFunctionCapturesMutableLocal() { + // The nested function mutates an outer local -> must be boxed like a lambda. + String src = + "class C {\n" + + " int run() {\n" + + " int total = 0;\n" + + " void add(int x) {\n" + + " total = total + x;\n" + + " }\n" + + " add(2);\n" + + " add(3);\n" + + " return total;\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void functionTypedFieldInvocation() { + String src = + "class Btn {\n" + + " final void Function() onTap;\n" + + " final void Function(String value) onChanged;\n" + + " Btn(this.onTap, this.onChanged);\n" + + " void fire() {\n" + + " onTap();\n" + + " onChanged('hi');\n" + + " }\n" + + "}\n" + + "class Host {\n" + + " final Btn b;\n" + + " Host(this.b);\n" + + " void go() {\n" + + " b.onTap();\n" + + " b.onChanged('x');\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void explicitCallAndNullAwareCall() { + String src = + "class C {\n" + + " void run(void Function()? cb, int Function(int) f) {\n" + + " cb?.call();\n" + + " final int y = f.call(3);\n" + + " print(y);\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } + + @Test + public void instanceMethodTearOff() { + String src = + "class Src {\n" + + " void handle(bool? v) {}\n" + + "}\n" + + "class Wire {\n" + + " final Src s;\n" + + " Wire(this.s);\n" + + " void Function(bool?) wireUp() {\n" + + " return s.handle;\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"main.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TypeInferenceTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TypeInferenceTest.java new file mode 100644 index 00000000000..1a750cf63bf --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/TypeInferenceTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler; + +import com.codename1.dart.transpiler.api.Diagnostic; +import com.codename1.dart.transpiler.harness.TestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Type-inference behaviours exercised by the real new_gallery app: flow-based + * {@code is}-promotion, ternary least-upper-bound, and cascade-diagnostic + * suppression on a receiver whose type already fell to {@code dynamic} from a + * reported root cause. + */ +public class TypeInferenceTest { + + private static long count(TestSupport.Result r, String code) { + long n = 0; + for (Diagnostic d : r.diags.asList()) { + if (d.code.equals(code)) { + n++; + } + } + return n; + } + + private static String generated(TestSupport.Result r, String simpleName) { + for (com.codename1.dart.transpiler.api.GeneratedFile f : r.files) { + if (f.relativePath.endsWith(simpleName + ".java")) { + return f.content; + } + } + StringBuilder paths = new StringBuilder(); + for (com.codename1.dart.transpiler.api.GeneratedFile f : r.files) { + paths.append(f.relativePath).append(' '); + } + throw new AssertionError("no " + simpleName + " in: " + paths); + } + + /** `other is T && other.member` must promote `other` to T (with a cast) — no error. */ + @Test + public void isPromotionInAndChain() { + String src = + "class Point {\n" + + " final int x;\n" + + " final int y;\n" + + " Point(this.x, this.y);\n" + + " bool same(Object other) => other is Point && other.x == x && other.y == y;\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"point.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + String out = generated(r, "Point"); + assertTrue(out.contains("((Point) other)"), out); + } + + /** `if (x is T) { x.member }` promotes x inside the then-branch. */ + @Test + public void isPromotionInIfThen() { + String src = + "class Box {\n" + + " final int v;\n" + + " Box(this.v);\n" + + " int read(Object o) {\n" + + " if (o is Box) {\n" + + " return o.v;\n" + + " }\n" + + " return 0;\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"box.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + String out = generated(r, "Box"); + assertTrue(out.contains("((Box) o)"), out); + } + + /** + * A chain rooted at an unresolved identifier (`Missing.a.b.c`) must report the + * single root cause once (E0129) and NOT a cascade of member errors down the chain. + */ + @Test + public void unresolvedRootDoesNotCascade() { + String src = + "class User {\n" + + " void go() {\n" + + " print(Missing.a.b.c);\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"user.dart", src}}); + assertEquals(1, count(r, "E0129"), "root: " + r.diags.asList()); + assertEquals(0, count(r, "E0132"), "no cascade: " + r.diags.asList()); + } + + /** An `assert(...)` statement in a method body is dropped, not flagged unsupported. */ + @Test + public void assertStatementDropped() { + String src = + "class Guard {\n" + + " int half(int n) {\n" + + " assert(n > 0, 'must be positive');\n" + + " return n ~/ 2;\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"guard.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + assertEquals(0, count(r, "E0115"), "assert should be dropped: " + r.diags.asList()); + String out = generated(r, "Guard"); + assertFalse(out.contains("assert("), out); + } + + /** A ternary over two related types resolves to their common ancestor, not dynamic. */ + @Test + public void ternaryLeastUpperBound() { + String src = + "class Animal { String noise() => 'x'; }\n" + + "class Cat extends Animal {}\n" + + "class Dog extends Animal {}\n" + + "class Zoo {\n" + + " String pick(bool b) {\n" + + " var a = b ? Cat() : Dog();\n" + + " return a.noise();\n" + + " }\n" + + "}\n"; + TestSupport.Result r = TestSupport.transpile(new String[][] {{"zoo.dart", src}}); + assertFalse(r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/BehaviorTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/BehaviorTest.java new file mode 100644 index 00000000000..f3db554ba3f --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/BehaviorTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.harness; + +import com.codename1.dart.transpiler.api.GeneratedFile; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Behavioral execution tests: each dir under src/test/resources/behavior/ + * holds main.dart + expect.txt. The dart is transpiled, compiled with the + * JDK 17 javac (JAVA17_HOME), executed, and stdout diffed against + * expect.txt. Pins semantics: int math, double formatting, map ordering, + * closure capture. + * + *

Skipped when JAVA17_HOME or the dart-runtime jar is unavailable.

+ */ +public class BehaviorTest { + + private static final File BEHAVIOR_ROOT = new File("src/test/resources/behavior"); + + @TestFactory + public List behaviorCases() { + List tests = new ArrayList(); + File[] cases = BEHAVIOR_ROOT.listFiles(); + if (cases != null) { + for (File dir : cases) { + if (dir.isDirectory()) { + tests.add(DynamicTest.dynamicTest(dir.getName(), () -> runCase(dir))); + } + } + } + assertTrue(!tests.isEmpty(), "no behavior cases found under " + BEHAVIOR_ROOT.getAbsolutePath()); + return tests; + } + + private void runCase(File dir) throws Exception { + File java17 = TestSupport.java17Home(); + File dartRuntime = TestSupport.findJar("codenameone-dart-runtime"); + File core = TestSupport.findJar("codenameone-core"); + assumeTrue(java17 != null, "JAVA17_HOME not set — skipping behavioral execution"); + assumeTrue(dartRuntime != null && core != null, "runtime jars not built — skipping"); + String rtClasspath = dartRuntime.getAbsolutePath() + File.pathSeparator + core.getAbsolutePath(); + // A case that touches a widget API needs the Flutter runtime to link against. + // Cases that do not are unaffected by its presence, so this is unconditional + // rather than another thing each case has to declare. + File flutterRuntime = TestSupport.findJar("codenameone-flutter-runtime"); + if (flutterRuntime != null) { + rtClasspath = rtClasspath + File.pathSeparator + flutterRuntime.getAbsolutePath(); + } + + List stubEntries = new ArrayList(); + if (flutterRuntime != null) { + stubEntries.add(flutterRuntime); + } + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"main.dart", TestSupport.read(new File(dir, "main.dart"))} + }, stubEntries); + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + + File work = Files.createTempDirectory("dart-behavior-" + dir.getName()).toFile(); + File srcDir = new File(work, "src/" + TestSupport.PKG.replace('.', '/')); + List javacArgs = new ArrayList(); + for (GeneratedFile gf : r.files) { + File out = new File(srcDir, gf.relativePath); + TestSupport.write(out, gf.content); + javacArgs.add(out.getAbsolutePath()); + } + File runner = new File(work, "src/Runner.java"); + TestSupport.write(runner, "public class Runner {\n" + + " public static void main(String[] args) {\n" + + " " + TestSupport.PKG + ".MainLib.main$();\n" + + " }\n" + + "}\n"); + javacArgs.add(runner.getAbsolutePath()); + + File classes = new File(work, "classes"); + classes.mkdirs(); + List javac = new ArrayList(); + javac.add(new File(java17, "bin/javac").getAbsolutePath()); + javac.add("-cp"); + javac.add(rtClasspath); + javac.add("-d"); + javac.add(classes.getAbsolutePath()); + javac.addAll(javacArgs); + Object[] compileResult = TestSupport.run(javac, work); + assertEquals(0, compileResult[0], "javac failed:\n" + compileResult[1]); + + List java = new ArrayList(); + java.add(new File(java17, "bin/java").getAbsolutePath()); + java.add("-cp"); + java.add(classes.getAbsolutePath() + File.pathSeparator + rtClasspath); + java.add("Runner"); + Object[] runResult = TestSupport.run(java, work); + assertEquals(0, runResult[0], "execution failed:\n" + runResult[1]); + + String expected = TestSupport.read(new File(dir, "expect.txt")); + assertEquals(expected.trim().replace("\r\n", "\n"), + ((String) runResult[1]).trim().replace("\r\n", "\n"), + "behavioral output mismatch for " + dir.getName()); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/CompileGeneratedTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/CompileGeneratedTest.java new file mode 100644 index 00000000000..64afc96cfd5 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/CompileGeneratedTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.harness; + +import com.codename1.dart.transpiler.api.GeneratedFile; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Compiles the transpiled counter app with the real JDK 17 javac against the + * dart-runtime and flutter-runtime jars — proves the emitter's output links + * against the hand-written runtime API (mirrors svg-transcoder's + * CompileGeneratedSourceTest). + */ +public class CompileGeneratedTest { + + @Test + public void m2DemoOutputCompilesAgainstRuntimes() throws Exception { + compileFixture("src/test/resources/fixtures/m2_demo.dart"); + } + + @Test + public void counterAppOutputCompilesAgainstRuntimes() throws Exception { + compileFixture("src/test/resources/fixtures/counter_main.dart"); + } + + private void compileFixture(String fixture) throws Exception { + File java17 = TestSupport.java17Home(); + File dartRuntime = TestSupport.findJar("codenameone-dart-runtime"); + File flutterRuntime = TestSupport.findJar("codenameone-flutter-runtime"); + File core = TestSupport.findJar("codenameone-core"); + assumeTrue(java17 != null, "JAVA17_HOME not set — skipping compile check"); + assumeTrue(dartRuntime != null && flutterRuntime != null && core != null, + "runtime jars not built — skipping compile check"); + + TestSupport.Result r = TestSupport.transpile(new String[][] { + {"main.dart", TestSupport.read(new File(fixture))} + }); + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + + File work = Files.createTempDirectory("dart-compile-check").toFile(); + File srcDir = new File(work, TestSupport.PKG.replace('.', '/')); + List args = new ArrayList(); + for (GeneratedFile gf : r.files) { + File out = new File(srcDir, gf.relativePath); + TestSupport.write(out, gf.content); + args.add(out.getAbsolutePath()); + } + File classes = new File(work, "classes"); + classes.mkdirs(); + List javac = new ArrayList(); + javac.add(new File(java17, "bin/javac").getAbsolutePath()); + javac.add("-cp"); + javac.add(dartRuntime.getAbsolutePath() + File.pathSeparator + + flutterRuntime.getAbsolutePath() + File.pathSeparator + core.getAbsolutePath()); + javac.add("-d"); + javac.add(classes.getAbsolutePath()); + javac.addAll(args); + Object[] result = TestSupport.run(javac, work); + assertEquals(0, result[0], "generated counter app failed to compile:\n" + result[1]); + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/GoldenTest.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/GoldenTest.java new file mode 100644 index 00000000000..2b087ac40e7 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/GoldenTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.harness; + +import com.codename1.dart.transpiler.api.GeneratedFile; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Golden-file transpilation tests: each directory under + * src/test/resources/golden/<case>/ holds input .dart files and an + * expected/ dir of .java outputs. Run with -Dgolden.update=true to + * regenerate the expected outputs after an intentional emitter change. + */ +public class GoldenTest { + + private static final File GOLDEN_ROOT = new File("src/test/resources/golden"); + + @TestFactory + public List goldenCases() { + List tests = new ArrayList(); + File[] cases = GOLDEN_ROOT.listFiles(); + if (cases != null) { + for (File dir : cases) { + if (dir.isDirectory()) { + tests.add(DynamicTest.dynamicTest(dir.getName(), () -> runCase(dir))); + } + } + } + assertTrue(!tests.isEmpty(), "no golden cases found under " + GOLDEN_ROOT.getAbsolutePath()); + return tests; + } + + private void runCase(File dir) throws Exception { + boolean update = Boolean.getBoolean("golden.update"); + List sources = new ArrayList(); + File[] inputs = dir.listFiles(); + if (inputs != null) { + for (File f : inputs) { + if (f.getName().endsWith(".dart")) { + sources.add(new String[] {f.getName(), TestSupport.read(f)}); + } + } + } + TestSupport.Result r = TestSupport.transpile(sources.toArray(new String[0][])); + assertTrue(!r.diags.hasErrors(), "diagnostics: " + r.diags.asList()); + + File expectedDir = new File(dir, "expected"); + if (update) { + // wipe and regenerate + File[] old = expectedDir.listFiles(); + if (old != null) { + for (File f : old) { + f.delete(); + } + } + for (GeneratedFile gf : r.files) { + TestSupport.write(new File(expectedDir, gf.relativePath), gf.content); + } + return; + } + File[] expected = expectedDir.listFiles(); + if (expected == null || expected.length == 0) { + fail("no expected outputs for golden case '" + dir.getName() + + "' — run with -Dgolden.update=true to seed them"); + } + assertEquals(expected.length, r.files.size(), + "generated file count differs for case " + dir.getName()); + for (GeneratedFile gf : r.files) { + File exp = new File(expectedDir, gf.relativePath); + assertTrue(exp.exists(), "unexpected new generated file " + gf.relativePath); + assertEquals(TestSupport.read(exp), gf.content, + "golden mismatch: " + dir.getName() + "/" + gf.relativePath + + " (run -Dgolden.update=true if intentional)"); + } + } +} diff --git a/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/TestSupport.java b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/TestSupport.java new file mode 100644 index 00000000000..778cc6f7992 --- /dev/null +++ b/maven/dart-transpiler/src/test/java/com/codename1/dart/transpiler/harness/TestSupport.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.dart.transpiler.harness; + +import com.codename1.dart.transpiler.analyze.Program; +import com.codename1.dart.transpiler.analyze.StubRegistry; +import com.codename1.dart.transpiler.api.Diagnostics; +import com.codename1.dart.transpiler.api.GeneratedFile; +import com.codename1.dart.transpiler.codegen.JavaEmitter; +import com.codename1.dart.transpiler.parser.AstBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +/** + * Shared helpers for the golden/compile/behavioral test harness. + */ +public final class TestSupport { + + public static final String PKG = "com.codename1.generated.flutter"; + + private TestSupport() { + } + + public static class Result { + public final List files; + public final Diagnostics diags; + + Result(List files, Diagnostics diags) { + this.files = files; + this.diags = diags; + } + } + + /** Transpiles a set of in-memory dart sources (fileName -> content). */ + public static Result transpile(String[][] sources) { + return transpile(sources, java.util.Collections.emptyList()); + } + + /** + * Transpiles against the Dart stubs published by {@code stubEntries} — the jars or + * class directories an app would have on its classpath. + * + *

This is how the real mojo resolves the API surface, and a case that touches a + * widget needs it: the embedded stub set is only the built-ins plus material, so + * anything a runtime module declares for itself (Curves, the animation family) is + * unresolvable without the jar that declares it.

+ */ + public static Result transpile(String[][] sources, java.util.List stubEntries) { + Diagnostics diags = new Diagnostics(); + AstBuilder builder = new AstBuilder(diags); + Program program = new Program(); + for (String[] s : sources) { + program.add(builder.parse(s[0], s[1])); + } + // loadFromClasspath falls back to the embedded set when nothing contributes, so an + // empty list keeps the previous behaviour exactly. + StubRegistry stubs = StubRegistry.loadFromClasspath(stubEntries, diags); + JavaEmitter emitter = new JavaEmitter(program, stubs, diags, PKG); + return new Result(emitter.emit(), diags); + } + + public static String read(File f) throws IOException { + return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8); + } + + public static void write(File f, String content) throws IOException { + f.getParentFile().mkdirs(); + Files.write(f.toPath(), content.getBytes(StandardCharsets.UTF_8)); + } + + /** Finds JAVA17_HOME (env or tools/env.sh defaults); null if unavailable. */ + public static File java17Home() { + String env = System.getenv("JAVA17_HOME"); + if (env != null && new File(env, "bin/javac").exists()) { + return new File(env); + } + return null; + } + + /** Locates a sibling-module or local-repo jar; null if not built yet. */ + public static File findJar(String artifactId) { + String version = "8.0-SNAPSHOT"; + File[] candidates = new File[] { + new File("../" + moduleDir(artifactId) + "/target/" + artifactId + "-" + version + ".jar"), + new File("/tmp/cn1-local-repo/com/codenameone/" + artifactId + "/" + version + "/" + + artifactId + "-" + version + ".jar"), + new File(System.getProperty("user.home"), + ".m2/repository/com/codenameone/" + artifactId + "/" + version + "/" + + artifactId + "-" + version + ".jar"), + }; + for (File f : candidates) { + if (f.exists()) { + return f; + } + } + return null; + } + + private static String moduleDir(String artifactId) { + if (artifactId.equals("codenameone-dart-runtime")) { + return "dart-runtime"; + } + if (artifactId.equals("codenameone-flutter-runtime")) { + return "flutter-runtime"; + } + if (artifactId.equals("codenameone-core")) { + return "core"; + } + return artifactId; + } + + /** Runs a process, returns [exitCode, stdout+stderr]. */ + public static Object[] run(List cmd, File dir) throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(dir); + pb.redirectErrorStream(true); + Process p = pb.start(); + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = p.getInputStream().read(chunk)) > 0) { + out.write(chunk, 0, n); + } + int code = p.waitFor(); + return new Object[] {code, new String(out.toByteArray(), StandardCharsets.UTF_8)}; + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/expect.txt new file mode 100644 index 00000000000..10d4453e236 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/expect.txt @@ -0,0 +1,6 @@ +async throw returned a future +caught through the future +caught after await +false +true +2 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/main.dart new file mode 100644 index 00000000000..beca8120421 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_async_errors_and_identity/main.dart @@ -0,0 +1,35 @@ +// An async function's exceptions belong to the Future it returns, and Dart's +// List, Map and Set compare by identity. + +Future fails() async { + throw StateError('boom'); +} + +Future failsAfterAwait() async { + await Future.value(1); + throw StateError('late'); +} + +void main() { + Future? f; + try { + f = fails(); + print('async throw returned a future'); + } catch (e) { + print('async throw escaped synchronously'); + } + f?.catchError((e) { + print('caught through the future'); + }); + failsAfterAwait().catchError((e) { + print('caught after await'); + }); + + print([1] == [1]); + var a = [1]; + print(a == a); + var m = {}; + m[[1]] = 1; + m[[1]] = 2; + print(m.length); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/expect.txt new file mode 100644 index 00000000000..12f418e8e52 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/expect.txt @@ -0,0 +1,13 @@ +aXcabc +abcaXc +abcabY +both true +right side skipped: true +or reached the right side +42 +null +int ~/ 0 is an UnsupportedError +int % 0 is an UnsupportedError +double ~/ 0 is an UnsupportedError +Future.wait returned +the future carried true diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/main.dart new file mode 100644 index 00000000000..99de8b048a7 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_await_logic_and_division_round16/main.dart @@ -0,0 +1,58 @@ +// replaceFirst with and without a start; && and || over dynamic operands keep +// short-circuiting; awaiting a value that is not a Future is the value; division +// and remainder by zero are UnsupportedErrors; Future.wait over an iterable that +// throws completes with that error. +Iterable> bad() sync* { + throw StateError('iter'); +} + +bool checked = false; +bool check() { + checked = true; + return true; +} + +Future main() async { + print('abcabc'.replaceFirst('b', 'X')); + print('abcabc'.replaceFirst('b', 'X', 2)); + print('abcabc'.replaceFirst(RegExp('c'), 'Y', 3)); + + dynamic on = true; + dynamic off = false; + if (on && check()) { + print('both true'); + } + checked = false; + if (off && check()) { + print('never'); + } + print('right side skipped: ${!checked}'); + if (off || check()) { + print('or reached the right side'); + } + + print(await 42); + print(await null); + final int z = int.parse('0'); + try { + print(1 ~/ z); + } on UnsupportedError { + print('int ~/ 0 is an UnsupportedError'); + } + try { + print(1 % z); + } on UnsupportedError { + print('int % 0 is an UnsupportedError'); + } + try { + print(1.0 ~/ 0.0); + } on UnsupportedError { + print('double ~/ 0 is an UnsupportedError'); + } + final f = Future.wait(bad()); + print('Future.wait returned'); + await f.catchError((e) { + print('the future carried ${e is StateError}'); + return []; + }); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/expect.txt new file mode 100644 index 00000000000..a5f0d9b76d2 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/expect.txt @@ -0,0 +1,10 @@ +List.from refused an int +[a, b] +Uint8List.fromList refused a string +123 +42 +true +true +true +caught as the Dart error: true +offset near the limit is a RangeError diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/main.dart new file mode 100644 index 00000000000..5ec0ff813fb --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_checks_and_hashes_round15/main.dart @@ -0,0 +1,51 @@ +// List.from and Uint8List.fromList check element types; int.parse trims Dart's +// whitespace; Object.hash/hashAll agree with == across int and double; a caught +// concurrent modification is Dart's error; ByteData offsets near the long limit +// are range errors. +import 'dart:typed_data'; + +class Money { + final num amount; + Money(this.amount); + + @override + bool operator ==(Object other) => other is Money && other.amount == amount; + + @override + int get hashCode => Object.hashAll([amount]); +} + +void main() { + final dynamic mixed = [1]; + try { + print(List.from(mixed)); + } on TypeError { + print('List.from refused an int'); + } + print(List.from(['a', 'b'])); + final dynamic notInts = ['x']; + try { + print(Uint8List.fromList(notInts)); + } on TypeError { + print('Uint8List.fromList refused a string'); + } + print(int.parse('\u00A0123\u00A0')); + print(int.tryParse('\u2003 42 \u3000')); + print(Object.hashAll([1]) == Object.hashAll([1.0])); + print(Object.hash(2, 'x') == Object.hash(2.0, 'x')); + print({Money(1)}.contains(Money(1.0))); + final Map m = {'a': 1}; + try { + m.forEach((k, v) { + m['b$k'] = v; + }); + } on ConcurrentModificationError catch (e) { + print('caught as the Dart error: ${e is ConcurrentModificationError}'); + } + final bd = ByteData(8); + try { + bd.getInt32(9223372036854775807); + } on RangeError { + print('offset near the limit is a RangeError'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/expect.txt new file mode 100644 index 00000000000..b192f5fee74 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/expect.txt @@ -0,0 +1,14 @@ +toInt refused NaN +floor refused infinity +clamp refused the range +5 +9 +4 +view is read-only +null +null +1500.0 +0.5 +string plus int refused +true +fixed length diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/main.dart new file mode 100644 index 00000000000..10df2957539 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_conversions_views_round8/main.dart @@ -0,0 +1,59 @@ +// Non-finite conversions and a bad clamp range throw; asMap is a live, +// unmodifiable view; double.tryParse follows Dart's grammar; String + dynamic +// dispatches; Point has value equality; toList(growable: false) is fixed. +import 'dart:math'; + +void main() { + double bad = double.nan; + try { + print(bad.toInt()); + } on UnsupportedError { + print('toInt refused NaN'); + } + double inf = double.infinity; + try { + print(inf.floor()); + } on UnsupportedError { + print('floor refused infinity'); + } + try { + print(5.clamp(10, 0)); + } on ArgumentError { + print('clamp refused the range'); + } + print(7.clamp(0, 5)); + + final list = [1, 2]; + final view = list.asMap(); + list[0] = 9; + print(view[0]); + print(view.length + view[1]!); + try { + view[0] = 5; + print('wrote'); + } on UnsupportedError { + print('view is read-only'); + } + + print(double.tryParse('1d')); + print(double.tryParse('0x1.0p0')); + print(double.tryParse(' 1.5e3 ')); + print(double.tryParse('.5')); + + dynamic n = 1; + try { + print('a' + n); + } catch (e) { + print('string plus int refused'); + } + + print(Point(1, 2) == Point(1, 2)); + + final fixed = [1, 2].toList(growable: false); + try { + fixed.add(3); + print('grew'); + } on UnsupportedError { + print('fixed length'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/expect.txt new file mode 100644 index 00000000000..0cb55b55864 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/expect.txt @@ -0,0 +1,11 @@ +255 +31 +1295 +ISTANBUL +mixed +0.0001 +10000000000000000.0 +1e+21 +false +998 +true diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/main.dart new file mode 100644 index 00000000000..bd11c025a89 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_core_parsing_and_printing/main.dart @@ -0,0 +1,18 @@ +// int.parse's radix, locale-free case conversion, Dart's double notation, +// microsecond DateTimes and Uri equality, through transpiled code. + +void main() { + print(int.parse('ff', radix: 16)); + print(int.tryParse('0x1F')); + print(int.tryParse('zz', radix: 36)); + print('istanbul'.toUpperCase()); + print('MiXeD'.toLowerCase()); + print(0.0001); + print(1e16); + print(1e21); + final a = DateTime.utc(2024, 1, 15, 10, 30, 0, 0, 1); + final b = DateTime.utc(2024, 1, 15, 10, 30, 0, 0, 999); + print(a == b); + print(b.difference(a).inMicroseconds); + print(Uri.parse('https://EXAMPLE.com/a') == Uri.parse('https://example.com/a')); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/expect.txt new file mode 100644 index 00000000000..37a2b61fddf --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/expect.txt @@ -0,0 +1,3 @@ +handled +-2 +onValue error escaped diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/main.dart new file mode 100644 index 00000000000..da79331f7fd --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_future_handlers_round7/main.dart @@ -0,0 +1,24 @@ +// then's named onError handles the source failure; catchError's named test +// decides which failures the handler sees. + +Future fails() async { + throw StateError('source failed'); +} + +void main() async { + final handled = await fails().then((v) => 'value', onError: (e) => 'handled'); + print(handled); + + final selective = await fails() + .catchError((e) => -1, test: (e) => e is ArgumentError) + .catchError((e) => -2, test: (e) => e is StateError); + print(selective); + + try { + await Future.value(1).then((v) { + throw ArgumentError('from onValue'); + }, onError: (e) => 'not me'); + } catch (e) { + print('onValue error escaped'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/expect.txt new file mode 100644 index 00000000000..af2f8c0c906 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/expect.txt @@ -0,0 +1,16 @@ +true +false +false +true +false +1 +9007199254740993 +9007199254740992.0 +0 +2 +1 +0 +[1, 2, 3, 4] [2, 4] +[2, 4] +[1, [...]] +{k: {...}} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/main.dart new file mode 100644 index 00000000000..74aa1afd0e4 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_int_double_and_list_order_round12/main.dart @@ -0,0 +1,48 @@ +// An int against a double: ==, <, >, min and max convert the int (only compareTo is +// exact), as the Dart VM does. lastIndexWhere/lastIndexOf honour their start; +// removeWhere asks its predicate front to back; a collection that contains itself +// prints a cycle marker. +import 'dart:math'; + +void main() { + final int big = int.parse('9007199254740993'); + final double near = double.parse('9007199254740992'); + print(big == near); + print(big > near); + print(near < big); + Object a = big; + Object b = near; + print(a == b); + dynamic da = big; + dynamic db = near; + print(da > db); + print(big.compareTo(near)); + print(min(big, near)); + print(max(near, big)); + + print([1, 2, 1].lastIndexWhere((x) => x == 1, 1)); + print([1, 2, 1].lastIndexWhere((x) => x == 1)); + final List nums = [1, 2.0, 1]; + print(nums.lastIndexOf(2)); + print(nums.lastIndexOf(1, 1)); + + var flip = false; + final order = []; + final l = [1, 2, 3, 4]; + l.removeWhere((x) { + order.add(x); + flip = !flip; + return flip; + }); + print('$order $l'); + final kept = [1, 2, 3, 4]; + kept.retainWhere((x) => x.isEven); + print(kept); + + final self = [1]; + self.add(self); + print(self); + final m = {}; + m['k'] = m; + print(m); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/expect.txt new file mode 100644 index 00000000000..4adf37aa6d1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/expect.txt @@ -0,0 +1,3 @@ +direct a,b,c +forEach a,b,c +length 3 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/main.dart new file mode 100644 index 00000000000..baee84796aa --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_iterable_field_getter/main.dart @@ -0,0 +1,75 @@ +// A class can satisfy an interface's GETTER with a plain FIELD, and Dart makes +// no distinction between the two — `it.current` reads the same either way. +// +// The Java emitter does distinguish: a stub interface's `E get current` becomes +// the method `current()`, while a program class's field `current` becomes a +// private field with `get$current()` accessors. A class that supplies the +// property as a field therefore never implements the interface method, and Java +// keeps the interface's default. Nothing fails to compile and nothing throws — +// the iteration simply hands back the default forever. +// +// This is exactly how the 2D-transformations demo lost its board: `Board with +// IterableMixin` walked its points correctly and yielded null for every one. + +import 'dart:collection' show IterableMixin; + +class Cell { + const Cell(this.label); + final String label; +} + +class _CellIterator implements Iterator { + _CellIterator(this.cells); + + final List cells; + int? index; + + // A FIELD, not a getter — the whole point of this fixture. + @override + Cell? current; + + @override + bool moveNext() { + index = index == null ? 0 : index! + 1; + if (index! >= cells.length) { + current = null; + return false; + } + current = cells[index!]; + return true; + } +} + +class Grid extends Object with IterableMixin { + Grid(this._cells); + + final List _cells; + + @override + Iterator get iterator => _CellIterator(_cells); +} + +void main() { + final Grid grid = Grid([const Cell('a'), const Cell('b'), const Cell('c')]); + + // Reached through the interface's own protocol. + final Iterator it = grid.iterator; + final List direct = []; + while (it.moveNext()) { + direct.add(it.current!.label); + } + print('direct ${direct.join(",")}'); + + // Reached through IterableMixin, which is what the demo's painter uses: a + // named function with a declared parameter type, exactly as the board's + // painter passes `drawBoardPoint` to `board.forEach`. + final List visited = []; + void visit(Cell? c) { + visited.add(c!.label); + } + + grid.forEach(visit); + print('forEach ${visited.join(",")}'); + + print('length ${grid.length}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/expect.txt new file mode 100644 index 00000000000..a843c224729 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/expect.txt @@ -0,0 +1,23 @@ +count not initialized +name already initialized +a:5 +label not initialized +L +[] +[1, 2] +[start, mid] +[1, 2] +[start, mid, start, mid] +[1] +[1, 2] +true +true +from refused a double +forEach refused the change +query parameters are read-only +false +true +false +eager true +wait failed with an ArgumentError: true +[1, 4] diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/main.dart new file mode 100644 index 00000000000..70764bed730 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_late_generators_round11/main.dart @@ -0,0 +1,127 @@ +// late fields are checked; a sync* body runs when iterated, once per iteration; +// Object.hashAll hashes elements; List.from checks element types; forEach +// refuses a length change; queryParameters is read-only; contains honours its +// start; Future.wait honours eagerError and cleanUp. +import 'dart:async'; + +class Holder { + late int count; + late final String name; + static late String label; + + String describe() => '$name:$count'; +} + +class Point2 { + final int x; + final int y; + Point2(this.x, this.y); + + @override + bool operator ==(Object other) => other is Point2 && other.x == x && other.y == y; + + @override + int get hashCode => Object.hashAll([x, y]); +} + +Iterable counted(List log) sync* { + log.add('start'); + yield 1; + log.add('mid'); + yield 2; +} + +Iterable stopsEarly(bool stop) sync* { + yield 1; + if (stop) { + return; + } + yield 2; +} + +Future main() async { + final h = Holder(); + try { + print(h.count); + } on Error { + print('count not initialized'); + } + h.count = 3; + h.count += 1; + h.count++; + h.name = 'a'; + try { + h.name = 'b'; + } on Error { + print('name already initialized'); + } + print(h.describe()); + try { + print(Holder.label); + } on Error { + print('label not initialized'); + } + Holder.label = 'L'; + print(Holder.label); + + final log = []; + final gen = counted(log); + print(log); + print(gen.toList()); + print(log); + print(gen.toList()); + print(log); + print(stopsEarly(true).toList()); + print(stopsEarly(false).toList()); + + print(Point2(1, 2).hashCode == Point2(1, 2).hashCode); + print({Point2(1, 2)}.contains(Point2(1, 2))); + + try { + print(List.from([1.9])); + } on Error { + print('from refused a double'); + } + + final grow = [1, 2]; + try { + grow.forEach((e) { + if (grow.length < 4) { + grow.add(3); + } + }); + print('no error'); + } on ConcurrentModificationError { + print('forEach refused the change'); + } + + try { + Uri.parse('https://x/?a=1').queryParameters['b'] = '2'; + print('mutable'); + } on UnsupportedError { + print('query parameters are read-only'); + } + + print('abc'.contains('a', 1)); + print('abc'.contains('c', 1)); + print('abc'.contains(RegExp('a'), 1)); + + final never = Completer().future; + final List> inputs = [Future.error(StateError('boom')), never]; + final eager = await Future.wait(inputs, eagerError: true) + .then((v) => 'ok', onError: (e) => 'eager ${e is StateError}'); + print(eager); + final cleaned = []; + final late4 = Completer(); + final waited = Future.wait([Future.value(1), Future.error(ArgumentError('x')), late4.future], + cleanUp: (v) { + cleaned.add(v); + }); + late4.complete(4); + try { + await waited; + } catch (e) { + print('wait failed with an ArgumentError: ${e is ArgumentError}'); + } + print(cleaned); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/expect.txt new file mode 100644 index 00000000000..5b36c11596d --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/expect.txt @@ -0,0 +1,13 @@ +loop ran 2 times +false +null +42 +null +parse threw FormatException +true +make ran +41 +43 +no label +set once +expensive initialised on its own first read diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/main.dart new file mode 100644 index 00000000000..e5bc4ed57b7 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_statics_and_null_coalescing/main.dart @@ -0,0 +1,69 @@ +// ?? evaluates its left side where the expression is, int.tryParse answers null, +// and a class's statics initialise one at a time, on first read. + +int _calls = 0; +int? next() { + _calls++; + return _calls < 3 ? _calls : null; +} + +bool? sideEffect() { + print('side effect ran'); + return null; +} + +int fail() { + throw StateError('expensive initialised too early'); +} + +int _made = 0; +int make() { + _made++; + print('make ran'); + return 40 + _made; +} + +class Config { + static const bool ready = true; + static final int expensive = fail(); + static int counter = make(); + static String? label; +} + +void main() { + // ?? in a loop condition runs every iteration + var seen = 0; + while ((next() ?? 0) > 0) { + seen++; + } + print('loop ran $seen times'); + + // ?? in a short-circuited operand never runs + final skipped = false && (sideEffect() ?? true); + print(skipped); + + // tryParse / parse + print(int.tryParse('x1')); + print(int.tryParse(' 42 ')); + print(double.tryParse('nope')); + try { + int.parse('nope'); + } on FormatException { + print('parse threw FormatException'); + } + + // statics: reading one does not initialise the others + print(Config.ready); + print(Config.counter); + Config.counter += 2; + print(Config.counter); + print(Config.label ?? 'no label'); + Config.label ??= 'set once'; + Config.label ??= 'set twice'; + print(Config.label); + try { + print(Config.expensive); + } on StateError { + print('expensive initialised on its own first read'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/expect.txt new file mode 100644 index 00000000000..b473cfb0508 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/expect.txt @@ -0,0 +1,3 @@ +palette:shrine via shrine +palette:shrine +visits 3 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/main.dart new file mode 100644 index 00000000000..bc316d7b2aa --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_lazy_top_level/main.dart @@ -0,0 +1,33 @@ +// Top-level variables initialise LAZILY in Dart, on first read — so a variable +// may be written ABOVE the ones its initialiser depends on, and Shrine's theme +// is written exactly that way: +// +// final ThemeData shrineTheme = _buildShrineTheme(); // reads the scheme +// final ColorScheme _shrineColorScheme = ColorScheme(...); +// +// Emitted as plain Java static fields, in textual order, the first one reads +// null and the whole library dies in its static initialiser. + +class Palette { + Palette(this.name); + final String name; + String describe() => 'palette:$name'; +} + +// Declared FIRST, depends on two things declared after it. +final String summary = _describe(); +final Palette palette = Palette(paletteName); +const String paletteName = 'shrine'; + +String _describe() => '${palette.describe()} via $paletteName'; + +// A mutable top-level variable still assigns. +int visits = 0; + +void main() { + print(summary); + print(palette.describe()); + visits = visits + 2; + visits++; + print('visits $visits'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/expect.txt new file mode 100644 index 00000000000..ea816c37148 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/expect.txt @@ -0,0 +1,15 @@ +a=2 b=1 +next1,next2 +a=2 b=1 +bump1,bump2 +a=second b=first +Tag(first),Tag(second) +a=3 b=2 +peek2,next3 +a=5 b=1 +a=7 b=7 +a=8 b=2 +a=11 b=2 +a=21 b=L11 +a=2 b=1 +tick1,tick2 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/main.dart new file mode 100644 index 00000000000..df0952566a0 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_named_argument_effects_round17/main.dart @@ -0,0 +1,94 @@ +// Named arguments are evaluated in the order they are written, whatever the +// argument's shape: a method on a value, a static method, a constructor, a +// getter, and a plain read that a later call changes. +final List trace = []; +int level = 1; + +int raise() { + level += 10; + return level; +} + +class Counter { + int n = 0; + static int total = 0; + int next() { + n++; + trace.add('next$n'); + return n; + } + + int get peek { + trace.add('peek$n'); + return n; + } + + int get tick { + n++; + trace.add('tick$n'); + return n; + } + + static int bump() { + total++; + trace.add('bump$total'); + return total; + } +} + +class Tag { + final String name; + Tag(this.name) { + trace.add('Tag($name)'); + } +} + +class Pair { + final Object a; + final Object b; + Pair({required this.a, required this.b}); + String describe() => 'a=$a b=$b'; +} + +class Box { + final int value; + Box(this.value); +} + +void main() { + final c = Counter(); + print(Pair(b: c.next(), a: c.next()).describe()); + print(trace.join(',')); + trace.clear(); + + print(Pair(b: Counter.bump(), a: Counter.bump()).describe()); + print(trace.join(',')); + trace.clear(); + + print(Pair(b: Tag('first').name, a: Tag('second').name).describe()); + print(trace.join(',')); + trace.clear(); + + print(Pair(b: c.peek, a: c.next()).describe()); + print(trace.join(',')); + trace.clear(); + + final box = Box(1); + print(Pair(b: box.value, a: c.next() + box.value).describe()); + trace.clear(); + + // An index read is operator [], ordered against the call that changes the list. + final xs = [7, 8, 9]; + print(Pair(b: xs[0], a: xs.removeAt(0)).describe()); + print(Pair(b: xs.length, a: xs.removeAt(0)).describe()); + + // A variable read nested in an expression is read where it is written. + print(Pair(b: level + 1, a: raise()).describe()); + print(Pair(b: 'L$level', a: raise()).describe()); + + // Two getters with effects and no call among the arguments. + final d = Counter(); + print(Pair(b: d.tick, a: d.tick).describe()); + print(trace.join(',')); + trace.clear(); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/expect.txt new file mode 100644 index 00000000000..4b7487e873b --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/expect.txt @@ -0,0 +1,12 @@ +CALL box:x +CALL null +STATEMENT survived +CHAIN box:y +CHAIN null +CHAIN null +MIXED 7 +MIXED null +SIDE_EFFECTS 1 +DEFAULTED fallback +DEFAULTED box:x +CALLBACKS 1 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/main.dart new file mode 100644 index 00000000000..5031a087a79 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_null_aware_calls/main.dart @@ -0,0 +1,68 @@ +// Null-aware member access, `?.`, in every position it actually gets written. +// +// The interesting half is the METHOD call. `a?.b` was shorted correctly, but `a?.m()` +// only honoured the `?.` when the receiver happened to be function-valued; every other +// receiver emitted a plain, unguarded invocation. That is silent until the receiver is +// null, and then it is a NullPointerException in code the Dart says cannot throw - the +// gallery's `_timeDilationTimer?.cancel()` threw the first time slow motion was toggled, +// because that timer is null until something has been dilated. +// +// Everything here is plain Dart, so both implementations must agree exactly. +class Box { + Box(this.label); + final String label; + int calls = 0; + + String describe() { + calls++; + return 'box:$label'; + } + + Box? get self => this; + Box? get nothing => null; + int get size => 7; +} + +Box? maybe(bool present) => present ? Box('x') : null; + +void main() { + // 1. The call itself: shorted on null, run on non-null. + print('CALL ${maybe(true)?.describe()}'); + print('CALL ${maybe(false)?.describe()}'); + + // 2. In STATEMENT position, where the result is discarded - this is where the missing + // guard actually bit, because nothing forced the expression into a value context. + final Box? absent = maybe(false); + absent?.describe(); + print('STATEMENT survived'); + + // 3. Chains: a null anywhere shorts the WHOLE chain, and the rest must not run. + final Box present = Box('y'); + print('CHAIN ${present.self?.describe()}'); + print('CHAIN ${present.nothing?.describe()}'); + print('CHAIN ${present.nothing?.self?.describe()}'); + + // 4. A property read after a call, and a call after a property read. + print('MIXED ${maybe(true)?.self?.size}'); + print('MIXED ${maybe(false)?.self?.size}'); + + // 5. Short-circuiting is REAL: the receiver expression runs once, and a shorted call + // must not invoke the method at all. + final Box counted = Box('z'); + final Box? nullBox = null; + counted.describe(); + nullBox?.describe(); + print('SIDE_EFFECTS ${counted.calls}'); + + // 6. ?. combined with ?? - the usual way a nullable result gets a default. + print('DEFAULTED ${maybe(false)?.describe() ?? "fallback"}'); + print('DEFAULTED ${maybe(true)?.describe() ?? "fallback"}'); + + // 7. On a function-valued field, which was the one case that already worked. + void Function()? nullCallback; + int fired = 0; + void Function()? liveCallback = () => fired++; + nullCallback?.call(); + liveCallback?.call(); + print('CALLBACKS $fired'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/expect.txt new file mode 100644 index 00000000000..aea90a759d1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/expect.txt @@ -0,0 +1,12 @@ +2 +true +true +3.0 +3 +true +1.5 +ababab +abab +fixed length +fixed length +2 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/main.dart new file mode 100644 index 00000000000..533a9bed220 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_num_strings_and_lists_round5/main.dart @@ -0,0 +1,38 @@ +// num arithmetic keeps an int an int and compares exactly; String * int in typed +// code; List.from(growable: false); List.indexOf with a start. + +void main() { + num n = 1; + print(n + 1); + print((n + 1) is int); + num big = 9007199254740993; + print(big > 9007199254740992); + num half = 1.5; + print(half * 2); + n += 2; + print(n); + print(n is int); + print(n / 2); + + String s = 'ab'; + print(s * 3); + s *= 2; + print(s); + + final fixed = List.from([1, 2], growable: false); + try { + fixed.add(3); + print('grew'); + } on UnsupportedError { + print('fixed length'); + } + final names = List.from(['a'], growable: false); + try { + names.add('b'); + print('grew'); + } on UnsupportedError { + print('fixed length'); + } + + print([1, 2, 1].indexOf(1, 1)); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/expect.txt new file mode 100644 index 00000000000..cd8e9a6da77 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/expect.txt @@ -0,0 +1,29 @@ +1 +-1 +false +false +1 +false +false +[1, 1.5, 9007199254740992.0, 9007199254740993] +1 +-1 +radix 1 refused +radix 37 refused +ff +true +false +1 +true +2.5 +8 +true +0.5 +8.0 +5 +1 [cleanup] +cleanup failed +2 [cleaned] +3 [cleaned, cleaned] +late cleanup failed +op failed diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/main.dart new file mode 100644 index 00000000000..5d66e24ce59 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_numbers_regexp_futures_round9/main.dart @@ -0,0 +1,93 @@ +// int against double compares exactly (statically, through num and through +// dynamic); toRadixString refuses a radix outside 2..36; RegExp honours +// dotAll; min/max/pow keep ints ints; whenComplete waits for an async cleanup +// and surfaces its error. +import 'dart:math'; + +final List cleanupLog = []; + +Future asyncCleanup() async { + await Future.delayed(const Duration(milliseconds: 10)); + cleanupLog.add('cleaned'); +} + +Future failingCleanup() async { + await Future.delayed(const Duration(milliseconds: 1)); + throw StateError('late cleanup failed'); +} + +Future main() async { + int big = 9007199254740993; + double near = 9007199254740992.0; + print(big.compareTo(near)); + print(near.compareTo(big)); + print(big > near); + print(near < big); + num a = big; + num b = near; + print(a.compareTo(b)); + print(a > b); + dynamic da = big; + dynamic db = near; + print(da > db); + final nums = [near, big, 1.5, 1]; + nums.sort(); + print(nums); + print(0.compareTo(-0.0)); + print(1.compareTo(double.nan)); + + try { + print(255.toRadixString(1)); + } on RangeError { + print('radix 1 refused'); + } + try { + print(255.toRadixString(37)); + } on RangeError { + print('radix 37 refused'); + } + print(255.toRadixString(16)); + + print(RegExp('a.b', dotAll: true).hasMatch('a\nb')); + print(RegExp('a.b').hasMatch('a\nb')); + + print(min(1, 2)); + print(max(3, 2) is int); + print(max(1, 2.5)); + print(pow(2, 3)); + print(pow(2, 3) is int); + print(pow(2, -1)); + print(pow(2.0, 3)); + int m = min(4, 7); + print(m + 1); + + final log = []; + final v = await Future.value(1).whenComplete(() async { + await Future.delayed(const Duration(milliseconds: 10)); + log.add('cleanup'); + }); + print('$v $log'); + try { + await Future.value(1).whenComplete(() async { + throw StateError('cleanup failed'); + }); + print('not caught'); + } on StateError { + print('cleanup failed'); + } + final w = await Future.value(2).whenComplete(() => asyncCleanup()); + print('$w $cleanupLog'); + final t = await Future.value(3).whenComplete(asyncCleanup); + print('$t $cleanupLog'); + try { + await Future.value(4).whenComplete(() => failingCleanup()); + print('not caught'); + } on StateError { + print('late cleanup failed'); + } + try { + await Future.error(ArgumentError('op failed')).whenComplete(() async {}); + } on ArgumentError { + print('op failed'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/expect.txt new file mode 100644 index 00000000000..dac7add23d2 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/expect.txt @@ -0,0 +1,28 @@ +[1] +[2] +2 +first +set count 3 +3 +3 +2.5 +-2.5 +3 +3 +true +8 +abc +abab +true +true +true +eval b +eval a +ab +eval 2 +eval 1 +1-2 +[a, b, c] +-a-b-c- +1.00000000000000000000 +1.00 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/main.dart new file mode 100644 index 00000000000..4ba3914b6d8 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_and_argument_order/main.dart @@ -0,0 +1,76 @@ +// Null-aware assignment through an index or a setter, operators on dynamic +// values, and named arguments evaluated in the order they are written. + +String log(String s) { + print('eval $s'); + return s; +} + +class Pair { + final String a; + final String b; + Pair({required this.a, required this.b}); + String describe({required String first, required String second}) => '$first-$second'; +} + +class Holder { + String? name; + int _count = 0; + int? get count => _count == 0 ? null : _count; + set count(int? v) { + print('set count $v'); + _count = v ?? 0; + } +} + +void main() { + // ??= on an index, as a statement and as a value + var m = >{}; + m['a'] ??= []; + m['a']!.add(1); + m['a'] ??= [99]; + print(m['a']); + var got = m['b'] ??= [2]; + print(got); + print(m.length); + + // ??= on a field and through a setter + var h = Holder(); + h.name ??= 'first'; + h.name ??= 'second'; + print(h.name); + h.count ??= 3; + h.count ??= 4; + print(h.count); + + // operators on dynamic values + dynamic x = 1; + print(x + 2); + x += 1.5; + print(x); + print(-x); + dynamic i = 7; + print(i ~/ 2); + print(i % 4); + print(i > 2); + i++; + print(i); + dynamic s = 'ab'; + print(s + 'c'); + print(s * 2); + print(1 == 1.0); + dynamic one = 1; + print(one == 1.0); + print([1].contains(1.0)); + + // named arguments run in source order + var p = Pair(b: log('b'), a: log('a')); + print('${p.a}${p.b}'); + print(p.describe(second: log('2'), first: log('1'))); + + // String patterns and fixed-point formatting + print('a1b22c'.split(RegExp(r'[0-9]+'))); + print('abc'.replaceAll(RegExp(''), '-')); + print(1.0.toStringAsFixed(20)); + print(1.005.toStringAsFixed(2)); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/expect.txt new file mode 100644 index 00000000000..3c245cb80de --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/expect.txt @@ -0,0 +1,17 @@ +0 +-1 +0 +negative shift refused +0 +8 +-2 +3 +-2.0 +0 +6 +true +true false +true +[11, 22] +12 +modified during iteration diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/main.dart new file mode 100644 index 00000000000..d561c7d3555 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_operators_collections_round4/main.dart @@ -0,0 +1,56 @@ +// Shifts, rounding, trim and startsWith with Dart's rules; Set.add's result; a +// mixed int/double conditional; Map.entries; Iterable.forEach; and a +// list modified while it is iterated. + +void main() { + var big = 64; + var neg = -1; + print(1 >> big); + print(-8 >> big); + print(1 << big); + try { + print(1 << neg); + } on ArgumentError { + print('negative shift refused'); + } + var n = 5; + n <<= big; + print(n); + print(1 << 3); + + print((-1.5).round()); + print((2.5).round()); + print((-1.5).roundToDouble()); + print(0.49999999999999994.round()); + + print('\u00a0 padded \u2003'.trim().length); + print('abc'.startsWith('b', 1)); + + final seen = {}; + var added = seen.add(1); + var again = seen.add(1); + print('$added $again'); + + var flag = true; + Object picked = flag ? 1 : 1.5; + print(picked is int); + + final counts = {1: 10, 2: 20}; + print(counts.entries.map((e) => e.key + e.value).toList()); + + var total = 0; + [1, 2, 3].map((x) => x * 2).forEach((x) { + total += x; + }); + print(total); + + final grow = [1, 2]; + try { + for (final x in grow) { + grow.add(x); + } + print('no error'); + } catch (e) { + print('modified during iteration'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/expect.txt new file mode 100644 index 00000000000..374bdcd12da --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/expect.txt @@ -0,0 +1,7 @@ +constructor refused the pattern +[] +[abab] +[] +null +false +b diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/main.dart new file mode 100644 index 00000000000..edf92ab1b5f --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_regexp_ctor_and_edges_round13/main.dart @@ -0,0 +1,21 @@ +// A malformed RegExp throws from its constructor. Pinned alongside: what the Dart +// VM does for a negative string repetition, a negative Iterable.generate count and +// a map key that only rounds to the looked-up double. +void main() { + try { + RegExp('['); + print('constructed'); + } on FormatException { + print('constructor refused the pattern'); + } + print('[${'x' * -1}]'); + print('[${'ab' * 2}]'); + print(Iterable.generate(-1, (i) => i).toList()); + final int big = int.parse('9007199254740993'); + final double near = double.parse('9007199254740992'); + final Map m = {big: 'a'}; + print(m[near]); + print(m.containsKey(near)); + final Map exact = {9007199254740992: 'b'}; + print(exact[near]); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/expect.txt new file mode 100644 index 00000000000..45e4074407f --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/expect.txt @@ -0,0 +1,27 @@ +[2, 4, 6] +[2, 4] +4 +1 +-1 +RangeError +set 5 +set 3 +set 4 +set 3 +3 +set 13 +13 calls=1 +true +false +list 1 +true +false +false +true +false +true +calls=1 +TypeError +TypeError for null +null +6 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/main.dart new file mode 100644 index 00000000000..85f70742e8f --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round18/main.dart @@ -0,0 +1,83 @@ +// Set.retainWhere/removeWhere, String.lastIndexOf with a start, compound and +// ++/-- assignment through an app getter/setter pair, `is` against a generic and a +// nullable type, and a failed `as` that must be Dart's TypeError. +class Counter { + int _v = 0; + int get value => _v; + set value(int v) { + _v = v; + print('set $v'); + } +} + +int calls = 0; +Counter make(Counter c) { + calls++; + return c; +} + +Object? pick(Object? v) { + calls++; + return v; +} + +void main() { + final s = {1, 2, 3, 4, 5, 6}; + s.retainWhere((e) => e.isEven); + print(s.toList()); + s.removeWhere((e) => e > 4); + print(s.toList()); + + final str = 'abcabc'; + print(str.lastIndexOf('b')); + print(str.lastIndexOf('b', 3)); + print(str.lastIndexOf('c', 1)); + try { + str.lastIndexOf('b', 99); + } on RangeError { + print('RangeError'); + } + + final counter = Counter(); + counter.value += 5; + counter.value -= 2; + counter.value++; + counter.value--; + print(counter.value); + calls = 0; + make(counter).value += 10; + print('${counter.value} calls=$calls'); + + Object value = ['a']; + print(value is List); + print(value is! List); + final described = switch (value) { + List l => 'list ${l.length}', + _ => 'other', + }; + print(described); + Object? none; + print(none is int?); + print(none is int); + print(none is! int?); + Object five = 5; + print(five is int?); + print(five is String?); + calls = 0; + print(pick(null) is int?); + print('calls=$calls'); + + Object text = 'x'; + try { + print(text as int); + } on TypeError { + print('TypeError'); + } + try { + print(none as int); + } on TypeError { + print('TypeError for null'); + } + print(none as int?); + print((five as int) + 1); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/expect.txt new file mode 100644 index 00000000000..9d52209dbb1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/expect.txt @@ -0,0 +1,32 @@ +while 5 loops=4 calls=5 +for [4, 6] passes=3 calls=7 +false +true +short 7 calls=0 +true +taken 8 calls=1 +makes=3 value=9 +[] nextCalls=0 +[1] nextCalls=1 +spread [] +spread [4, 5, 6] +spread [] +set {} +map {} +grow [0, 1, 2] calls=4 +[1] calls=0 +[9] calls=1 +[3] calls=1 +null-short calls=0 +null-short [1] calls=1 +null +null-short value calls=0 +[1] +[2] +slots {k: [1], j: [2]} calls=1 +caught +42 +attempts=2 +caught static +loaded +staticAttempts=2 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/main.dart new file mode 100644 index 00000000000..678bdf380ad --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round19/main.dart @@ -0,0 +1,179 @@ +// Statements the emitter lifts out of an operand Java evaluates lazily or +// repeatedly -- a loop condition or update, the right side of && || ??, a +// conditional's arm, a null-short's arguments -- must run where that operand is +// evaluated: every iteration, and never on the branch not taken. Also a lazily +// initialised top-level whose initialiser throws runs again on the next read. +class Box { + int _v = 0; + int get value => _v; + set value(int v) { + _v = v; + } +} + +int calls = 0; +int nextIndex() { + calls++; + return 0; +} + +bool nexts = false; +int nextCalls = 0; +bool next() { + nextCalls++; + return nexts; +} + +Box made = Box(); +int makes = 0; +Box make() { + makes++; + return made; +} + +int attempts = 0; +int flaky() { + attempts++; + if (attempts == 1) { + throw StateError('first read fails'); + } + return 42; +} + +final int lazyValue = flaky(); + +int staticAttempts = 0; +class Config { + static final String name = load(); + static String load() { + staticAttempts++; + if (staticAttempts == 1) { + throw StateError('first read fails'); + } + return 'loaded'; + } +} + +List source(List xs) { + calls++; + return xs; +} + +void main() { + // compound index assignment in a while condition: every test + final items = [0]; + calls = 0; + var loops = 0; + while ((items[nextIndex()] += 1) < 5) { + loops++; + } + print('while ${items[0]} loops=$loops calls=$calls'); + + // ... and in a for condition and update + final counts = [0, 0]; + calls = 0; + var passes = 0; + for (var i = 0; (counts[nextIndex()] += 1) < 4; counts[1 + nextIndex()] += 2) { + passes++; + if (passes == 1) { + continue; + } + } + print('for $counts passes=$passes calls=$calls'); + + // compound index on the right of a false && / true ||: not evaluated + calls = 0; + final a = [7]; + final f = false; + final t = true; + print(f && (a[nextIndex()] += 1) > 0); + print(t || (a[nextIndex()] += 1) > 0); + print('short ${a[0]} calls=$calls'); + print(t && (a[nextIndex()] += 1) > 0); + print('taken ${a[0]} calls=$calls'); + + // getter/setter compound assignment in a for update: every pass, the receiver + // evaluated once per pass + makes = 0; + for (var k = 0; k < 3; k++, make().value += 3) {} + print('makes=$makes value=${made.value}'); + + // a collection-if in an untaken conditional arm + nextCalls = 0; + final flag = false; + final l1 = flag ? [if (next()) 1] : []; + print('$l1 nextCalls=$nextCalls'); + nexts = true; + final l2 = !flag ? [if (next()) 1] : []; + print('$l2 nextCalls=$nextCalls'); + + // spreading a null list behind its own null check + List? none; + final spread = none == null ? [] : [...none]; + print('spread $spread'); + List? some = [4, 5]; + final spread2 = some == null ? [] : [...some, 6]; + print('spread $spread2'); + final List spread3 = none == null ? [] : [...none]; + print('spread $spread3'); + + // set and map literals with spreads/if in an untaken arm + final Set? noSet = null; + final s = noSet == null ? {} : {...noSet}; + print('set $s'); + final Map? noMap = null; + final m = noMap == null ? {} : {...noMap, 'k': 1}; + print('map $m'); + + // a spread list in a while condition is rebuilt every iteration + final grow = []; + calls = 0; + while ([...source(grow), 0].length < 4) { + grow.add(grow.length); + } + print('grow $grow calls=$calls'); + + // ?? and ??= with a lifting default + calls = 0; + List? present = [1]; + final r1 = present ?? [...source([9])]; + print('$r1 calls=$calls'); + final r2 = none ?? [...source([9])]; + print('$r2 calls=$calls'); + List? cache = [3]; + cache ??= [...source([8])]; + print('$cache calls=$calls'); + + // a null-short's arguments + List? target; + calls = 0; + target?.addAll([...source([1])]); + print('null-short calls=$calls'); + target = []; + target?.addAll([...source([1])]); + print('null-short $target calls=$calls'); + List? noList; + calls = 0; + print(noList?.contains([...source([1])].first)); + print('null-short value calls=$calls'); + final slots = >{'k': [1]}; + print(slots['k'] ??= [...source([2])]); + print(slots['j'] ??= [...source([2])]); + print('slots $slots calls=$calls'); + + // a throwing lazy initialiser runs again on the next read + try { + print(lazyValue); + } on StateError { + print('caught'); + } + print(lazyValue); + print('attempts=$attempts'); + try { + print(Config.name); + } on StateError { + print('caught static'); + } + print(Config.name); + print('staticAttempts=$staticAttempts'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/expect.txt new file mode 100644 index 00000000000..dba388d3fa2 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/expect.txt @@ -0,0 +1,55 @@ +null +null +shorted [] +6 +7 +6 3 7 +assigned [side 1, side 2, side 3, side 4, set p 4, side 5, set p 9, side 6, side 7, set p 7] +null +null +null +shorted chain [] +0 +[9, 8] +chain [side 0, side 0, side 9, side 1, side 8] +true +true +4 +6 +6 +4 +11 +11 +makes=8 p=4 q=11 +setter values [set p 3, set p 4, set p 5, set p 6, set p 5, set p 4, set q 11] +false +true +n=6 makes=8 p=0 +lazy setter values [set p 7, set p 6, set p 5, set p 4, set p 3, set p 2, set p 1, set p 0] +5 +3 +3 +v=4 makes=2 +4 +6 +5 +6 +7 +8 +[0, 8, 0] +5 +7 +7 +1 +2 +null +5 +6 +7 +7 8 +40 +2 +values [set top 4, set top 6, set top 5, set top 6, cell 1=7, cell 1=8, set p 8, set p 40, set q null, set q 2] +null +4 5 +cascade [side 4, side 5, set p 5] diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/main.dart new file mode 100644 index 00000000000..4c53b9d5547 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_review_round20/main.dart @@ -0,0 +1,173 @@ +// A null-aware selector shorts the WHOLE rest of the chain when its receiver is +// null -- an assignment through it, an index after it, later selectors -- so +// nothing to its right is evaluated. And an assignment through a setter is an +// expression whose value is the assigned value, compound and ++/-- included. +final log = []; + +int side(int v) { + log.add('side $v'); + return v; +} + +class Node { + int v = 0; + int? maybe; + List items = [0, 0]; + Node get self => this; + int _p = 0; + int get p => _p; + set p(int x) { + _p = x; + log.add('set p $x'); + } + + int? _q; + int? get q => _q; + set q(int? x) { + _q = x; + log.add('set q $x'); + } +} + +class Grid { + final cells = [0, 0, 0]; + int operator [](int i) => cells[i]; + void operator []=(int i, int v) { + cells[i] = v; + log.add('cell $i=$v'); + } +} + +int _top = 0; +int get top => _top; +set top(int v) { + _top = v; + log.add('set top $v'); +} + +int lazyCounter = int.parse('5'); + +class Late { + late int z; +} + +Node shared = Node(); +int makes = 0; +Node make() { + makes++; + return shared; +} + +void show(String what) { + print('$what $log'); + log.clear(); +} + +void main() { + Node? none; + final Node? some = Node(); + + // 1. assignment through a null-aware selector + none?.v = side(1); + none?.v += side(2); + none?.maybe ??= side(3); + none?.p = side(4); + none?.p += side(5); + print(none?.v = side(6)); + print(none?.p = side(7)); + show('shorted'); + some?.v = side(1); + some?.v += side(2); + some?.maybe ??= side(3); + some?.maybe ??= side(30); + some?.p = side(4); + some?.p += side(5); + print(some?.v = side(6)); + print(some?.p = side(7)); + print('${some!.v} ${some.maybe} ${some.p}'); + show('assigned'); + + // 2. index and later selectors after a null-aware selector + print(none?.items[side(0)]); + print(none?.self.items[side(1)]); + print(none?.self.v); + none?.items[side(0)] = side(9); + none?.self.items[side(1)] += side(9); + show('shorted chain'); + print(some?.items[side(0)]); + some?.items[side(0)] = side(9); + some?.self.items[side(1)] += side(8); + print(some?.self.items); + show('chain'); + + // 3. assignments through a setter used as values + makes = 0; + print((make().p += 3) > 0); + print((make().p = 4) > 0); + print(make().p++); + print(++make().p); + print(make().p--); + print(--make().p); + print(make().q ??= 11); + print(make().q ??= 12); + print('makes=$makes p=${shared.p} q=${shared.q}'); + show('setter values'); + // ... and in an operand evaluated lazily + makes = 0; + final f = false; + print(f && (make().p += 3) > 0); + print(!f && (make().p += 3) > 0); + var n = 0; + while ((make().p -= 1) > 0) { + n++; + } + print('n=$n makes=$makes p=${shared.p}'); + show('lazy setter values'); + + // 4. more assignment values: plain fields, receivers evaluated once, top-level + // setters, operator []=, accessor-backed postfix, ++/-- behind ?. + makes = 0; + print(some!.v = 5); + print(make().v += 3); + print(make().v++); + print('v=${shared.v} makes=$makes'); + print(top = 4); + print(top += 2); + top -= 1; + print(top++); + print(top); + final grid = Grid(); + print(grid[1] = 7); + print(grid[1] += 1); + print(grid.cells); + print(lazyCounter++); + print(++lazyCounter); + print(lazyCounter); + final late = Late(); + late.z = 1; + print(late.z++); + print(late.z); + print(none?.v++); + none?.v++; + none?.p--; + print(some.v++); + print(some?.v++); + print(some?.p++); + print('${some.v} ${some.p}'); + final void Function() cb = () => shared.p = 40; + cb(); + print(shared.p); + some.q = null; + some.q ??= 2; + some.q ??= 3; + print(some.q); + show('values'); + + // 5. a null-aware cascade shorts all of its sections + none?..v = side(1)..p = side(2); + print(none?..v = side(3)); + final Node? other = Node(); + other?..v = side(4)..p = side(5); + print('${other?.v} ${other?.p}'); + show('cascade'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/expect.txt new file mode 100644 index 00000000000..1992a6a11e2 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/expect.txt @@ -0,0 +1,14 @@ +true 7 +[boom] true +only the catch-all caught boom +Error: true +Exception: true +Token 7 +catchError got [async boom] true +0 +await rethrew the same token: true +int map refused the change +map refused the change +unmodifiable list refused a store +pathSegments is read-only +true diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/main.dart new file mode 100644 index 00000000000..2c686a5ca10 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_thrown_values_round14/main.dart @@ -0,0 +1,94 @@ +// Dart throws any object, and a catch binds that exact object. `on Error` and +// `on Exception` are type tests that a thrown string passes neither of, and the +// clauses of one try are tried in order. +import 'dart:async'; + +class Token { + final int id; + Token(this.id); +} + +Future main() async { + final t = Token(7); + try { + throw t; + } catch (e) { + print('${identical(e, t)} ${(e as Token).id}'); + } + try { + throw 'boom'; + } catch (e) { + print('[$e] ${e is String}'); + } + try { + throw 'boom'; + } on Exception { + print('Exception caught a string'); + } on Error { + print('Error caught a string'); + } catch (e) { + print('only the catch-all caught $e'); + } + try { + throw StateError('bad'); + } on Exception { + print('Exception caught an Error'); + } on Error catch (e) { + print('Error: ${e is StateError}'); + } + try { + throw FormatException('fmt'); + } on Error { + print('Error caught an Exception'); + } on Exception catch (e) { + print('Exception: ${e is FormatException}'); + } + try { + throw t; + } on String { + print('wrong clause'); + } on Token catch (tok) { + print('Token ${tok.id}'); + } + final v = await Future.error('async boom').catchError((e) { + print('catchError got [$e] ${e is String}'); + return 0; + }); + print(v); + try { + await Future.error(t); + } catch (e) { + print('await rethrew the same token: ${identical(e, t)}'); + } + + final Map ints = {1: 1}; + try { + ints.forEach((k, v) { + ints[k + 10] = v; + }); + } on ConcurrentModificationError { + print('int map refused the change'); + } + final Map named = {'a': 1}; + try { + named.forEach((k, v) { + named['b$k'] = v; + }); + } on ConcurrentModificationError { + print('map refused the change'); + } + final fixed = List.unmodifiable([1, 2]); + try { + fixed[0] = 9; + print('replaced'); + } on UnsupportedError { + print('unmodifiable list refused a store'); + } + try { + Uri.parse('https://x/a/b').pathSegments.add('c'); + print('grew'); + } on UnsupportedError { + print('pathSegments is read-only'); + } + print(identical(1, 1)); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/expect.txt new file mode 100644 index 00000000000..f07966970e1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/expect.txt @@ -0,0 +1,21 @@ +[9, 2] +range checked eagerly +true +true +https://x/a%20b +/a%20b +https://x/a%2Fb~A%25zz +https://x/p?q=a%20b#f%20g +https://x.com/~user +https://x/caf%C3%A9 +https://x/a/b?c%5Cd +[a b] +predicate error completes the future +1582-10-10 -12219724800000 7 +1582-10-05 00:00:00.000Z +-62135596800000 +2024-03-01 00:00:00.000Z +2023-12-01 00:00:00.000Z +-0001-12-31 23:59:59.999Z +0000-12-31 23:59:59.999Z +3 diff --git a/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/main.dart b/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/main.dart new file mode 100644 index 00000000000..c8bf8e457ca --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/dart_views_uri_dates_round10/main.dart @@ -0,0 +1,50 @@ +// getRange is a live view; containsValue and int-keyed lookups use Dart's ==; +// Uri.parse normalizes its components; a throwing catchError test completes the +// returned future; DateTime is proleptic Gregorian. +import 'dart:async'; + +Future main() async { + final list = [1, 2, 3]; + final range = list.getRange(0, 2); + list[0] = 9; + print(range.toList()); + try { + list.getRange(1, 5); + } on RangeError { + print('range checked eagerly'); + } + + final Map ints = {0: 1}; + print(ints.containsValue(1)); + final Map nums = {'a': 1}; + print(nums.containsValue(1.0)); + + print(Uri.parse('https://x/a b').toString()); + print(Uri.parse('https://x/a b').path); + print(Uri.parse('https://x/a%2fb%7e%41%zz').toString()); + print(Uri.parse('https://x/p?q=a b#f g').toString()); + print(Uri.parse('HTTPS://X.COM/%7Euser').toString()); + print(Uri.parse('https://x/café').toString()); + print(Uri.parse('https://x/a\\b?c\\d').toString()); + print(Uri.parse('https://x/a b').pathSegments); + + try { + await Future.error(StateError('op')).catchError((e) => 1, + test: (e) { + throw ArgumentError('predicate'); + }); + print('not reached'); + } on ArgumentError { + print('predicate error completes the future'); + } + + final d = DateTime.utc(1582, 10, 10); + print('${d.year}-${d.month}-${d.day} ${d.millisecondsSinceEpoch} ${d.weekday}'); + print(DateTime.utc(1582, 10, 4).add(const Duration(days: 1))); + print(DateTime.utc(1, 1, 1).millisecondsSinceEpoch); + print(DateTime.utc(2024, 2, 30)); + print(DateTime.utc(2024, 0, 1)); + print(DateTime.utc(-1, 12, 31, 23, 59, 59, 999)); + print(DateTime.fromMillisecondsSinceEpoch(-62135596800001, isUtc: true)); + print(DateTime.utc(2000, 3, 1).weekday); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/expect.txt new file mode 100644 index 00000000000..70b9f4e8499 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/expect.txt @@ -0,0 +1,153 @@ +SCALE -40 0 +SCALE -39 0 +SCALE -38 0 +SCALE -37 0 +SCALE -36 0 +SCALE -35 0 +SCALE -34 0 +SCALE -33 1736 +SCALE -32 6561 +SCALE -31 11471 +SCALE -30 16067 +SCALE -29 20720 +SCALE -28 24976 +SCALE -27 29462 +SCALE -26 33577 +SCALE -25 37806 +SCALE -24 41830 +SCALE -23 45903 +SCALE -22 49707 +SCALE -21 53513 +SCALE -20 57011 +SCALE -19 60621 +SCALE -18 64039 +SCALE -17 67395 +SCALE -16 70538 +SCALE -15 73726 +SCALE -14 76554 +SCALE -13 79522 +SCALE -12 82122 +SCALE -11 84594 +SCALE -10 87029 +SCALE -9 89298 +SCALE -8 91301 +SCALE -7 93205 +SCALE -6 94904 +SCALE -5 96322 +SCALE -4 97627 +SCALE -3 98586 +SCALE -2 99361 +SCALE -1 99838 +SCALE 0 100000 +SCALE 1 99838 +SCALE 2 99361 +SCALE 3 98586 +SCALE 4 97627 +SCALE 5 96322 +SCALE 6 94904 +SCALE 7 93205 +SCALE 8 91301 +SCALE 9 89298 +SCALE 10 87029 +SCALE 11 84594 +SCALE 12 82122 +SCALE 13 79522 +SCALE 14 76554 +SCALE 15 73726 +SCALE 16 70538 +SCALE 17 67395 +SCALE 18 64039 +SCALE 19 60621 +SCALE 20 57011 +SCALE 21 53513 +SCALE 22 49707 +SCALE 23 45903 +SCALE 24 41830 +SCALE 25 37806 +SCALE 26 33577 +SCALE 27 29462 +SCALE 28 24976 +SCALE 29 20720 +SCALE 30 16067 +SCALE 31 11471 +SCALE 32 6561 +SCALE 33 1736 +SCALE 34 0 +SCALE 35 0 +SCALE 36 0 +SCALE 37 0 +SCALE 38 0 +SCALE 39 0 +SCALE 40 0 +POINT 0.0 100000 +POINT 0.25 99010 +POINT 0.5 96322 +POINT 1.0 87029 +POINT 2.0 57011 +POINT 3.3 1736 +POINT 10.0 0 +SYMMETRIC true +MONOTONIC true +ENTRANCE 0.0 0 6000000 +ENTRANCE 0.0 1 5175117 +ENTRANCE 0.0 2 3543032 +ENTRANCE 0.0 3 2133460 +ENTRANCE 0.0 4 1188046 +ENTRANCE 0.0 5 593349 +ENTRANCE 0.0 6 236703 +ENTRANCE 0.0 7 53566 +ENTRANCE 0.0 8 0 +ENTRANCE 0.0 9 0 +ENTRANCE 0.0 10 0 +ENTRANCE 0.0 11 0 +ENTRANCE 0.0 12 0 +ENTRANCE 0.0 13 0 +ENTRANCE 0.0 14 0 +ENTRANCE 0.0 15 0 +ENTRANCE 0.0 16 0 +ENTRANCE 0.0 17 0 +ENTRANCE 0.0 18 0 +ENTRANCE 0.0 19 0 +ENTRANCE 0.0 20 0 +ENTRANCE 0.2 0 6000000 +ENTRANCE 0.2 1 6000000 +ENTRANCE 0.2 2 6000000 +ENTRANCE 0.2 3 6000000 +ENTRANCE 0.2 4 6000000 +ENTRANCE 0.2 5 5175117 +ENTRANCE 0.2 6 3543032 +ENTRANCE 0.2 7 2133460 +ENTRANCE 0.2 8 1188046 +ENTRANCE 0.2 9 593349 +ENTRANCE 0.2 10 236703 +ENTRANCE 0.2 11 53566 +ENTRANCE 0.2 12 1 +ENTRANCE 0.2 13 0 +ENTRANCE 0.2 14 0 +ENTRANCE 0.2 15 0 +ENTRANCE 0.2 16 0 +ENTRANCE 0.2 17 0 +ENTRANCE 0.2 18 0 +ENTRANCE 0.2 19 0 +ENTRANCE 0.2 20 0 +SLIDE 0 3200000 +SLIDE 1 2760062 +SLIDE 2 1889617 +SLIDE 3 1137846 +SLIDE 4 633625 +SLIDE 5 316453 +SLIDE 6 126242 +SLIDE 7 28569 +SLIDE 8 0 +SLIDE 9 0 +SLIDE 10 0 +SLIDE 11 0 +SLIDE 12 0 +SLIDE 13 0 +SLIDE 14 0 +SLIDE 15 0 +SLIDE 16 0 +SLIDE 17 0 +SLIDE 18 0 +SLIDE 19 0 +SLIDE 20 0 diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/main.dart b/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/main.dart new file mode 100644 index 00000000000..247d82c554b --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_carousel_motion/main.dart @@ -0,0 +1,84 @@ +// The motion of the gallery's home carousel, as a function you can measure. +// +// Every card is wrapped in a Transform.scale whose scale comes from the card's +// distance from the centred page. That expression IS the carousel's feel: if the two +// implementations disagree anywhere along it, cards grow and shrink differently as they +// pass the centre, which is exactly the kind of thing that reads as "they shift oddly" +// and is almost impossible to judge by eye. +// +// The body of cardScale is lifted verbatim from _CarouselState.builder in +// pages/home.dart, so this measures the real thing rather than a paraphrase of it. +// +// Run on BOTH implementations: +// real Flutter - benchcn1/tools/dart-reference.sh (flutter test, writes expect.txt) +// this runtime - BehaviorTest transpiles it, runs it on the JVM, diffs the output +import 'package:flutter/material.dart'; + +/// The scale a card gets when it sits [pageDelta] pages from the centre. +double cardScale(double pageDelta) { + double value = pageDelta; + // .3 is an approximation of the curve used in the design. + value = (1 - (value.abs() * .3)).clamp(0, 1).toDouble(); + value = Curves.easeOut.transform(value); + return value; +} + +/// Reported as an integer so a difference in double FORMATTING cannot be mistaken for a +/// difference in the curve. Five digits is far finer than a pixel: the card is 296dp +/// wide, so 1e-5 of scale is about three thousandths of a pixel. +int fixed(double v) => (v * 100000).round(); + +void main() { + // A sweep across four pages either side of centre, which covers the whole visible + // range and both clamped tails. + for (int i = -40; i <= 40; i++) { + final double delta = i / 10.0; + print('SCALE $i ${fixed(cardScale(delta))}'); + } + + // The exact points the eye actually notices: the centred card, its neighbours, and + // where the curve reaches its floor. + for (final double d in [0.0, 0.25, 0.5, 1.0, 2.0, 3.3, 10.0]) { + print('POINT $d ${fixed(cardScale(d))}'); + } + + // The curve must be symmetric about the centre - a card approaching from the left has + // to grow exactly as one leaving to the right shrinks, or the carousel breathes. + bool symmetric = true; + for (int i = 1; i <= 40; i++) { + if (fixed(cardScale(i / 10.0)) != fixed(cardScale(-i / 10.0))) { + symmetric = false; + } + } + print('SYMMETRIC $symmetric'); + + // ...and monotonic: moving further from the centre may never make a card BIGGER. + bool monotonic = true; + for (int i = 0; i < 40; i++) { + if (fixed(cardScale((i + 1) / 10.0)) > fixed(cardScale(i / 10.0))) { + monotonic = false; + } + } + print('MONOTONIC $monotonic'); + + // The OTHER motion on this screen: the staggered entrance. Each category item slides + // up from 60px of top padding over its own slice of one controller, via + // Interval(delay, delay + .4, curve: Curves.ease) driving a Tween. Two items with + // different delays are sampled together because the stagger is the point - if the + // slices do not line up the same way, the page assembles itself unevenly. + for (final double delay in [0.0, 0.2]) { + final Interval interval = Interval(0.0 + delay, 0.400 + delay, curve: Curves.ease); + final Tween pad = Tween(begin: 60.0, end: 0.0); + for (int i = 0; i <= 20; i++) { + final double t = i / 20.0; + print('ENTRANCE $delay $i ${fixed(pad.transform(interval.transform(t)))}'); + } + } + + // And the horizontal one the carousel itself rides in on. + final Interval slide = Interval(0.0, 0.400, curve: Curves.ease); + final Tween start = Tween(begin: 32.0, end: 0.0); + for (int i = 0; i <= 20; i++) { + print('SLIDE $i ${fixed(start.transform(slide.transform(i / 20.0)))}'); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/expect.txt new file mode 100644 index 00000000000..d69e599831e --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/expect.txt @@ -0,0 +1,64 @@ +INSIDE 0 10000 +INSIDE 1000 10000 +INSIDE 500000 10000 +INSIDE 999000 10000 +INSIDE 1000000 10000 +EDGE_TOP 0 -10000 +EDGE_TOP 15 -5135 +EDGE_TOP 30 -4943 +EDGE_TOP 45 -4755 +EDGE_TOP 60 -4570 +EDGE_TOP 75 -4389 +EDGE_TOP 90 -4212 +EDGE_TOP 105 -4038 +EDGE_TOP 120 -3868 +EDGE_TOP 135 -3702 +EDGE_TOP 150 -3539 +EDGE_TOP 165 -3380 +EDGE_TOP 180 -3225 +EDGE_TOP 195 -3073 +EDGE_TOP 210 -2925 +EDGE_TOP 225 -2781 +EDGE_TOP 240 -2640 +EDGE_TOP 255 -2503 +EDGE_TOP 270 -2369 +EDGE_TOP 285 -2239 +EDGE_TOP 300 -2113 +EDGE_BOTTOM 0 10000 +EDGE_BOTTOM 15 5135 +EDGE_BOTTOM 30 4943 +EDGE_BOTTOM 45 4755 +EDGE_BOTTOM 60 4570 +EDGE_BOTTOM 75 4389 +EDGE_BOTTOM 90 4212 +EDGE_BOTTOM 105 4038 +EDGE_BOTTOM 120 3868 +EDGE_BOTTOM 135 3702 +EDGE_BOTTOM 150 3539 +EDGE_BOTTOM 165 3380 +EDGE_BOTTOM 180 3225 +EDGE_BOTTOM 195 3073 +EDGE_BOTTOM 210 2925 +EDGE_BOTTOM 225 2781 +EDGE_BOTTOM 240 2640 +EDGE_BOTTOM 255 2503 +EDGE_BOTTOM 270 2369 +EDGE_BOTTOM 285 2239 +EDGE_BOTTOM 300 2113 +EASE 30 -4943 4817 +EASE 60 -4570 4449 +EASE 90 -4212 4096 +EASE 120 -3868 3757 +EASE 150 -3539 3433 +EASE 180 -3225 3123 +EASE 210 -2925 2828 +EASE 240 -2640 2548 +EASE 270 -2369 2282 +EASE 300 -2113 2031 +DELTA 1000 -399 +DELTA 5000 -2019 +DELTA 20000 -8424 +DELTA 80000 -39546 +DELTA 200000 -148053 +TOLERANCE 100000 8000000 18000 +OUTOFRANGE true true diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/main.dart b/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/main.dart new file mode 100644 index 00000000000..213804f4a4c --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_scroll_physics/main.dart @@ -0,0 +1,79 @@ +// How a scrollable resists being dragged past its edge, and how far a fling carries. +// +// This is the "tensile" behaviour: Codename One has its own overscroll model, Flutter has +// BouncingScrollPhysics, and the two do not agree. Rather than tune ours by feel, this +// measures Flutter's directly - the rubber band is a published, deterministic function of +// (how far you are already past the edge, how far you just dragged, how big the viewport +// is), so it can be sampled exactly. +// +// Run on BOTH implementations: +// real Flutter - benchcn1/tools/dart-reference.sh (flutter test, writes expect.txt) +// this runtime - BehaviorTest transpiles it, runs it on the JVM, diffs the output +import 'package:flutter/widgets.dart'; + +const double kMin = 0.0; +const double kMax = 1000.0; +const double kViewport = 800.0; + +ScrollMetrics metricsAt(double pixels) => FixedScrollMetrics( + minScrollExtent: kMin, + maxScrollExtent: kMax, + pixels: pixels, + viewportDimension: kViewport, + axisDirection: AxisDirection.down, + devicePixelRatio: 3.0, +); + +/// Reported as an integer so double FORMATTING cannot be mistaken for a difference in +/// the physics. Three decimals is far below anything a finger could feel. +int fixed(double v) => (v * 1000).round(); + +void main() { + final BouncingScrollPhysics physics = const BouncingScrollPhysics(); + + // 1. Inside the range the drag is passed through untouched - one pixel of finger is + // one pixel of content, and any resistance here would feel like drag. + for (final double p in [0.0, 1.0, 500.0, 999.0, 1000.0]) { + print('INSIDE ${fixed(p)} ${fixed(physics.applyPhysicsToUserOffset(metricsAt(p), 10.0))}'); + } + + // 2. Past the top edge. NOTE the sign convention: Flutter applies the result as + // `pixels -= offset`, so a POSITIVE offset pushes further out and a negative one + // eases back. The labels below are deliberately neutral about direction; what + // matters is that resistance grows with how far out you already are, and that curve + // is the whole feel of the rubber band. + for (int over = 0; over <= 300; over += 15) { + final ScrollMetrics m = metricsAt(kMin - over.toDouble()); + print('EDGE_TOP $over ${fixed(physics.applyPhysicsToUserOffset(m, -10.0))}'); + } + + // 3. ...and past the bottom edge, where the same two branches swap over. + for (int over = 0; over <= 300; over += 15) { + final ScrollMetrics m = metricsAt(kMax + over.toDouble()); + print('EDGE_BOTTOM $over ${fixed(physics.applyPhysicsToUserOffset(m, 10.0))}'); + } + + // 4. The two directions are NOT symmetric: from the same overscroll, dragging one way + // resists differently from dragging the other, because Flutter recomputes the + // friction from where the drag ENDS rather than where it starts. Which sign is + // which is left to the numbers - the point is that they differ, and by how much. + for (int over = 30; over <= 300; over += 30) { + final ScrollMetrics m = metricsAt(kMin - over.toDouble()); + final int neg = fixed(physics.applyPhysicsToUserOffset(m, -10.0)); + final int pos = fixed(physics.applyPhysicsToUserOffset(m, 10.0)); + print('EASE $over $neg $pos'); + } + + // 5. Drag size matters too: resistance is not a constant factor, so a big drag from + // the same position is not just a small one scaled up. + for (final double delta in [1.0, 5.0, 20.0, 80.0, 200.0]) { + final ScrollMetrics m = metricsAt(kMin - 100.0); + print('DELTA ${fixed(delta)} ${fixed(physics.applyPhysicsToUserOffset(m, -delta))}'); + } + + // 6. Whether a fling is even allowed to start, and the minimum it must beat. + print('TOLERANCE ${fixed(physics.minFlingVelocity)} ${fixed(physics.maxFlingVelocity)}' + ' ${fixed(physics.minFlingDistance)}'); + print('OUTOFRANGE ${physics.shouldAcceptUserOffset(metricsAt(500.0))}' + ' ${physics.shouldAcceptUserOffset(metricsAt(-50.0))}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/expect.txt new file mode 100644 index 00000000000..8d8aa4f6605 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/expect.txt @@ -0,0 +1,19 @@ +SLOT displayLarge present +SLOT displayMedium present +SLOT displaySmall present +SLOT headlineLarge present +SLOT headlineMedium present +SLOT headlineSmall present +SLOT titleLarge present +SLOT titleMedium present +SLOT titleSmall present +SLOT bodyLarge present +SLOT bodyMedium present +SLOT bodySmall present +SLOT labelLarge present +SLOT labelMedium present +SLOT labelSmall present +NONNULL 15 of 15 +COPYWITH kept_size true kept_weight true +OVERRIDE 9900 +OVERRIDE_KEEPS present diff --git a/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/main.dart b/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/main.dart new file mode 100644 index 00000000000..a9c39dd6c54 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/flutter_text_theme/main.dart @@ -0,0 +1,72 @@ +// Every slot of the default text theme, because Flutter code assumes they all exist. +// +// Material code reaches for these with a null assertion all the time - +// `theme.textTheme.bodyLarge!.copyWith(...)` is the ordinary way to write it, and the +// gallery does exactly that in its settings list. In Flutter that never throws, because +// ThemeData always materialises a full TextTheme from Typography and merges any partial +// override onto it. A runtime whose default TextTheme is empty turns every one of those +// call sites into a TypeError, and the widget that was being built silently disappears. +// +// So this sweeps all fifteen slots and reports the numbers that decide how text looks. +// +// Run on BOTH implementations: +// real Flutter - benchcn1/tools/dart-reference.sh (flutter test, writes expect.txt) +// this runtime - BehaviorTest transpiles it, runs it on the JVM, diffs the output +import 'package:flutter/material.dart'; + +/// PRESENCE, not metrics. +/// +/// The metrics legitimately differ: a bare ThemeData in Flutter leaves fontSize null and +/// resolves it from Typography when a Theme is applied, while this runtime fills in the +/// Material 3 default size immediately. Pinning the numbers would therefore fail for a +/// difference nobody wants to remove. What both implementations MUST agree on - and what +/// the settings page actually depends on - is that no slot is ever null, because Material +/// code reaches for them with `!`. +String describe(String name, TextStyle? s) { + return s == null ? 'SLOT $name NULL' : 'SLOT $name present'; +} + +void main() { + final TextTheme t = ThemeData(brightness: Brightness.light).textTheme; + + print(describe('displayLarge', t.displayLarge)); + print(describe('displayMedium', t.displayMedium)); + print(describe('displaySmall', t.displaySmall)); + print(describe('headlineLarge', t.headlineLarge)); + print(describe('headlineMedium', t.headlineMedium)); + print(describe('headlineSmall', t.headlineSmall)); + print(describe('titleLarge', t.titleLarge)); + print(describe('titleMedium', t.titleMedium)); + print(describe('titleSmall', t.titleSmall)); + print(describe('bodyLarge', t.bodyLarge)); + print(describe('bodyMedium', t.bodyMedium)); + print(describe('bodySmall', t.bodySmall)); + print(describe('labelLarge', t.labelLarge)); + print(describe('labelMedium', t.labelMedium)); + print(describe('labelSmall', t.labelSmall)); + + // The property every one of those `!` call sites depends on. + final List all = [ + t.displayLarge, t.displayMedium, t.displaySmall, + t.headlineLarge, t.headlineMedium, t.headlineSmall, + t.titleLarge, t.titleMedium, t.titleSmall, + t.bodyLarge, t.bodyMedium, t.bodySmall, + t.labelLarge, t.labelMedium, t.labelSmall, + ]; + print('NONNULL ${all.where((TextStyle? s) => s != null).length} of ${all.length}'); + + // copyWith must preserve what it is not asked to change - the gallery layers colour + // onto these styles and keeps the metrics. + final TextStyle body = t.bodyLarge!; + final TextStyle tinted = body.copyWith(color: const Color(0xFF00FF00)); + print('COPYWITH kept_size ${tinted.fontSize == body.fontSize}' + ' kept_weight ${tinted.fontWeight == body.fontWeight}'); + + // ...and an explicit override must win over the default, or an app theme cannot theme. + final TextTheme custom = ThemeData( + textTheme: const TextTheme(bodyLarge: TextStyle(fontSize: 99)), + ).textTheme; + print('OVERRIDE ${((custom.bodyLarge?.fontSize ?? -1) * 100).round()}'); + // The slots it did NOT override must still be there. + print('OVERRIDE_KEEPS ${custom.titleLarge == null ? 'NULL' : 'present'}'); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/language_basics/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/language_basics/expect.txt new file mode 100644 index 00000000000..6abf8af3912 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/language_basics/expect.txt @@ -0,0 +1,20 @@ +10 +widgets has many (8) +nothing +gear x2 +1 +{z: 1, a: 2} +3 +2 +0.5 +3.0 +true +[2, 4, 6, 8] +2 +HELLO WORLD +[hello, world] +true +world +5 +1 +2 diff --git a/maven/dart-transpiler/src/test/resources/behavior/language_basics/main.dart b/maven/dart-transpiler/src/test/resources/behavior/language_basics/main.dart new file mode 100644 index 00000000000..ce609103627 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/language_basics/main.dart @@ -0,0 +1,61 @@ +int addAll(List values) { + int sum = 0; + for (int i = 0; i < values.length; i++) { + sum += values[i]; + } + return sum; +} + +String describe(String name, int count) { + if (count > 3) { + return '$name has many (${count * 2})'; + } else if (count == 0) { + return name.isEmpty ? 'nothing' : name; + } + return name + ' x' + count.toString(); +} + +void main() { + List nums = [1, 2, 3, 4]; + print(addAll(nums)); + print(describe('widgets', 4)); + print(describe('', 0)); + print(describe('gear', 2)); + Map ages = {'z': 1, 'a': 2}; + print(ages['z']); + print(ages); + print(7 ~/ 2); + print(-7 % 3); + print(1 / 2); + double d = 3.0; + print(d); + bool flag = nums.isNotEmpty && ages.length == 2; + print(flag); + var doubled = nums.map((n) => n * 2).toList(); + print(doubled); + int counter = 0; + var inc = () { + counter++; + }; + inc(); + inc(); + print(counter); + String s = 'hello world'; + print(s.toUpperCase()); + print(s.split(' ')); + print(s.contains('wor')); + print(s.substring(6)); + while (counter < 5) { + counter = counter + 1; + } + print(counter); + for (int v in nums) { + if (v == 3) { + continue; + } + if (v > 3) { + break; + } + print(v); + } +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m2_language/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m2_language/expect.txt new file mode 100644 index 00000000000..57d20f89589 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m2_language/expect.txt @@ -0,0 +1,9 @@ +(2.0, 3.0) +(0.0, 0.0) +true +5 +2 +[0, 1, 2, 99, 0, 10] +[A, B] +3 +base+derived diff --git a/maven/dart-transpiler/src/test/resources/behavior/m2_language/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m2_language/main.dart new file mode 100644 index 00000000000..4e875a12ff0 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m2_language/main.dart @@ -0,0 +1,77 @@ +int total = 0; + +class Vec { + final double x; + final double y; + Vec(this.x, this.y); + Vec.unit() + : x = 1.0, + y = 1.0; + factory Vec.origin() { + return Vec(0.0, 0.0); + } + Vec operator +(Vec other) { + return Vec(x + other.x, y + other.y); + } + bool operator ==(Vec other) { + return x == other.x && y == other.y; + } + String describe() { + return '($x, $y)'; + } +} + +class Counter { + int value = 0; + void bump() { + value++; + total++; + } + + void add(int n) { + value += n; + } +} + +class Base { + String greet() { + return 'base'; + } +} + +class Derived extends Base { + @override + String greet() { + return super.greet() + '+derived'; + } +} + +void main() { + Vec a = Vec(1.0, 2.0); + Vec b = Vec.unit(); + Vec c = a + b; + print(c.describe()); + print(Vec.origin().describe()); + print(a + b == Vec(2.0, 3.0)); + Counter k = Counter(); + k + ..bump() + ..bump() + ..add(3); + print(k.value); + print(total); + List base = [1, 2]; + bool extra = true; + List combined = [ + 0, + ...base, + if (extra) 99 else 98, + for (int i = 0; i < 2; i++) i * 10, + ]; + print(combined); + List names = [for (final n in ['a', 'b']) n.toUpperCase()]; + print(names); + int p = 1, q = 2; + print(p + q); + print(Derived().greet()); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m3_async/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m3_async/expect.txt new file mode 100644 index 00000000000..cf91d7489eb --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m3_async/expect.txt @@ -0,0 +1,7 @@ +start +42 +DART +caught: FormatException: bad +done +generic: Exception: boom +[41, 41] diff --git a/maven/dart-transpiler/src/test/resources/behavior/m3_async/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m3_async/main.dart new file mode 100644 index 00000000000..cade90e6763 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m3_async/main.dart @@ -0,0 +1,32 @@ +Future compute() async { + await Future.delayed(Duration(milliseconds: 40)); + return 41; +} + +Future fetchName() async => 'dart'; + +void main() async { + print('start'); + int v = await compute(); + print(v + 1); + String n = await fetchName(); + print(n.toUpperCase()); + try { + throw FormatException('bad'); + } on FormatException catch (e) { + print('caught: $e'); + } catch (e) { + print('other'); + } finally { + print('done'); + } + try { + throw Exception('boom'); + } on FormatException catch (e) { + print('wrong'); + } catch (e) { + print('generic: $e'); + } + var results = await Future.wait([compute(), compute()]); + print(results); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/expect.txt new file mode 100644 index 00000000000..9083b7ef0b1 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/expect.txt @@ -0,0 +1,9 @@ +DART! +d +abab +25 +Hello, World +2 +Hi, Again +base +Hello, Mix diff --git a/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/main.dart new file mode 100644 index 00000000000..cfd6d508776 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m4_ext_mixin/main.dart @@ -0,0 +1,54 @@ +extension StringX on String { + String shout() { + return this.toUpperCase() + '!'; + } + + String get first => this[0]; + + String repeatTwice() => this + this; +} + +extension IntX on int { + int squared() => this * this; +} + +mixin Greeter { + String greeting = 'Hello'; + String greet(String who) { + return '$greeting, $who'; + } +} + +mixin Counter { + int count = 0; + void bump() { + count++; + } +} + +class Host with Greeter, Counter { +} + +class Base2 { + String id() => 'base'; +} + +class Sub2 extends Base2 with Greeter { +} + +void main() { + print('dart'.shout()); + print('dart'.first); + print('ab'.repeatTwice()); + print(5.squared()); + Host h = Host(); + print(h.greet('World')); + h.bump(); + h.bump(); + print(h.count); + h.greeting = 'Hi'; + print(h.greet('Again')); + Sub2 s = Sub2(); + print(s.id()); + print(s.greet('Mix')); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/expect.txt new file mode 100644 index 00000000000..55b7c6605e5 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/expect.txt @@ -0,0 +1,6 @@ +big 10 +small 3 +other +{2, 3} +{1: a, 2: b, 3: c} +1a2b3c diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/main.dart new file mode 100644 index 00000000000..ff8473d4e01 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_collection_patterns/main.dart @@ -0,0 +1,40 @@ +// Dart 3 if-case statements, pattern for-in (record destructuring), and +// collection-for inside typed set/map literals. + +String describe(Object v) { + if (v case int x when x > 5) { + return 'big $x'; + } else if (v case int x) { + return 'small $x'; + } else { + return 'other'; + } +} + +void main() { + print(describe(10)); + print(describe(3)); + print(describe('hi')); + + var pairs = [(1, 'a'), (2, 'b'), (3, 'c')]; + + // typed set literal with collection-for over a record pattern + collection-if + Set big = { + for (final (int i, String s) in pairs) + if (i > 1) i, + }; + print(big); + + // map literal with collection-for + Map m = { + for (final (int i, String s) in pairs) i: s, + }; + print(m); + + // pattern for-in statement + var out = ''; + for (final (int i, String s) in pairs) { + out += '$i$s'; + } + print(out); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/expect.txt new file mode 100644 index 00000000000..60928345c56 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/expect.txt @@ -0,0 +1,21 @@ +12.56 +9.0 +10.0 +zero +small +small +many +negative +zero +positive +A +B +C +F +3 +4 +10 +20 +origin +point 5,6 +other diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/main.dart new file mode 100644 index 00000000000..ac9783d13cc --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_patterns/main.dart @@ -0,0 +1,88 @@ +sealed class Shape {} + +class Circle extends Shape { + final double radius; + Circle(this.radius); +} + +class Square extends Shape { + final double side; + Square(this.side); +} + +class Rect extends Shape { + final double w; + final double h; + Rect(this.w, this.h); +} + +double area(Shape s) { + return switch (s) { + Circle(radius: var r) => 3.14 * r * r, + Square(side: var x) => x * x, + Rect(w: var w, h: var h) => w * h, + }; +} + +String describe(int n) { + switch (n) { + case 0: + return 'zero'; + case 1: + case 2: + return 'small'; + default: + return 'many'; + } +} + +String sign(int n) { + return switch (n) { + < 0 => 'negative', + 0 => 'zero', + _ => 'positive', + }; +} + +String grade(int score) { + return switch (score) { + >= 90 => 'A', + >= 80 => 'B', + int x when x >= 70 => 'C', + _ => 'F', + }; +} + +String classify(Object v) { + return switch (v) { + (0, 0) => 'origin', + (var x, var y) => 'point $x,$y', + _ => 'other', + }; +} + +void main() { + print(area(Circle(2.0))); + print(area(Square(3.0))); + print(area(Rect(2.0, 5.0))); + print(describe(0)); + print(describe(1)); + print(describe(2)); + print(describe(9)); + print(sign(-3)); + print(sign(0)); + print(sign(7)); + print(grade(95)); + print(grade(85)); + print(grade(72)); + print(grade(50)); + var p = (3, 4); + print(p.$1); + print(p.$2); + var named = (x: 10, y: 20); + print(named.x); + print(named.y); + print(classify((0, 0))); + print(classify((5, 6))); + print(classify('hi')); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/expect.txt new file mode 100644 index 00000000000..355e1e490c7 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/expect.txt @@ -0,0 +1,5 @@ +false +true +false +true +499500 diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/main.dart new file mode 100644 index 00000000000..f35d4485346 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_stopwatch/main.dart @@ -0,0 +1,14 @@ +void main() { + final sw = Stopwatch(); + print(sw.isRunning); + sw.start(); + print(sw.isRunning); + int x = 0; + for (int i = 0; i < 1000; i++) { + x += i; + } + sw.stop(); + print(sw.isRunning); + print(sw.elapsedMicroseconds >= 0); + print(x); +} diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/expect.txt b/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/expect.txt new file mode 100644 index 00000000000..320d5039ea5 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/expect.txt @@ -0,0 +1 @@ +0 1 2 3 99 diff --git a/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/main.dart b/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/main.dart new file mode 100644 index 00000000000..f20f9809d46 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/behavior/m5_sync_star/main.dart @@ -0,0 +1,21 @@ +// sync* generators lowered to a list-collecting body. + +Iterable countTo(int n) sync* { + for (int i = 1; i <= n; i++) { + yield i; + } +} + +Iterable combined() sync* { + yield 0; + yield* countTo(3); + yield 99; +} + +void main() { + var out = ''; + for (final x in combined()) { + out += '$x '; + } + print(out); +} diff --git a/maven/dart-transpiler/src/test/resources/fixtures/counter_main.dart b/maven/dart-transpiler/src/test/resources/fixtures/counter_main.dart new file mode 100644 index 00000000000..b8ecf5fdc44 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/fixtures/counter_main.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + Text('$_counter', style: Theme.of(context).textTheme.headlineMedium), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/maven/dart-transpiler/src/test/resources/fixtures/m2_demo.dart b/maven/dart-transpiler/src/test/resources/fixtures/m2_demo.dart new file mode 100644 index 00000000000..abdbfa3a435 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/fixtures/m2_demo.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const DemoApp()); +} + +class DemoApp extends StatelessWidget { + const DemoApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'M2 Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const DemoPage(), + ); + } +} + +class DemoPage extends StatefulWidget { + const DemoPage({super.key}); + + @override + State createState() => _DemoPageState(); +} + +class _DemoPageState extends State { + final List _items = ['Alpha', 'Beta', 'Gamma']; + int _taps = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('M2 Widgets')), + body: Column( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _addItem, + child: const Text('Add'), + ), + OutlinedButton( + onPressed: _clear, + child: const Text('Clear'), + ), + TextButton( + onPressed: () { + setState(() { + _taps++; + }); + }, + child: Text('Taps: $_taps'), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + return Card( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Row( + children: [ + const Icon(Icons.favorite), + SizedBox(width: 8.0), + Expanded(child: Text(_items[index])), + Text('#$index'), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + void _addItem() { + setState(() { + _items.add('Item ${_items.length + 1}'); + }); + } + + void _clear() { + setState(() { + _items.clear(); + _taps = 0; + }); + } +} diff --git a/maven/dart-transpiler/src/test/resources/fixtures/m3_demo.dart b/maven/dart-transpiler/src/test/resources/fixtures/m3_demo.dart new file mode 100644 index 00000000000..847717afa47 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/fixtures/m3_demo.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const M3App()); +} + +class M3App extends StatelessWidget { + const M3App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'M3 Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const FormPage(), + ); + } +} + +class FormPage extends StatefulWidget { + const FormPage({super.key}); + + @override + State createState() => _FormPageState(); +} + +class _FormPageState extends State { + final TextEditingController _name = TextEditingController(text: 'World'); + bool _subscribe = true; + bool _dark = false; + double _volume = 0.4; + String _status = 'idle'; + + Future _save() async { + setState(() { + _status = 'saving...'; + }); + await Future.delayed(Duration(milliseconds: 900)); + setState(() { + _status = 'saved'; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Saved ${_name.text}')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('M3 Inputs')), + body: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _name, + decoration: InputDecoration(labelText: 'Name', hintText: 'Enter a name'), + ), + SizedBox(height: 12.0), + Row( + children: [ + Checkbox( + value: _subscribe, + onChanged: (v) { + setState(() { + _subscribe = v; + }); + }, + ), + const Text('Subscribe'), + SizedBox(width: 24.0), + Switch( + value: _dark, + onChanged: (v) { + setState(() { + _dark = v; + }); + }, + ), + const Text('Dark'), + ], + ), + Slider( + value: _volume, + min: 0.0, + max: 1.0, + onChanged: (v) { + setState(() { + _volume = v; + }); + }, + ), + Text('Volume: $_volume status: $_status'), + SizedBox(height: 12.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _save, + child: const Text('Save'), + ), + OutlinedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => DetailPage(_name.text)), + ); + }, + child: const Text('Details'), + ), + TextButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('About'), + content: const Text('Flutter running on Codename One.'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + }, + child: const Text('About'), + ), + ], + ), + ], + ), + ), + ); + } +} + +class DetailPage extends StatelessWidget { + final String name; + const DetailPage(this.name, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Hello $name'), + SizedBox(height: 16.0), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Back'), + ), + ], + ), + ), + ); + } +} diff --git a/maven/dart-transpiler/src/test/resources/fixtures/m4_demo.dart b/maven/dart-transpiler/src/test/resources/fixtures/m4_demo.dart new file mode 100644 index 00000000000..3276c1bb252 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/fixtures/m4_demo.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const M4App()); +} + +class M4App extends StatefulWidget { + const M4App({super.key}); + + @override + State createState() => _M4AppState(); +} + +class _M4AppState extends State { + bool _dark = false; + + void _toggle(bool v) { + setState(() { + _dark = v; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'M4 Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + themeMode: _dark ? ThemeMode.dark : ThemeMode.light, + home: ThemePage(_dark, _toggle), + ); + } +} + +class ThemePage extends StatelessWidget { + final bool dark; + final BoolCallback onModeChanged; + + const ThemePage(this.dark, this.onModeChanged, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('M4 Theming')), + body: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Switch(value: dark, onChanged: onModeChanged), + const Text('Dark mode'), + ], + ), + SizedBox(height: 16.0), + RichText( + text: TextSpan( + text: 'Flutter ', + style: TextStyle(fontSize: 20.0), + children: [ + TextSpan( + text: 'rich text', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.deepPurple), + ), + TextSpan(text: ' running on '), + TextSpan( + text: 'Codename One', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: ' with per-span styles that wrap across lines.'), + ], + ), + ), + SizedBox(height: 16.0), + Card( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('MediaQuery'), + Text('size: ${MediaQuery.of(context).size.width} x ${MediaQuery.of(context).size.height}'), + Text('dpr: ${MediaQuery.of(context).devicePixelRatio}'), + ], + ), + ), + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton(onPressed: () {}, child: const Text('Elevated')), + OutlinedButton(onPressed: () {}, child: const Text('Outlined')), + TextButton(onPressed: () {}, child: const Text('Text')), + ], + ), + ], + ), + ), + ); + } +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/FlutterRegistry.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/FlutterRegistry.java new file mode 100644 index 00000000000..5b0d0ac17c5 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/FlutterRegistry.java @@ -0,0 +1,12 @@ +package com.codename1.generated.flutter; + +/** Generated entry-point registry for transpiled Flutter code. */ +public final class FlutterRegistry { + private FlutterRegistry() { + } + + /** Invokes the Dart main() of the application's main library. */ + public static void invokeMain() { + MainLib.main$(); + } +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java new file mode 100644 index 00000000000..c86e4031b6c --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java @@ -0,0 +1,15 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.FlutterUI; + +// Generated from main.dart — do not edit. +public final class MainLib { + + private MainLib() { + } + + public static void main$() { + FlutterUI.runApp(new MyApp(null)); + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java new file mode 100644 index 00000000000..1498ece897a --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java @@ -0,0 +1,31 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.material.ColorScheme; +import com.codename1.flutter.Colors; +import com.codename1.flutter.Key; +import com.codename1.flutter.material.MaterialApp; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.material.ThemeData; +import com.codename1.flutter.Widget; + +// Generated from main.dart — do not edit. +public class MyApp extends StatelessWidget { + + public MyApp(Key key) { + this.key(key); + } + + @Override + public Widget build(BuildContext context) { + var $t0 = new MaterialApp(); + $t0.title("Flutter Demo"); + var $t1 = new ThemeData(); + $t1.colorScheme(ColorScheme.fromSeed(Colors.deepPurple, null)); + $t1.useMaterial3(true); + $t0.theme($t1); + $t0.home(new MyHomePage(null, "Flutter Demo Home Page")); + return $t0; + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java new file mode 100644 index 00000000000..2066115485b --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java @@ -0,0 +1,25 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.Key; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulWidget; + +// Generated from main.dart — do not edit. +public class MyHomePage extends StatefulWidget { + + private String title; + public String get$title() { + return title; + } + + public MyHomePage(Key key, String title) { + this.key(key); + this.title = title; + } + + @Override + public State createState() { + return new _MyHomePageState(); + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java b/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java new file mode 100644 index 00000000000..354fdf50aea --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java @@ -0,0 +1,59 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.material.AppBar; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.widgets.Center; +import com.codename1.flutter.widgets.Column; +import dart.core.DartList; +import dart.runtime.DartRuntime; +import com.codename1.flutter.material.FloatingActionButton; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.Icons; +import com.codename1.flutter.MainAxisAlignment; +import com.codename1.flutter.material.Scaffold; +import com.codename1.flutter.State; +import com.codename1.flutter.widgets.Text; +import com.codename1.flutter.material.Theme; +import com.codename1.flutter.Widget; + +// Generated from main.dart — do not edit. +public class _MyHomePageState extends State { + + private long _counter = 0L; + long get$_counter() { + return _counter; + } + void set$_counter(long v) { + this._counter = v; + } + + void _incrementCounter() { + this.setState(() -> { + this._counter++; + }); + } + + @Override + public Widget build(BuildContext context) { + var $t0 = new Scaffold(); + var $t1 = new AppBar(); + $t1.backgroundColor(Theme.of(context).colorScheme().inversePrimary()); + $t1.title(new Text(this.widget().get$title())); + $t0.appBar($t1); + var $t2 = new Center(); + var $t3 = new Column(); + $t3.mainAxisAlignment(MainAxisAlignment.center); + var $t4 = new Text(DartRuntime.str(this._counter)); + $t4.style(Theme.of(context).textTheme().headlineMedium()); + $t3.children(DartList.of(new Text("You have pushed the button this many times:"), $t4)); + $t2.child($t3); + $t0.body($t2); + var $t5 = new FloatingActionButton(); + $t5.onPressed(this::_incrementCounter); + $t5.tooltip("Increment"); + $t5.child(new Icon(Icons.add)); + $t0.floatingActionButton($t5); + return $t0; + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/counter/main.dart b/maven/dart-transpiler/src/test/resources/golden/counter/main.dart new file mode 100644 index 00000000000..b8ecf5fdc44 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/counter/main.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + Text('$_counter', style: Theme.of(context).textTheme.headlineMedium), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java new file mode 100644 index 00000000000..ebf6428bac8 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java @@ -0,0 +1,31 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.material.ColorScheme; +import com.codename1.flutter.Colors; +import com.codename1.flutter.Key; +import com.codename1.flutter.material.MaterialApp; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.material.ThemeData; +import com.codename1.flutter.Widget; + +// Generated from main.dart — do not edit. +public class DemoApp extends StatelessWidget { + + public DemoApp(Key key) { + this.key(key); + } + + @Override + public Widget build(BuildContext context) { + var $t0 = new MaterialApp(); + $t0.title("M2 Demo"); + var $t1 = new ThemeData(); + $t1.colorScheme(ColorScheme.fromSeed(Colors.blue, null)); + $t1.useMaterial3(true); + $t0.theme($t1); + $t0.home(new DemoPage(null)); + return $t0; + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java new file mode 100644 index 00000000000..a0155c32fa5 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java @@ -0,0 +1,19 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.Key; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulWidget; + +// Generated from main.dart — do not edit. +public class DemoPage extends StatefulWidget { + + public DemoPage(Key key) { + this.key(key); + } + + @Override + public State createState() { + return new _DemoPageState(); + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/FlutterRegistry.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/FlutterRegistry.java new file mode 100644 index 00000000000..5b0d0ac17c5 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/FlutterRegistry.java @@ -0,0 +1,12 @@ +package com.codename1.generated.flutter; + +/** Generated entry-point registry for transpiled Flutter code. */ +public final class FlutterRegistry { + private FlutterRegistry() { + } + + /** Invokes the Dart main() of the application's main library. */ + public static void invokeMain() { + MainLib.main$(); + } +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java new file mode 100644 index 00000000000..95e44e85523 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java @@ -0,0 +1,15 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.FlutterUI; + +// Generated from main.dart — do not edit. +public final class MainLib { + + private MainLib() { + } + + public static void main$() { + FlutterUI.runApp(new DemoApp(null)); + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java new file mode 100644 index 00000000000..1f2d34917c7 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java @@ -0,0 +1,102 @@ +package com.codename1.generated.flutter; + +import com.codename1.flutter.material.AppBar; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.material.Card; +import com.codename1.flutter.widgets.Column; +import dart.core.DartList; +import dart.runtime.DartRuntime; +import com.codename1.flutter.material.Divider; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.material.ElevatedButton; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.Icons; +import com.codename1.flutter.widgets.ListView; +import com.codename1.flutter.MainAxisAlignment; +import com.codename1.flutter.material.OutlinedButton; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.material.Scaffold; +import com.codename1.flutter.widgets.SizedBox; +import com.codename1.flutter.State; +import com.codename1.flutter.widgets.Text; +import com.codename1.flutter.material.TextButton; +import com.codename1.flutter.Widget; + +// Generated from main.dart — do not edit. +public class _DemoPageState extends State { + + private final DartList _items = DartList.of("Alpha", "Beta", "Gamma"); + DartList get$_items() { + return _items; + } + + private long _taps = 0L; + long get$_taps() { + return _taps; + } + void set$_taps(long v) { + this._taps = v; + } + + @Override + public Widget build(BuildContext context) { + var $t0 = new Scaffold(); + var $t1 = new AppBar(); + $t1.title(new Text("M2 Widgets")); + $t0.appBar($t1); + var $t2 = new Column(); + var $t3 = new Padding(); + $t3.padding(EdgeInsets.all(8.0)); + var $t4 = new Row(); + $t4.mainAxisAlignment(MainAxisAlignment.spaceEvenly); + var $t5 = new ElevatedButton(); + $t5.onPressed(this::_addItem); + $t5.child(new Text("Add")); + var $t6 = new OutlinedButton(); + $t6.onPressed(this::_clear); + $t6.child(new Text("Clear")); + var $t7 = new TextButton(); + $t7.onPressed(() -> { + this.setState(() -> { + this._taps++; + }); + }); + $t7.child(new Text("Taps: " + DartRuntime.str(this._taps))); + $t4.children(DartList.of($t5, $t6, $t7)); + $t3.child($t4); + var $t8 = new Expanded(); + $t8.child(ListView.builder(null, this._items.length(), (context$0, index) -> { + var $t9 = new Card(); + var $t10 = new Padding(); + $t10.padding(EdgeInsets.all(12.0)); + var $t11 = new Row(); + var $t12 = new SizedBox(); + $t12.width(8.0); + var $t13 = new Expanded(); + $t13.child(new Text(this._items.idx(index))); + $t11.children(DartList.of(new Icon(Icons.favorite), $t12, $t13, new Text("#" + DartRuntime.str(index)))); + $t10.child($t11); + $t9.child($t10); + return $t9; + }, null, null, null, null, null, null, null, null, null)); + $t2.children(DartList.of($t3, new Divider(), $t8)); + $t0.body($t2); + return $t0; + } + + void _addItem() { + this.setState(() -> { + this._items.add("Item " + DartRuntime.str(this._items.length() + 1L)); + }); + } + + void _clear() { + this.setState(() -> { + this._items.clear(); + this._taps = 0L; + }); + } + +} diff --git a/maven/dart-transpiler/src/test/resources/golden/m2demo/main.dart b/maven/dart-transpiler/src/test/resources/golden/m2demo/main.dart new file mode 100644 index 00000000000..abdbfa3a435 --- /dev/null +++ b/maven/dart-transpiler/src/test/resources/golden/m2demo/main.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const DemoApp()); +} + +class DemoApp extends StatelessWidget { + const DemoApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'M2 Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const DemoPage(), + ); + } +} + +class DemoPage extends StatefulWidget { + const DemoPage({super.key}); + + @override + State createState() => _DemoPageState(); +} + +class _DemoPageState extends State { + final List _items = ['Alpha', 'Beta', 'Gamma']; + int _taps = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('M2 Widgets')), + body: Column( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _addItem, + child: const Text('Add'), + ), + OutlinedButton( + onPressed: _clear, + child: const Text('Clear'), + ), + TextButton( + onPressed: () { + setState(() { + _taps++; + }); + }, + child: Text('Taps: $_taps'), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + return Card( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Row( + children: [ + const Icon(Icons.favorite), + SizedBox(width: 8.0), + Expanded(child: Text(_items[index])), + Text('#$index'), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + void _addItem() { + setState(() { + _items.add('Item ${_items.length + 1}'); + }); + } + + void _clear() { + setState(() { + _items.clear(); + _taps = 0; + }); + } +} diff --git a/maven/flutter-runtime/READINESS.md b/maven/flutter-runtime/READINESS.md new file mode 100644 index 00000000000..f2da122c014 --- /dev/null +++ b/maven/flutter-runtime/READINESS.md @@ -0,0 +1,284 @@ +# Flutter runtime: readiness + +Where the transpiled gallery does **not** yet match the reference, and what closing +each gap requires. Every line here is measured, not estimated; each has a check that +says when it is done. + +Kept in the module rather than under `docs/`, which is published. + +**Baseline, 2026-09-20** + +| Axis | State | +|---|---| +| Routes rendering | 48 / 48, **0 runtime errors** | +| Static parity | mean **2.46%** wrong pixels, worst **7.60%**, none above 8% | +| Motion steps | 8, **all eight** at or below their ratchet | +| Tests | 474 green (flutter-runtime 394, dart-transpiler 60, dart-runtime 20) | +| Static analysis | SpotBugs over core: **0 findings** (the gate's requirement) | +| Ports | JavaSE simulator and iOS both build, launch and measure | + +## How to reproduce the numbers + +```bash +cd benchcn1 +tools/relaunch.sh --app # rebuild runtime + app, restart the simulator +PORT=8765 python3 tools/parity.py # static: 48 routes +tools/relaunch.sh # ALWAYS relaunch between the two -- the sweep +PORT=8765 python3 tools/motion/capture.py # leaves the app on the last route, and the +python3 tools/motion/compare.py # push steps then start from the wrong screen +``` + +`JAVA17_HOME` must be set or 16 of the transpiler's 60 tests skip silently -- the ones +that compile and execute generated code. + +--- + +## 1. Interaction: callbacks the gallery passes that do nothing + +The largest gap, and the one the pixel metrics cannot see: a screen can render at 2% +wrong and do nothing when touched. These are stored by the widget and read by nothing +in the runtime. Counts are how many times the gallery's generated code passes them. + +| Widget | Inert callbacks | Uses | +|---|---|---| +| `MaterialApp` | `onGenerateRoute` | 10 | +| `TextFormField` | `onSaved`, `onFieldSubmitted` | 6 | +| `Focus` | `onKeyEvent`, `onFocusChange` | 5 | +| `FormField` | `onSaved` | 4 | +| `KeyboardListener` | `onKeyEvent` | 3 | +| `CupertinoSegmentedControl` | `onValueChanged` | 2 | +| `CupertinoSlidingSegmentedControl` | `onValueChanged` | 2 | +| `NavigationRail` | `onDestinationSelected` | 2 | +| `GestureDetector` | `onTapUp`, `onVerticalDragUpdate` | 2 | +| `Chip` | `onDeleted` | 1 | +| `InputChip` | `onDeleted` | 1 | +| `InteractiveViewer` | `onInteractionStart` | 1 | +| `WillPopScope` | `onWillPop` | 1 | +| `Listener` | `onPointerDown` | 1 | +| `Dismissible` | `onDismissed` | 1 | + +The three Cupertino picker callbacks left this table on 2026-09-20: the wheels are +real now and report as they turn. + +### Verified by use, not by counting + +The census counts call sites; it does not know whether anything compensates. Two +checked so far, by driving the running app: + +- **`MaterialApp.onGenerateRoute` -- compensated, NOT a live defect.** It is the + gallery's whole route factory (`main.dart:69`) and reads as the largest gap in the + table, but `Navigator` resolves named routes through its own table. Tapping a + category, then a demo, navigates and renders correctly with zero errors. Latent + risk: a route only the factory can produce would not resolve. +- **`Dismissible.onDismissed` -- LIVE DEFECT, confirmed.** A full left swipe across a + mail card in `/reply` changes **0.0%** of the list. Swipe-to-dismiss does nothing at + all; the reference dismisses the mail. + +A further **40** inert callbacks exist that the gallery never passes. They are latent +rather than broken and are not tracked here until something needs them. + +## 2. Parameters captured and ignored + +Widgets that render their child correctly and drop what was asked of them. Verified +per parameter -- a widget appears only where nothing outside its own file reads the +field. `Visibility.visible` and `ClipRRect.borderRadius`, for instance, ARE honoured +and are not listed. + +| Widget | Ignored parameters the gallery sets | +|---|---| +| `MouseRegion` | `cursor` (x20) -- desktop only | +| `Semantics` | `sortKey` (x15), `hint` | +| `Focus` | `focusNode` (x5), `onKeyEvent`, `onFocusChange`, `canRequestFocus` | +| `KeyboardListener` | `focusNode` (x5), `onKeyEvent` | +| `Tooltip` | `excludeFromSemantics` (x5) -- and no tooltip is shown at all | +| `Visibility` | `maintainState`, `maintainAnimation` | +| `AspectRatio` | **`aspectRatio`** -- the ratio is ignored entirely | +| `ClipRect` / `ClipRRect` / `ClipOval` | `clipper` (custom clip shapes) | +| `Form` | `autovalidateMode` | +| `WillPopScope` | `onWillPop` | + +## 2b. RESOLVED: a list mis-rendered when it lost an item + +Deleting a mail left every row below it drawing its new content over its old, and a +provider lookup in a rebuilt row answered null. + +**Cause: `DismissibleRenderElement` did not override `visitChildren`.** +`Element.visitChildren` is empty by default, so an element that owns children and does +not override it owns them invisibly -- and the walk that unmounts a subtree is the one +that suffers. Deleting a mail deactivated five Dismissible elements and reached none of +their subtrees: their elements stayed mounted, their components stayed in the scene, and +the lists inside those rows were never unmounted at all. + +It was introduced with swipe-to-dismiss, which is why nothing had shown it before. + +Worth keeping, because the symptom pointed everywhere but at the cause. The element tree +looked right at every level -- keys emitted, `ObjectKey` equality sound, the keyed +reconciler faithful, `attachOrder` matching the container's child count exactly, no +element mounted twice, no component attached while already parented, and +`RenderHost.detach` never once skipping its guard. That last fact was the tell rather +than an acquittal: detach was not skipping, it was never being CALLED. Logging every +deactivation showed five Dismissibles going while the lists inside them never unmounted, +which places the fault between deactivation and unmount -- and that is `visitChildren`. + +The bug CLASS is closed, not just this instance: a census of every element that owns +children finds no other that fails to override `visitChildren`. Two tests pin it. + +## 2c. RETRACTED: the nested GestureDetector was the instrument + +Recorded here as blocking, and wrong. `/demo/cupertino-picker` is not inert: every row +opens its picker, and always did. + +`bench_pointer`'s `steps` defaulted to **8**, so a tap injected without naming steps +carried eight `pointerDragged` events at the SAME coordinates. A zero-distance drag has +no dominant axis -- `draggedOnX` is `0 > 0`, false -- so `shouldGrabScrollEvents` reduces +to `isScrollableY()`, a scrollable ancestor claims the gesture, and `Form.pointerReleased` +then takes the `dragged != null` path and never delivers to what was pressed. + +Proved by instrumenting `Form.pointerReleased` rather than by reading it: + +```text +picker, steps default (8) dragged=ScrollPane origPressedCmp=OverlayComponent enabled=true +home, steps default (8) dragged=null origPressedCmp=OverlayComponent enabled=true +picker, steps: 0 dragged=null -> the popup opens, 99.5% of the screen changes +``` + +The floor had been fixed once so `steps: 0` could express a tap; the DEFAULT was left at +8. It now depends on the gesture: a tap defaults to zero steps, a drag to eight. + +**Every "changed 0.0%" verdict taken with a defaulted tap is void.** Re-checked since: + +| Route | With a real tap | +|---|---| +| `/demo/cupertino-picker` | opens its popup -- works | +| `/demo/chip` action chip | still 0.0%, and CORRECT: the gallery passes `onPressed: () {}`, an empty callback | + +The Dismissible verdict stands -- it was taken with a genuine multi-step swipe. + +### RESOLVED: the Cupertino pickers are real wheels + +They were stubs -- `CupertinoDatePicker` built a bare `Container` so its sheet came up +empty, and the other two built a plain `Column` of their children. + +All three are the same control, and Codename One has it five times over in +`com.codename1.ui.spinner`. The only thing wrong with those classes was that every one +was package private. They are public now (`Spinner3D`, `DateSpinner3D`, `TimeSpinner3D`, +`DateTimeSpinner3D`, `DurationSpinner3D`, and the `InternalPickerWidget` contract), and +nothing about them changed. + +Driven in the simulator, all five pickers in `/demo/cupertino-picker` open a working +wheel and none errors. `CupertinoDatePickerMode.monthYear` has no spinner of its own and +borrows the date wheel, so it shows one column more than it was asked for; the gallery +does not use that mode. + +**Still not visible to the sweep**, which photographs settled routes: the rows render +correctly either way and the wheel only ever appears inside the popup. + +## 2d. OPEN: the text field is wrong in four separate ways + +Measured on `/demo/text-field`, which is where all four show at once. None of them +is a layout problem; the field is in the right place and the wrong shape. + +| What | Measured | +|---|---| +| The requested size never reaches the label | The field asks for **48px** (16sp at 3x, and 16 is right -- demo pages run the Material 2018 scale, not the gallery's Montserrat) and the label renders at **76px**. Glyph heights confirm it: our `Name*` run is 55px against the reference's 34 | +| `TextField.maxLines` is stored and never read | The demo's Life story field asks for 3 lines and renders as one; the reference's box is three lines tall | +| `InputDecoration.helperText` is stored and never read | The reference carries `0/14` under the phone number and "Keep it short, this is just a demo." under Life story. We show neither, and the counter is missing too | +| `suffixIcon` is dropped | The password field's visibility toggle is absent | + +The size is the one to fix first, because it is not confined to this demo: every +field in the app is set in a face a half larger than it asked for, and the extra +height pushes everything below it down. + +## 3. Structural gaps + +| Gap | Consequence | +|---|---| +| ~~A button **consumes** its child into a label string or icon char~~ | **RESOLVED.** A button now mounts a child it cannot reduce to a `Text` or an `Icon`, and makes it transparent to touch so the press still lands on the button underneath. It was not an exotic case: the compose page's account row is a `PopupMenuButton` whose child is a Row, and the row was a blank band | +| `Notification.dispatch` is a no-op | needs the listener's type, and `NotificationListener` erases `T`; delivering without a type token would hand every `ScrollNotification` to a listener waiting for something else. Wants a type token from the transpiler | +| `CommonTransitions` drives both pages from one `Motion` | the iOS push works around it with its own transition; the outgoing page should ride `linearToEaseOut` while the incoming rides the three-point curve | +| `Image.scaled` is a **point sampler** on two of three ports | the desktop port reads one source pixel per destination pixel and Android asks `createScaledBitmap` not to filter, so every downscaled picture aliases. `Image.scaledSmooth`/`fillSmooth` were added beside them and the runtime uses those; the ports themselves are untouched, because fixing them moves ~500 committed screenshot baselines and that is not a call to make silently | + +## 4. Known per-screen differences + +| Screen | Difference | Measured | +|---|---|---| +| `/crane` | front layer detail | 7.60% | +| `/demo/typography` | line box for styles that state no height | 7.51% | +| `/demo/grid-lists` | **photo resampling only.** The tile geometry is identical to within one pixel on both axes and the two screens are indistinguishable; mean error 6.8 per channel. Was 7.74% before the filtered downscale | 5.90% | +| `/demo/text-field` | field decoration detail | 5.12% | +| `/shrine` | field corners are rounded where Shrine's are beveled | 2.47% | +| `/demo/cupertino-picker` | the wheel sheet is not centred quite as Flutter centres it (popup only, invisible to the sweep) | 3.03% | + +Motion is at 8/8 within ratchet. The two container transforms were the last failures: +`reply_compose` 12.48% -> 5.82% worst and `compose_back` 13.36% -> 5.68%, once the +painted surface followed the shape rather than the rectangle (a circle squashes the box +toward a square about its centre), the rounded-rect path stopped sweeping its corners the +long way round, the tapped thing's content was cut out of the page rather than painted +from its component, and the closed content stopped fading -- the fade variant holds it +opaque and simply covers it. + +## 5. Harness blind spots + +The suites measure settled screenshots and eight scripted transitions. They cannot see: + +- **Reachability** -- a screen with no way out scores perfectly. The splash page was + enter-only for as long as it existed, because `IgnorePointer` ignored nothing and + `onVerticalDragEnd` was never invoked. +- **Interaction state** -- only one step (`reply_card_press`) touches anything. +- **Anything the gallery does not exercise** -- the 40 latent callbacks above. +- **The harness itself is not in this repository.** `benchcn1` and `new-gallery-ref` + are working copies on one machine. Master now carries `scripts/fidelity-app` as an + in-repo precedent. + +## Full route table + +``` +/crane 7.60 +/demo/typography 7.51 +/demo/grid-lists 5.90 +/demo/text-field 5.12 +/fortnightly 4.58 +/demo/bottom-navigation 4.56 +/demo/colors 4.24 +/demo/cupertino-scrollbar 4.19 +/demo/data-table 3.99 +/demo/motion 3.90 +/reply 3.64 +/demo/cupertino-buttons 3.60 +/demo/bottom-app-bar 3.32 +/demo/2d-transformations 3.20 +/demo/cupertino-picker 3.03 +/rally 2.94 +/demo/banner 2.93 +/demo/sliders 2.84 +/shrine 2.47 +/demo/bottom-sheet 2.36 +/demo/cupertino-text-field 2.29 +/demo/cupertino-switch 2.15 +/demo/cupertino-navigation-bar 2.05 +/demo/cupertino-slider 2.05 +/demo/cupertino-context-menu 1.98 +/demo/cupertino-tab-bar 1.96 +/demo/cupertino-search-text-field 1.96 +/demo/cupertino-alerts 1.87 +/demo/cupertino-segmented-control 1.81 +/starter 1.69 +/ 1.54 +/demo/nav_rail 1.43 +/demo/card 1.33 +/demo/lists 1.32 +/demo/pickers 1.16 +/demo/nav_drawer 1.15 +/demo/snackbars 1.13 +/demo/dialog 0.91 +/demo/selection-controls 0.91 +/demo/tabs 0.79 +/demo/button 0.77 +/demo/menu 0.73 +/demo/chip 0.72 +/demo/progress-indicator 0.69 +/demo/tooltip 0.68 +/demo/cupertino-activity-indicator 0.54 +/demo/app-bar 0.35 +/demo/divider 0.34 +``` diff --git a/maven/flutter-runtime/javac.20260816_142742.args b/maven/flutter-runtime/javac.20260816_142742.args new file mode 100644 index 00000000000..aa1057febb9 --- /dev/null +++ b/maven/flutter-runtime/javac.20260816_142742.args @@ -0,0 +1 @@ +@/private/var/folders/zk/c7v7vr9d4s98dkx18yc7by7c0000gn/T/org.codehaus.plexus.compiler.javac.JavacCompiler2009260328672813326arguments diff --git a/maven/flutter-runtime/pom.xml b/maven/flutter-runtime/pom.xml new file mode 100644 index 00000000000..f82eb3018f2 --- /dev/null +++ b/maven/flutter-runtime/pom.xml @@ -0,0 +1,102 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-flutter-runtime + 8.0-SNAPSHOT + jar + codenameone-flutter-runtime + + Java implementation of the Flutter widget framework on top of + Codename One components: Widget/State/Element reconciliation, the + Flutter box-constraint layout engine, the material widget catalog and + the ThemeData-to-UIManager theme overlay. Transpiled Dart widget code + (from the codenameone-dart-transpiler) runs against this API. Ships + Dart signature stubs under META-INF/dart which the transpiler reads + to resolve this API from Dart source. + + + + UTF-8 + 17 + 17 + + + + + + maven-compiler-plugin + + + 17 + 17 + 17 + + + + + + + + java17-fork + + + env.JAVA17_HOME + + + + + + maven-compiler-plugin + + + 17 + 17 + 17 + true + ${env.JAVA17_HOME}/bin/javac + + + + maven-surefire-plugin + + ${env.JAVA17_HOME}/bin/java + + + + + + + + + + com.codenameone + codenameone-core + provided + + + com.codenameone + codenameone-dart-runtime + 8.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter + test + + + diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Alignment.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Alignment.java new file mode 100644 index 00000000000..d76d53c9375 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Alignment.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A point within a rectangle expressed in Flutter's -1..1 coordinate system: + * (-1,-1) is the top left, (0,0) the center, (1,1) the bottom right. + */ +public class Alignment { + + public static final Alignment topLeft = new Alignment(-1, -1); + public static final Alignment topCenter = new Alignment(0, -1); + public static final Alignment topRight = new Alignment(1, -1); + public static final Alignment centerLeft = new Alignment(-1, 0); + public static final Alignment center = new Alignment(0, 0); + public static final Alignment centerRight = new Alignment(1, 0); + public static final Alignment bottomLeft = new Alignment(-1, 1); + public static final Alignment bottomCenter = new Alignment(0, 1); + public static final Alignment bottomRight = new Alignment(1, 1); + + private final double x; + private final double y; + + public Alignment(double x, double y) { + this.x = x; + this.y = y; + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + /** + * The offset of a child of the given extent within a parent of the given + * extent, along one axis. + */ + public static double along(double alignment, double parentExtent, double childExtent) { + return (parentExtent - childExtent) * (alignment + 1) / 2; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Alignment)) { + return false; + } + Alignment a = (Alignment) o; + return a.x == x && a.y == y; + } + + @Override + public int hashCode() { + long bits = ValueHash.bits(x) * 31 + ValueHash.bits(y); + return (int) (bits ^ (bits >>> 32)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java new file mode 100644 index 00000000000..88e5344e800 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AlignmentDirectional.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A point within a rectangle expressed with a text-direction-relative + * horizontal axis (start/end) — Flutter's {@code AlignmentDirectional}. + * + *

The {@code start} coordinate is -1 and {@code end} is +1; resolving to a + * concrete {@link Alignment} assumes left-to-right text (start = left) for + * this milestone.

+ */ +public class AlignmentDirectional extends Alignment { + + public static final AlignmentDirectional topStart = new AlignmentDirectional(-1, -1); + public static final AlignmentDirectional topCenter = new AlignmentDirectional(0, -1); + public static final AlignmentDirectional topEnd = new AlignmentDirectional(1, -1); + public static final AlignmentDirectional centerStart = new AlignmentDirectional(-1, 0); + public static final AlignmentDirectional center = new AlignmentDirectional(0, 0); + public static final AlignmentDirectional centerEnd = new AlignmentDirectional(1, 0); + public static final AlignmentDirectional bottomStart = new AlignmentDirectional(-1, 1); + public static final AlignmentDirectional bottomCenter = new AlignmentDirectional(0, 1); + public static final AlignmentDirectional bottomEnd = new AlignmentDirectional(1, 1); + + public AlignmentDirectional(double start, double y) { + // start maps to the x axis under the LTR assumption of this milestone. + super(start, y); + } + + public double start() { + return x(); + } + + /** + * Resolves to a concrete {@link Alignment} assuming left-to-right text. + */ + public Alignment resolve() { + return new Alignment(x(), y()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/AssetImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AssetImage.java new file mode 100644 index 00000000000..cb826a8739b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AssetImage.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An {@link ImageProvider} that loads a bundled asset — Flutter's + * {@code AssetImage}. The optional {@code package} qualifies the asset's + * owning package ({@code packages//}), matching Flutter's + * asset resolution. + */ +public class AssetImage extends ImageProvider { + + private final String assetName; + private String packageName; + private Object bundle; + + public AssetImage(String assetName) { + this.assetName = assetName; + } + + /** + * Named parameter setter for the Dart {@code package:} parameter. The + * transpiler escapes the reserved word {@code package} to {@code package_}. + */ + public void package_(String v) { + this.packageName = v; + } + + /** Named parameter setter for the Dart {@code bundle:} parameter. */ + public void bundle(Object v) { + this.bundle = v; + } + + public String getAssetName() { + return assetName; + } + + public String getPackage() { + return packageName; + } + + /** + * The classpath-relative asset path, honoring the optional package + * qualifier ({@code packages//}). + */ + public String resolvedName() { + return qualify(assetName, packageName); + } + + /** + * Prefixes an asset name with its owning package, Flutter's + * {@code packages//}. Shared so every way of naming an asset — + * {@code AssetImage}, {@code Image.asset} — resolves to the same file. + */ + public static String qualify(String assetName, String packageName) { + if (assetName != null && packageName != null && !assetName.startsWith("packages/")) { + return "packages/" + packageName + "/" + assetName; + } + return assetName; + } + + @Override + public String sourceKey() { + return "asset:" + resolvedName(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Axis.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Axis.java new file mode 100644 index 00000000000..ef28cb97614 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Axis.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The direction along which boxes are laid out — Flutter's {@code Axis}. + */ +public enum Axis { + horizontal, vertical +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/AxisDirection.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AxisDirection.java new file mode 100644 index 00000000000..07e4aefc4c2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/AxisDirection.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The direction a scrollable's content grows in — Flutter's {@code AxisDirection}. + * + *

Named for where the content END lies, not where the finger moves: a vertical list + * that scrolls downwards through its content is {@code down}.

+ */ +public enum AxisDirection { + up, right, down, left +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BeveledRectangleBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BeveledRectangleBorder.java new file mode 100644 index 00000000000..13f08852cf4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BeveledRectangleBorder.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A rectangular border with flattened (beveled) corners — Flutter's {@code BeveledRectangleBorder}. */ +public class BeveledRectangleBorder extends OutlinedBorder { + + private Object borderRadius; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public Object getBorderRadius() { + return borderRadius; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BlendMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BlendMode.java new file mode 100644 index 00000000000..3fa45daa2ae --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BlendMode.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The Porter-Duff / separable blend modes passed to {@code Canvas.drawVertices} + * and related painting APIs — dart:ui's {@code BlendMode}. new_gallery's + * transformations demo uses {@link #color}; the full standard set is declared + * for fidelity. + */ +public enum BlendMode { + clear, src, dst, srcOver, dstOver, srcIn, dstIn, srcOut, dstOut, srcATop, + dstATop, xor, plus, modulate, screen, overlay, darken, lighten, colorDodge, + colorBurn, hardLight, softLight, difference, exclusion, multiply, hue, + saturation, color, luminosity +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Border.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Border.java new file mode 100644 index 00000000000..62be677f497 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Border.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A border drawn around a box, with an independent {@link BorderSide} on each + * edge — Flutter's {@code Border}. + */ +public final class Border extends BoxBorder { + + private BorderSide top = BorderSide.none; + private BorderSide right = BorderSide.none; + private BorderSide bottom = BorderSide.none; + private BorderSide left = BorderSide.none; + + public Border() { + } + + /** {@code Border.all(color: ..., width: ..., style: ...)}. */ + public static Border all(Color color, double width, BorderStyle style, + Double strokeAlign) { + BorderSide side = new BorderSide(); + if (color != null) { + side.color(color); + } + side.width(width); + side.style(style == null ? BorderStyle.solid : style); + Border b = new Border(); + b.top = side; + b.right = side; + b.bottom = side; + b.left = side; + return b; + } + + /** {@code Border.symmetric(vertical: ..., horizontal: ...)}. */ + public static Border symmetric(BorderSide vertical, BorderSide horizontal) { + Border b = new Border(); + BorderSide v = vertical == null ? BorderSide.none : vertical; + BorderSide h = horizontal == null ? BorderSide.none : horizontal; + b.top = v; + b.bottom = v; + b.left = h; + b.right = h; + return b; + } + + public void top(BorderSide v) { + this.top = v == null ? BorderSide.none : v; + } + + public void right(BorderSide v) { + this.right = v == null ? BorderSide.none : v; + } + + public void bottom(BorderSide v) { + this.bottom = v == null ? BorderSide.none : v; + } + + public void left(BorderSide v) { + this.left = v == null ? BorderSide.none : v; + } + + public BorderSide top() { + return top; + } + + public BorderSide right() { + return right; + } + + public BorderSide bottom() { + return bottom; + } + + public BorderSide left() { + return left; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadius.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadius.java new file mode 100644 index 00000000000..37c9e7c33ea --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadius.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An immutable set of corner radii for a box — Flutter's {@code BorderRadius}. + */ +public final class BorderRadius extends BorderRadiusGeometry { + + public static final BorderRadius zero = + new BorderRadius(Radius.zero, Radius.zero, Radius.zero, Radius.zero); + + private final Radius topLeft; + private final Radius topRight; + private final Radius bottomLeft; + private final Radius bottomRight; + + private BorderRadius(Radius topLeft, Radius topRight, Radius bottomLeft, Radius bottomRight) { + this.topLeft = topLeft == null ? Radius.zero : topLeft; + this.topRight = topRight == null ? Radius.zero : topRight; + this.bottomLeft = bottomLeft == null ? Radius.zero : bottomLeft; + this.bottomRight = bottomRight == null ? Radius.zero : bottomRight; + } + + public static BorderRadius all(Radius radius) { + return new BorderRadius(radius, radius, radius, radius); + } + + public static BorderRadius circular(double radius) { + return all(Radius.circular(radius)); + } + + public static BorderRadius only(Radius topLeft, Radius topRight, + Radius bottomLeft, Radius bottomRight) { + return new BorderRadius(topLeft, topRight, bottomLeft, bottomRight); + } + + public static BorderRadius vertical(Radius top, Radius bottom) { + return new BorderRadius(top, top, bottom, bottom); + } + + public static BorderRadius horizontal(Radius left, Radius right) { + return new BorderRadius(left, right, left, right); + } + + public Radius topLeft() { + return topLeft; + } + + public Radius topRight() { + return topRight; + } + + public Radius bottomLeft() { + return bottomLeft; + } + + public Radius bottomRight() { + return bottomRight; + } + + public RRect toRRect(Rect rect) { + return RRect.fromRectAndCorners(rect, topLeft, topRight, bottomLeft, bottomRight); + } + + /** + * Dart's {@code BorderRadius.lerp(a, b, t)}: per-corner linear interpolation. + * Returns null only when both inputs are null (mirroring Flutter). + */ + public static BorderRadius lerp(BorderRadius a, BorderRadius b, double t) { + if (a == null && b == null) return null; + // A null end is zero, as in Flutter: the other end's corners scale by t (or + // 1 - t). Returning it unchanged made the tween jump to, or from, fully rounded. + if (a == null) return lerp(zero, b, t); + if (b == null) return lerp(a, zero, t); + return new BorderRadius( + Radius.lerp(a.topLeft, b.topLeft, t), + Radius.lerp(a.topRight, b.topRight, t), + Radius.lerp(a.bottomLeft, b.bottomLeft, t), + Radius.lerp(a.bottomRight, b.bottomRight, t)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusDirectional.java new file mode 100644 index 00000000000..a3ae0dc620d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusDirectional.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Direction-relative corner radii ({@code start}/{@code end} corners) — + * Flutter's {@code BorderRadiusDirectional}. + */ +public final class BorderRadiusDirectional extends BorderRadiusGeometry { + + public static final BorderRadiusDirectional zero = + new BorderRadiusDirectional(Radius.zero, Radius.zero, Radius.zero, Radius.zero); + + private final Radius topStart; + private final Radius topEnd; + private final Radius bottomStart; + private final Radius bottomEnd; + + private BorderRadiusDirectional(Radius topStart, Radius topEnd, + Radius bottomStart, Radius bottomEnd) { + this.topStart = topStart == null ? Radius.zero : topStart; + this.topEnd = topEnd == null ? Radius.zero : topEnd; + this.bottomStart = bottomStart == null ? Radius.zero : bottomStart; + this.bottomEnd = bottomEnd == null ? Radius.zero : bottomEnd; + } + + public static BorderRadiusDirectional all(Radius radius) { + return new BorderRadiusDirectional(radius, radius, radius, radius); + } + + public static BorderRadiusDirectional circular(double radius) { + return all(Radius.circular(radius)); + } + + public static BorderRadiusDirectional only(Radius topStart, Radius topEnd, + Radius bottomStart, Radius bottomEnd) { + return new BorderRadiusDirectional(topStart, topEnd, bottomStart, bottomEnd); + } + + public static BorderRadiusDirectional vertical(Radius top, Radius bottom) { + return new BorderRadiusDirectional(top, top, bottom, bottom); + } + + public static BorderRadiusDirectional horizontal(Radius start, Radius end) { + return new BorderRadiusDirectional(start, end, start, end); + } + + public Radius topStart() { + return topStart; + } + + public Radius topEnd() { + return topEnd; + } + + public Radius bottomStart() { + return bottomStart; + } + + public Radius bottomEnd() { + return bottomEnd; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusGeometry.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusGeometry.java new file mode 100644 index 00000000000..709ae89f659 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderRadiusGeometry.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The common supertype of {@link BorderRadius} and + * {@link BorderRadiusDirectional} — Flutter's {@code BorderRadiusGeometry}. + */ +public abstract class BorderRadiusGeometry { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderSide.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderSide.java new file mode 100644 index 00000000000..becc3585dfa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderSide.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A side of a border: its color, width and line style — Flutter's + * {@code BorderSide}. + */ +public final class BorderSide { + + public static final BorderSide none = makeNone(); + + private Color color = new Color(0xFF000000L); + private double width = 1.0; + private BorderStyle style = BorderStyle.solid; + + /// Where the stroke sits relative to the path it follows: -1 fully inside, 0 centred + /// on it, 1 fully outside. Codename One strokes centred, so these are carried for the + /// geometry pass rather than honoured today - but a border naming one has to compile. + public static final double strokeAlignInside = -1.0; + public static final double strokeAlignCenter = 0.0; + public static final double strokeAlignOutside = 1.0; + + private Double strokeAlign; + + public void strokeAlign(double v) { + this.strokeAlign = v; + } + + public Double getStrokeAlign() { + return strokeAlign; + } + + public BorderSide() { + } + + private static BorderSide makeNone() { + BorderSide b = new BorderSide(); + b.width = 0.0; + b.style = BorderStyle.none; + return b; + } + + public Color color() { + return color; + } + + public void color(Color v) { + this.color = v; + } + + public double width() { + return width; + } + + public void width(double v) { + this.width = v; + } + + public BorderStyle style() { + return style; + } + + public void style(BorderStyle v) { + this.style = v == null ? BorderStyle.solid : v; + } + + /** + * Dart's {@code BorderSide.lerp(a, b, t)}: linear interpolation between two + * sides. Widths interpolate; the color and style are taken from the side the + * blend is closest to. Deferred rendering does not read the result, so a + * simple threshold blend is sufficient. + */ + public static BorderSide lerp(BorderSide a, BorderSide b, double t) { + if (a == null) return b; + if (b == null) return a; + BorderSide r = new BorderSide(); + r.width = a.width + (b.width - a.width) * t; + BorderSide dominant = t < 0.5 ? a : b; + r.color = dominant.color; + r.style = dominant.style; + return r; + } + + /** + * Dart's {@code BorderSide.toPaint()}: a stroking {@link Paint} for this + * side (its color at its width, or a hairline fill when the style is none). + */ + public Paint toPaint() { + Paint p = new Paint(); + p.color(color); + p.strokeWidth(width); + p.style(PaintingStyle.stroke); + return p; + } + + /** Flutter's default {@code BorderSide()}: opaque black, one logical pixel. */ + public static BorderSide solidBlack() { + BorderSide s = new BorderSide(); + s.color(new Color(0xFF000000L)); + s.width(1); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderStyle.java new file mode 100644 index 00000000000..603517e06e2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BorderStyle.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** Whether and how a border line is drawn — Flutter's {@code BorderStyle}. */ +public enum BorderStyle { + none, + solid +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxBorder.java new file mode 100644 index 00000000000..f1eda648a02 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxBorder.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The base class for borders that outline a box — Flutter's {@code BoxBorder} + * (supertype of {@link Border}). + */ +public abstract class BoxBorder extends ShapeBorder { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxDecoration.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxDecoration.java new file mode 100644 index 00000000000..cbe6da3c194 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxDecoration.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An immutable description of how to paint a box — Flutter's + * {@code BoxDecoration}. Only the {@link #getColor() background color} and + * {@link #getShape() shape} are honored by the Codename One runtime for this + * milestone; border/borderRadius/boxShadow/gradient/image are retained as + * opaque values (owned by other API categories) but not yet painted. + */ +public class BoxDecoration extends Decoration { + + private Color color; + private Object image; + private Object border; + private Object borderRadius; + private Object boxShadow; + private Object gradient; + private Object backgroundBlendMode; + private BoxShape shape = BoxShape.rectangle; + + public void color(Color v) { + this.color = v; + } + + public void image(Object v) { + this.image = v; + } + + public void border(Object v) { + this.border = v; + } + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public void boxShadow(Object v) { + this.boxShadow = v; + } + + public void gradient(Object v) { + this.gradient = v; + } + + public void backgroundBlendMode(Object v) { + this.backgroundBlendMode = v; + } + + public void shape(BoxShape v) { + this.shape = v == null ? BoxShape.rectangle : v; + } + + public Color getColor() { + return color; + } + + public Object getImage() { + return image; + } + + public Object getBorder() { + return border; + } + + public Object getBorderRadius() { + return borderRadius; + } + + public Object getBoxShadow() { + return boxShadow; + } + + public Object getGradient() { + return gradient; + } + + public Object getBackgroundBlendMode() { + return backgroundBlendMode; + } + + public BoxShape getShape() { + return shape; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxFit.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxFit.java new file mode 100644 index 00000000000..86abbc9c91f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxFit.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How a box (e.g. an image) should be inscribed into another box, mirroring + * Flutter's {@code BoxFit}. + */ +public enum BoxFit { + fill, contain, cover, fitWidth, fitHeight, none, scaleDown +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxShape.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxShape.java new file mode 100644 index 00000000000..8c537f776b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BoxShape.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The shape to fill a box's background — Flutter's {@code BoxShape}. + */ +public enum BoxShape { + rectangle, circle +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Brightness.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Brightness.java new file mode 100644 index 00000000000..3cb4d57a78f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Brightness.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The overall brightness of a theme or of the platform, mirroring Flutter's + * {@code Brightness}. + */ +public enum Brightness { + light, dark +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildContext.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildContext.java new file mode 100644 index 00000000000..15e977be427 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildContext.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A handle to the location of a widget in the element tree. Implemented by + * {@link Element}. Passed to build methods so widgets can look up inherited + * configuration (e.g. {@code Theme.of(context)}). + */ +public interface BuildContext { + + /** + * Walks up the element tree and returns the nearest ancestor widget whose + * runtime class is exactly {@code widgetType}, or null when there is none. + */ + W findAncestorWidgetOfExactType(Class widgetType); + + /** + * Walks up the element tree and returns the nearest ancestor widget that is + * an instance of {@code type} (Flutter's InheritedWidget dependency lookup), + * or null when there is none. The {@code } type witness the Dart call + * carries is threaded here as {@code type} by the transpiler. + */ + W dependOnInheritedWidgetOfExactType(Class type); + + /** + * As {@link #dependOnInheritedWidgetOfExactType(Class)}, but SILENT when + * nothing above provides the value. For the lookups whose callers have a + * documented fallback ({@code Theme.of}, {@code MediaQuery.of}, ...), where + * a miss is an ordinary answer rather than a fault worth a diagnostic. + */ + default W maybeDependOnInheritedWidgetOfExactType(Class type) { + return dependOnInheritedWidgetOfExactType(type); + } + + /** + * The no-type-argument form ({@code context.dependOnInheritedWidgetOfExactType()}), where Dart + * infers the widget type from the surrounding context. Java infers {@code W} from the call's + * target type. Not tree-walked at this milestone — returns null. + */ + default W dependOnInheritedWidgetOfExactType() { + return null; + } + + /** + * Walks up the element tree and returns the nearest ancestor {@code State} + * of the given type ({@code BuildContext.findAncestorStateOfType}), or null. + * Not tree-walked at this milestone — returns null. + */ + default T findAncestorStateOfType(Class type) { + return null; + } + + /** + * provider's {@code context.watch()}: the nearest ancestor-provided value + * assignable to {@code type} (rebuild-on-change is not modeled in this pass). + */ + T watch(Class type); + + /** + * provider's {@code context.read()}: the nearest ancestor-provided value + * assignable to {@code type}, without subscribing to changes. + */ + T read(Class type); + + /** + * The nearest value published by an ancestor {@link InheritedValueProvider} + * (Provider / ScopedModel) that is assignable to {@code type}, or null. + */ + Object providerValueOfType(Class type); + + /** + * {@link #providerValueOfType(Class)}, registering this context as a dependent -- + * rebuilt when the value changes -- only when {@code listen} is true. A read + * ({@code context.read}, {@code Provider.of(context, listen: false)}) must not + * subscribe, or the reader rebuilds on every notification that only watchers should + * get. + */ + default Object providerValueOfType(Class type, boolean listen) { + return providerValueOfType(type); + } + + /** + * Whether the element backing this context is still in the tree + * ({@code BuildContext.mounted}). Elements override this; the default is + * {@code true} for lightweight contexts that never detach. + */ + default boolean mounted() { + return true; + } + + /** + * The render object for this context — {@code BuildContext.findRenderObject}. + * + *

Returns a {@link com.codename1.flutter.rendering.RenderBox} backed by + * the nearest render element at or below this context, so its {@code size} + * and {@code localToGlobal} report the live layout. Null when this context + * has no render element below it (nothing has been laid out yet).

+ */ + default Object findRenderObject() { + if (!(this instanceof Element)) { + return null; + } + RenderElement r = RenderElement.findRenderElement((Element) this); + return r == null ? null : new com.codename1.flutter.rendering.RenderBox(r); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildOwner.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildOwner.java new file mode 100644 index 00000000000..4fd10489b4b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/BuildOwner.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.io.Log; +import com.codename1.ui.CN; +import com.codename1.ui.Display; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Tracks dirty elements and coalesces their rebuilds into one flush per + * frame, scheduled via {@code CN.callSerially}. Elements rebuild parents + * before children (depth order) so a parent rebuild that already updated a + * dirty child doesn't rebuild it twice. After the flush every affected host + * container is revalidated, which re-runs the Flutter constraint pass through + * {@code FlutterRootLayout}. + */ +public class BuildOwner { + + private final List dirtyElements = new ArrayList(); + private boolean flushScheduled; + + private static final Comparator BY_DEPTH = new Comparator() { + @Override + public int compare(Element a, Element b) { + return a.depth - b.depth; + } + }; + + /** + * Adds a dirty element and schedules a coalesced flush on the EDT. When + * no Display is initialized (headless unit tests) nothing is scheduled; + * tests drive {@link #flushSync()} directly. + */ + public void scheduleBuildFor(Element element) { + FlutterUI.assertEdt(); + if (!dirtyElements.contains(element)) { + dirtyElements.add(element); + } + if (!flushScheduled) { + flushScheduled = true; + if (Display.isInitialized()) { + CN.callSerially(new Runnable() { + @Override + public void run() { + flushBuild(); + } + }); + } + } + } + + /** + * Test hook: flushes the dirty list synchronously. + */ + public void flushSync() { + flushBuild(); + } + + /// Frames slower than this are worth knowing about: at 60fps the whole budget is 16ms, + /// so a build flush that costs more than this cannot keep up with a finger. + private static final long SLOW_FRAME_MS = 16; + + private static boolean traceFrames; + private static long framesTraced; + private static long buildMsTotal; + private static long revalidateMsTotal; + private static long rebuiltTotal; + private static long worstFrameMs; + + /// Starts or stops recording what each build flush costs. Off by default - this is a + /// diagnostic for "the UI feels slow", which is a question about where the frame went, + /// not about whether anything is broken. + public static void traceFrames(boolean on) { + traceFrames = on; + if (on) { + framesTraced = 0; + buildMsTotal = 0; + revalidateMsTotal = 0; + rebuiltTotal = 0; + worstFrameMs = 0; + RenderElement.resetLayoutCounters(); + } + com.codename1.flutter.animation.AnimationTrace.trace(on); + } + + /// What the traced frames cost, as a one-line summary. + public static String frameStats() { + return "{\"frames\":" + framesTraced + + ",\"elementsRebuilt\":" + rebuiltTotal + + ",\"buildMs\":" + buildMsTotal + + ",\"revalidateMs\":" + revalidateMsTotal + + ",\"worstFrameMs\":" + worstFrameMs + + ",\"layoutCalls\":" + RenderElement.layoutCalls + + ",\"layoutHits\":" + RenderElement.layoutHits + + ",\"missDirty\":" + RenderElement.layoutMissDirty + + ",\"missConstraints\":" + RenderElement.layoutMissConstraints + + "," + com.codename1.flutter.animation.AnimationTrace.stats() + + ",\"hot\":" + RenderElement.hotLayoutClasses(6) + "}"; + } + + /// Adds the hosts owned by anything inside {@code e}'s subtree to {@code out}. + private static void collectNestedHosts(Element e, final Set out) { + e.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Element child) { + if (child == null) { + return; + } + if (child.host() != null) { + out.add(child.host()); + } + collectNestedHosts(child, out); + } + }); + } + + /// How many times we look for hosts that appeared during the pass we just ran. One + /// extra round covers a subtree that builds late; the bound stops a tree that + /// somehow keeps producing hosts from looping here forever. + private static final int HOST_SETTLE_ROUNDS = 3; + + /** + * Lays out the hosts that did not exist when this flush chose what to lay out. + * + *

The host set is collected from the rebuilt subtrees BEFORE anything is measured, + * which assumes every host exists by then. Not all do. A LayoutBuilder sits out a + * speculative measurement pass and inflates its subtree on a later one -- during the + * very revalidate above -- and each thing it inflates can bring a host of its own: an + * effect pane, a scroll pane. Those hosts were never in the set, so they were never + * laid out, and the elements inside them kept the offsets their parent had stored + * while they still measured zero.

+ * + *

On screen that is a page with nothing on it. Coming back from Reply's search page + * rebuilt two elements and found seven hosts; the mail list underneath them owns + * thirty, one per card, all created while those seven were being laid out. Every card + * ended up at the list's origin inside a pane of zero size, and a zero-sized pane + * clips what it hosts, so the list was invisible rather than merely stacked. It stayed + * that way until some unrelated rebuild -- opening the mailbox drawer -- happened to + * find all thirty and lay them out.

+ */ + private static void revalidateHostsBornDuringLayout(List rebuiltRoots, + Set alreadyDone) { + for (int round = 0; round < HOST_SETTLE_ROUNDS; round++) { + Set found = new HashSet(); + for (Element e : rebuiltRoots) { + if (e.mounted) { + if (e.host() != null) { + found.add(e.host()); + } + collectNestedHosts(e, found); + } + } + found.removeAll(alreadyDone); + if (found.isEmpty()) { + return; + } + alreadyDone.addAll(found); + for (RenderHost h : found) { + h.revalidate(); + } + } + } + + void flushBuild() { + long started = traceFrames ? System.currentTimeMillis() : 0; + int rebuilt = 0; + flushScheduled = false; + Set affectedHosts = new HashSet(); + List rebuiltRoots = new ArrayList(); + int guard = 0; + while (!dirtyElements.isEmpty()) { + if (++guard > 10000) { + dirtyElements.clear(); + throw new IllegalStateException("Flutter build did not settle; an element keeps marking itself dirty during build"); + } + Collections.sort(dirtyElements, BY_DEPTH); + Element e = dirtyElements.remove(0); + if (!e.mounted || !e.dirty) { + continue; + } + e.rebuild(); + rebuilt++; + // Invalidate cached layout up this branch so the coming + // revalidate recomputes it. + for (Element a = e; a != null; a = a.parent) { + if (a instanceof RenderElement) { + ((RenderElement) a).markNeedsLayout(); + break; + } + } + if (e.host != null) { + affectedHosts.add(e.host); + } + // ...and every NESTED host inside what was just rebuilt. An element records the + // host it was mounted into, but a rebuilt subtree can contain scroll panes and + // effect panes that carry hosts of their own, and those hold the components. + // Revalidating only the outer host leaves a nested one holding children it + // never laid out - which renders as a page whose scaffold is present and whose + // contents have simply vanished, with no error anywhere. + collectNestedHosts(e, affectedHosts); + rebuiltRoots.add(e); + } + long built = traceFrames ? System.currentTimeMillis() : 0; + for (RenderHost h : affectedHosts) { + h.revalidate(); + } + revalidateHostsBornDuringLayout(rebuiltRoots, affectedHosts); + if (traceFrames) { + long now = System.currentTimeMillis(); + long buildMs = built - started; + long revalidateMs = now - built; + long frameMs = now - started; + framesTraced++; + rebuiltTotal += rebuilt; + buildMsTotal += buildMs; + revalidateMsTotal += revalidateMs; + worstFrameMs = Math.max(worstFrameMs, frameMs); + if (frameMs >= SLOW_FRAME_MS) { + Log.p("Flutter frame: " + frameMs + "ms (build " + buildMs + "ms for " + + rebuilt + " elements, revalidate " + revalidateMs + "ms across " + + affectedHosts.size() + " host(s))"); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java new file mode 100644 index 00000000000..61838580830 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Canvas.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The drawing surface handed to a CustomPainter — Flutter's dart:ui + * {@code Canvas}. This milestone provides the API surface (so painters can be + * transpiled and their draw/transform calls resolve); the concrete backend + * that binds these calls to a Codename One {@code Graphics} is supplied by the + * render layer. + */ +public class Canvas { + + public void drawPath(Path path, Paint paint) { + } + + public void drawRect(Rect rect, Paint paint) { + } + + public void drawRRect(RRect rrect, Paint paint) { + } + + public void drawCircle(Offset c, double radius, Paint paint) { + } + + public void drawOval(Rect rect, Paint paint) { + } + + public void drawLine(Offset p1, Offset p2, Paint paint) { + } + + public void drawArc(Rect rect, double startAngle, double sweepAngle, boolean useCenter, Paint paint) { + } + + public void drawPoints(Object pointMode, Object points, Paint paint) { + } + + public void drawColor(Color color, Object blendMode) { + } + + public void drawShadow(Path path, Color color, double elevation, boolean transparentOccluder) { + } + + public void drawVertices(Object vertices, Object blendMode, Paint paint) { + } + + public void drawImage(Object image, Offset offset, Paint paint) { + } + + public void translate(double dx, double dy) { + } + + public void scale(double sx, double sy) { + } + + public void rotate(double radians) { + } + + public void skew(double sx, double sy) { + } + + public void save() { + } + + public void saveLayer(Rect bounds, Paint paint) { + } + + public void restore() { + } + + public void clipRect(Rect rect) { + } + + public void clipRRect(RRect rrect) { + } + + public void clipPath(Path path) { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/CircleBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/CircleBorder.java new file mode 100644 index 00000000000..8bd047ff975 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/CircleBorder.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A circular (or elliptical) border — Flutter's {@code CircleBorder}. */ +public class CircleBorder extends OutlinedBorder { + + private double eccentricity; + + public void eccentricity(double v) { + this.eccentricity = v; + } + + public double getEccentricity() { + return eccentricity; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Clip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Clip.java new file mode 100644 index 00000000000..61b264d5695 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Clip.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Clipping modes — Flutter's {@code Clip}. Codename One clips on a rectangle boundary, so + * {@link com.codename1.flutter.widgets.ClipRect} honours these; the rounded and custom + * clippers still pass their child through and report the gap. + */ +public enum Clip { + none, hardEdge, antiAlias, antiAliasWithSaveLayer +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Color.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Color.java new file mode 100644 index 00000000000..855f1d397ca --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Color.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An immutable 32-bit ARGB color, mirroring Flutter's {@code Color}. + * {@code new Color(0xFF2196F3)} is fully opaque material blue. + */ +public class Color { + + private final int value; + + public Color(long argb) { + // Dart `int` maps to Java `long` in the transpiler, and opaque ARGB + // literals (0xFF......) exceed the signed-int range; truncate to the + // 32-bit ARGB word. `new Color(intLiteral)` still widens in. + this.value = (int) argb; + } + + /** + * {@code Color.fromRGBO}: red/green/blue channels (0..255) with a + * floating-point opacity (0.0..1.0) that becomes the alpha channel. + */ + public static Color fromRGBO(long r, long g, long b, double opacity) { + long a = Math.round(opacity * 255.0) & 0xFF; + return new Color((a << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)); + } + + /** + * The full ARGB value, as Dart sees it — {@code Color.value}. + * + *

Unsigned, and a {@code long}, because Dart's {@code int} is 64-bit and + * the gallery prints this: {@code color.value.toRadixString(16)}. Returning + * the signed 32-bit word made every opaque colour negative, so the colors + * demo listed "#000-1412" beside each swatch instead of "#FFFFEBEE".

+ */ + public long value() { + return value & 0xFFFFFFFFL; + } + + /** The same word as a signed 32-bit int, for Codename One's style API. */ + public int argb() { + return value; + } + + public int alpha() { + return (value >> 24) & 0xFF; + } + + public int red() { + return (value >> 16) & 0xFF; + } + + public int green() { + return (value >> 8) & 0xFF; + } + + public int blue() { + return value & 0xFF; + } + + /** + * The 24-bit RGB portion — the form CN1 style colors use. + */ + public int rgb() { + return value & 0xFFFFFF; + } + + /** + * A copy of this color with the alpha channel replaced so it is {@code + * opacity} (0..1) of fully opaque; RGB is unchanged. + */ + public Color withOpacity(double opacity) { + int a = (int) Math.round(Math.max(0, Math.min(1, opacity)) * 255.0); + return withAlpha(a); + } + + /** + * A copy of this color with the alpha channel set to {@code a} (0..255). + * The parameter is a {@code long} because the transpiler maps Dart + * {@code int} to Java {@code long}. + */ + public Color withAlpha(long a) { + int alpha = (int) (a & 0xFF); + return new Color(((long) alpha << 24) | (value & 0xFFFFFFL)); + } + + /** + * A copy with the supplied (non-null) 8-bit channels overridden; unset + * channels keep this color's value. Mirrors the older component form of + * Flutter's {@code Color.copyWith}. The boxed parameters are {@code Long} + * because the transpiler maps Dart {@code int} to Java {@code long}. + */ + public Color copyWith(Long alpha, Long red, Long green, Long blue) { + int a = alpha != null ? (int) (alpha & 0xFF) : alpha(); + int r = red != null ? (int) (red & 0xFF) : red(); + int g = green != null ? (int) (green & 0xFF) : green(); + int b = blue != null ? (int) (blue & 0xFF) : blue(); + return new Color(((long) a << 24) | (r << 16) | (g << 8) | b); + } + + /** + * Composites {@code foreground} over {@code background} using + * source-over alpha blending (Flutter's {@code Color.alphaBlend}); the + * result is fully opaque when {@code background} is opaque. + */ + public static Color alphaBlend(Color foreground, Color background) { + int fa = foreground.alpha(); + if (fa == 0xFF) { + return foreground; + } + if (fa == 0) { + return background; + } + double af = fa / 255.0; + double ab = background.alpha() / 255.0; + double ao = af + ab * (1 - af); + if (ao == 0) { + return new Color(0); + } + int r = blendChannel(foreground.red(), af, background.red(), ab, ao); + int g = blendChannel(foreground.green(), af, background.green(), ab, ao); + int b = blendChannel(foreground.blue(), af, background.blue(), ab, ao); + int a = (int) Math.round(ao * 255.0); + return new Color(((long) a << 24) | (r << 16) | (g << 8) | b); + } + + private static int blendChannel(int cf, double af, int cb, double ab, double ao) { + double v = (cf * af + cb * ab * (1 - af)) / ao; + return Math.max(0, Math.min(255, (int) Math.round(v))); + } + + @Override + public boolean equals(Object o) { + return o instanceof Color && ((Color) o).value == value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "Color(0x" + String.format("%08X", value) + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Colors.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Colors.java new file mode 100644 index 00000000000..34579eda463 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Colors.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.generated.flutter.MaterialAccentColor; +import com.codename1.generated.flutter.MaterialColor; + +/** + * The material color swatch primaries (500 values), accent variants, and the + * black/white opacity constants, mirroring Flutter's {@code Colors}. + * + *

The named primaries are typed {@link MaterialColor} and the accents + * {@link MaterialAccentColor} (both extend {@link Color}) so the colors demo can + * index their shades — matching Flutter, where {@code Colors.red} is a swatch, + * not a plain color.

+ */ +public final class Colors { + + private Colors() { + } + + public static final Color transparent = new Color(0x00000000); + + public static final MaterialColor red = new MaterialColor(0xFFF44336L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFFEBEEL, 0xFFFFCDD2L, 0xFFEF9A9AL, 0xFFE57373L, 0xFFEF5350L, 0xFFF44336L, 0xFFE53935L, 0xFFD32F2FL, 0xFFC62828L, 0xFFB71C1CL}); + public static final MaterialAccentColor redAccent = new MaterialAccentColor(0xFFFF5252L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFF8A80L, 0xFFFF5252L, 0xFFFF1744L, 0xFFD50000L}); + public static final MaterialColor pink = new MaterialColor(0xFFE91E63L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFCE4ECL, 0xFFF8BBD0L, 0xFFF48FB1L, 0xFFF06292L, 0xFFEC407AL, 0xFFE91E63L, 0xFFD81B60L, 0xFFC2185BL, 0xFFAD1457L, 0xFF880E4FL}); + public static final MaterialAccentColor pinkAccent = new MaterialAccentColor(0xFFFF4081L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFF80ABL, 0xFFFF4081L, 0xFFF50057L, 0xFFC51162L}); + public static final MaterialColor purple = new MaterialColor(0xFF9C27B0L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFF3E5F5L, 0xFFE1BEE7L, 0xFFCE93D8L, 0xFFBA68C8L, 0xFFAB47BCL, 0xFF9C27B0L, 0xFF8E24AAL, 0xFF7B1FA2L, 0xFF6A1B9AL, 0xFF4A148CL}); + public static final MaterialAccentColor purpleAccent = new MaterialAccentColor(0xFFE040FBL, + new long[] {100, 200, 400, 700}, + new long[] {0xFFEA80FCL, 0xFFE040FBL, 0xFFD500F9L, 0xFFAA00FFL}); + public static final MaterialColor deepPurple = new MaterialColor(0xFF673AB7L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFEDE7F6L, 0xFFD1C4E9L, 0xFFB39DDBL, 0xFF9575CDL, 0xFF7E57C2L, 0xFF673AB7L, 0xFF5E35B1L, 0xFF512DA8L, 0xFF4527A0L, 0xFF311B92L}); + public static final MaterialAccentColor deepPurpleAccent = new MaterialAccentColor(0xFF7C4DFFL, + new long[] {100, 200, 400, 700}, + new long[] {0xFFB388FFL, 0xFF7C4DFFL, 0xFF651FFFL, 0xFF6200EAL}); + public static final MaterialColor indigo = new MaterialColor(0xFF3F51B5L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE8EAF6L, 0xFFC5CAE9L, 0xFF9FA8DAL, 0xFF7986CBL, 0xFF5C6BC0L, 0xFF3F51B5L, 0xFF3949ABL, 0xFF303F9FL, 0xFF283593L, 0xFF1A237EL}); + public static final MaterialAccentColor indigoAccent = new MaterialAccentColor(0xFF536DFEL, + new long[] {100, 200, 400, 700}, + new long[] {0xFF8C9EFFL, 0xFF536DFEL, 0xFF3D5AFEL, 0xFF304FFEL}); + public static final MaterialColor blue = new MaterialColor(0xFF2196F3L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE3F2FDL, 0xFFBBDEFBL, 0xFF90CAF9L, 0xFF64B5F6L, 0xFF42A5F5L, 0xFF2196F3L, 0xFF1E88E5L, 0xFF1976D2L, 0xFF1565C0L, 0xFF0D47A1L}); + public static final MaterialAccentColor blueAccent = new MaterialAccentColor(0xFF448AFFL, + new long[] {100, 200, 400, 700}, + new long[] {0xFF82B1FFL, 0xFF448AFFL, 0xFF2979FFL, 0xFF2962FFL}); + public static final MaterialColor lightBlue = new MaterialColor(0xFF03A9F4L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE1F5FEL, 0xFFB3E5FCL, 0xFF81D4FAL, 0xFF4FC3F7L, 0xFF29B6F6L, 0xFF03A9F4L, 0xFF039BE5L, 0xFF0288D1L, 0xFF0277BDL, 0xFF01579BL}); + public static final MaterialAccentColor lightBlueAccent = new MaterialAccentColor(0xFF40C4FFL, + new long[] {100, 200, 400, 700}, + new long[] {0xFF80D8FFL, 0xFF40C4FFL, 0xFF00B0FFL, 0xFF0091EAL}); + public static final MaterialColor cyan = new MaterialColor(0xFF00BCD4L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE0F7FAL, 0xFFB2EBF2L, 0xFF80DEEAL, 0xFF4DD0E1L, 0xFF26C6DAL, 0xFF00BCD4L, 0xFF00ACC1L, 0xFF0097A7L, 0xFF00838FL, 0xFF006064L}); + public static final MaterialAccentColor cyanAccent = new MaterialAccentColor(0xFF18FFFFL, + new long[] {100, 200, 400, 700}, + new long[] {0xFF84FFFFL, 0xFF18FFFFL, 0xFF00E5FFL, 0xFF00B8D4L}); + public static final MaterialColor teal = new MaterialColor(0xFF009688L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE0F2F1L, 0xFFB2DFDBL, 0xFF80CBC4L, 0xFF4DB6ACL, 0xFF26A69AL, 0xFF009688L, 0xFF00897BL, 0xFF00796BL, 0xFF00695CL, 0xFF004D40L}); + public static final MaterialAccentColor tealAccent = new MaterialAccentColor(0xFF64FFDAL, + new long[] {100, 200, 400, 700}, + new long[] {0xFFA7FFEBL, 0xFF64FFDAL, 0xFF1DE9B6L, 0xFF00BFA5L}); + public static final MaterialColor green = new MaterialColor(0xFF4CAF50L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFE8F5E9L, 0xFFC8E6C9L, 0xFFA5D6A7L, 0xFF81C784L, 0xFF66BB6AL, 0xFF4CAF50L, 0xFF43A047L, 0xFF388E3CL, 0xFF2E7D32L, 0xFF1B5E20L}); + public static final MaterialAccentColor greenAccent = new MaterialAccentColor(0xFF69F0AEL, + new long[] {100, 200, 400, 700}, + new long[] {0xFFB9F6CAL, 0xFF69F0AEL, 0xFF00E676L, 0xFF00C853L}); + public static final MaterialColor lightGreen = new MaterialColor(0xFF8BC34AL, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFF1F8E9L, 0xFFDCEDC8L, 0xFFC5E1A5L, 0xFFAED581L, 0xFF9CCC65L, 0xFF8BC34AL, 0xFF7CB342L, 0xFF689F38L, 0xFF558B2FL, 0xFF33691EL}); + public static final MaterialAccentColor lightGreenAccent = new MaterialAccentColor(0xFFB2FF59L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFCCFF90L, 0xFFB2FF59L, 0xFF76FF03L, 0xFF64DD17L}); + public static final MaterialColor lime = new MaterialColor(0xFFCDDC39L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFF9FBE7L, 0xFFF0F4C3L, 0xFFE6EE9CL, 0xFFDCE775L, 0xFFD4E157L, 0xFFCDDC39L, 0xFFC0CA33L, 0xFFAFB42BL, 0xFF9E9D24L, 0xFF827717L}); + public static final MaterialAccentColor limeAccent = new MaterialAccentColor(0xFFEEFF41L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFF4FF81L, 0xFFEEFF41L, 0xFFC6FF00L, 0xFFAEEA00L}); + public static final MaterialColor yellow = new MaterialColor(0xFFFFEB3BL, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFFFDE7L, 0xFFFFF9C4L, 0xFFFFF59DL, 0xFFFFF176L, 0xFFFFEE58L, 0xFFFFEB3BL, 0xFFFDD835L, 0xFFFBC02DL, 0xFFF9A825L, 0xFFF57F17L}); + public static final MaterialAccentColor yellowAccent = new MaterialAccentColor(0xFFFFFF00L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFFFF8DL, 0xFFFFFF00L, 0xFFFFEA00L, 0xFFFFD600L}); + public static final MaterialColor amber = new MaterialColor(0xFFFFC107L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFFF8E1L, 0xFFFFECB3L, 0xFFFFE082L, 0xFFFFD54FL, 0xFFFFCA28L, 0xFFFFC107L, 0xFFFFB300L, 0xFFFFA000L, 0xFFFF8F00L, 0xFFFF6F00L}); + public static final MaterialAccentColor amberAccent = new MaterialAccentColor(0xFFFFD740L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFFE57FL, 0xFFFFD740L, 0xFFFFC400L, 0xFFFFAB00L}); + public static final MaterialColor orange = new MaterialColor(0xFFFF9800L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFFF3E0L, 0xFFFFE0B2L, 0xFFFFCC80L, 0xFFFFB74DL, 0xFFFFA726L, 0xFFFF9800L, 0xFFFB8C00L, 0xFFF57C00L, 0xFFEF6C00L, 0xFFE65100L}); + public static final MaterialAccentColor orangeAccent = new MaterialAccentColor(0xFFFFAB40L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFFD180L, 0xFFFFAB40L, 0xFFFF9100L, 0xFFFF6D00L}); + public static final MaterialColor deepOrange = new MaterialColor(0xFFFF5722L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFFBE9E7L, 0xFFFFCCBCL, 0xFFFFAB91L, 0xFFFF8A65L, 0xFFFF7043L, 0xFFFF5722L, 0xFFF4511EL, 0xFFE64A19L, 0xFFD84315L, 0xFFBF360CL}); + public static final MaterialAccentColor deepOrangeAccent = new MaterialAccentColor(0xFFFF6E40L, + new long[] {100, 200, 400, 700}, + new long[] {0xFFFF9E80L, 0xFFFF6E40L, 0xFFFF3D00L, 0xFFDD2C00L}); + public static final MaterialColor brown = new MaterialColor(0xFF795548L, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFEFEBE9L, 0xFFD7CCC8L, 0xFFBCAAA4L, 0xFFA1887FL, 0xFF8D6E63L, 0xFF795548L, 0xFF6D4C41L, 0xFF5D4037L, 0xFF4E342EL, 0xFF3E2723L}); + public static final MaterialColor grey = new MaterialColor(0xFF9E9E9EL, + new long[] {50, 100, 200, 300, 350, 400, 500, 600, 700, 800, 850, 900}, + new long[] {0xFFFAFAFAL, 0xFFF5F5F5L, 0xFFEEEEEEL, 0xFFE0E0E0L, 0xFFD6D6D6L, 0xFFBDBDBDL, 0xFF9E9E9EL, 0xFF757575L, 0xFF616161L, 0xFF424242L, 0xFF303030L, 0xFF212121L}); + public static final MaterialColor blueGrey = new MaterialColor(0xFF607D8BL, + new long[] {50, 100, 200, 300, 400, 500, 600, 700, 800, 900}, + new long[] {0xFFECEFF1L, 0xFFCFD8DCL, 0xFFB0BEC5L, 0xFF90A4AEL, 0xFF78909CL, 0xFF607D8BL, 0xFF546E7AL, 0xFF455A64L, 0xFF37474FL, 0xFF263238L}); + + public static final Color white = new Color(0xFFFFFFFF); + public static final Color white70 = new Color(0xB3FFFFFF); + public static final Color white60 = new Color(0x99FFFFFF); + public static final Color white54 = new Color(0x8AFFFFFF); + public static final Color white38 = new Color(0x62FFFFFF); + public static final Color white30 = new Color(0x4DFFFFFF); + public static final Color white24 = new Color(0x3DFFFFFF); + public static final Color white12 = new Color(0x1FFFFFFF); + public static final Color white10 = new Color(0x1AFFFFFF); + + public static final Color black = new Color(0xFF000000); + public static final Color black87 = new Color(0xDD000000); + public static final Color black54 = new Color(0x8A000000); + public static final Color black45 = new Color(0x73000000); + public static final Color black38 = new Color(0x61000000); + public static final Color black26 = new Color(0x42000000); + public static final Color black12 = new Color(0x1F000000); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ComposedElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ComposedElement.java new file mode 100644 index 00000000000..ec574aded8c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ComposedElement.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.Funcs; + +/** + * Base class for elements that compose exactly one child by calling a build + * method (Flutter's ComponentElement): {@link StatelessElement} and + * {@link StatefulElement}. + */ +public abstract class ComposedElement extends Element { + + private Element child; + + protected ComposedElement(Widget widget) { + super(widget); + } + + public Element child() { + return child; + } + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + firstBuild(); + } + + protected void firstBuild() { + dirty = true; + performRebuild(); + } + + @Override + public void update(Widget newWidget) { + super.update(newWidget); + dirty = true; + performRebuild(); + } + + private static long diagMs; + private static long buildMs; + private static long updateMs; + private static int builds; + + /// Build cost per widget class, keyed by the Class itself so the hot path + /// never formats a name. "115 builds cost 462ms" is not actionable; knowing + /// WHICH build method owns them is. + private static final java.util.Map, long[]> BY_CLASS = + new java.util.HashMap, long[]>(); + + /** Where a composed element's rebuild time goes, worst build methods first. */ + public static String rebuildCost() { + java.util.List, long[]>> rows = + new java.util.ArrayList, long[]>>(BY_CLASS.entrySet()); + java.util.Collections.sort(rows, new java.util.Comparator, long[]>>() { + @Override + public int compare(java.util.Map.Entry, long[]> a, + java.util.Map.Entry, long[]> b) { + return Long.compare(b.getValue()[0], a.getValue()[0]); + } + }); + StringBuilder sb = new StringBuilder(); + sb.append(builds).append(" build(s) diag=").append(diagMs).append("ms build=") + .append(buildMs).append("ms; hottest:"); + for (int i = 0; i < rows.size() && i < 6; i++) { + java.util.Map.Entry, long[]> e = rows.get(i); + String n = e.getKey().getName(); + int dot = n.lastIndexOf('.'); + sb.append(' ').append(dot < 0 ? n : n.substring(dot + 1)) + .append('=').append(e.getValue()[0]).append("ms/") + .append(e.getValue()[1]).append('x'); + } + return sb.toString(); + } + + @Override + protected void performRebuild() { + dirty = false; + // Name the widget being built, so a failure inside it (a Dart `!` on + // something that turned out null, most often) reports where it + // happened. The transpiled build methods are inlined into the + // framework's frame on some backends, so the stack trace alone shows + // nothing but this class's own recursion. The WIDGET is handed over + // rather than a description of it: describing costs a String per + // build, and nothing reads the description unless a build throws. + Object previous = dart.runtime.DartRuntime.diagnosticContextValue(); + dart.runtime.DartRuntime.diagnosticContext(widget); + if (!Trace.on()) { + try { + child = updateChild(child, build(), 0); + } finally { + dart.runtime.DartRuntime.diagnosticContext(previous); + } + return; + } + builds++; + long d1 = System.currentTimeMillis(); + Widget built; + try { + built = build(); + } finally { + dart.runtime.DartRuntime.diagnosticContext(previous); + } + long d2 = System.currentTimeMillis(); + buildMs += d2 - d1; + if (widget != null) { + long[] row = BY_CLASS.get(widget.getClass()); + if (row == null) { + row = new long[2]; + BY_CLASS.put(widget.getClass(), row); + } + row[0] += d2 - d1; + row[1]++; + } + child = updateChild(child, built, 0); + updateMs += System.currentTimeMillis() - d2; + } + + /** + * Calls the widget's (or state's) build method. + */ + protected abstract Widget build(); + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (child != null) { + visitor.call(child); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ContinuousRectangleBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ContinuousRectangleBorder.java new file mode 100644 index 00000000000..dc41cd35d24 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ContinuousRectangleBorder.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A rectangular border with continuous, smoothly-tapered corners — Flutter's {@code ContinuousRectangleBorder}. */ +public class ContinuousRectangleBorder extends OutlinedBorder { + + private Object borderRadius; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public Object getBorderRadius() { + return borderRadius; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/CrossAxisAlignment.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/CrossAxisAlignment.java new file mode 100644 index 00000000000..4d25ce02088 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/CrossAxisAlignment.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How Flex (Row/Column) positions children along its cross axis. + */ +public enum CrossAxisAlignment { + start, end, center, stretch +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Decoration.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Decoration.java new file mode 100644 index 00000000000..c72323459be --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Decoration.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Base class for things that can decorate a box (paint behind/in front of its + * child) — Flutter's {@code Decoration}. The concrete runtime type handled by + * the Codename One layout is {@link BoxDecoration}. + */ +public abstract class Decoration { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/DecorationImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/DecorationImage.java new file mode 100644 index 00000000000..b7f6e072417 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/DecorationImage.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An image painted into a {@link BoxDecoration} — Flutter's + * {@code DecorationImage}. Retained as configuration; painting the decoration + * image is deferred for this milestone. + */ +public class DecorationImage { + + private ImageProvider image; + private BoxFit fit; + private Object alignment; + private Object colorFilter; + private Object repeat; + private double scale = 1.0; + private double opacity = 1.0; + private boolean matchTextDirection; + + public void image(ImageProvider v) { + this.image = v; + } + + public void fit(BoxFit v) { + this.fit = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public void colorFilter(Object v) { + this.colorFilter = v; + } + + public void repeat(Object v) { + this.repeat = v; + } + + public void scale(double v) { + this.scale = v; + } + + public void opacity(double v) { + this.opacity = v; + } + + public void matchTextDirection(boolean v) { + this.matchTextDirection = v; + } + + public ImageProvider getImage() { + return image; + } + + public BoxFit getFit() { + return fit; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsets.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsets.java new file mode 100644 index 00000000000..b17b3c6d5c4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsets.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Immutable offsets for each of the four box edges, in logical pixels. + */ +public class EdgeInsets extends EdgeInsetsGeometry { + + public static final EdgeInsets zero = new EdgeInsets(0, 0, 0, 0); + + private final double left; + private final double top; + private final double right; + private final double bottom; + + protected EdgeInsets(double left, double top, double right, double bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + public static EdgeInsets all(double value) { + return new EdgeInsets(value, value, value, value); + } + + public static EdgeInsets only(double left, double top, double right, double bottom) { + return new EdgeInsets(left, top, right, bottom); + } + + public static EdgeInsets symmetric(double horizontal, double vertical) { + return new EdgeInsets(horizontal, vertical, horizontal, vertical); + } + + public static EdgeInsets fromLTRB(double left, double top, double right, double bottom) { + return new EdgeInsets(left, top, right, bottom); + } + + public double left() { + return left; + } + + public double top() { + return top; + } + + public double right() { + return right; + } + + public double bottom() { + return bottom; + } + + /** + * {@code EdgeInsetsGeometry.add}: the edge-wise sum of this and {@code other}. + * When {@code other} is a direction-relative inset it cannot be resolved + * without a text direction, so only the absolute component contributes. + */ + /** + * Interpolates between two insets -- Flutter's {@code EdgeInsets.lerp}. + * + *

Two directional insets interpolate to a directional one, so an animation + * between them does not silently become left-to-right at the first frame. Anything + * else interpolates as LTRB, which is what a mixed pair resolves to here anyway -- + * see {@link EdgeInsetsDirectional}, which stores its LTR mapping.

+ */ + public static EdgeInsets lerp(EdgeInsets a, EdgeInsets b, double t) { + if (a == null && b == null) { + return null; + } + if (a == null) { + a = zero; + } + if (b == null) { + b = zero; + } + if (a instanceof EdgeInsetsDirectional && b instanceof EdgeInsetsDirectional) { + EdgeInsetsDirectional da = (EdgeInsetsDirectional) a; + EdgeInsetsDirectional db = (EdgeInsetsDirectional) b; + return EdgeInsetsDirectional.fromSTEB( + da.start() + (db.start() - da.start()) * t, + da.top() + (db.top() - da.top()) * t, + da.end() + (db.end() - da.end()) * t, + da.bottom() + (db.bottom() - da.bottom()) * t); + } + return fromLTRB(a.left() + (b.left() - a.left()) * t, + a.top() + (b.top() - a.top()) * t, + a.right() + (b.right() - a.right()) * t, + a.bottom() + (b.bottom() - a.bottom()) * t); + } + + public EdgeInsets add(EdgeInsetsGeometry other) { + if (other instanceof EdgeInsets) { + EdgeInsets o = (EdgeInsets) other; + return new EdgeInsets(left + o.left, top + o.top, right + o.right, bottom + o.bottom); + } + return this; + } + + public double horizontal() { + return left + right; + } + + public double vertical() { + return top + bottom; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof EdgeInsets)) { + return false; + } + EdgeInsets e = (EdgeInsets) o; + return e.left == left && e.top == top && e.right == right && e.bottom == bottom; + } + + @Override + public int hashCode() { + long bits = ValueHash.bits(left); + bits = bits * 31 + ValueHash.bits(top); + bits = bits * 31 + ValueHash.bits(right); + bits = bits * 31 + ValueHash.bits(bottom); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "EdgeInsets(" + left + ", " + top + ", " + right + ", " + bottom + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsDirectional.java new file mode 100644 index 00000000000..e6cc80613a6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsDirectional.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Direction-relative edge insets ({@code start}/{@code end} instead of + * {@code left}/{@code right}) — Flutter's {@code EdgeInsetsDirectional}. + * + *

It extends {@link EdgeInsets} so it remains assignable to the + * {@code EdgeInsets}-typed padding parameters the widget stubs declare. Under + * the default left-to-right text direction {@code start} maps to {@code left} + * and {@code end} to {@code right}; full bidi resolution is deferred to the + * render layer.

+ */ +public final class EdgeInsetsDirectional extends EdgeInsets { + + public static final EdgeInsetsDirectional zero = new EdgeInsetsDirectional(0, 0, 0, 0); + + private final double start; + private final double end; + + private EdgeInsetsDirectional(double start, double top, double end, double bottom) { + // LTR mapping: start -> left, end -> right. + super(start, top, end, bottom); + this.start = start; + this.end = end; + } + + public static EdgeInsetsDirectional all(double value) { + return new EdgeInsetsDirectional(value, value, value, value); + } + + public static EdgeInsetsDirectional only(double start, double top, double end, double bottom) { + return new EdgeInsetsDirectional(start, top, end, bottom); + } + + public static EdgeInsetsDirectional symmetric(double horizontal, double vertical) { + return new EdgeInsetsDirectional(horizontal, vertical, horizontal, vertical); + } + + public static EdgeInsetsDirectional fromSTEB(double start, double top, double end, double bottom) { + return new EdgeInsetsDirectional(start, top, end, bottom); + } + + public double start() { + return start; + } + + public double end() { + return end; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsGeometry.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsGeometry.java new file mode 100644 index 00000000000..e7a32b4d3b6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/EdgeInsetsGeometry.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The common supertype of {@link EdgeInsets} and {@link EdgeInsetsDirectional} + * — Flutter's {@code EdgeInsetsGeometry}. Lets padding/margin values flow + * through APIs that accept either the absolute or the direction-relative form. + */ +public abstract class EdgeInsetsGeometry { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Element.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Element.java new file mode 100644 index 00000000000..56efa79a597 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Element.java @@ -0,0 +1,1162 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.RenderHost; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * An instantiation of a {@link Widget} at a particular location in the tree. + * Elements are the retained structure: they survive across rebuilds when the + * incoming widget {@link Widget#canUpdate(Widget, Widget) can update} the one + * they currently hold. This class implements Flutter's reconciliation + * decision table ({@link #updateChild}) and the keyed linear multi-child + * reconciler ({@link #updateChildren}). + */ +public abstract class Element implements BuildContext { + + Widget widget; + Element parent; + int slot; + int depth; + boolean dirty; + boolean mounted; + BuildOwner owner; + RenderHost host; + /** Ancestor-lookup continuation for a route root — see {@link #ancestorOf}. */ + private Element contextFallback; + + protected Element(Widget widget) { + this.widget = widget; + } + + // ------------------------------------------------------------------ + // Accessors + // ------------------------------------------------------------------ + + public Widget widget() { + return widget; + } + + public Element parent() { + return parent; + } + + public boolean isMounted() { + return mounted; + } + + public boolean isDirty() { + return dirty; + } + + public int depth() { + return depth; + } + + public BuildOwner owner() { + return owner; + } + + public RenderHost host() { + return host; + } + + // ------------------------------------------------------------------ + // BuildContext + // ------------------------------------------------------------------ + + /** + * The next element up for a {@code BuildContext} ancestor lookup: the + * structural parent, or — at the root of a tree pushed as a route — the + * context that pushed it. + * + *

In Flutter a route's page builds below the Navigator, so it inherits + * the whole app above it. Here a pushed route mounts as a fresh + * element-tree root (its own CN1 Form), so its structural parent chain + * ends immediately and every {@code Foo.of(context)} in the page would + * resolve to null. The fallback restores the inheritance without joining + * the two trees structurally — the render/host logic (which decides, for + * instance, whether a Scaffold owns the Form's Toolbar) still sees a + * genuine root.

+ */ + private static Element ancestorOf(Element e) { + return e.parent != null ? e.parent : e.contextFallback; + } + + /** + * The next element up the inheritance chain — {@link #parent()}, or the + * pushing context at a route root. Use this rather than {@code parent()} + * when walking for an ancestor widget, or the walk stops at every route. + */ + public Element ancestor() { + return ancestorOf(this); + } + + /** + * Walks up the tree for the nearest ancestor {@code State} of {@code type} + * ({@code BuildContext.findAncestorStateOfType}). + * + *

This returned null unconditionally, and the cost was not a missing + * lookup but a THROWN one: the gallery's feature discovery asks for its + * controller this way and raises a FlutterError when the answer is null, so + * the coach mark the reference shows over the demo options button was never + * inserted -- the Overlay it would have gone into was mounted and working + * the whole time.

+ * + *

Walks {@link #ancestor()}, not {@link #parent()}. In Flutter the whole + * app is one element tree and a State above the Navigator is an ancestor of + * every route; here each route is rooted in its own form, so the structural + * parent chain stops at the route and never reaches it. The gallery's + * discovery controller wraps the app, which is exactly that case.

+ */ + @Override + public T findAncestorStateOfType(Class type) { + if (type == null) { + return null; + } + Element e = ancestor(); + while (e != null) { + if (e instanceof StatefulElement) { + State st = ((StatefulElement) e).state(); + if (isInstanceOf(type, st)) { + return type.cast(st); + } + } + e = ancestorOf(e); + } + return null; + } + + /** + * Whether {@code o} is an instance of {@code type} — the single predicate + * the whole inherited-widget mechanism rests on, kept in one place so any + * future portability question about it has exactly one answer to change. + */ + public static boolean isInstanceOf(Class type, Object o) { + return type != null && o != null && type.isInstance(o); + } + + /** + * Links this root element's ancestor lookups to the context that pushed + * it. Set by the Navigator when mounting a route. + */ + public void contextFallback(Element e) { + this.contextFallback = e; + if (parent == null && inheritedElements == null) { + // A route root inherits what the context that pushed it could see; + // the fallback is usually set after mount, so pick the map up here. + this.inheritedElements = inheritedFrom(e); + } + } + + @Override + public W findAncestorWidgetOfExactType(Class widgetType) { + Element a = ancestorOf(this); + while (a != null) { + if (a.widget != null && a.widget.getClass() == widgetType) { + return widgetType.cast(a.widget); + } + a = ancestorOf(a); + } + return null; + } + + /** + * The inherited widgets visible from here, by type — Flutter's + * {@code _inheritedElements}. + * + *

Shared BY REFERENCE with the parent, because the overwhelming majority + * of elements inherit exactly what their parent could see; only an + * {@link com.codename1.flutter.widgets.InheritedElement} copies the map to + * add itself. So the whole tree costs one map per inherited widget, not one + * per element. + * + *

Without it, {@code dependOnInheritedWidgetOfExactType} is a walk to the + * root — and it is on the hottest path there is. Every {@code Text} asks for + * the ambient text style, every {@code Icon} for the icon theme, and every + * themed widget for the theme; a screen with a few hundred widgets in a tree + * twenty-five deep pays thousands of pointer hops per build, and a lookup + * that finds NOTHING pays the full depth every time.

+ */ + private java.util.Map, Element> inheritedElements; + + /** The map a child mounted under this element should see. */ + java.util.Map, Element> inheritedElementsForChild() { + return inheritedElements; + } + + /** + * Publishes this element under {@code type} and every inherited supertype. + * + *

Keyed by the whole chain because a lookup here matches on + * {@code instanceof}, not on the exact class: an app that subclasses an + * inherited widget must still be found by a query for the base type.

+ */ + protected void publishAsInherited() { + java.util.Map, Element> map = + new java.util.HashMap, Element>( + inheritedElements == null + ? java.util.Collections., Element>emptyMap() + : inheritedElements); + for (Class c = widget == null ? null : widget.getClass(); + c != null && Widget.class.isAssignableFrom(c); c = c.getSuperclass()) { + map.put(c, this); + } + inheritedElements = map; + } + + @Override + public W maybeDependOnInheritedWidgetOfExactType(Class type) { + return lookUpInherited(type, false); + } + + @Override + public W dependOnInheritedWidgetOfExactType(Class type) { + return lookUpInherited(type, true); + } + + /** + * The inherited lookup, with or without a diagnostic when it comes up empty. + * + *

{@code report} is false for the lookups that have a documented + * fallback -- {@code Theme.of}, {@code MediaQuery.of}, + * {@code IconTheme.of}, {@code DefaultTextStyle.of} all answer sensibly + * when nothing above them provides a value, so a miss is normal rather + * than a fault. Reporting it anyway was not merely noisy: the report walks + * two dozen ancestors building a string and writes it through + * {@code Log.p}, which on a device is file IO, and the gallery's root page + * paid that on its first build. The diagnostic is for the case it was + * written for -- a {@code Foo.of(context)!} that is about to throw.

+ */ + @SuppressWarnings("unchecked") + private W lookUpInherited(Class type, boolean report) { + Element a = inheritedElements == null ? null : inheritedElements.get(type); + if (a == null && inheritedElements == null) { + // No map (an element mounted outside the normal path): fall back to + // the walk rather than answering a wrong "nothing here". + a = ancestorOf(this); + while (a != null && !isInstanceOf(type, a.widget)) { + a = ancestorOf(a); + } + } + if (a != null) { + // REGISTER, do not merely read: the name is depend-on. Flutter records this + // element as a dependent so a later change to the widget rebuilds it, and + // without that every consumer is a one-shot read. + if (a instanceof com.codename1.flutter.widgets.InheritedElement) { + ((com.codename1.flutter.widgets.InheritedElement) a).addDependent(this); + dependsOn(a); + } + return type.cast(a.widget); + } + if (report) { + reportMissingAncestor(type); + } + return null; + } + + /** + * Names the ancestors that were searched when an inherited-widget lookup + * comes up empty. + * + *

Dart code almost always writes {@code Foo.of(context)!}, so a failed + * lookup surfaces as a null-check TypeError somewhere else entirely, with + * no indication of WHICH widget was missing or what the context could + * actually see. Reporting it at the point of failure turns that into a + * one-line diagnosis. Reported ONCE PER TYPE rather than capped at a flat + * count: a provider that is missing is missing on every build of every + * frame, so a flat cap is spent entirely on whichever lookup happens to + * fail first and the genuinely interesting second and third failures never + * print. An overall ceiling still applies as a backstop.

+ */ + private static int missingAncestorReports; + private static final java.util.Set REPORTED_MISSING = + new java.util.HashSet(); + + private void reportMissingAncestor(Class type) { + if (missingAncestorReports >= 40) { + return; + } + if (!REPORTED_MISSING.add(type == null ? "?" : type.getName())) { + return; + } + missingAncestorReports++; + try { + StringBuilder sb = new StringBuilder("Flutter runtime: no "); + sb.append(type == null ? "?" : type.getName()); + sb.append(" above this context; ancestors were:"); + Element a = ancestorOf(this); + int depth = 0; + while (a != null && depth < 24) { + sb.append(depth == 0 ? " " : " < "); + sb.append(a.widget == null ? "null" : a.widget.getClass().getName()); + a = ancestorOf(a); + depth++; + } + com.codename1.io.Log.p(sb.toString()); + } catch (Throwable t) { + // diagnostics must never become the failure + } + } + + @Override + public Object providerValueOfType(Class type) { + return providerValueOfType(type, true); + } + + @Override + public Object providerValueOfType(Class type, boolean listen) { + Element a = ancestorOf(this); + int providers = 0; + while (a != null) { + if (a.widget instanceof InheritedValueProvider) { + providers++; + InheritedValueProvider p = (InheritedValueProvider) a.widget; + // Matched by type, not by a non-null value: a matching provider may hold null. + if (p.providesType(type)) { + Object v = p.providedValueFor(type); + // Remember that WE read this, so a change to the model can come back + // and rebuild us. Without it a provider could notice its model change + // and rebuild itself and nothing else: its build returns the same + // child WIDGET INSTANCE, reconciliation sees widget == newWidget and + // returns early, and the subtree that actually reads the value never + // runs again. Every control whose job is to set a field on a model + // then looked dead -- the handler ran, the model changed, the screen + // did not. + if (listen) { + a.addProviderDependent(this); + dependsOn(a); + } + return v; + } + } + a = ancestorOf(a); + } + reportMissingProvider(type, providers); + return null; + } + + /// Elements that read a provided value from this element. + /// + /// Flutter tracks this as an InheritedWidget dependency; a provider here is an + /// ordinary widget, so the dependency has to be recorded by hand. + private java.util.List providerDependents; + + /// What this element has registered itself with as a dependent -- inherited + /// elements and providers -- so unmount can take itself back out. Flutter keeps the + /// same set. Without it only the forward edge existed: a popped route's elements + /// stayed in a long-lived Theme's or provider's list, and with them, through their + /// parent links, the whole popped tree, until that ancestor happened to notify. + private java.util.List dependencies; + + private void dependsOn(Element on) { + if (dependencies == null) { + dependencies = new java.util.ArrayList(2); + } + if (!dependencies.contains(on)) { + dependencies.add(on); + } + } + + void removeProviderDependent(Element dependent) { + if (providerDependents != null) { + providerDependents.remove(dependent); + } + } + + void addProviderDependent(Element dependent) { + if (dependent == null || dependent == this) { + return; + } + if (providerDependents == null) { + providerDependents = new java.util.ArrayList(); + } + if (!providerDependents.contains(dependent)) { + providerDependents.add(dependent); + } + } + + /** + * Marks everything that read a value from this element for rebuild. + * + *

Unmounted readers are dropped as they are found: a dependency list that only + * grows would hold a whole popped route alive and keep rebuilding it.

+ */ + /** + * Something this element depends on -- an inherited widget or a provided value -- + * changed. Rebuilds it; a StatefulElement also runs its State's + * didChangeDependencies first, as Flutter does. + */ + public void didChangeDependencies() { + markNeedsBuild(); + } + + public void rebuildProviderDependents() { + if (providerDependents == null) { + return; + } + java.util.List snapshot = + new java.util.ArrayList(providerDependents); + for (Element e : snapshot) { + if (e.mounted) { + e.didChangeDependencies(); + } else { + providerDependents.remove(e); + } + } + } + + private void reportMissingProvider(Class type, int providersSeen) { + if (missingAncestorReports >= 5) { + return; + } + missingAncestorReports++; + // The summary goes out FIRST and on its own: the ancestor walk below + // touches every ancestor's class, and if any step of that throws the + // whole report would vanish into the guard. + try { + com.codename1.io.Log.p("Flutter runtime: no provider of " + + (type == null ? "?" : type.getName()) + + " (" + providersSeen + " provider(s) searched)"); + } catch (Throwable t) { + return; + } + try { + StringBuilder sb = new StringBuilder("Flutter runtime: ancestors were:"); + Element a = ancestorOf(this); + int depth = 0; + while (a != null && depth < 30) { + sb.append(depth == 0 ? " " : " < "); + sb.append(a.widget == null ? "null" : a.widget.getClass().getName()); + a = ancestorOf(a); + depth++; + } + com.codename1.io.Log.p(sb.toString()); + } catch (Throwable t) { + // diagnostics must never become the failure + } + } + + @Override + @SuppressWarnings("unchecked") + public T watch(Class type) { + return (T) providerValueOfType(type); + } + + @Override + @SuppressWarnings("unchecked") + public T read(Class type) { + return (T) providerValueOfType(type, false); + } + + // ------------------------------------------------------------------ + // Lifecycle + // ------------------------------------------------------------------ + + /** + * Assigns the owner and render host of a root element before mounting. + * Non-root elements inherit both from their parent during {@link #mount}. + */ + public void bootstrap(BuildOwner owner, RenderHost host) { + this.owner = owner; + this.host = host; + } + + /** + * Adds this element to the tree. Subclasses extend this to create their + * retained objects (State, CN1 components) and inflate their children. + */ + /// Elements mounted so far. The comparison that matters is not "how fast is + /// each runtime" but "is each one doing the same work" — a first frame that + /// built a tenth of the tree is not a faster first frame. + private static int mountedElements; + + /** How many elements have been mounted. */ + public static int mountedCount() { + return mountedElements; + } + + public void mount(Element parent, int slot) { + mountedElements++; + this.parent = parent; + this.slot = slot; + if (parent != null) { + this.owner = parent.owner; + this.host = parent.hostForChild(slot); + this.depth = parent.depth + 1; + } + this.inheritedElements = inheritedFrom(parent != null ? parent : contextFallback); + this.mounted = true; + registerGlobalKey(); + } + + /// Points a GlobalKey on this element's widget at this element, as Flutter's + /// Element.mount registers it. + /// + /// GlobalKey.attach and detach existed and nothing called them, so + /// currentState, currentContext and currentWidget stayed null for the life of + /// every keyed widget -- `formKey.currentState!.validate()` and navigator-key + /// access failed with the target visibly on screen. Done here, the one path + /// every element mounts through, rather than per widget type. + @SuppressWarnings({"unchecked", "rawtypes"}) + protected final void registerGlobalKey() { + Key k = widget == null ? null : widget.getKey(); + if (k instanceof GlobalKey) { + ((GlobalKey) k).attach(globalKeyState(), this, widget); + } + } + + /// The State a GlobalKey exposes as currentState: null for elements that have + /// none. StatefulElement answers its State. + protected Object globalKeyState() { + return null; + } + + private static java.util.Map, Element> inheritedFrom(Element from) { + return from == null ? null : from.inheritedElementsForChild(); + } + + /** + * The render host a child mounted in {@code slot} should attach its CN1 + * components to. Overridden by elements that route a child subtree into a + * different CN1 container (e.g. a root Scaffold's appBar into the + * Toolbar's title area). + */ + protected RenderHost hostForChild(int slot) { + return host; + } + + /** + * Absorbs a new widget configuration. Callers guarantee + * {@code Widget.canUpdate(this.widget, newWidget)}. + */ + public void update(Widget newWidget) { + this.widget = newWidget; + // canUpdate guarantees the same key, so a GlobalKey stays with this + // element; its currentWidget has to follow the new configuration. + registerGlobalKey(); + } + + /** + * Removes this element (only) from the tree. Subclasses release their + * retained resources here. Use {@link #deactivateChild} to remove a whole + * subtree. + */ + public void unmount() { + this.mounted = false; + this.dirty = false; + if (dependencies != null) { + for (Element on : dependencies) { + if (on instanceof com.codename1.flutter.widgets.InheritedElement) { + ((com.codename1.flutter.widgets.InheritedElement) on).removeDependent(this); + } + on.removeProviderDependent(this); + } + dependencies = null; + } + Key k = widget == null ? null : widget.getKey(); + // Only if the key still points HERE: a GlobalKey that moved to another + // element must not be cleared by the one it left. + if (k instanceof GlobalKey && ((GlobalKey) k).currentContext() == this) { + ((GlobalKey) k).detach(); + } + } + + /** + * Visits the direct children of this element in tree order. + */ + public void visitChildren(Funcs.VoidFunc1 visitor) { + } + + /** + * Notifies this subtree that the effective theme changed (M4): render + * elements re-apply their programmatic, theme-derived styling. Called by + * MaterialAppElement after re-installing the UIManager overlay — a plain + * rebuild would miss subtrees whose widget INSTANCES were reused + * (Element.updateChild's identity shortcut skips their update()). + */ + public void themeChanged() { + visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + c.themeChanged(); + } + }); + } + + // ------------------------------------------------------------------ + // Building + // ------------------------------------------------------------------ + + /** + * Marks this element dirty and schedules it with the build owner. + */ + public void markNeedsBuild() { + FlutterUI.assertEdt(); + if (!mounted || dirty) { + return; + } + dirty = true; + if (owner != null) { + owner.scheduleBuildFor(this); + } + } + + /** + * Rebuilds this element if it is dirty. + */ + public void rebuild() { + if (!mounted || !dirty) { + return; + } + performRebuild(); + } + + /** + * Actually rebuilds: composition elements call build() and reconcile the + * result, render elements re-sync their configuration and children. + * Implementations must clear the dirty flag. + */ + protected abstract void performRebuild(); + + // ------------------------------------------------------------------ + // Reconciliation + // ------------------------------------------------------------------ + + /// Subtrees thrown away and rebuilt because reconciliation refused to + /// update them in place, by widget class. + /// + /// A REPLACEMENT is the expensive outcome: the old element tree is + /// discarded along with every Codename One component under it, and an + /// equivalent one is built from scratch. One high in the tree costs the + /// whole screen twice. Flutter's rule is that a widget of the same runtime + /// type and key updates in place, so a replacement of a widget that "looks + /// the same" is a reconciliation bug, not a cost of doing business. + private static final java.util.Map REPLACED = + new java.util.HashMap(); + + private static void noteReplacement(Widget from, Widget to) { + if (!Trace.on()) { + return; + } + String key = (from == null ? "null" : from.getClass().getSimpleName()) + + "->" + (to == null ? "null" : to.getClass().getSimpleName()); + int[] n = REPLACED.get(key); + if (n == null) { + n = new int[1]; + REPLACED.put(key, n); + } + n[0]++; + } + + /** The replacement census, worst first; see {@link #REPLACED}. */ + public static String replacementCensus(int top) { + java.util.List> all = + new java.util.ArrayList>(REPLACED.entrySet()); + java.util.Collections.sort(all, new java.util.Comparator>() { + @Override + public int compare(java.util.Map.Entry a, java.util.Map.Entry b) { + return b.getValue()[0] - a.getValue()[0]; + } + }); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < Math.min(top, all.size()); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(all.get(i).getKey()).append('=').append(all.get(i).getValue()[0]); + } + return sb.append(']').toString(); + } + + /** + * Flutter's updateChild decision table: + *
+     *                     newWidget == null      newWidget != null
+     * child == null       returns null           returns new Element
+     * child != null       old child removed      old child updated in place
+     *                                            when canUpdate, else removed
+     *                                            and a new Element inflated
+     * 
+ */ + protected Element updateChild(Element child, Widget newWidget, int newSlot) { + if (newWidget == null) { + if (child != null) { + deactivateChild(child); + markEnclosingLayoutDirty(); + } + return null; + } + if (child != null) { + if (child.widget == newWidget) { + child.slot = newSlot; + return child; + } + if (Widget.canUpdate(child.widget, newWidget)) { + child.slot = newSlot; + child.update(newWidget); + return child; + } + noteReplacement(child.widget, newWidget); + // Mid-life replacement: anchor the host's attach cursor at the + // flat-container index the replaced subtree's components occupy, + // so the replacement's components land there (element-tree order) + // instead of at the end of the container (z-order drift). + RenderHost childHost = child.host; + int anchor = childHost == null ? -1 : childHost.firstAttachIndex(child); + deactivateChild(child); + if (anchor >= 0) { + int prev = childHost.beginInsertion(anchor); + try { + return inflatedChild(newWidget, newSlot); + } finally { + childHost.endInsertion(prev); + } + } + } + // A FRESH child, not a replacement. Anchor it too: appending puts its components + // at the END of the flat host container, which is the top of the paint order. + // + // A subtree does not always arrive in tree order. A LayoutBuilder sits out a + // speculative measurement pass and builds on a later one, by which time its + // siblings are attached -- so a Scaffold's BODY, which is a LayoutBuilder in the + // mail study, was created after the bottom bar and the floating action button and + // painted over both of them. The bar and the button were not missing or + // mispositioned; they were underneath the body, showing only where it did not + // cover them. + // + // Anchor before the first already-attached sibling that belongs AFTER this slot, + // so the new subtree lands where the element tree says it goes. + int anchor = anchorForSlot(newSlot); + if (anchor >= 0) { + RenderHost childHost = hostForNewChild(newSlot); + if (childHost != null) { + int prev = childHost.beginInsertion(anchor); + try { + return inflatedChild(newWidget, newSlot); + } finally { + childHost.endInsertion(prev); + } + } + } + return inflatedChild(newWidget, newSlot); + } + + /** + * Inflates a new child and invalidates the layout that was measured without it. + * + *

A cached layout is only valid for the children it was measured over, so an + * element that gains or loses one has a stale size and stale offsets for everything + * it does contain. Nothing used to say so: {@code updateChild} rebuilt the element + * tree and left every ancestor's {@code needsLayout} clear. The rebuild queue marks + * the branch it rebuilds, but a child inflated during a LAYOUT pass -- a LayoutBuilder + * building on a pass it had sat out -- never goes through that queue at all.

+ * + *

Only a STRUCTURAL change invalidates. A child updated in place keeps its element, + * and whether that dirties layout is its own business: treating every rebuild as a + * layout change is what made the carousel stutter, one relayout of the page per frame + * of the drag.

+ */ + private Element inflatedChild(Widget newWidget, int newSlot) { + Element inflated = inflateWidget(newWidget, newSlot); + markEnclosingLayoutDirty(); + return inflated; + } + + /// Marks the nearest enclosing render element -- and through it every render + /// ancestor -- as needing layout. + private void markEnclosingLayoutDirty() { + for (Element a = this; a != null; a = a.parent) { + if (a instanceof RenderElement) { + ((RenderElement) a).markNeedsLayout(); + return; + } + } + } + + /// Where a child of {@code slot} should attach, or -1 when appending is already right. + /// + /// The first attach index of any already-attached child whose slot sorts AFTER this + /// one. Nothing after it means the end of the container is the correct place, which + /// is what appending already does. + private int anchorForSlot(int slot) { + RenderHost childHost = hostForNewChild(slot); + if (childHost == null || precedesNewChild(childHost.lastAttached(), slot)) { + return -1; + } + // The next element in TREE order after where this child goes: first among our own + // later children, then -- since the late builder is usually a leaf with no later + // sibling of its own -- the first later sibling of an ancestor. A Scaffold's body + // is a LayoutBuilder, so the sibling that has to be painted over is the bottom + // bar three levels up, not anything the builder can see. + int at = laterSiblingAttachIndex(this, slot, childHost); + if (at >= 0) { + return at; + } + Element node = this; + while (node != null && node.parent() != null) { + at = laterSiblingAttachIndex(node.parent(), node.slot, childHost); + if (at >= 0) { + return at; + } + node = node.parent(); + } + return -1; + } + + /// Whether {@code last} -- the element at the END of the host's attach order -- comes + /// before a new child of this element at {@code slot} in tree order, so that + /// appending is already where the search below would put the child. + /// + /// The host keeps its components in tree order; that is what this anchoring, the + /// replacement anchor in updateChild and reorderToTreeOrder exist to maintain. So + /// when its last component precedes the new child, none follows it, and the search + /// -- which walks every ancestor and visits each one's children, allocating a visitor + /// per level, for EVERY element inflated -- can only answer -1. On a first build that + /// is nearly every call: the gallery inflated 1066 elements at start-up and this + /// search was a fifth of the time spent mounting them. + /// + /// "Before" is the same relation the search uses: below the two paths' common + /// ancestor, the child on {@code last}'s side has a slot no greater than the one on + /// ours. An ancestor of the new child, or this element itself, precedes it. When the + /// two are not in one tree the answer is false, and the search decides as before. + private boolean precedesNewChild(Element last, int slot) { + if (last == null) { + return true; + } + Element a = last; + Element b = this; + Element aChild = null; + Element bChild = null; + while (a != null && b != null && a.depth > b.depth) { + aChild = a; + a = a.parent; + } + while (a != null && b != null && b.depth > a.depth) { + bChild = b; + b = b.parent; + } + while (a != null && b != null && a != b) { + aChild = a; + a = a.parent; + bChild = b; + b = b.parent; + } + if (a == null || b == null) { + return false; + } + if (aChild == null) { + // last is this element or one of its ancestors. + return true; + } + return aChild.slot <= (bChild == null ? slot : bChild.slot); + } + + /// The earliest attach index, in {@code host}, of a child of {@code parent} whose slot + /// sorts after {@code slot}. -1 when there is none. + private static int laterSiblingAttachIndex(Element parent, final int slot, + final RenderHost host) { + if (parent == null) { + return -1; + } + final int[] best = {-1}; + parent.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Element c) { + if (c == null || c.slot <= slot) { + return; + } + int at = host.firstAttachIndex(c); + if (at >= 0 && (best[0] < 0 || at < best[0])) { + best[0] = at; + } + } + }); + return best[0]; + } + + /// The host a child of this slot attaches into. + private RenderHost hostForNewChild(int slot) { + if (this instanceof RenderElement) { + RenderHost h = ((RenderElement) this).hostForChild(slot); + if (h != null) { + return h; + } + } + return host(); + } + + protected Element inflateWidget(Widget newWidget, int newSlot) { + Element child = newWidget.createElement(); + child.mount(this, newSlot); + return child; + } + + /** + * Removes a child subtree from the tree. M1 has no GlobalKey + * reactivation, so deactivation unmounts immediately and recursively. + */ + protected void deactivateChild(Element child) { + noteDiscard(child); + child.unmountRecursively(); + child.parent = null; + } + + /// Subtrees THROWN AWAY, by widget class — the other half of the + /// replacement census. + /// + /// A child that becomes null is discarded without ever being offered a + /// replacement, so it does not show up as a failed reconciliation; it is + /// simply a subtree that was built and then dropped. On a start-up trace + /// that is the difference between "the app was built once" and "the app was + /// built, discarded and built again". + private static final java.util.Map DISCARDED = + new java.util.HashMap(); + + private static void noteDiscard(Element child) { + if (!Trace.on() || child == null) { + return; + } + String key = child.widget == null ? "null" : child.widget.getClass().getSimpleName(); + int[] n = DISCARDED.get(key); + if (n == null) { + n = new int[1]; + DISCARDED.put(key, n); + } + n[0]++; + } + + /** The discard census, worst first; see {@link #DISCARDED}. */ + public static String discardCensus(int top) { + java.util.List> all = + new java.util.ArrayList>(DISCARDED.entrySet()); + java.util.Collections.sort(all, new java.util.Comparator>() { + @Override + public int compare(java.util.Map.Entry a, java.util.Map.Entry b) { + return b.getValue()[0] - a.getValue()[0]; + } + }); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < Math.min(top, all.size()); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(all.get(i).getKey()).append('=').append(all.get(i).getValue()[0]); + } + return sb.append(']').toString(); + } + + final void unmountRecursively() { + visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + c.unmountRecursively(); + } + }); + unmount(); + } + + /** + * Flutter's keyed linear multi-child reconciler + * (RenderObjectElement.updateChildren): sync a leading run and a trailing + * run by canUpdate, match the middle by key, inflate everything else, + * deactivate leftovers. + */ + protected List updateChildren(List oldChildren, List newWidgets) { + int newChildrenTop = 0; + int oldChildrenTop = 0; + int newChildrenBottom = newWidgets.size() - 1; + int oldChildrenBottom = oldChildren.size() - 1; + + Element[] newChildren = new Element[newWidgets.size()]; + + // Update the top of the list. + while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { + Element oldChild = oldChildren.get(oldChildrenTop); + Widget newWidget = newWidgets.get(newChildrenTop); + if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) { + break; + } + newChildren[newChildrenTop] = updateChild(oldChild, newWidget, newChildrenTop); + newChildrenTop++; + oldChildrenTop++; + } + + // Scan the bottom of the list (matched pairs are synced later so + // middle inserts/removes keep correct slots). + while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { + Element oldChild = oldChildren.get(oldChildrenBottom); + Widget newWidget = newWidgets.get(newChildrenBottom); + if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) { + break; + } + oldChildrenBottom--; + newChildrenBottom--; + } + + // Scan the old middle: collect keyed children, drop the rest. + Map oldKeyedChildren = null; + boolean haveOldChildren = oldChildrenTop <= oldChildrenBottom; + if (haveOldChildren) { + oldKeyedChildren = new HashMap(); + while (oldChildrenTop <= oldChildrenBottom) { + Element oldChild = oldChildren.get(oldChildrenTop); + if (oldChild != null) { + Key k = oldChild.widget == null ? null : oldChild.widget.getKey(); + if (k != null) { + oldKeyedChildren.put(k, oldChild); + } else { + deactivateChild(oldChild); + } + } + oldChildrenTop++; + } + } + + // Update the new middle, reusing keyed matches. + while (newChildrenTop <= newChildrenBottom) { + Element oldChild = null; + Widget newWidget = newWidgets.get(newChildrenTop); + if (haveOldChildren) { + Key key = newWidget.getKey(); + if (key != null) { + oldChild = oldKeyedChildren.get(key); + if (oldChild != null) { + if (Widget.canUpdate(oldChild.widget, newWidget)) { + oldKeyedChildren.remove(key); + } else { + oldChild = null; + } + } + } + } + newChildren[newChildrenTop] = updateChild(oldChild, newWidget, newChildrenTop); + newChildrenTop++; + } + + // Sync the bottom run that was scanned earlier. + newChildrenBottom = newWidgets.size() - 1; + oldChildrenBottom = oldChildren.size() - 1; + while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { + Element oldChild = oldChildren.get(oldChildrenTop); + Widget newWidget = newWidgets.get(newChildrenTop); + newChildren[newChildrenTop] = updateChild(oldChild, newWidget, newChildrenTop); + newChildrenTop++; + oldChildrenTop++; + } + + // Deactivate leftover keyed children that were not reused. + if (haveOldChildren && !oldKeyedChildren.isEmpty()) { + for (Element leftover : oldKeyedChildren.values()) { + deactivateChild(leftover); + } + } + + List result = new ArrayList(newChildren.length); + for (Element e : newChildren) { + result.add(e); + } + // Keyed children matched in a NEW order keep their elements (and CN1 + // components) but those components still sit at their OLD flat + // container indices; move them so paint order and hit-testing match + // the new tree order. + // + // A first build has no survivors to move. Every child was inflated fresh, in + // slot order, with no later sibling yet in existence, so each subtree's + // components were attached where tree order puts them -- and the walk here + // (a set of every component in the host, then every element under every + // child) would only confirm it. It ran once per multi-child element, over that + // element's whole subtree, which over a first frame is quadratic in its depth. + if (!oldChildren.isEmpty()) { + reattachInTreeOrder(result); + } + return result; + } + + /** + * Ensures the flat container components of the given children (this + * host's attach entries under each child subtree) appear in the + * container in tree order, moving survivors as needed. Subtree-internal + * order is preserved — nested reorders are each child's own concern. + */ + private void reattachInTreeOrder(List childrenInTreeOrder) { + if (host == null) { + return; + } + java.util.Set attached = new HashSet(host.attachOrder()); + List desired = new ArrayList(); + AttachedCollector collector = new AttachedCollector(attached, desired); + for (int i = 0, n = childrenInTreeOrder.size(); i < n; i++) { + Element c = childrenInTreeOrder.get(i); + if (c != null) { + collector.visit(c); + } + } + host.reorderToTreeOrder(desired); + } + + /** + * Collects this host's attach entries under a subtree, in tree order. + * + *

One visitor for the whole traversal rather than one per node: the + * recursion used to hand visitChildren a fresh capturing callback at every + * element, even though the state it captured -- the attached set, the + * output list, and the enclosing element's host -- is identical at every + * level. An allocation census of a single screen counted 5,019 of those + * callbacks, the largest anonymous-class count in the runtime, and they + * are pure overhead: the same object serves the entire walk.

+ * + *

Safe to reuse down the recursion because it holds no per-node state; + * `attached` is read-only here and `out` only ever accumulates.

+ */ + private final class AttachedCollector implements Funcs.VoidFunc1 { + private final java.util.Set attached; + private final List out; + + AttachedCollector(java.util.Set attached, List out) { + this.attached = attached; + this.out = out; + } + + @Override + public void call(Element c) { + visit(c); + } + + void visit(Element e) { + if (e.host == Element.this.host && attached.contains(e)) { + out.add((RenderElement) e); + } + e.visitChildren(this); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlexFit.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlexFit.java new file mode 100644 index 00000000000..d8fa15dbb64 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlexFit.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How a {@code Flexible} child fills the available main-axis space — + * Flutter's {@code FlexFit}. {@code tight} forces the child to fill its + * share (as {@code Expanded} does); {@code loose} lets it be smaller. + */ +public enum FlexFit { + tight, loose +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterAssets.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterAssets.java new file mode 100644 index 00000000000..307011b27c7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterAssets.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Maps a Flutter asset path onto the Codename One resource name that the build + * emits for it. + * + *

Flutter asset keys are relative paths ({@code assets/studies/reply_card.png}, + * or {@code packages//} for a package asset). Codename One resources, + * however, are flat: {@code getResourceAsStream} rejects any name + * containing a {@code '/'} past the leading one, on every port. So the build + * flattens the asset tree into root-level resources and the runtime resolves + * through the same mangling.

+ * + *

The encoding makes {@code '_'} an escape that is ALWAYS followed by one + * character: {@code "__"} for a literal underscore, {@code "_s"} for the path + * separator. Every other character is itself. That is prefix-free, so it + * decodes one way only and no two paths share a name. The earlier rule -- + * double every {@code '_'}, then use a single {@code '_'} as the separator -- + * was not: {@code a_/b} and {@code a/_b} both became {@code a___b}, and the + * build silently wrote one asset over the other. No shell- or + * bundler-hostile punctuation is introduced. The {@code cn1f_} prefix + * namespaces Flutter assets away from other app resources (and keeps them off + * the reserved {@code raw} prefix). File extensions survive, so native builders + * that classify bundled files by extension still see a {@code .png} as a + * {@code .png}.

+ * + *

Example: {@code packages/gallery_assets/assets/studies/reply_card.png} → + * {@code /cn1f_packages_sgallery__assets_sassets_sstudies_sreply__card.png}

+ * + *

Keep in sync with the identical encoder in the build's + * {@code TranscodeFlutterMojo} — the two halves of this contract are + * deliberately tiny and duplicated rather than sharing a module, because the + * Maven plugin must not depend on the Flutter runtime.

+ */ +public class FlutterAssets { + + /** Prefix marking a flattened Flutter asset resource. */ + public static final String PREFIX = "cn1f_"; + + private FlutterAssets() { + } + + /** + * The absolute Codename One resource name for a Flutter asset path. + * + * @param assetPath the Flutter asset key, e.g. {@code assets/foo/bar.png} + * @return the flat resource name, e.g. {@code /cn1f_assets_sfoo_sbar.png} + */ + public static String resourceName(String assetPath) { + return "/" + flatName(assetPath); + } + + /** + * The ratios probed when no manifest lists the asset, ascending. A variant of + * {@code dir/name.ext} lives at {@code dir/x/name.ext}. + */ + private static final double[] VARIANTS = {1.5, 2.0, 3.0, 4.0}; + + /** + * Every asset path that may hold a variant of {@code assetPath}: the scaled + * variants, densest-useful first, and the unscaled asset last. Flutter reads + * the available variants from a build-generated manifest; we probe these + * instead, and {@link #chooseVariant} picks among the ones present. + */ + public static String[] variantCandidates(String assetPath, double dpr) { + return variantCandidates(assetPath, dpr, manifest()); + } + + /** + * As {@link #variantCandidates(String, double)}, from {@code manifest} (main asset + * key to every bundled key that is it or a variant of it). An asset the manifest + * lists is probed at exactly the variants it lists, whatever their scale; one it + * does not -- or no manifest at all, from an older build or resources placed by + * hand -- falls back to the fixed ratios below. + */ + static String[] variantCandidates(String assetPath, double dpr, java.util.Map> manifest) { + java.util.List listed = manifest == null ? null : manifest.get(stripSlash(assetPath)); + if (listed != null) { + return listed.toArray(new String[listed.size()]); + } + int slash = assetPath.lastIndexOf('/'); + String dir = slash < 0 ? "" : assetPath.substring(0, slash + 1); + String file = slash < 0 ? assetPath : assetPath.substring(slash + 1); + + String[] out = new String[VARIANTS.length + 1]; + int n = 0; + // ascending from the first variant >= dpr + for (int i = 0; i < VARIANTS.length; i++) { + if (VARIANTS[i] >= dpr) { + out[n++] = dir + ratioDir(VARIANTS[i]) + file; + } + } + // then descending through the ones below it + for (int i = VARIANTS.length - 1; i >= 0; i--) { + if (VARIANTS[i] < dpr) { + out[n++] = dir + ratioDir(VARIANTS[i]) + file; + } + } + out[n++] = assetPath; + + String[] trimmed = new String[n]; + System.arraycopy(out, 0, trimmed, 0, n); + return trimmed; + } + + private static String ratioDir(double ratio) { + // Flutter names these "1.5x", "2.0x", "3.0x" — one decimal, always + long whole = (long) ratio; + long tenth = Math.round((ratio - whole) * 10); + return whole + "." + tenth + "x/"; + } + + /** An opened asset together with the density it was authored for. */ + public static class Resolved { + private final java.io.InputStream stream; + private final double ratio; + + Resolved(java.io.InputStream stream, double ratio) { + this.stream = stream; + this.ratio = ratio; + } + + public java.io.InputStream stream() { + return stream; + } + + /** + * Device pixels per logical pixel in the loaded file — 1 for the + * unscaled asset, 3 for a {@code 3.0x} variant. Divide the decoded + * pixel size by this to get the image's logical size. + */ + public double ratio() { + return ratio; + } + } + + /** + * Opens the best available variant of a Flutter asset for the current + * screen density, or null when no candidate resolves. + */ + public static Resolved open(Class cls, String assetPath) { + double dpr = 1; + try { + if (com.codename1.ui.Display.isInitialized()) { + dpr = com.codename1.flutter.rendering.Dp.scale(); + } + } catch (Throwable t) { + // headless: keep the unscaled asset + } + // Every variant is probed and the choice made from what is there: Flutter's + // rule depends on WHICH variants exist, so no fixed probe order can express + // it. Taking the first present one of a fixed order chose 3.0x over 2.0x at + // 2.1x, decoding over twice the pixels Flutter would. + String[] candidates = variantCandidates(assetPath, dpr); + java.io.InputStream[] streams = new java.io.InputStream[candidates.length]; + double[] ratios = new double[candidates.length]; + int found = 0; + for (int i = 0; i < candidates.length; i++) { + java.io.InputStream is = com.codename1.ui.Display.getInstance() + .getResourceAsStream(cls, resourceName(candidates[i])); + if (is != null) { + streams[found] = is; + ratios[found] = ratioOf(candidates[i], assetPath); + found++; + } + } + if (found == 0) { + return null; + } + double[] available = new double[found]; + System.arraycopy(ratios, 0, available, 0, found); + double best = chooseVariant(dpr, available); + Resolved chosen = null; + for (int i = 0; i < found; i++) { + if (chosen == null && ratios[i] == best) { + chosen = new Resolved(streams[i], ratios[i]); + } else { + try { + streams[i].close(); + } catch (java.io.IOException ignored) { + // a resource stream that will not close holds nothing we need + } + } + } + return chosen; + } + + /** Flutter's {@code _kLowDprLimit}. */ + private static final double LOW_DPR_LIMIT = 2.0; + + /** + * Flutter's {@code AssetImage._findBestVariant}: the exact ratio if present; + * otherwise, between the nearest lower and upper variants, the upper one on a + * low-density screen (below 2.0, where upscaling artefacts show) or past their + * midpoint, and the lower one otherwise; with only one side present, that side. + * The unscaled asset counts as 1.0. + */ + static double chooseVariant(double dpr, double[] available) { + double lower = Double.NaN; + double upper = Double.NaN; + for (int i = 0; i < available.length; i++) { + double r = available[i]; + if (r == dpr) { + return r; + } + if (r < dpr && (Double.isNaN(lower) || r > lower)) { + lower = r; + } + if (r > dpr && (Double.isNaN(upper) || r < upper)) { + upper = r; + } + } + if (Double.isNaN(lower)) { + return upper; + } + if (Double.isNaN(upper)) { + return lower; + } + return dpr < LOW_DPR_LIMIT || dpr > (lower + upper) / 2 ? upper : lower; + } + + /** The density a resolved candidate was authored for. */ + static double ratioOf(String candidate, String assetPath) { + if (stripSlash(candidate).equals(stripSlash(assetPath))) { + return 1; + } + int end = candidate.lastIndexOf('/'); + int start = candidate.lastIndexOf('/', end - 1); + String dir = end < 0 ? "" : candidate.substring(start + 1, end); + return isRatioDir(dir) ? Double.parseDouble(dir.substring(0, dir.length() - 1)) : 1; + } + + /** The bundled manifest's resource name; the build writes it (TranscodeFlutterMojo). */ + static final String MANIFEST = "cn1f_AssetManifest.txt"; + + private static java.util.Map> manifest; + private static boolean manifestRead; + + /** The bundled manifest, read once, or null when the build wrote none. */ + private static java.util.Map> manifest() { + if (!manifestRead) { + manifestRead = true; + try { + if (com.codename1.ui.Display.isInitialized()) { + java.io.InputStream in = com.codename1.ui.Display.getInstance() + .getResourceAsStream(FlutterAssets.class, "/" + MANIFEST); + if (in != null) { + try { + manifest = parseManifest(new String(com.codename1.io.Util.readInputStream(in), "UTF-8")); + } finally { + com.codename1.io.Util.cleanup(in); + } + } + } + } catch (Exception unreadable) { + manifest = null; // probe the fixed ratios, as without one + } + } + return manifest; + } + + /** + * Groups the manifest's asset keys by the asset they are variants of. A key + * {@code dir/x/name} is the N-ratio variant of {@code dir/name} when N is a + * positive decimal number, as Flutter's own rule has it; every other key is a + * main asset. + */ + static java.util.Map> parseManifest(String text) { + java.util.Map> out = new java.util.HashMap>(); + int pos = 0; + while (pos < text.length()) { + int nl = text.indexOf('\n', pos); + if (nl < 0) { + nl = text.length(); + } + String key = stripSlash(text.substring(pos, nl).trim()); + pos = nl + 1; + if (key.length() == 0) { + continue; + } + String main = key; + int end = key.lastIndexOf('/'); + if (end > 0) { + int start = key.lastIndexOf('/', end - 1); + if (isRatioDir(key.substring(start + 1, end))) { + main = key.substring(0, start + 1) + key.substring(end + 1); + } + } + java.util.List variants = out.get(main); + if (variants == null) { + variants = new java.util.ArrayList(); + out.put(main, variants); + } + variants.add(key); + } + return out; + } + + /** Whether {@code dir} names a resolution variant: a positive decimal and an 'x'. */ + static boolean isRatioDir(String dir) { + int n = dir.length(); + if (n < 2 || dir.charAt(n - 1) != 'x') { + return false; + } + boolean dot = false; + boolean digit = false; + for (int i = 0; i < n - 1; i++) { + char c = dir.charAt(i); + if (c >= '0' && c <= '9') { + digit = true; + } else if (c == '.' && !dot && i > 0 && i < n - 2) { + dot = true; + } else { + return false; + } + } + return digit && Double.parseDouble(dir.substring(0, n - 1)) > 0; + } + + private static String stripSlash(String p) { + while (p.startsWith("/")) { + p = p.substring(1); + } + return p; + } + + /** + * The flattened file name (no leading slash) for a Flutter asset path — + * what the build writes into the output directory. + */ + public static String flatName(String assetPath) { + String p = assetPath; + while (p.startsWith("/")) { + p = p.substring(1); + } + StringBuilder sb = new StringBuilder(PREFIX); + for (int i = 0; i < p.length(); i++) { + char c = p.charAt(i); + if (c == '_') { + sb.append("__"); + } else if (c == '/') { + sb.append("_s"); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterErrorReport.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterErrorReport.java new file mode 100644 index 00000000000..8f58b0b8b2c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterErrorReport.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.io.Log; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Collects the errors a Flutter tree throws, keeps the app running, and reports them + * in a form you can act on. + * + *

Without this, one failing screen ends the session: an uncaught error on the EDT + * raises Codename One's modal error dialog, which blocks the EDT, so every screen after + * it stalls too. During development that turns "this screen is broken" into "the app is + * dead", and it hides how many OTHER screens would have worked.

+ * + *

What makes a report usable is the context, not the stack: transpiled build methods + * are inlined into the framework's frame on some backends, so a trace often names + * nothing but {@code Element.updateChild} repeated. Each error is therefore recorded + * with the widget that was building and the route that was on screen, and identical + * errors are counted rather than repeated — a rebuild loop would otherwise bury + * everything else.

+ * + *

Enable with {@link #install()}; {@link #summary()} returns the inventory, which is + * what a sweep over an app's screens should be judged by.

+ */ +public final class FlutterErrorReport { + + /// One distinct failure and how often it happened. + public static final class Entry { + private final String type; + private final String message; + private final String building; + private final String route; + private final String origin; + private int count; + + Entry(String type, String message, String building, String route, String origin) { + this.type = type; + this.message = message; + this.building = building; + this.route = route; + this.origin = origin; + this.count = 1; + } + + /// The first few frames of the throw site, or null when the platform has none. + /// + /// "Null check operator used on a null value" names no widget and no line on its + /// own, and transpiled code has plenty of `!` in it; without this, finding one + /// means bisecting the tree by hand. + public String origin() { + return origin; + } + + public String type() { + return type; + } + + public String message() { + return message; + } + + /// The widget that was building when this happened, or null. + public String building() { + return building; + } + + /// The route that was on screen, or null. + public String route() { + return route; + } + + public int count() { + return count; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(count).append("x ").append(type); + if (message != null) { + sb.append(": ").append(message); + } + if (building != null) { + sb.append(" [while ").append(building).append("]"); + } + if (route != null) { + sb.append(" [route ").append(route).append("]"); + } + if (origin != null) { + sb.append(" [at ").append(origin).append("]"); + } + return sb.toString(); + } + } + + private static final Map ENTRIES = new HashMap(); + private static final List ORDER = new ArrayList(); + private static boolean installed; + private static String currentRoute; + + private FlutterErrorReport() { + } + + /** + * Starts collecting. Errors are logged and swallowed instead of stopping the app. + * + *

Call this from a debug or test build. A shipping app usually wants the opposite — + * the default dialog, or its own crash reporting — so this is opt-in rather than + * something {@code runApp} does for you.

+ */ + public static synchronized void install() { + if (installed) { + return; + } + installed = true; + Display.getInstance().addEdtErrorHandler(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + record(evt.getSource()); + // Consume: the default handling is a modal dialog, which would block the + // EDT and take every later screen down with this one. + evt.consume(); + } + }); + } + + /** Whether collecting is active. */ + public static synchronized boolean isInstalled() { + return installed; + } + + /// The first frames of a throwable's own stack, trimmed to the generated and runtime + /// code that actually matters. Best effort: a platform that reports no frames simply + /// yields null rather than failing the report. + private static String originOf(Object error) { + if (!(error instanceof Throwable)) { + return null; + } + try { + StackTraceElement[] frames = ((Throwable) error).getStackTrace(); + if (frames == null || frames.length == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + int shown = 0; + for (int i = 0; i < frames.length && shown < 4; i++) { + String cn = frames[i].getClassName(); + if (cn.startsWith("dart.runtime.DartRuntime")) { + continue; // the thrower itself, never the answer + } + if (sb.length() > 0) { + sb.append(" <- "); + } + sb.append(cn).append('.').append(frames[i].getMethodName()); + if (frames[i].getLineNumber() > 0) { + sb.append(':').append(frames[i].getLineNumber()); + } + shown++; + } + return sb.length() == 0 ? null : sb.toString(); + } catch (Throwable ignored) { + return null; + } + } + + /** Records the route now on screen, so later errors can name where they happened. */ + public static synchronized void route(String name) { + currentRoute = name; + } + + /** + * Records one failure. Public so a caller that catches an error itself — a route + * mount, a painter — can report it with the same context and de-duplication. + */ + public static synchronized void record(Object error) { + String type = error == null ? "unknown" : error.getClass().getName(); + String message = error instanceof Throwable ? ((Throwable) error).getMessage() + : (error == null ? null : String.valueOf(error)); + String building = dart.runtime.DartRuntime.diagnosticContext(); + String origin = originOf(error); + String key = type + "|" + message + "|" + building + "|" + currentRoute; + Entry existing = ENTRIES.get(key); + if (existing != null) { + existing.count++; + return; // already reported once; counting is enough + } + Entry entry = new Entry(type, message, building, currentRoute, origin); + ENTRIES.put(key, entry); + ORDER.add(entry); + try { + Log.p("Flutter error: " + entry); + } catch (Throwable ignored) { + // headless: Log has no storage backend + } + } + + /** + * Records a route name that resolved to nothing. + * + *

This used to be a log line, and a screen that never opened therefore read + * as a screen that opened cleanly: a sweep that walks every route and asks for + * the error inventory said "0 routes reported something" while five of the six + * studies had silently not opened at all, leaving whatever was already showing + * on screen to be screenshotted in their place.

+ * + * @param name the route that was asked for + * @param detail what went wrong, or null for "nothing claimed the name" + */ + public static synchronized void noRoute(String name, String detail) { + String message = "no route for '" + name + "'" + (detail == null ? "" : " " + detail); + String key = "no-route|" + message; + Entry existing = ENTRIES.get(key); + if (existing != null) { + existing.count++; + return; + } + Entry entry = new Entry("no-route", message, + dart.runtime.DartRuntime.diagnosticContext(), name, null); + ENTRIES.put(key, entry); + ORDER.add(entry); + try { + Log.p("Flutter error: " + entry); + } catch (Throwable ignored) { + // headless: Log has no storage backend + } + } + + /** + * Records that a widget rendered without its intended effect — a stub that passes + * its child through, or draws nothing at all. + * + *

These are the failures that are hardest to find, because nothing throws: the + * screen simply comes up empty or subtly wrong, and a sweep reports "no errors". + * Reporting them turns a blank screen into a list of what it needed and did not + * get.

+ * + * @param widget the widget that is not fully implemented + * @param effect what is missing, phrased so it reads as a gap ("scale and rotation + * are ignored") + */ + public static synchronized void unimplemented(String widget, String effect) { + String key = "unimplemented|" + widget + "|" + effect + "|" + currentRoute; + Entry existing = ENTRIES.get(key); + if (existing != null) { + existing.count++; + return; + } + // No origin: an unimplemented report names its own widget already. + Entry entry = new Entry("unimplemented", widget + ": " + effect, + dart.runtime.DartRuntime.diagnosticContext(), currentRoute, null); + ENTRIES.put(key, entry); + ORDER.add(entry); + try { + Log.p("Flutter gap: " + entry); + } catch (Throwable ignored) { + // headless: Log has no storage backend + } + } + + /** Every distinct failure, in the order first seen. */ + public static synchronized List entries() { + return new ArrayList(ORDER); + } + + /** Distinct failures, and the total including repeats. */ + public static synchronized String summary() { + int total = 0; + for (Entry e : ORDER) { + total += e.count; + } + StringBuilder sb = new StringBuilder(); + sb.append(ORDER.size()).append(" distinct error(s), ").append(total).append(" total"); + for (Entry e : ORDER) { + sb.append('\n').append(" ").append(e); + } + return sb.toString(); + } + + /** Forgets everything collected so far — lets a sweep measure one screen at a time. */ + public static synchronized void reset() { + ENTRIES.clear(); + ORDER.clear(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterUI.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterUI.java new file mode 100644 index 00000000000..b454c347f2f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FlutterUI.java @@ -0,0 +1,692 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.FlutterRootLayout; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.ui.CN; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BorderLayout; + +/** + * Entry points binding a Flutter widget tree to Codename One. + * + *
    + *
  • {@link #runApp(Widget)} — creates a host Form, mounts the tree and + * shows the form. Call from a CN1 lifecycle (Display initialized).
  • + *
  • {@link #wrap(Widget)} — returns a CN1 Container hosting the subtree, + * for embedding Flutter content inside an ordinary CN1 UI.
  • + *
+ */ +public final class FlutterUI { + + private FlutterUI() { + } + + /** + * Inflates {@code app} as the root of a new element tree hosted in a new + * CN1 Form and shows the form. + */ + public static void runApp(Widget app) { + assertEdt(); + long t0 = System.currentTimeMillis(); + installMaterialBaseTheme(); + if (startupTrace()) { + probeComponentCost(); + } + long t1 = System.currentTimeMillis(); + RenderHost host = mountInNewForm(app); + long t2 = System.currentTimeMillis(); + host.form().show(); + long t3 = System.currentTimeMillis(); + // From here on, artwork is resolved in the frame that asks for it. + com.codename1.flutter.widgets.ImageRenderElement.firstFrameShown(); + collectStartupGarbage(host.form()); + // Attribution for the first frame, gated so it costs nothing normally. + // "The app takes 250ms to start" is not actionable; knowing whether that + // is the theme, the widget build, or the first layout is. + if (startupTrace()) { + System.out.println("BENCH:STARTUP theme=" + (t1 - t0) + "ms mount=" + (t2 - t1) + + "ms show=" + (t3 - t2) + "ms images=" + + com.codename1.flutter.widgets.ImageRenderElement.scalingCost() + + " components=" + RenderElement.componentCost() + + " " + com.codename1.flutter.rendering.FlutterRootLayout.rootLayoutCost() + + " elements=" + Element.mountedCount() + + " layoutBuilder[" + com.codename1.flutter.widgets.LayoutBuilderElement.cost() + "]" + + " composed[" + ComposedElement.rebuildCost() + "]" + + " icons=" + com.codename1.flutter.widgets.IconRenderElement.glyphCost() + + " createdBy:" + RenderElement.componentBreakdown() + + " layoutSelf:" + RenderElement.hotLayoutClasses(8) + + " replaced:" + Element.replacementCensus(6) + + " discarded:" + Element.discardCensus(6) + + " display:" + com.codename1.flutter.MediaQueryData.sizeHistory()); + System.out.flush(); + } + } + + /** + * Asks for one collection once the first screen is up. + * + *

Start-up is when a UI toolkit makes the most garbage it will ever + * make: every image decoded at a size it was then resampled from, every + * builder temporary, every string built to look something up once. None of + * it is referenced by the frame now on screen, and an application that then + * sits idle gives the collector no reason to run — so the peak stays + * charged to the process. Measured on the Mac build, the collector's own + * freed-but-unreturned pages alone were 21MB against 0.1MB for the same + * app built with another toolchain.

+ * + *

Deferred, so the collection lands after the frame rather than inside + * it, and it runs on the collector's thread either way.

+ */ + private static void collectStartupGarbage(final Form form) { + try { + com.codename1.ui.CN.callSerially(new Runnable() { + @Override + public void run() { + System.gc(); + } + }); + // And once more a moment later. Start-up garbage clears in two + // waves: the first collection frees the objects, and only then do + // the allocator's pages become wholly empty and returnable. One + // pass leaves most of them still holding a single survivor. + if (form != null) { + com.codename1.ui.util.UITimer.timer(1200, false, form, new Runnable() { + @Override + public void run() { + System.gc(); + } + }); + } + } catch (Throwable ignore) { + // headless, or a port with no collector to ask + } + } + + /** + * Isolates what creating one Codename One component actually costs. + * + *

Attribution said ~0.3ms per component, uniformly across every element + * type — which rules out per-widget logic and points at something every + * component pays. This separates the three candidates: constructing the + * component, resolving its four styles out of the theme, and mutating + * those styles. + */ + private static void probeComponentCost() { + final int n = 200; + com.codename1.ui.Label[] kept = new com.codename1.ui.Label[n]; + long t0 = System.currentTimeMillis(); + for (int i = 0; i < n; i++) { + kept[i] = new com.codename1.ui.Label("x", "FlutterText"); + } + long t1 = System.currentTimeMillis(); + for (int i = 0; i < n; i++) { + kept[i].getAllStyles(); + } + long t2 = System.currentTimeMillis(); + for (int i = 0; i < n; i++) { + kept[i].getAllStyles().setPadding(0, 0, 0, 0); + } + long t3 = System.currentTimeMillis(); + System.out.println("BENCH:PROBE " + n + " labels: construct=" + (t1 - t0) + + "ms resolveStyles=" + (t2 - t1) + "ms mutateStyles=" + (t3 - t2) + "ms"); + System.out.flush(); + } + + /** + * Times the FIRST paint of the root container and reports it once. + * + *

Building and laying out the tree is only half of a first frame; the + * other half is rasterising it, and that half is invisible to every counter + * that stops when {@code show()} returns. Without this the gap between + * "the app finished building" and "the marker printed" is unattributed + * time, which is where wrong explanations come from. + */ + private static class TimedRootContainer extends Container { + private final boolean timed; + private boolean painted; + + TimedRootContainer(com.codename1.ui.layouts.Layout layout) { + this(layout, true); + } + + TimedRootContainer(com.codename1.ui.layouts.Layout layout, boolean timed) { + super(layout); + this.timed = timed; + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + if (painted || !timed) { + super.paint(g); + return; + } + long t0 = System.currentTimeMillis(); + try { + super.paint(g); + } finally { + painted = true; + System.out.println("BENCH:STARTUP firstPaint=" + + (System.currentTimeMillis() - t0) + "ms"); + System.out.flush(); + } + } + } + + /** {@code cn1.flutter.startupTrace} — prints the first-frame phase split. */ + private static boolean startupTrace() { + try { + return "true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.startupTrace", "false")); + } catch (Throwable t) { + return false; + } + } + + /** + * Inflates {@code root} in a fresh CN1 Form (the runApp mounting pattern, + * minus theme installation and showing). Used by runApp and by + * Navigator.push for every pushed route; returns the host, from which the + * Form ({@code host.form()}) and root element ({@code host.rootElement()}) + * are reachable. + */ + public static RenderHost mountInNewForm(Widget root) { + return mountInNewForm(root, null); + } + + /** + * As {@link #mountInNewForm(Widget)}, but the new tree's ancestor lookups + * continue from {@code contextFallback} once its own root is reached — + * how a pushed route inherits the app's Theme, Localizations and providers + * despite living in its own Form. See {@code Element.contextFallback}. + */ + public static RenderHost mountInNewForm(Widget root, Element contextFallback) { + assertEdt(); + // Every route lives inside an Overlay, as it does in Flutter, so that + // anything reaching for `Overlay.of(context)` — a dialog, a modal + // sheet, a coach mark — finds the one belonging to the route it is on + // rather than nothing at all. + root = com.codename1.flutter.widgets.Overlay.hosting(root); + Form f = new Form(new BorderLayout()); + // Flutter owns the whole canvas: the widget tree draws its own padding + // and safe areas, so any CN1 chrome inset on the Form or its content + // pane is a margin Flutter never asked for (it left pushed pages + // floating inside a frame). + stripChrome(f); + stripChrome(f.getContentPane()); + RenderHost host = new RenderHost(); + host.form(f); + Container c = startupTrace() ? new TimedRootContainer(new FlutterRootLayout(host)) + : new Container(new FlutterRootLayout(host)); + host.container(c); + long mt0 = System.currentTimeMillis(); + Element mounted = mount(root, host, new BuildOwner(), contextFallback); + if (startupTrace()) { + System.out.println("BENCH:STARTUP build=" + (System.currentTimeMillis() - mt0) + "ms"); + System.out.flush(); + } + // Kept on the Form rather than in a static: the Form owns its tree, so a popped + // route's element cannot outlive it here and currentContext() always answers for + // whatever is actually showing. + f.putClientProperty(ROOT_ELEMENT, mounted); + f.add(BorderLayout.CENTER, c); + hideUnusedToolbar(f, host); + return host; + } + + /** + * Hides the Form's Toolbar unless a root Scaffold claimed it for its AppBar. + * + *

Every CN1 Form has a Toolbar, and an empty one is invisible on a display with + * no cutout -- which is every desktop, which is why this cost nothing in any sweep. + * On a phone it takes the status-bar inset, so an empty Toolbar paints a band of + * the FORM's own colour across the top and pushes the content pane down under it. + * On iOS that was 222px of the Material baseline surface above a page whose own + * background is the theme's, where the reference simply carries on behind the + * status bar.

+ * + *

The rule is the one {@code Navigator.push} already applied to pushed routes; + * it belongs here so that the app's first screen -- which is mounted by + * {@code runApp} and never went through the navigator -- obeys it too.

+ */ + private static void hideUnusedToolbar(Form f, RenderHost host) { + com.codename1.ui.Toolbar tb = f.getToolbar(); + if (tb != null && !host.isFormToolbarBound()) { + tb.setVisible(false); + tb.setHidden(true); + } + } + + private static final String ROOT_ELEMENT = "cn1$flutterRootElement"; + + /** + * A BuildContext for the tree currently on screen, or null when the current Form + * is not a Flutter one. + * + *

Exists because {@code Navigator.pushNamed} needs a context to inherit from: a + * route pushed with a null context gets no ancestor chain, so {@code Theme.of}, + * {@code MediaQuery.of}, {@code Localizations.of} and every provider above it find + * nothing — the pushed page then throws on the first thing it looks up. Widgets + * always have their own context and should pass it; this is for callers OUTSIDE the + * tree — a deep link, a notification tap, a test harness — which have none of their + * own and would otherwise pass null.

+ */ + public static BuildContext currentContext() { + if (!Display.isInitialized()) { + return null; + } + Form f = Display.getInstance().getCurrent(); + Object e = f == null ? null : f.getClientProperty(ROOT_ELEMENT); + return e instanceof Element && ((Element) e).mounted ? (Element) e : null; + } + + /** + * Removes a component's theme-supplied padding and margin. The units are + * set to pixels first: styles derived from the Material theme carry + * MILLIMETRE units, under which a zero is still zero but any later + * non-zero write would be reinterpreted at ~18x. + */ + private static void stripChrome(com.codename1.ui.Component c) { + if (c == null) { + return; + } + com.codename1.ui.plaf.Style s = c.getAllStyles(); + s.setPaddingUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + s.setMarginUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS, + com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + s.setPadding(0, 0, 0, 0); + s.setMargin(0, 0, 0, 0); + if (c instanceof com.codename1.ui.Container) { + // The safe area is a LAYOUT inset, not padding, so zeroing the style + // above does not touch it. Codename One holds a full-screen form's + // content off the display cutout by itself; Flutter's tree does that + // for itself through MediaQuery, so leaving the flag on insets the + // canvas twice and leaves a band of the FORM's own colour above + // everything the app drew -- a white strip across the top of the + // gallery on iOS, where the reference shows the page carrying on + // behind the status bar. It costs nothing on a port with no cutout, + // which is why it survived every desktop sweep. + ((com.codename1.ui.Container) c).setSafeArea(false); + } + } + + /** + * Unmounts a whole element subtree (recursively). Used by + * Navigator.pop/Dialogs when a route or dialog is torn down. + */ + public static void unmountTree(Element root) { + if (root != null) { + root.unmountRecursively(); + } + } + + /** + * Installs the bundled Material 3 theme (AndroidMaterialTheme) as the + * base look for a Flutter-owned app. Flutter renders Material Design on + * every platform, so a full-app Flutter boot replaces the platform + * default theme; embedded subtrees (wrap()) deliberately do NOT install + * it to avoid restyling the host app. + */ + private static void installMaterialBaseTheme() { + // The runtime's own entries -- the Flutter* UIID derives and the scroll physics + // constants -- go in WITH the base theme rather than as two addThemeProps calls + // after it. Each addThemeProps is a whole theme rebuild: it re-merges, drops + // every cached style and derived font, rebuilds the default styles and runs the + // look and feel's refresh again, and the first frame paid for that three times + // to end where one install ends. Merged first, the overlay still wins over the + // base theme key for key, exactly as layering it did. + java.util.Hashtable overlay = flutterUiidDerives(); + overlay.putAll(scrollPhysicsProps()); + try { + com.codename1.ui.util.Resources r = com.codename1.ui.util.Resources.open( + "/CN1FlutterMaterialTheme.res"); + String[] names = r.getThemeResourceNames(); + if (names.length > 0) { + java.util.Hashtable theme = r.getTheme(names[0]); + theme.putAll(overlay); + com.codename1.ui.plaf.UIManager.getInstance().setThemeProps(theme); + return; + } + } catch (Throwable t) { + com.codename1.io.Log.p("Flutter runtime: could not install Material base theme: " + t); + } + // No base theme: the runtime's entries still go on top of whatever is there. + try { + com.codename1.ui.plaf.UIManager.getInstance().addThemeProps(overlay); + } catch (Throwable t) { + com.codename1.io.Log.p("Flutter runtime: could not install UIID derives: " + t); + } + } + + /** + * Matches Codename One's fling to Flutter's scroll physics. + * + *

The CURVE already agrees: CN1's exponential decay uses a 500ms time constant, and + * Flutter's iOS {@code FrictionSimulation} (drag 0.135) e-folds at 1/-ln(0.135) = + * 499ms. Only the distance differs. CN1 coasts to + * {@code release velocity * DecayMotionScaleFactorInt}, 950 by default, while Flutter + * travels {@code -v/ln(0.135) = 0.4994 * v} — so an identical flick carries 1.90x too + * far, which reads as the list being slippery and overshooting where you meant to + * stop.

+ * + *

500 makes the two simulations agree to three decimal places rather than being a + * number tuned by eye.

+ * + *

The OVERSCROLL agrees by construction too, once the coefficient matches. Codename + * One compresses an over-edge drag with {@code c*x*D/(c*x + D)} for finger distance + * {@code x} and viewport {@code D}. Flutter looks nothing like that at first glance - + * {@link com.codename1.flutter.widgets.BouncingScrollPhysics} damps each individual + * drag delta by {@code 0.52*(1 - overscroll/D)^2} - but integrating that friction over + * a continuous drag gives {@code 0.52*x*D/(0.52*x + D)}: the SAME curve, and the only + * difference is the coefficient. Codename One uses 0.55, which is UIScrollView's; + * Flutter uses 0.52. + * + *

So this is one constant rather than a reimplementation, and + * {@code RubberBandParityTest} checks the two really do agree rather than taking the + * derivation's word for it.

+ * + *

These are app-level theme constants, so they apply to the whole app rather than + * only to Flutter subtrees. That is right for {@code runApp}, which owns the app; a + * host app embedding Flutter through {@code wrap} can set them back afterwards.

+ */ + /** + * The theme constants that make Codename One's scrolling behave like Flutter's. + * + *

Package-private and separate from the install so a test can check the KEYS, which + * is not a formality: a theme constant is only a constant if its key carries the + * {@code @}, because {@code buildTheme} strips that prefix to decide what is a constant + * and what is an ordinary style property. Without it the entries are stored happily, + * {@code addThemeProps} reports nothing wrong, and {@code getThemeConstant} keeps + * returning the default — which is exactly what happened here: the fling-distance + * constant looked installed for a long time and never once took effect.

+ */ + static java.util.Hashtable scrollPhysicsProps() { + java.util.Hashtable physics = new java.util.Hashtable(); + physics.put("@DecayMotionScaleFactorInt", "500"); + // Hundredths: 52 = 0.52, Flutter's BouncingScrollPhysics friction factor. + physics.put("@rubberBandCoefficientInt", "52"); + // The Material 3 slider painter is opt-in: without these the slider + // falls back to a legacy full-height fill with no thumb, which is what + // made the sliders demo a row of flat lavender bars. Sizes are + // Material's, converted from logical pixels at 160dpi: a 4dp track and + // a 20dp round thumb. The track runs CONTINUOUSLY under the thumb, as + // Flutter's slider draws it, rather than M3's gapped two-segment track. + physics.put("@sliderTrackThicknessMM", "0.635"); + physics.put("@sliderThumbWidthMM", "3.175"); + physics.put("@sliderThumbHeightMM", "3.175"); + physics.put("@sliderContinuousTrackBool", "true"); + return physics; + } + + /** + * The Flutter* UIIDs have no entries in the theme, so without these + * derive mappings every Flutter component falls back to the tiny default + * system font instead of the Material theme's fonts. Geometry (padding, + * margins) is still zeroed programmatically by the render elements — + * only fonts and colors flow in from the derived UIIDs. + */ + static java.util.Hashtable flutterUiidDerives() { + java.util.Hashtable derives = new java.util.Hashtable(); + String[][] map = { + {"FlutterText", "Label"}, + {"FlutterIcon", "Label"}, + {"FlutterImage", "Label"}, + {"FlutterDivider", "Label"}, + {"FlutterElevatedButton", "Button"}, + {"FlutterTextButton", "Button"}, + {"FlutterOutlinedButton", "Button"}, + {"FlutterIconButton", "Button"}, + {"FlutterCard", "Container"}, + {"FlutterScroll", "Container"}, + {"FlutterGesture", "Container"}, + {"FlutterAppBar", "TitleArea"}, + {"FlutterTextField", "TextField"}, + {"FlutterCheckbox", "CheckBox"}, + {"FlutterSwitch", "Switch"}, + {"FlutterRadio", "RadioButton"}, + {"FlutterSlider", "Slider"}, + {"FlutterListTile", "Container"}, + {"FlutterBottomNavigationBar", "Container"}, + {"FlutterDrawer", "Container"}, + }; + for (String[] m : map) { + derives.put(m[0] + ".derive", m[1]); + } + // Every Flutter UIID gets a zero margin, in all four states. + // + // These UIIDs derive from Codename One base UIIDs, so they inherit a + // margin meant for Codename One layouts -- Container's 2px, Switch's + // 10/15 -- which Flutter geometry must not have: the widget tree + // decides its own spacing. RenderElement.neutralizeCn1Behaviors was + // already forcing it to zero, but per COMPONENT, and getAllStyles() + // creates the selected, pressed and disabled styles plus a proxy to + // do it: five Style objects each, ~1900 during the first frame of + // the gallery, on the one primitive ParparVM is slowest at. + // + // Declaring it in the theme instead makes the components arrive + // already correct, so the per-component undo can be skipped + // entirely. Same rendered result -- the runtime set these to zero + // anyway -- for none of the allocation. + String[] uiids = { + "FlutterText", "FlutterIcon", "FlutterImage", "FlutterDivider", + "FlutterElevatedButton", "FlutterTextButton", "FlutterOutlinedButton", + "FlutterIconButton", "FlutterCard", "FlutterScroll", "FlutterGesture", + "FlutterAppBar", "FlutterTextField", "FlutterCheckbox", "FlutterSwitch", + "FlutterRadio", "FlutterSlider", "FlutterListTile", + "FlutterBottomNavigationBar", "FlutterDrawer", + // Not derived above -- they take the theme's default UIID, which + // is where Container's 2px margin comes from. + "FlutterBox", "FlutterEffect", "FlutterScaffold", "FlutterCustomPaint", + }; + for (String u : uiids) { + derives.put(u + ".margin", "0,0,0,0"); + derives.put(u + ".sel#margin", "0,0,0,0"); + derives.put(u + ".press#margin", "0,0,0,0"); + derives.put(u + ".dis#margin", "0,0,0,0"); + } + return derives; + } + + /** + * Inflates {@code w} into a CN1 Container that can be embedded anywhere + * in a regular CN1 component hierarchy. + */ + public static Container wrap(Widget w) { + return wrap(w, null); + } + + /** + * As {@link #wrap(Widget)}, with the tree's ancestor lookups continuing from + * {@code contextFallback} past its own root -- how a dialog, mounted as an + * unrelated root, still sees the app's Theme, providers and localizations, as + * {@link #mountInNewForm(Widget, Element)} does for a pushed route. + */ + public static Container wrap(Widget w, Element contextFallback) { + RenderHost host = new RenderHost(); + EmbeddedRoot c = new EmbeddedRoot(new FlutterRootLayout(host), startupTrace(), w, host, + contextFallback); + host.container(c); + c.mountTree(); + return c; + } + + /** + * The container {@link #wrap} returns, which owns the lifetime of the tree + * mounted into it. + * + *

Removing a wrapped subtree must unmount it: that is what disposes each + * State, detaches the notifiers it listened to and stops its animations. + * {@code wrap} used to hand back a plain Container and drop the mounted + * root, so a host that swapped embedded Flutter screens in and out kept every + * old tree's listeners and frame-driver registrations alive after its UI was + * gone.

+ * + *

The catch is that Codename One calls {@code deinitialize} for two + * different things: the component being REMOVED, and its form merely being + * hidden -- navigating to another form and back. Unmounting on the second + * would throw away the embedded state on an ordinary back-navigation, which + * Flutter keeps. The two are told apart after the fact: removal clears the + * parent once {@code deinitialize} returns, while a hidden form's components + * keep theirs. So the check is deferred one EDT cycle, which also treats a + * component moved between parents in the same cycle as kept.

+ * + *

A container added again after it was unmounted mounts a fresh tree from + * the same widget, as re-inserting a widget does in Flutter. What this cannot + * see is a whole Form abandoned without its components ever being removed; + * Codename One has no signal for that, and such a form becomes garbage along + * with the tree it holds.

+ */ + private static final class EmbeddedRoot extends TimedRootContainer { + private final Widget widget; + private final RenderHost host; + private final Element contextFallback; + private final EmbeddedLifetime lifetime = new EmbeddedLifetime(); + + EmbeddedRoot(com.codename1.ui.layouts.Layout layout, boolean timed, Widget widget, RenderHost host, + Element contextFallback) { + super(layout, timed); + this.widget = widget; + this.host = host; + this.contextFallback = contextFallback; + } + + void mountTree() { + mount(widget, host, new BuildOwner(), contextFallback); + lifetime.mounted(); + } + + @Override + protected void initComponent() { + super.initComponent(); + if (lifetime.onInit()) { + removeAll(); + mountTree(); + } + } + + @Override + protected void deinitialize() { + super.deinitialize(); + if (lifetime.onDeinit()) { + com.codename1.ui.CN.callSerially(new Runnable() { + @Override + public void run() { + if (lifetime.settle(getParent() != null)) { + unmountTree(host.rootElement()); + } + } + }); + } + } + } + + /** + * The removed-or-merely-hidden decision {@link EmbeddedRoot} makes, kept free + * of components so it can be tested without a display. Each method answers + * what the caller must do next. + */ + static final class EmbeddedLifetime { + private boolean mounted; + private boolean unmountPending; + + void mounted() { + mounted = true; + } + + /** Initialized (shown, or added back). @return true to mount a fresh tree. */ + boolean onInit() { + // Coming back cancels a pending unmount: this was a hide, or a move. + unmountPending = false; + return !mounted; + } + + /** Deinitialized. @return true to schedule {@link #settle} one EDT cycle later. */ + boolean onDeinit() { + if (!mounted || unmountPending) { + return false; + } + unmountPending = true; + return true; + } + + /** + * One cycle after a deinitialize. @param attached whether the container + * still has a parent @return true to unmount the tree now. + */ + boolean settle(boolean attached) { + if (!unmountPending) { + return false; + } + unmountPending = false; + if (attached || !mounted) { + return false; + } + mounted = false; + return true; + } + } + + /** + * Low-level mount used by {@link #runApp}/{@link #wrap} and by unit + * tests (with a componentless RenderHost). + */ + public static Element mount(Widget root, RenderHost host, BuildOwner owner) { + return mount(root, host, owner, null); + } + + /** + * As {@link #mount(Widget, RenderHost, BuildOwner)}, with an ancestor-lookup + * continuation for the new root. It must be linked before the mount, since + * the first build runs there and may already do a {@code Foo.of(context)}. + */ + public static Element mount(Widget root, RenderHost host, BuildOwner owner, Element contextFallback) { + assertEdt(); + Element rootElement = root.createElement(); + host.rootElement(rootElement); + rootElement.bootstrap(owner, host); + if (contextFallback != null) { + rootElement.contextFallback(contextFallback); + } + rootElement.mount(null, 0); + return rootElement; + } + + /** + * Framework mutations must happen on the EDT — but only when a Display + * exists; headless unit tests run without one. + */ + static void assertEdt() { + if (Display.isInitialized() && !CN.isEdt()) { + throw new IllegalStateException("Flutter framework mutation off the EDT; use CN.callSerially"); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FocusNode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FocusNode.java new file mode 100644 index 00000000000..da3ada08f4b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FocusNode.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * An object that can request keyboard focus ({@code FocusNode} in Flutter). The + * gallery creates these in {@code initState}, hands them to text fields via the + * {@code focusNode:} parameter, and disposes them. + * + *

A node is bridged to the component it is given to through a {@link Host}: + * requestFocus and unfocus move the real Codename One focus (and so the caret + * and the keyboard), and the component reports focus it gains or loses by + * itself back through {@link #hostFocusChanged}. Tracking only a private flag + * reported hasFocus true while nothing on screen was focused, so "focus the + * first invalid field" did nothing. As in Flutter, one node holds the primary + * focus at a time.

+ */ +public class FocusNode { + + /** + * The component a node is attached to. Framework plumbing, public only + * because the field render elements live in another package. + */ + public interface Host { + /** Gives the component the focus and starts editing it. */ + void focusHost(); + + /** Takes the focus away from the component and stops editing it. */ + void blurHost(); + } + + /** The node holding the primary focus, if any. */ + private static FocusNode primary; + + private String debugLabel; + private boolean skipTraversal; + private boolean canRequestFocus = true; + private boolean hasFocus; + private Host host; + private final List listeners = new ArrayList(); + + public FocusNode() { + } + + // Named constructor parameter setters. + + public void debugLabel(String v) { + this.debugLabel = v; + } + + public void skipTraversal(boolean v) { + this.skipTraversal = v; + } + + public void canRequestFocus(boolean v) { + this.canRequestFocus = v; + } + + public boolean hasFocus() { + return hasFocus; + } + + public boolean hasPrimaryFocus() { + return hasFocus; + } + + public void requestFocus() { + requestFocus(null); + } + + public void requestFocus(FocusNode node) { + if (node != null && node != this) { + node.requestFocus(); + return; + } + if (!canRequestFocus) { + return; + } + setHasFocus(true); + if (host != null) { + host.focusHost(); + } + } + + public void unfocus() { + if (hasFocus && host != null) { + host.blurHost(); + } + setHasFocus(false); + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + listeners.remove(listener); + } + + public void dispose() { + listeners.clear(); + if (primary == this) { + primary = null; + } + host = null; + } + + // ------------------------------------------------------------------ + // Framework plumbing + // ------------------------------------------------------------------ + + /** The node holding the primary focus, or null. */ + public static FocusNode primaryFocus() { + return primary; + } + + /** + * Attaches the component this node focuses. A focus requested before the + * component existed -- in initState, say -- is handed to it now. + */ + public void attachHost(Host h) { + this.host = h; + if (h != null && hasFocus) { + h.focusHost(); + } + } + + /** Detaches {@code h}, if it is still the attached component. */ + public void detachHost(Host h) { + if (host == h) { + host = null; + } + } + + /** The attached component gained or lost the focus by itself (a tap, the keyboard closing). */ + public void hostFocusChanged(boolean focused) { + setHasFocus(focused); + } + + void setHasFocus(boolean focus) { + if (this.hasFocus == focus) { + return; + } + if (focus) { + FocusNode previous = primary; + primary = this; + if (previous != null && previous != this) { + // The node that had the focus loses it; its component is left to + // Codename One, which moves the focus off it when this one takes it. + previous.setHasFocus(false); + } + } else if (primary == this) { + primary = null; + } + this.hasFocus = focus; + com.codename1.flutter.foundation.Listeners.notify(listeners); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontStyle.java new file mode 100644 index 00000000000..f973f8e2b2b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontStyle.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** Upright or italic — Flutter's {@code FontStyle}. */ +public enum FontStyle { + normal, italic +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontWeight.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontWeight.java new file mode 100644 index 00000000000..5af4335c76f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/FontWeight.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Font weights w100..w900 with Flutter's {@code normal} (w400) and + * {@code bold} (w700) aliases. CN1 fonts only distinguish plain/bold, so + * weights of w600 and up render bold. + */ +public enum FontWeight { + w100, w200, w300, w400, w500, w600, w700, w800, w900; + + public static final FontWeight normal = w400; + public static final FontWeight bold = w700; + + /** + * Whether this weight maps to CN1's bold style. + */ + public boolean isBold() { + return ordinal() >= w600.ordinal(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/GlobalKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/GlobalKey.java new file mode 100644 index 00000000000..e28ddd12af3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/GlobalKey.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A key that is unique across the entire app and provides access to the element, + * state and context it is attached to ({@code GlobalKey} in Flutter). The + * gallery mostly uses global keys as stable identity tokens passed to widgets; + * {@link #currentState()} / {@link #currentContext()} return the live targets + * once the keyed widget is mounted (null until then). + * + * @param the {@code State} (or other) type exposed via {@link #currentState()} + */ +public class GlobalKey extends Key { + + private String debugLabel; + private T state; + private BuildContext context; + private Widget widget; + + public GlobalKey() { + } + + /** Named constructor parameter {@code debugLabel:}. */ + public void debugLabel(String label) { + this.debugLabel = label; + } + + public T currentState() { + return state; + } + + public BuildContext currentContext() { + return context; + } + + public Widget currentWidget() { + return widget; + } + + // ------------------------------------------------------------------ + // Framework plumbing + // ------------------------------------------------------------------ + + public void attach(T state, BuildContext context, Widget widget) { + this.state = state; + this.context = context; + this.widget = widget; + } + + public void detach() { + this.state = null; + this.context = null; + this.widget = null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Gradient.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Gradient.java new file mode 100644 index 00000000000..ea36a3862da --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Gradient.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A description of a gradient that can produce a {@link Shader} for a given + * rectangle — Flutter's {@code Gradient}. Concrete subtypes are + * {@link LinearGradient}, {@link RadialGradient} and {@link SweepGradient}. + * + *

Only the color list is retained by the Codename One runtime for this + * milestone; geometry (begin/end/center/stops) is held but not yet painted.

+ */ +public abstract class Gradient { + + Object begin; + Object end; + Object center; + Object colors; + Object stops; + TileMode tileMode = TileMode.clamp; + Object transform; + + public void begin(Object v) { + this.begin = v; + } + + public void end(Object v) { + this.end = v; + } + + public void center(Object v) { + this.center = v; + } + + public void colors(Object v) { + this.colors = v; + } + + public void stops(Object v) { + this.stops = v; + } + + public void tileMode(TileMode v) { + this.tileMode = v == null ? TileMode.clamp : v; + } + + public void transform(Object v) { + this.transform = v; + } + + public Object getColors() { + return colors; + } + + public Object getBegin() { + return begin; + } + + public Object getEnd() { + return end; + } + + /** + * The gradient's colour ramp as ARGB values, in order, or an empty array when the + * gradient carries no usable colours. Dart hands the list over as a {@code DartList} of + * {@link Color}, so it arrives here as an untyped {@link java.util.List}. + */ + public int[] colorRamp() { + if (!(colors instanceof java.util.List)) { + return new int[0]; + } + java.util.List list = (java.util.List) colors; + java.util.List out = new java.util.ArrayList(); + for (Object o : list) { + if (o instanceof Color) { + out.add(Integer.valueOf(((Color) o).argb())); + } + } + int[] ramp = new int[out.size()]; + for (int i = 0; i < ramp.length; i++) { + ramp[i] = out.get(i).intValue(); + } + return ramp; + } + + /** Produces a shader painting this gradient over {@code rect}. */ + public Shader createShader(Rect rect, Object textDirection) { + return new GradientShader(this, rect); + } + + /** A concrete {@link Shader} bound to a gradient and a rectangle. */ + public static final class GradientShader extends Shader { + final Gradient gradient; + final Rect rect; + + GradientShader(Gradient gradient, Rect rect) { + this.gradient = gradient; + this.rect = rect; + } + + public Gradient gradient() { + return gradient; + } + + public Rect rect() { + return rect; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/IconData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/IconData.java new file mode 100644 index 00000000000..c2428a81c50 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/IconData.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A glyph in the CN1 material design icon font, identified by its codepoint + * (one of the {@code FontImage.MATERIAL_*} char constants). + */ +public final class IconData { + + private final char codePoint; + private String fontFamily; + + public IconData(char codePoint) { + this.codePoint = codePoint; + } + + /** + * Convenience overload for the transpiler, which emits every Dart {@code int} + * codepoint literal as a Java {@code long}. Gallery-font codepoints live in the + * BMP private-use area, so the narrowing to {@code char} is lossless in practice. + */ + public IconData(long codePoint) { + this((char) codePoint); + } + + public char codePoint() { + return codePoint; + } + + /** The named icon font this glyph belongs to (Dart's {@code IconData.fontFamily}). */ + public String fontFamily() { + return fontFamily; + } + + public void fontFamily(String v) { + this.fontFamily = v; + } + + @Override + public boolean equals(Object o) { + return o instanceof IconData && ((IconData) o).codePoint == codePoint; + } + + @Override + public int hashCode() { + return codePoint; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Icons.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Icons.java new file mode 100644 index 00000000000..7dcac18c683 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Icons.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.ui.FontImage; + +/** + * Material icons, named as in Flutter's {@code Icons} class and backed by + * the CN1 material icon font codepoints. The set covers every {@code Icons.*} + * constant referenced by the Flutter new_gallery integration app. + */ +public final class Icons { + + private Icons() { + } + + public static final IconData access_alarm = new IconData(FontImage.MATERIAL_ACCESS_ALARM); + public static final IconData access_time = new IconData(FontImage.MATERIAL_ACCESS_TIME); + public static final IconData account_circle = new IconData(FontImage.MATERIAL_ACCOUNT_CIRCLE); + public static final IconData add = new IconData(FontImage.MATERIAL_ADD); + public static final IconData add_circle = new IconData(FontImage.MATERIAL_ADD_CIRCLE); + public static final IconData add_circle_outline = new IconData(FontImage.MATERIAL_ADD_CIRCLE_OUTLINE); + public static final IconData add_comment = new IconData(FontImage.MATERIAL_ADD_COMMENT); + public static final IconData add_shopping_cart = new IconData(FontImage.MATERIAL_ADD_SHOPPING_CART); + public static final IconData airplanemode_active = new IconData(FontImage.MATERIAL_AIRPLANEMODE_ACTIVE); + public static final IconData alarm_on = new IconData(FontImage.MATERIAL_ALARM_ON); + public static final IconData arrow_back = new IconData(FontImage.MATERIAL_ARROW_BACK); + public static final IconData arrow_back_ios = new IconData(FontImage.MATERIAL_ARROW_BACK_IOS); + public static final IconData arrow_drop_down = new IconData(FontImage.MATERIAL_ARROW_DROP_DOWN); + public static final IconData arrow_drop_up = new IconData(FontImage.MATERIAL_ARROW_DROP_UP); + public static final IconData arrow_forward = new IconData(FontImage.MATERIAL_ARROW_FORWARD); + public static final IconData arrow_forward_ios = new IconData(FontImage.MATERIAL_ARROW_FORWARD_IOS); + public static final IconData arrow_left = new IconData(FontImage.MATERIAL_ARROW_LEFT); + public static final IconData attach_money = new IconData(FontImage.MATERIAL_ATTACH_MONEY); + public static final IconData book = new IconData(FontImage.MATERIAL_BOOK); + public static final IconData bookmark_border = new IconData(FontImage.MATERIAL_BOOKMARK_BORDER); + public static final IconData brightness_5 = new IconData(FontImage.MATERIAL_BRIGHTNESS_5); + public static final IconData calendar_today = new IconData(FontImage.MATERIAL_CALENDAR_TODAY); + public static final IconData camera_enhance = new IconData(FontImage.MATERIAL_CAMERA_ENHANCE); + public static final IconData check = new IconData(FontImage.MATERIAL_CHECK); + public static final IconData check_circle = new IconData(FontImage.MATERIAL_CHECK_CIRCLE); + public static final IconData check_circle_outline = new IconData(FontImage.MATERIAL_CHECK_CIRCLE_OUTLINE); + public static final IconData chevron_left = new IconData(FontImage.MATERIAL_CHEVRON_LEFT); + public static final IconData chevron_right = new IconData(FontImage.MATERIAL_CHEVRON_RIGHT); + public static final IconData close = new IconData(FontImage.MATERIAL_CLOSE); + public static final IconData code = new IconData(FontImage.MATERIAL_CODE); + public static final IconData comment = new IconData(FontImage.MATERIAL_COMMENT); + public static final IconData create = new IconData(FontImage.MATERIAL_CREATE); + public static final IconData credit_card = new IconData(FontImage.MATERIAL_CREDIT_CARD); + public static final IconData date_range = new IconData(FontImage.MATERIAL_DATE_RANGE); + public static final IconData delete = new IconData(FontImage.MATERIAL_DELETE); + public static final IconData directions_bike = new IconData(FontImage.MATERIAL_DIRECTIONS_BIKE); + public static final IconData edit = new IconData(FontImage.MATERIAL_EDIT); + public static final IconData email = new IconData(FontImage.MATERIAL_EMAIL); + public static final IconData favorite = new IconData(FontImage.MATERIAL_FAVORITE); + public static final IconData favorite_border = new IconData(FontImage.MATERIAL_FAVORITE_BORDER); + public static final IconData feedback = new IconData(FontImage.MATERIAL_FEEDBACK); + public static final IconData format_bold = new IconData(FontImage.MATERIAL_FORMAT_BOLD); + public static final IconData format_italic = new IconData(FontImage.MATERIAL_FORMAT_ITALIC); + public static final IconData format_underline = new IconData(FontImage.MATERIAL_FORMAT_UNDERLINE); + public static final IconData fullscreen = new IconData(FontImage.MATERIAL_FULLSCREEN); + public static final IconData help = new IconData(FontImage.MATERIAL_HELP); + public static final IconData home = new IconData(FontImage.MATERIAL_HOME); + public static final IconData hotel = new IconData(FontImage.MATERIAL_HOTEL); + public static final IconData info = new IconData(FontImage.MATERIAL_INFO); + public static final IconData info_outline = new IconData(FontImage.MATERIAL_INFO_OUTLINE); + public static final IconData keyboard_arrow_down = new IconData(FontImage.MATERIAL_KEYBOARD_ARROW_DOWN); + public static final IconData keyboard_arrow_up = new IconData(FontImage.MATERIAL_KEYBOARD_ARROW_UP); + public static final IconData library_books = new IconData(FontImage.MATERIAL_LIBRARY_BOOKS); + public static final IconData link = new IconData(FontImage.MATERIAL_LINK); + public static final IconData lock = new IconData(FontImage.MATERIAL_LOCK); + public static final IconData menu = new IconData(FontImage.MATERIAL_MENU); + public static final IconData mic = new IconData(FontImage.MATERIAL_MIC); + public static final IconData money_off = new IconData(FontImage.MATERIAL_MONEY_OFF); + public static final IconData more_vert = new IconData(FontImage.MATERIAL_MORE_VERT); + public static final IconData more_horiz = new IconData(FontImage.MATERIAL_MORE_HORIZ); + public static final IconData not_interested = new IconData(FontImage.MATERIAL_NOT_INTERESTED); + public static final IconData notifications = new IconData(FontImage.MATERIAL_NOTIFICATIONS); + public static final IconData person = new IconData(FontImage.MATERIAL_PERSON); + public static final IconData person_add = new IconData(FontImage.MATERIAL_PERSON_ADD); + public static final IconData phone = new IconData(FontImage.MATERIAL_PHONE); + public static final IconData photo = new IconData(FontImage.MATERIAL_PHOTO); + public static final IconData photo_library = new IconData(FontImage.MATERIAL_PHOTO_LIBRARY); + public static final IconData pie_chart = new IconData(FontImage.MATERIAL_PIE_CHART); + public static final IconData place = new IconData(FontImage.MATERIAL_PLACE); + public static final IconData remove = new IconData(FontImage.MATERIAL_REMOVE); + public static final IconData remove_circle_outline = new IconData(FontImage.MATERIAL_REMOVE_CIRCLE_OUTLINE); + public static final IconData refresh = new IconData(FontImage.MATERIAL_REFRESH); + public static final IconData replay = new IconData(FontImage.MATERIAL_REPLAY); + public static final IconData reply_all = new IconData(FontImage.MATERIAL_REPLY_ALL); + public static final IconData restaurant_menu = new IconData(FontImage.MATERIAL_RESTAURANT_MENU); + public static final IconData search = new IconData(FontImage.MATERIAL_SEARCH); + public static final IconData security = new IconData(FontImage.MATERIAL_SECURITY); + public static final IconData settings = new IconData(FontImage.MATERIAL_SETTINGS); + public static final IconData share = new IconData(FontImage.MATERIAL_SHARE); + public static final IconData shopping_cart = new IconData(FontImage.MATERIAL_SHOPPING_CART); + public static final IconData sort = new IconData(FontImage.MATERIAL_SORT); + public static final IconData star = new IconData(FontImage.MATERIAL_STAR); + public static final IconData star_border = new IconData(FontImage.MATERIAL_STAR_BORDER); + public static final IconData table_chart = new IconData(FontImage.MATERIAL_TABLE_CHART); + public static final IconData tune = new IconData(FontImage.MATERIAL_TUNE); + public static final IconData vertical_split = new IconData(FontImage.MATERIAL_VERTICAL_SPLIT); + public static final IconData visibility = new IconData(FontImage.MATERIAL_VISIBILITY); + public static final IconData visibility_off = new IconData(FontImage.MATERIAL_VISIBILITY_OFF); + public static final IconData web_asset = new IconData(FontImage.MATERIAL_WEB_ASSET); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageConfiguration.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageConfiguration.java new file mode 100644 index 00000000000..f49994b690b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.Size; + +/** + * The context handed to a custom {@code Decoration}'s box painter + * ({@code ImageConfiguration} in Flutter): the target size, device pixel ratio, + * text direction and locale. new_gallery's tab-indicator and pie-chart painters + * read {@link #size()} to lay out their geometry. + */ +public class ImageConfiguration { + + /** The empty configuration (Dart's {@code ImageConfiguration.empty}). */ + public static final ImageConfiguration empty = new ImageConfiguration(); + + private Size size; + private Double devicePixelRatio; + private TextDirection textDirection; + private Locale locale; + + public ImageConfiguration() { + } + + // Named-parameter setters. + public void size(Size v) { + this.size = v; + } + + public void devicePixelRatio(double v) { + this.devicePixelRatio = v; + } + + public void textDirection(TextDirection v) { + this.textDirection = v; + } + + public void locale(Locale v) { + this.locale = v; + } + + public Size size() { + return size; + } + + public Double devicePixelRatio() { + return devicePixelRatio; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageProvider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageProvider.java new file mode 100644 index 00000000000..05bfda0266b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ImageProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Identifies an image without committing to how it is loaded — Flutter's + * {@code ImageProvider}. Concrete providers: {@link AssetImage}, + * {@link NetworkImage}. + */ +public abstract class ImageProvider { + + /** + * A stable identity for the image source, used by consumers to detect + * source changes across in-place widget updates. + */ + public abstract String sourceKey(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/InheritedValueProvider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/InheritedValueProvider.java new file mode 100644 index 00000000000..a34296f9f3b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/InheritedValueProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Implemented by widgets that publish a value to their subtree by type + * (provider's {@code Provider}/{@code ChangeNotifierProvider} and scoped_model's + * {@code ScopedModel}). {@link BuildContext#providerValueOfType(Class)} walks the + * element tree and asks each ancestor provider whether it supplies the requested + * type. + */ +public interface InheritedValueProvider { + + /** + * The published value when it is assignable to {@code type}, otherwise null. + */ + Object providedValueFor(Class type); + + /** + * Whether this provider answers a lookup for {@code type} -- decided separately from + * the value, because a provider can match and publish null ({@code Provider} + * before sign-in). Read from the value alone, such a provider was skipped: the lookup + * went past it to an outer one, or to nothing, and never subscribed, so the reader + * was not rebuilt when the value arrived. The default keeps the value-based answer + * for providers with no type of their own. + */ + default boolean providesType(Class type) { + return providedValueFor(type) != null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/InputBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/InputBorder.java new file mode 100644 index 00000000000..6312a1eed10 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/InputBorder.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The border drawn around a Material text field — Flutter's {@code InputBorder}. + */ +public abstract class InputBorder extends ShapeBorder { + + /** {@code InputBorder.none}: the "no border" sentinel. */ + public static final InputBorder none = new NoInputBorder(); + + // Flutter's default is an ordinary BorderSide -- opaque black, one logical + // pixel -- not "none". Defaulting to none meant an OutlineInputBorder that + // states no side of its own drew a rounded background with an INVISIBLE + // outline, so the text-field demo's Life story and Salary boxes, whose + // whole decoration is that outline, rendered as bare labels. + BorderSide borderSide = BorderSide.solidBlack(); + + public void borderSide(BorderSide v) { + this.borderSide = v == null ? BorderSide.none : v; + } + + public BorderSide getBorderSide() { + return borderSide; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Key.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Key.java new file mode 100644 index 00000000000..f0ddb0c0379 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Key.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Base class for widget keys. Keys control how the element reconciler matches + * widgets across rebuilds: two widgets can only update the same element when + * their runtime class matches and their keys are equal. + */ +public class Key { + + private final Object value; + + protected Key() { + this.value = null; + } + + /** + * Dart's {@code Key(String value)} is a factory returning a value key; model it + * as a concrete key carrying the value so {@code new Key(...)} instantiates. + */ + public Key(Object value) { + this.value = value; + } + + public Object keyValue() { + return value; + } + + /// A key WITH a value compares by that value; one without compares by + /// identity. Only the protected no-argument constructor makes a key without a + /// value, and the keys built through it -- UniqueKey, GlobalKey -- are the + /// ones Flutter compares by identity. Treating two null values as equal made + /// every UniqueKey of a class equal to every other (hash 0 for all), so + /// Widget.canUpdate kept an element and its State that a fresh UniqueKey was + /// meant to replace, and keyed reconciliation collapsed distinct siblings; + /// every GlobalKey likewise equalled every other. ValueKey and ObjectKey keep + /// their own rules. + @Override + public boolean equals(Object o) { + if (!(o instanceof Key) || o.getClass() != getClass()) { + return false; + } + if (value == null) { + return this == o; + } + return value.equals(((Key) o).value); + } + + @Override + public int hashCode() { + return value == null ? System.identityHashCode(this) : value.hashCode(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/LinearGradient.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/LinearGradient.java new file mode 100644 index 00000000000..e194b43b0fe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/LinearGradient.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A 2D linear gradient — Flutter's {@code LinearGradient}. */ +public class LinearGradient extends Gradient { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Locale.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Locale.java new file mode 100644 index 00000000000..fdc2753bb33 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Locale.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A Unicode locale identifier: a required language code and an optional country + * code (Flutter's {@code Locale(languageCode, [countryCode])}). + */ +public class Locale { + + private final String languageCode; + private final String countryCode; + + public Locale(String languageCode) { + this(languageCode, null); + } + + public Locale(String languageCode, String countryCode) { + this.languageCode = languageCode; + this.countryCode = countryCode; + } + + public String languageCode() { + return languageCode; + } + + public String countryCode() { + return countryCode; + } + + @Override + public String toString() { + return countryCode == null ? languageCode : languageCode + "_" + countryCode; + } + + /// Value equality over the codes, as Flutter's Locale has. Inheriting + /// identity made two separately built Locale("en", "US") values unequal, so a + /// check against Localizations.localeOf(context) failed and locale-keyed maps + /// and sets missed, although the codes matched. + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Locale)) { + return false; + } + Locale other = (Locale) o; + return same(languageCode, other.languageCode) && same(countryCode, other.countryCode); + } + + @Override + public int hashCode() { + int h = languageCode == null ? 0 : languageCode.hashCode(); + return 31 * h + (countryCode == null ? 0 : countryCode.hashCode()); + } + + private static boolean same(String a, String b) { + return a == null ? b == null : a.equals(b); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisAlignment.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisAlignment.java new file mode 100644 index 00000000000..de79e372c00 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisAlignment.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How Flex (Row/Column) distributes free space along its main axis. + */ +public enum MainAxisAlignment { + start, end, center, spaceBetween, spaceAround, spaceEvenly +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisSize.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisSize.java new file mode 100644 index 00000000000..f4d839faa4e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MainAxisSize.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Whether Flex (Row/Column) shrink-wraps its children (min) or fills the + * available main-axis extent (max). + */ +public enum MainAxisSize { + min, max +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MathUtil.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MathUtil.java new file mode 100644 index 00000000000..658c7ed8f1c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MathUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Small math helpers from dart:ui that new_gallery reaches by bare name. + * Currently only {@code lerpDouble}, used by the cut-corners input border to + * animate its notch. + */ +public final class MathUtil { + + private MathUtil() { + } + + /** + * dart:ui's top-level {@code lerpDouble(a, b, t)}: linearly interpolate + * between two nullable numbers. Returns null when both {@code a} and + * {@code b} are null; treats a lone null endpoint as 0. {@code a}/{@code b} + * are declared {@code Object} to mirror the {@code num?} stub (they arrive + * as boxed {@link Double}/{@link Long}). + */ + public static Double lerpDouble(Object a, Object b, double t) { + if (a == null && b == null) { + return null; + } + double da = toDouble(a); + double db = toDouble(b); + return da + (db - da) * t; + } + + private static double toDouble(Object n) { + if (n instanceof Number) { + return ((Number) n).doubleValue(); + } + return 0.0; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQuery.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQuery.java new file mode 100644 index 00000000000..9a879575638 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQuery.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Display-metric scope, mirroring Flutter's {@code MediaQuery.of(context)}. + * + *

An in-tree MediaQuery now actually SCOPES its subtree. It used to be decorative: + * {@code of(context)} ignored the context and always recomputed from the CN1 Display, so a + * widget that wrapped part of the app to override the metrics — a smaller size, a different + * text scale, the safe-area padding it wants its children to see — was silently overruled. + * Flutter's own {@code removePadding}/{@code copyWith} idiom depends on this working.

+ * + *

Without an ancestor the metrics still come from the Display, which is the right default + * for the root of the app.

+ * + *

Safe area: the one rule

+ * + *

Status bars, notches, display cutouts and home indicators are all the same thing here, + * and there is exactly one path from the device to the pixels. Nothing else may inset for + * them, on any platform:

+ * + *
    + *
  1. The port answers {@code Form.getSafeArea()}. This is the only platform-specific + * input, and it is the only place a platform difference is allowed to exist.
  2. + *
  3. {@code MediaQueryData.fromDisplay} converts that rectangle into {@code padding} once, + * turning device pixels into logical ones. Everything downstream reads padding and never + * asks the port again.
  4. + *
  5. {@code FlutterUI.stripChrome} turns Codename One's own safe-area layout OFF + * ({@code Container.setSafeArea(false)}). CN1 would otherwise hold the content off the + * cutout as well, and the inset would be applied twice -- which shows up as a band of the + * Form's colour above everything the app drew.
  6. + *
  7. A widget that spends some of the padding removes what it spent for its + * descendants, via {@code removePadding}: the Scaffold body drops the top when an AppBar + * stands in for it and the bottom under a bottom bar, and a scroll view drops its own + * axis after padding its content. {@code SafeArea} then applies whatever is left.
  8. + *
+ * + *

Because every consumer subtracts rather than recomputes, the invariant is that padding + * is applied exactly once along any path from the root to a leaf. Two things break it, and + * both have:

+ * + *
    + *
  • Insetting outside this chain. Any port-specific "hold it off the status bar" + * is a second application. The answer is always to read {@code MediaQuery.padding}.
  • + *
  • Carrying a subtraction across a boundary it does not belong to. A route is + * mounted with a fallback ancestor, and using the widget that pushed it let a scroll + * view's {@code removePadding} leak into the new page, whose SafeArea then resolved to + * zero. Routes anchor at the nearest navigator scope for this reason -- see + * {@code Navigator.pushingElement}.
  • + *
+ * + *

The rule is verified on desktop and iOS by opening a study, pushing a page out of a + * scrolled list, and checking that the pushed page starts at the inset rather than at zero.

+ */ +public class MediaQuery extends com.codename1.flutter.widgets.InheritedWidget { + + private MediaQueryData data; + + public MediaQuery() { + } + + /** The metrics this scope imposes on its subtree — Flutter's {@code MediaQuery.data}. */ + public void data(MediaQueryData v) { + this.data = v; + } + + public MediaQueryData getData() { + return data; + } + + @Override + public boolean updateShouldNotify(com.codename1.flutter.widgets.InheritedWidget oldWidget) { + return !(oldWidget instanceof MediaQuery) || ((MediaQuery) oldWidget).data != data; + } + + /** The nearest enclosing scope's metrics, else the Display's. */ + public static MediaQueryData of(BuildContext context) { + if (context != null) { + try { + MediaQuery q = context.maybeDependOnInheritedWidgetOfExactType(MediaQuery.class); + if (q != null && q.data != null) { + return q.data; + } + } catch (Throwable t) { + // fall back to the Display below + } + } + return MediaQueryData.fromDisplay(formOf(context)); + } + + /** + * The Form the subtree at {@code context} belongs to, or null. + * + *

Asked instead of {@code Display.getCurrent()} because the first screen is BUILT + * BEFORE IT IS SHOWN: {@code runApp} mounts the tree into a new Form and shows it + * afterwards, so during that first build nothing is current and every safe-area + * lookup answered zero. The gallery's home page then laid its title out under the + * status bar and its scroll view took no top inset, and the screen only corrected + * itself if something later re-mounted it -- which a route push does, and which is + * why this was invisible to any measurement taken after opening a route.

+ */ + private static com.codename1.ui.Form formOf(BuildContext context) { + if (!(context instanceof Element)) { + return null; + } + // WALK UP. An element's own host is not always the Form's: every paint effect -- + // Material, Opacity, Transform, a clip -- owns a nested RenderHost for its + // subtree, and a nested host does not necessarily carry the Form. Asking only the + // immediate host therefore answered null for anything under one, the safe area + // came back as zero, and the page laid its content out under the status bar. + // + // That is how the mail study's message view ended up with its title across the + // clock while the compose page beside it was correct: the message view is opened + // through an OpenContainer, which wraps the page in a Material, and the compose + // page is not. + for (Element e = (Element) context; e != null; e = e.parent()) { + com.codename1.flutter.rendering.RenderHost h = e.host(); + if (h != null && h.form() != null) { + return h.form(); + } + } + return null; + } + + /** {@code MediaQuery.sizeOf}: the ambient display size. */ + public static com.codename1.flutter.rendering.Size sizeOf(BuildContext context) { + return of(context).size(); + } + + /** {@code MediaQuery.paddingOf}: the ambient safe-area padding. */ + public static EdgeInsets paddingOf(BuildContext context) { + return of(context).padding(); + } + + /** + * {@code MediaQuery.viewInsetsOf}: the insets intruded by the system (e.g. + * the on-screen keyboard). This runtime does not model view insets, so the + * value is zero. + */ + public static EdgeInsets viewInsetsOf(BuildContext context) { + return EdgeInsets.zero; + } + + /** + * {@code MediaQuery.removePadding}: a subtree that sees the ambient metrics + * with the selected padding edges already spent. + */ + public static Widget removePadding(BuildContext context, Boolean removeLeft, Boolean removeTop, + Boolean removeRight, Boolean removeBottom, Widget child) { + return scope(of(context).removePadding( + removeLeft, removeTop, removeRight, removeBottom), child); + } + + /** A subtree that sees {@code data} instead of whatever is ambient. */ + public static MediaQuery scope(MediaQueryData data, Widget child) { + MediaQuery q = new MediaQuery(); + q.data(data); + q.child(child); + return q; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQueryData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQueryData.java new file mode 100644 index 00000000000..89f42f88115 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MediaQueryData.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Display; + +/** + * A snapshot of the display metrics {@link MediaQuery#of} returns, expressed + * the way Flutter expresses them: + *
    + *
  • {@link #size()} — the display size in LOGICAL pixels (CN1 device + * pixels divided by {@link Dp#scale()})
  • + *
  • {@link #devicePixelRatio()} — device pixels per logical pixel + * ({@link Dp#scale()}, bucketed like Android/Flutter density + * buckets)
  • + *
  • {@link #platformBrightness()} — the platform dark-mode setting + * (light when unknown)
  • + *
+ * + *

Headless (no Display) the sensible defaults are a 0x0 size, ratio 1.0 + * and light brightness.

+ */ +public class MediaQueryData { + + private final Size size; + private final double devicePixelRatio; + private final Brightness platformBrightness; + private final double textScaleFactor; + private final EdgeInsets padding; + + public MediaQueryData(Size size, double devicePixelRatio, Brightness platformBrightness) { + this(size, devicePixelRatio, platformBrightness, 1.0, EdgeInsets.all(0)); + } + + public MediaQueryData(Size size, double devicePixelRatio, Brightness platformBrightness, + double textScaleFactor, EdgeInsets padding) { + this.size = size; + this.devicePixelRatio = devicePixelRatio; + this.platformBrightness = platformBrightness == null ? Brightness.light : platformBrightness; + this.textScaleFactor = textScaleFactor; + this.padding = padding == null ? EdgeInsets.all(0) : padding; + } + + public Size size() { + return size; + } + + public double devicePixelRatio() { + return devicePixelRatio; + } + + public Brightness platformBrightness() { + return platformBrightness; + } + + /** + * The number of font pixels per logical pixel (legacy Flutter accessor; + * defaults to 1.0 — this pass does not read the platform text-scale). + */ + public double textScaleFactor() { + return textScaleFactor; + } + + /** + * The parts of the display partially obscured by system UI (defaults to + * {@link EdgeInsets#all(double) EdgeInsets.all(0)}). + */ + public EdgeInsets padding() { + return padding; + } + + /** + * The parts of the display obscured by system UI that the app can still + * draw under (e.g. the on-screen keyboard). This pass does not track the + * keyboard, so it reports no insets. + */ + public EdgeInsets viewInsets() { + return EdgeInsets.all(0); + } + + /** + * The parts of the display obscured by system UI regardless of whether the + * app can draw under them (e.g. a hardware notch, the home indicator). With no + * keyboard inset -- this runtime tracks none, so viewInsets is zero -- Flutter's + * viewPadding equals padding, which holds the safe-area insets fromDisplay read. + * It reported zero, so a bar sized from viewPadding ran under the notch. + */ + public EdgeInsets viewPadding() { + return padding; + } + + /** + * Returns a copy with the supplied (non-null) values overridden. Parameter + * order matches the Dart stub. + */ + public MediaQueryData copyWith(Size size, Double devicePixelRatio, Double textScaleFactor, + EdgeInsets padding, Brightness platformBrightness) { + return new MediaQueryData( + size != null ? size : this.size, + devicePixelRatio != null ? devicePixelRatio : this.devicePixelRatio, + platformBrightness != null ? platformBrightness : this.platformBrightness, + textScaleFactor != null ? textScaleFactor : this.textScaleFactor, + padding != null ? padding : this.padding); + } + + /** + * Returns a copy with the selected padding edges zeroed — Flutter's + * {@code MediaQueryData.removePadding}. + * + *

This is how a safe area is spent exactly once. A {@code SafeArea} or a + * {@code Scaffold} that insets its child for the notch hands the child a + * media query with that edge already consumed; without it, every nested + * safe area insets for the same notch again. Returning {@code this} made + * the whole idiom inert.

+ */ + public MediaQueryData removePadding(Boolean removeLeft, Boolean removeTop, + Boolean removeRight, Boolean removeBottom) { + boolean left = Boolean.TRUE.equals(removeLeft); + boolean top = Boolean.TRUE.equals(removeTop); + boolean right = Boolean.TRUE.equals(removeRight); + boolean bottom = Boolean.TRUE.equals(removeBottom); + if (!left && !top && !right && !bottom) { + return this; + } + EdgeInsets p = padding(); + return copyWith(null, null, null, EdgeInsets.fromLTRB( + left ? 0 : p.left(), + top ? 0 : p.top(), + right ? 0 : p.right(), + bottom ? 0 : p.bottom()), null); + } + + /** + * Builds the snapshot from the current CN1 Display, or the headless + * defaults when no Display is initialized. + */ + public static MediaQueryData fromDisplay() { + return fromDisplay(null); + } + + /** + * As {@link #fromDisplay()}, for a subtree that knows which Form it is in. + * + *

The safe area has to be asked of a Form, and the first screen is BUILT BEFORE + * IT IS SHOWN, so {@code Display.getCurrent()} is null throughout that build and + * every inset came back zero. Passing the Form the subtree actually belongs to + * removes the dependency on what happens to be on screen.

+ */ + public static MediaQueryData fromDisplay(com.codename1.ui.Form form) { + if (!Display.isInitialized()) { + return new MediaQueryData(new Size(0, 0), 1.0, Brightness.light); + } + Display d = Display.getInstance(); + Boolean dark = null; + try { + dark = d.isDarkMode(); + } catch (Throwable ignore) { + // ports without dark-mode detection + } + noteFirstSize(d.getDisplayWidth(), d.getDisplayHeight()); + return compute(d.getDisplayWidth(), d.getDisplayHeight(), Dp.scale(), dark, + safeAreaInsets(d, form)); + } + + /// The display size the FIRST ambient lookup saw, against the one in force + /// now. An adaptive application asks the media query which layout it is, so + /// if these differ the app was built for a screen it is not on. + private static String firstSize; + + private static void noteFirstSize(int w, int h) { + if (firstSize == null) { + firstSize = w + "x" + h; + } + } + + /** {@code first@now} display sizes; see {@link #firstSize}. */ + public static String sizeHistory() { + String now = "?"; + try { + if (Display.isInitialized()) { + Display d = Display.getInstance(); + now = d.getDisplayWidth() + "x" + d.getDisplayHeight(); + } + } catch (Throwable ignore) { + now = "?"; + } + return (firstSize == null ? "-" : firstSize) + "->" + now + "@" + Dp.scale(); + } + + /** + * The device's safe-area insets, in LOGICAL pixels — what {@code MediaQuery.padding} + * means in Flutter. + * + *

This used to be left at zero, and that is not a cosmetic omission: content ran + * under the notch, the status bar and the home indicator, and controls near the bottom + * edge could not be reached. Anything reading {@code MediaQuery.of(context).padding} got + * nothing to work with — the gallery sizes its settings button as + * {@code height + padding.top}, so that button came out short by the notch.

+ * + *

Codename One already knows the answer ({@code Form.getSafeArea()}, backed by the + * port's {@code getDisplaySafeArea}); the runtime simply never asked.

+ */ + private static EdgeInsets safeAreaInsets(Display d, com.codename1.ui.Form form) { + try { + // The caller's own Form first: during runApp's build nothing is current yet. + com.codename1.ui.Form f = form != null ? form : d.getCurrent(); + if (f == null) { + return EdgeInsets.all(0); + } + com.codename1.ui.geom.Rectangle safe = f.getSafeArea(); + if (safe == null || safe.getWidth() <= 0 || safe.getHeight() <= 0) { + return EdgeInsets.all(0); + } + double scale = Dp.scale(); + if (scale <= 0) { + scale = 1; + } + int w = d.getDisplayWidth(); + int h = d.getDisplayHeight(); + // The safe rectangle is in pixels; the insets are the margins around it. + double left = Math.max(0, safe.getX()); + double top = Math.max(0, safe.getY()); + double right = Math.max(0, w - (safe.getX() + safe.getWidth())); + double bottom = Math.max(0, h - (safe.getY() + safe.getHeight())); + return EdgeInsets.fromLTRB(left / scale, top / scale, right / scale, bottom / scale); + } catch (Throwable t) { + // a port without safe-area support behaves as it did before + return EdgeInsets.all(0); + } + } + + /** + * The pure metric math (headless-testable): logical size is the pixel + * size divided by the scale; a non-positive scale falls back to 1; a null + * or FALSE dark flag maps to light. + */ + public static MediaQueryData compute(int widthPx, int heightPx, double scale, Boolean darkMode) { + return compute(widthPx, heightPx, scale, darkMode, EdgeInsets.all(0)); + } + + /** As above, with the device's safe-area insets (already in logical pixels). */ + public static MediaQueryData compute(int widthPx, int heightPx, double scale, + Boolean darkMode, EdgeInsets padding) { + if (scale <= 0) { + scale = 1; + } + return new MediaQueryData( + new Size(widthPx / scale, heightPx / scale), + scale, + Boolean.TRUE.equals(darkMode) ? Brightness.dark : Brightness.light, + 1.0, + padding); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/MemoryImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MemoryImage.java new file mode 100644 index 00000000000..a98d981b8ff --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/MemoryImage.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.typed_data.Uint8List; + +/** + * An {@link ImageProvider} that decodes an image from an in-memory byte buffer + * — Flutter's {@code MemoryImage}. new_gallery uses it for asset thumbnails it + * has already loaded into a {@code Uint8List}. + */ +public class MemoryImage extends ImageProvider { + + private final Uint8List bytes; + private double scale = 1.0; + + public MemoryImage(Uint8List bytes) { + this.bytes = bytes; + } + + /** Named parameter setter for the Dart {@code scale:} parameter. */ + public void scale(double v) { + this.scale = v; + } + + public Uint8List getBytes() { + return bytes; + } + + public double getScale() { + return scale; + } + + @Override + public String sourceKey() { + return "memory:" + System.identityHashCode(bytes) + "@" + scale; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/NetworkImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/NetworkImage.java new file mode 100644 index 00000000000..df51d59626b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/NetworkImage.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An {@link ImageProvider} that fetches an image over the network — Flutter's + * {@code NetworkImage}. + */ +public class NetworkImage extends ImageProvider { + + private final String url; + private double scale = 1.0; + private Object headers; + + public NetworkImage(String url) { + this.url = url; + } + + /** Named parameter setter for the Dart {@code scale:} parameter. */ + public void scale(double v) { + this.scale = v; + } + + /** Named parameter setter for the Dart {@code headers:} parameter. */ + public void headers(Object v) { + this.headers = v; + } + + public String getUrl() { + return url; + } + + public double getScale() { + return scale; + } + + /// The Dart {@code headers:} map, or null. Sent with the request that fetches + /// the image -- an authenticated image endpoint answers 401 without them. + public Object getHeaders() { + return headers; + } + + @Override + public String sourceKey() { + return "url:" + url; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/NoInputBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/NoInputBorder.java new file mode 100644 index 00000000000..38703fcb2ee --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/NoInputBorder.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The concrete "no border" {@link InputBorder} behind {@code InputBorder.none} + * — Flutter's private {@code _NoInputBorder}. Draws nothing. + */ +final class NoInputBorder extends InputBorder { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ObjectKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ObjectKey.java new file mode 100644 index 00000000000..e2fb1ddf6aa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ObjectKey.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A {@link Key} that is equal to another only when they wrap the identical + * object (reference equality, Dart's {@code identical}) — Flutter's + * {@code ObjectKey}. The mail preview cards key themselves by their backing + * email instance so the framework preserves element state as the list reorders. + */ +public class ObjectKey extends Key { + + private final Object value; + + public ObjectKey(Object value) { + this.value = value; + } + + public Object value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || o.getClass() != getClass()) { + return false; + } + // Dart's identical(), not Java's ==: numbers and booleans are values in Dart, so + // two separately boxed ObjectKey(1000) are the same key and the keyed element + // keeps its state across a rebuild. + return dart.runtime.DartRuntime.identical(((ObjectKey) o).value, this.value); + } + + @Override + public int hashCode() { + // Consistent with identical(): a boxed number or bool hashes by value. + if (value instanceof Long || value instanceof Double || value instanceof Boolean) { + return value.hashCode(); + } + return System.identityHashCode(value); + } + + @Override + public String toString() { + return "ObjectKey(" + value + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Offset.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Offset.java new file mode 100644 index 00000000000..381910c658e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Offset.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An immutable 2D floating-point offset (dx, dy) in logical pixels — Flutter's + * dart:ui {@code Offset}. Used both as a displacement vector and, in painting + * code, as a point in a coordinate space. + */ +public final class Offset { + + public static final Offset zero = new Offset(0, 0); + public static final Offset infinite = + new Offset(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + + private final double dx; + private final double dy; + + public Offset(double dx, double dy) { + this.dx = dx; + this.dy = dy; + } + + /** {@code Offset.fromDirection(direction, distance)} — polar to cartesian. */ + public static Offset fromDirection(double direction, double distance) { + return new Offset(distance * Math.cos(direction), distance * Math.sin(direction)); + } + + public double dx() { + return dx; + } + + public double dy() { + return dy; + } + + public double distance() { + return Math.sqrt(dx * dx + dy * dy); + } + + public double distanceSquared() { + return dx * dx + dy * dy; + } + + public double direction() { + return Math.atan2(dy, dx); + } + + public Offset scale(double scaleX, double scaleY) { + return new Offset(dx * scaleX, dy * scaleY); + } + + public Offset translate(double translateX, double translateY) { + return new Offset(dx + translateX, dy + translateY); + } + + public Offset $plus(Offset other) { + return new Offset(dx + other.dx, dy + other.dy); + } + + public Offset $minus(Offset other) { + return new Offset(dx - other.dx, dy - other.dy); + } + + public Offset $times(double operand) { + return new Offset(dx * operand, dy * operand); + } + + public Offset $div(double operand) { + return new Offset(dx / operand, dy / operand); + } + + /** + * Dart's {@code Offset & Size}: the rectangle whose top-left is this offset + * and whose extent is the given size. + */ + public Rect $bitAnd(com.codename1.flutter.rendering.Size other) { + return Rect.fromLTWH(dx, dy, other.width(), other.height()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Offset)) { + return false; + } + Offset p = (Offset) o; + return p.dx == dx && p.dy == dy; + } + + @Override + public int hashCode() { + long bits = ValueHash.bits(dx) * 31 + ValueHash.bits(dy); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "Offset(" + dx + ", " + dy + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlineInputBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlineInputBorder.java new file mode 100644 index 00000000000..c769aa9d98c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlineInputBorder.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A rounded-rectangle outline drawn around a Material text field — Flutter's {@code OutlineInputBorder}. */ +public class OutlineInputBorder extends InputBorder { + + private Object borderRadius; + private double gapPadding = 4.0; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public void gapPadding(double v) { + this.gapPadding = v; + } + + public Object getBorderRadius() { + return borderRadius; + } + + public double getGapPadding() { + return gapPadding; + } + + // Dart-getter-named accessors the shrine study's CutCornersBorder reads off + // `this`/`super` (Dart `get borderSide` / `borderRadius` / `gapPadding`). + + public BorderSide borderSide() { + return borderSide; + } + + public BorderRadius borderRadius() { + if (borderRadius instanceof BorderRadius) { + return (BorderRadius) borderRadius; + } + // Material's default, rather than null. + // + // Flutter states this as a DEFAULT PARAMETER VALUE, and a subclass that + // inherits it through `super.borderRadius = ...` does not necessarily + // carry it across the transpile -- Shrine's CutCornersBorder is exactly + // that, and its fields came out with square corners against the + // reference's rounded ones. An outline input border is never square. + return BorderRadius.circular(DEFAULT_RADIUS_LP); + } + + /** Flutter's {@code OutlineInputBorder} default corner radius. */ + private static final double DEFAULT_RADIUS_LP = 4; + + public double gapPadding() { + return gapPadding; + } + + /** + * Dart's {@code ShapeBorder.lerpFrom} / {@code lerpTo}: interpolate this + * border to/from another. The base outline has no distinctive geometry to + * blend here, so the fallback returns null (subclasses like CutCornersBorder + * override with their own blend). + */ + public ShapeBorder lerpFrom(ShapeBorder a, double t) { + return null; + } + + public ShapeBorder lerpTo(ShapeBorder b, double t) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlinedBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlinedBorder.java new file mode 100644 index 00000000000..d02426bff21 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/OutlinedBorder.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A {@link ShapeBorder} that draws a uniform {@link BorderSide} outline around + * a closed shape — Flutter's {@code OutlinedBorder}. + */ +public abstract class OutlinedBorder extends ShapeBorder { + + BorderSide side = BorderSide.none; + + public void side(BorderSide v) { + this.side = v == null ? BorderSide.none : v; + } + + public BorderSide getSide() { + return side; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/PageStorageKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PageStorageKey.java new file mode 100644 index 00000000000..60963b885a4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PageStorageKey.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A {@link ValueKey} that also identifies a subtree's {@code PageStorage} + * bucket — Flutter's {@code PageStorageKey}. new_gallery tags each home + * carousel card with one so its scroll offset is preserved across rebuilds. + * Equality follows {@link ValueKey}: same runtime class and equal value. + * + * @param the wrapped value type + */ +public class PageStorageKey extends ValueKey { + + public PageStorageKey(T value) { + super(value); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Paint.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Paint.java new file mode 100644 index 00000000000..b03eec212df --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Paint.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A mutable description of how to stroke or fill a shape on a {@link Canvas} — + * Flutter's dart:ui {@code Paint}. Transpiled Dart mutates it field-by-field + * ({@code paint..color = ... ..style = ...}); each field is a getter/setter + * pair here. + */ +public final class Paint { + + private Color color = new Color(0xFF000000L); + private PaintingStyle style = PaintingStyle.fill; + private double strokeWidth; + private StrokeCap strokeCap = StrokeCap.butt; + private StrokeJoin strokeJoin = StrokeJoin.miter; + private double strokeMiterLimit = 4.0; + private boolean isAntiAlias = true; + private Shader shader; + private Object maskFilter; + private Object colorFilter; + private Object blendMode; + + public Paint() { + } + + public Color color() { + return color; + } + + public void color(Color v) { + this.color = v; + } + + public PaintingStyle style() { + return style; + } + + public void style(PaintingStyle v) { + this.style = v == null ? PaintingStyle.fill : v; + } + + public double strokeWidth() { + return strokeWidth; + } + + public void strokeWidth(double v) { + this.strokeWidth = v; + } + + public StrokeCap strokeCap() { + return strokeCap; + } + + public void strokeCap(StrokeCap v) { + this.strokeCap = v == null ? StrokeCap.butt : v; + } + + public StrokeJoin strokeJoin() { + return strokeJoin; + } + + public void strokeJoin(StrokeJoin v) { + this.strokeJoin = v == null ? StrokeJoin.miter : v; + } + + public double strokeMiterLimit() { + return strokeMiterLimit; + } + + public void strokeMiterLimit(double v) { + this.strokeMiterLimit = v; + } + + public boolean isAntiAlias() { + return isAntiAlias; + } + + public void isAntiAlias(boolean v) { + this.isAntiAlias = v; + } + + public Shader shader() { + return shader; + } + + public void shader(Shader v) { + this.shader = v; + } + + public Object maskFilter() { + return maskFilter; + } + + public void maskFilter(Object v) { + this.maskFilter = v; + } + + public Object colorFilter() { + return colorFilter; + } + + public void colorFilter(Object v) { + this.colorFilter = v; + } + + public Object blendMode() { + return blendMode; + } + + public void blendMode(Object v) { + this.blendMode = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/PaintingStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PaintingStyle.java new file mode 100644 index 00000000000..2b18430653f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/PaintingStyle.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** Whether to paint the interior of a shape or just its edge — Flutter's {@code PaintingStyle}. */ +public enum PaintingStyle { + fill, + stroke +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Path.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Path.java new file mode 100644 index 00000000000..372fa124e72 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Path.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import java.util.ArrayList; +import java.util.List; + +/** + * A mutable path built from move/line/curve segments — Flutter's dart:ui + * {@code Path}. For this milestone the path records its subpath commands + * structurally (so painters can be transpiled and driven); faithful + * rasterization is deferred to the render layer. + */ +public final class Path { + + /** A single recorded path command: a verb plus its raw coordinate operands. */ + public static final class Segment { + public final String verb; + public final double[] coords; + + Segment(String verb, double[] coords) { + this.verb = verb; + this.coords = coords; + } + } + + private final List segments = new ArrayList(); + private double currentX; + private double currentY; + + public Path() { + } + + public List segments() { + return segments; + } + + public void moveTo(double x, double y) { + currentX = x; + currentY = y; + segments.add(new Segment("moveTo", new double[] {x, y})); + } + + public void lineTo(double x, double y) { + currentX = x; + currentY = y; + segments.add(new Segment("lineTo", new double[] {x, y})); + } + + public void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + currentX = x3; + currentY = y3; + segments.add(new Segment("cubicTo", new double[] {x1, y1, x2, y2, x3, y3})); + } + + public void quadraticBezierTo(double x1, double y1, double x2, double y2) { + currentX = x2; + currentY = y2; + segments.add(new Segment("quadraticBezierTo", new double[] {x1, y1, x2, y2})); + } + + public void conicTo(double x1, double y1, double x2, double y2, double w) { + currentX = x2; + currentY = y2; + segments.add(new Segment("conicTo", new double[] {x1, y1, x2, y2, w})); + } + + public void arcTo(Rect rect, double startAngle, double sweepAngle, boolean forceMoveTo) { + segments.add(new Segment("arcTo", + new double[] {rect.left(), rect.top(), rect.right(), rect.bottom(), + startAngle, sweepAngle, forceMoveTo ? 1 : 0})); + } + + public void arcToPoint(Offset arcEnd, Radius radius, double rotation, + boolean largeArc, boolean clockwise) { + currentX = arcEnd.dx(); + currentY = arcEnd.dy(); + segments.add(new Segment("arcToPoint", + new double[] {arcEnd.dx(), arcEnd.dy(), radius == null ? 0 : radius.x(), + radius == null ? 0 : radius.y(), rotation, + largeArc ? 1 : 0, clockwise ? 1 : 0})); + } + + public void relativeMoveTo(double dx, double dy) { + moveTo(currentX + dx, currentY + dy); + } + + public void relativeLineTo(double dx, double dy) { + lineTo(currentX + dx, currentY + dy); + } + + public void addRect(Rect rect) { + segments.add(new Segment("addRect", + new double[] {rect.left(), rect.top(), rect.right(), rect.bottom()})); + } + + public void addOval(Rect oval) { + segments.add(new Segment("addOval", + new double[] {oval.left(), oval.top(), oval.right(), oval.bottom()})); + } + + public void addRRect(RRect rrect) { + Rect r = rrect.outerRect(); + segments.add(new Segment("addRRect", + new double[] {r.left(), r.top(), r.right(), r.bottom()})); + } + + public void addPolygon(List points, boolean close) { + boolean first = true; + for (Offset p : points) { + if (first) { + moveTo(p.dx(), p.dy()); + first = false; + } else { + lineTo(p.dx(), p.dy()); + } + } + if (close) { + close(); + } + } + + public void addPath(Path path, Offset offset) { + for (Segment s : path.segments) { + segments.add(s); + } + } + + public void close() { + segments.add(new Segment("close", new double[0])); + } + + public void reset() { + segments.clear(); + currentX = 0; + currentY = 0; + } + + public boolean contains(Offset point) { + return false; + } + + public Path shift(Offset offset) { + Path p = new Path(); + for (Segment s : segments) { + double[] c = s.coords.clone(); + for (int i = 0; i + 1 < c.length; i += 2) { + c[i] += offset.dx(); + c[i + 1] += offset.dy(); + } + p.segments.add(new Segment(s.verb, c)); + } + return p; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RRect.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RRect.java new file mode 100644 index 00000000000..fd9f06c7b41 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RRect.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A rounded rectangle: an axis-aligned {@link Rect} with a {@link Radius} at + * each corner — Flutter's dart:ui {@code RRect}. + */ +public final class RRect { + + private final Rect rect; + private final Radius topLeft; + private final Radius topRight; + private final Radius bottomLeft; + private final Radius bottomRight; + + private RRect(Rect rect, Radius topLeft, Radius topRight, Radius bottomLeft, Radius bottomRight) { + this.rect = rect; + this.topLeft = topLeft == null ? Radius.zero : topLeft; + this.topRight = topRight == null ? Radius.zero : topRight; + this.bottomLeft = bottomLeft == null ? Radius.zero : bottomLeft; + this.bottomRight = bottomRight == null ? Radius.zero : bottomRight; + } + + public static RRect fromRectAndRadius(Rect rect, Radius radius) { + return new RRect(rect, radius, radius, radius, radius); + } + + public static RRect fromLTRBR(double left, double top, double right, double bottom, Radius radius) { + return new RRect(Rect.fromLTRB(left, top, right, bottom), radius, radius, radius, radius); + } + + public static RRect fromRectAndCorners(Rect rect, Radius topLeft, Radius topRight, + Radius bottomLeft, Radius bottomRight) { + return new RRect(rect, topLeft, topRight, bottomLeft, bottomRight); + } + + public Rect outerRect() { + return rect; + } + + public Radius tlRadius() { + return topLeft; + } + + public Radius trRadius() { + return topRight; + } + + public Radius blRadius() { + return bottomLeft; + } + + public Radius brRadius() { + return bottomRight; + } + + /** + * {@code RRect.middleRect}: the rectangle inside the rounded corners, i.e. + * the outer rect inset on each edge by the larger of the two corner radii + * touching that edge. + */ + public Rect middleRect() { + return Rect.fromLTRB( + rect.left() + Math.max(bottomLeft.x(), topLeft.x()), + rect.top() + Math.max(topLeft.y(), topRight.y()), + rect.right() - Math.max(topRight.x(), bottomRight.x()), + rect.bottom() - Math.max(bottomRight.y(), bottomLeft.y())); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RadialGradient.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RadialGradient.java new file mode 100644 index 00000000000..8f59b3e096e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RadialGradient.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A 2D radial gradient — Flutter's {@code RadialGradient}. */ +public class RadialGradient extends Gradient { + + double radius = 0.5; + Object focal; + double focalRadius; + + public void radius(double v) { + this.radius = v; + } + + public void focal(Object v) { + this.focal = v; + } + + public void focalRadius(double v) { + this.focalRadius = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Radius.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Radius.java new file mode 100644 index 00000000000..5d2bc2a63f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Radius.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A radius for the corner of a rounded rectangle, with independent x and y + * components — Flutter's dart:ui {@code Radius}. + */ +public final class Radius { + + public static final Radius zero = new Radius(0, 0); + + private final double x; + private final double y; + + private Radius(double x, double y) { + this.x = x; + this.y = y; + } + + public static Radius circular(double radius) { + return new Radius(radius, radius); + } + + public static Radius elliptical(double x, double y) { + return new Radius(x, y); + } + + /** Dart's {@code Radius.lerp(a, b, t)}: per-component linear interpolation. */ + public static Radius lerp(Radius a, Radius b, double t) { + if (a == null && b == null) return null; + if (a == null) return new Radius(b.x * t, b.y * t); + if (b == null) return new Radius(a.x * (1.0 - t), a.y * (1.0 - t)); + return new Radius(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Radius)) { + return false; + } + Radius r = (Radius) o; + return r.x == x && r.y == y; + } + + @Override + public int hashCode() { + long bits = ValueHash.bits(x) * 31 + ValueHash.bits(y); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "Radius.elliptical(" + x + ", " + y + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Rect.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Rect.java new file mode 100644 index 00000000000..b1ca640006e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Rect.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.Size; + +/** + * An immutable axis-aligned rectangle given by its four edges (left, top, + * right, bottom) in logical pixels — Flutter's dart:ui {@code Rect}. + */ +public final class Rect { + + public static final Rect zero = new Rect(0, 0, 0, 0); + public static final Rect largest = fromLTRB( + -1.0E9, -1.0E9, 1.0E9, 1.0E9); + + private final double left; + private final double top; + private final double right; + private final double bottom; + + private Rect(double left, double top, double right, double bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + public static Rect fromLTWH(double left, double top, double width, double height) { + return new Rect(left, top, left + width, top + height); + } + + public static Rect fromLTRB(double left, double top, double right, double bottom) { + return new Rect(left, top, right, bottom); + } + + public static Rect fromCircle(Offset center, double radius) { + return new Rect(center.dx() - radius, center.dy() - radius, + center.dx() + radius, center.dy() + radius); + } + + public static Rect fromCenter(Offset center, double width, double height) { + return new Rect(center.dx() - width / 2, center.dy() - height / 2, + center.dx() + width / 2, center.dy() + height / 2); + } + + public static Rect fromPoints(Offset a, Offset b) { + return new Rect(Math.min(a.dx(), b.dx()), Math.min(a.dy(), b.dy()), + Math.max(a.dx(), b.dx()), Math.max(a.dy(), b.dy())); + } + + public double left() { + return left; + } + + public double top() { + return top; + } + + public double right() { + return right; + } + + public double bottom() { + return bottom; + } + + public double width() { + return right - left; + } + + public double height() { + return bottom - top; + } + + public double shortestSide() { + return Math.min(Math.abs(width()), Math.abs(height())); + } + + public double longestSide() { + return Math.max(Math.abs(width()), Math.abs(height())); + } + + public boolean isEmpty() { + return left >= right || top >= bottom; + } + + /** Flutter's {@code isFinite}: no coordinate infinite OR NaN -- a NaN rect is not finite. */ + public boolean isFinite() { + return finite(left) && finite(top) && finite(right) && finite(bottom); + } + + private static boolean finite(double v) { + return !Double.isInfinite(v) && !Double.isNaN(v); + } + + public boolean hasNaN() { + return Double.isNaN(left) || Double.isNaN(top) + || Double.isNaN(right) || Double.isNaN(bottom); + } + + public Offset center() { + return new Offset((left + right) / 2, (top + bottom) / 2); + } + + public Offset topLeft() { + return new Offset(left, top); + } + + public Offset topCenter() { + return new Offset((left + right) / 2, top); + } + + public Offset topRight() { + return new Offset(right, top); + } + + public Offset centerLeft() { + return new Offset(left, (top + bottom) / 2); + } + + public Offset centerRight() { + return new Offset(right, (top + bottom) / 2); + } + + public Offset bottomLeft() { + return new Offset(left, bottom); + } + + public Offset bottomCenter() { + return new Offset((left + right) / 2, bottom); + } + + public Offset bottomRight() { + return new Offset(right, bottom); + } + + public Size size() { + return new Size(width(), height()); + } + + public boolean contains(Offset offset) { + return offset.dx() >= left && offset.dx() < right + && offset.dy() >= top && offset.dy() < bottom; + } + + public Rect translate(double translateX, double translateY) { + return new Rect(left + translateX, top + translateY, + right + translateX, bottom + translateY); + } + + public Rect shift(Offset offset) { + return translate(offset.dx(), offset.dy()); + } + + public Rect inflate(double delta) { + return new Rect(left - delta, top - delta, right + delta, bottom + delta); + } + + public Rect deflate(double delta) { + return inflate(-delta); + } + + public Rect intersect(Rect other) { + return new Rect(Math.max(left, other.left), Math.max(top, other.top), + Math.min(right, other.right), Math.min(bottom, other.bottom)); + } + + public Rect expandToInclude(Rect other) { + return new Rect(Math.min(left, other.left), Math.min(top, other.top), + Math.max(right, other.right), Math.max(bottom, other.bottom)); + } + + public boolean overlaps(Rect other) { + return right > other.left && other.right > left + && bottom > other.top && other.bottom > top; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Rect)) { + return false; + } + Rect r = (Rect) o; + return r.left == left && r.top == top && r.right == right && r.bottom == bottom; + } + + @Override + public int hashCode() { + long bits = ValueHash.bits(left); + bits = bits * 31 + ValueHash.bits(top); + bits = bits * 31 + ValueHash.bits(right); + bits = bits * 31 + ValueHash.bits(bottom); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "Rect.fromLTRB(" + left + ", " + top + ", " + right + ", " + bottom + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RelativeRect.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RelativeRect.java new file mode 100644 index 00000000000..c0a53f35317 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RelativeRect.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.Size; + +/** + * A rectangle described as insets from the four edges of a containing box — + * Flutter's {@code RelativeRect} (used by Positioned/Stack and the + * RelativeRectTween). + */ +public final class RelativeRect { + + public static final RelativeRect fill = new RelativeRect(0, 0, 0, 0); + + private final double left; + private final double top; + private final double right; + private final double bottom; + + private RelativeRect(double left, double top, double right, double bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + public static RelativeRect fromLTRB(double left, double top, double right, double bottom) { + return new RelativeRect(left, top, right, bottom); + } + + public static RelativeRect fromRect(Rect rect, Rect container) { + return fromLTRB( + rect.left() - container.left(), + rect.top() - container.top(), + container.right() - rect.right(), + container.bottom() - rect.bottom()); + } + + public static RelativeRect fromSize(Rect rect, Size container) { + return fromLTRB( + rect.left(), + rect.top(), + container.width() - rect.right(), + container.height() - rect.bottom()); + } + + public double left() { + return left; + } + + public double top() { + return top; + } + + public double right() { + return right; + } + + public double bottom() { + return bottom; + } + + /** + * Interpolates between two rects -- Flutter's {@code RelativeRect.lerp}. + * + *

A null end is treated as the other end, which is what Flutter does, so an + * animation with only one side configured still runs.

+ */ + public static RelativeRect lerp(RelativeRect a, RelativeRect b, double t) { + if (a == null && b == null) { + return null; + } + if (a == null) { + return fromLTRB(b.left * t, b.top * t, b.right * t, b.bottom * t); + } + if (b == null) { + double k = 1.0 - t; + return fromLTRB(a.left * k, a.top * k, a.right * k, a.bottom * k); + } + return fromLTRB(a.left + (b.left - a.left) * t, + a.top + (b.top - a.top) * t, + a.right + (b.right - a.right) * t, + a.bottom + (b.bottom - a.bottom) * t); + } + + public Rect toRect(Rect container) { + return Rect.fromLTRB( + left + container.left(), + top + container.top(), + container.right() - right, + container.bottom() - bottom); + } + + @Override + public String toString() { + return "RelativeRect.fromLTRB(" + left + ", " + top + ", " + right + ", " + bottom + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java new file mode 100644 index 00000000000..898886677f2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RenderElement.java @@ -0,0 +1,869 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * An element that participates in layout — the RenderBox-lite protocol. + * + *

A render element may own a retained CN1 {@link Component} (Text owns a + * Label, FloatingActionButton owns a CN1 FAB, ...) or own none at all + * (Column, Center, Padding are pure positioning math). All owned components + * across a subtree are FLAT children of the host's single CN1 container; this + * class positions them absolutely from the results of the constraint pass.

+ * + *

Protocol: {@link #layout(BoxConstraints)} — constraints go down, sizes + * come up (cached per constraints until {@link #markNeedsLayout()}), then + * {@link #position(int, int)} — absolute coordinates accumulate down the + * render tree and are written to the CN1 components.

+ */ +public abstract class RenderElement extends Element { + + private Component component; + private Size size = Size.ZERO; + private BoxConstraints lastConstraints; + /// The dry-layout slot, kept apart from the real one so the two cannot evict each other. + private Size drySize; + private BoxConstraints lastDryConstraints; + private boolean needsLayout = true; + + /// True while this box is inside its own {@link #performLayout}. + private boolean inLayout; + + /// Set when something invalidates this box WHILE it is being laid out. + /// + /// A layout is only valid for the children it was measured over, and a child can + /// appear in the middle of the pass that measures its parent: a LayoutBuilder sits + /// out a speculative measurement and inflates its subtree on a later one, which + /// happens underneath the enclosing Column's performLayout. The inflation marks the + /// Column dirty, but the Column then finished the very pass it had invalidated and + /// cleared the flag on its way out, so the mark was lost and the offsets it had + /// stored for the children it did NOT have were kept. + /// + /// The visible result was a screen with nothing on it. Returning from Reply's search + /// page rebuilt the mail list this way; every card was then positioned at the list's + /// origin with a pane of zero size, and a zero-sized pane clips the subtree it hosts, + /// so the cards were not merely stacked, they were invisible. Nothing recovered it + /// either, because every later pass hit the same clean cache. + private boolean remarkedDuringLayout; + + /** Offset of this box within its parent render element, set by the parent's performLayout. */ + private double relX; + private double relY; + + /** Absolute position within the host container, set by position(). */ + private int absX; + private int absY; + + protected RenderElement(Widget widget) { + super(widget); + } + + // ------------------------------------------------------------------ + // Element lifecycle + // ------------------------------------------------------------------ + + /// How much of start-up goes into creating and styling Codename One + /// components, and how many there are. Read via {@link #componentCost()}. + /// Attribution, not a feature: the first frame is dominated by layout, and + /// "layout" here includes realising a component for every box. + private static long componentMs; + private static int componentCount; + + private static long attachMs; + private static long selfMountMs; + private static long neutralizeMs; + + /// Component creation cost per render-element class. Creating a Codename + /// One component is 330us on the native build, which is two orders of + /// magnitude more than allocating one should cost; this says which + /// elements own it. + private static final java.util.Map, long[]> CREATE_BY_CLASS = + new java.util.HashMap, long[]>(); + + /** Component creation, worst render-element classes first. */ + public static String componentBreakdown() { + java.util.List, long[]>> rows = + new java.util.ArrayList, long[]>>( + CREATE_BY_CLASS.entrySet()); + java.util.Collections.sort(rows, + new java.util.Comparator, long[]>>() { + @Override + public int compare(java.util.Map.Entry, long[]> a, + java.util.Map.Entry, long[]> b) { + return Long.compare(b.getValue()[0], a.getValue()[0]); + } + }); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rows.size() && i < 8; i++) { + java.util.Map.Entry, long[]> e = rows.get(i); + String n = e.getKey().getName(); + int dot = n.lastIndexOf('.'); + sb.append(' ').append(dot < 0 ? n : n.substring(dot + 1)) + .append('=').append(e.getValue()[0]).append("ms/") + .append(e.getValue()[1]).append('x'); + } + return sb.toString(); + } + + /** Components created so far, and what they cost. */ + public static String componentCost() { + return componentCount + " component(s) in " + componentMs + "ms" + + " (create=" + (componentMs - neutralizeMs) + "ms neutralize=" + neutralizeMs + + "ms attach=" + attachMs + "ms selfMount=" + selfMountMs + "ms)"; + } + + @Override + public void mount(Element parent, int slot) { + if (!Trace.on()) { + super.mount(parent, slot); + component = createComponent(); + neutralizeCn1Behaviors(component); + if (component != null) { + componentCount++; + } + if (host != null && ownsComponent()) { + host.attach(this); + } + dirty = true; + performRebuild(); + return; + } + // Everything here EXCEPT performRebuild is this element's own cost; + // performRebuild recurses into the subtree, so timing it would just + // report the total again. + long m0 = System.currentTimeMillis(); + super.mount(parent, slot); + long t0 = System.currentTimeMillis(); + component = createComponent(); + long tn = System.currentTimeMillis(); + neutralizeCn1Behaviors(component); + if (component != null) { + componentCount++; + long took = System.currentTimeMillis() - t0; + componentMs += took; + neutralizeMs += System.currentTimeMillis() - tn; + long[] row = CREATE_BY_CLASS.get(getClass()); + if (row == null) { + row = new long[2]; + CREATE_BY_CLASS.put(getClass(), row); + } + row[0] += took; + row[1]++; + } + long a0 = System.currentTimeMillis(); + if (host != null && ownsComponent()) { + // Components attach in mount (depth-first) order, which equals + // element-tree order; when this mount replaces an existing + // subtree, Element.updateChild anchors the host's insertion + // cursor at the replaced components' index so the flat + // container's z-order stays in sync with the tree. + host.attach(this); + } + attachMs += System.currentTimeMillis() - a0; + selfMountMs += System.currentTimeMillis() - m0; + dirty = true; + performRebuild(); + } + + /** + * Whether this element contributes a component to the host's flat + * container. Defaults to owning a real CN1 component; headless test + * doubles may override to exercise the attach-order bookkeeping without + * instantiating CN1 components (which require an initialized Display). + */ + protected boolean ownsComponent() { + return component != null; + } + + /** + * Disables CN1 component behaviors that fight the Flutter layout model: + *
    + *
  • Text tickers — CN1 starts a marquee on a focused Label/Button + * whose text doesn't fit; Flutter clips instead, and a single + * transient under-sized layout pass would otherwise latch the + * ticker permanently ("bouncing" UI).
  • + *
  • Margins — styles derived from the Material theme carry + * millimeter margins. Our flat layout ignores margins, but CN1's + * own bookkeeping (focus scroll-to-visible, outer-size math) still + * sees them as phantom inflation, causing focus-driven jitter.
  • + *
+ */ + static void neutralizeCn1Behaviors(Component c) { + if (c == null) { + return; + } + if (c instanceof com.codename1.ui.Label) { + ((com.codename1.ui.Label) c).setTickerEnabled(false); + } + try { + if (needsNeutralizing(c)) { + com.codename1.ui.plaf.Style all = c.getAllStyles(); + all.setMarginUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + all.setMargin(0, 0, 0, 0); + unifyStateMetrics(c); + } + } catch (Throwable ignore) { + // styles unavailable headless + } + } + + /** + * Flutter geometry is state-invariant, but theme-derived CN1 styles can + * give the selected/pressed state a DIFFERENT font (often larger) and + * padding than unselected. Our constraint pass sizes components from the + * unselected metrics, so on focus CN1 would paint and re-measure with + * bigger metrics — text overflow plus layout shuffle ("bounce"). Copy + * the unselected font and padding onto every other state so a state + * change can never alter geometry; state styles keep their own colors. + */ + /// Whether this component needs its CN1 styling neutralised at all. + /// + /// getAllStyles() is not a read: it CREATES the selected, pressed and + /// disabled styles -- UIManager documents that each "always return a new + /// style instance" -- and then a proxy over the four. That is five Style + /// objects per component, built during the first frame purely so a margin + /// can be set to zero and the state metrics unified. + /// + /// Almost never necessary. A UIID whose theme already gives every state a + /// zero margin and matching font and padding is already what neutralising + /// would make it, so the whole thing can be skipped and those five objects + /// never exist. Decided once per UIID from the THEME's own styles (which + /// UIManager caches) rather than once per component, and re-decided when the + /// theme generation changes. + /// + /// When a UIID genuinely does differ it is neutralised exactly as before, so + /// this is a cost cut and not a behaviour change. + private static final java.util.HashMap NEEDS_NEUTRALIZE = + new java.util.HashMap(); + private static int neutralizeGeneration = -1; + + /// A component that can never enter the selected, pressed or disabled state + /// never consults those styles, so unifying them is work for nobody. + /// + /// This is most of the tree. Text, icons, images, dividers and plain boxes + /// are not focusable, are not Buttons and are enabled, so Codename One will + /// only ever paint them from the unselected style -- yet each was made to + /// build all three state styles plus a proxy so their metrics could be + /// matched against a state that cannot happen. + /// + /// Deliberately narrow: anything focusable, any Button (which paints a + /// pressed style on touch without being focused) and anything already + /// disabled still goes through the full path, so the geometry guarantee + /// holds exactly where a state change is possible. + private static boolean canChangeState(Component c) { + return c.isFocusable() + || c instanceof com.codename1.ui.Button + || !c.isEnabled(); + } + + /// The style an element writes its component's look into. + /// + /// getAllStyles() for a component that can change state, so every state it can + /// paint in carries the change. For one that cannot -- by the same test as + /// neutralising above -- its unselected style, which is the only one Codename One + /// will ever paint it from. getAllStyles() is not a cheap accessor: on first use it + /// builds the selected, pressed and disabled styles out of the theme and a proxy + /// over all four, and every text, icon, image, divider and gesture box in the tree + /// asked for it while mounting. Measured on the gallery's start-up, those styles + /// were about a fifth of everything mounting did, for states nothing can enter. + public static com.codename1.ui.plaf.Style stylesToSet(Component c) { + return canChangeState(c) ? c.getAllStyles() : c.getUnselectedStyle(); + } + + private static boolean needsNeutralizing(Component c) { + if (!canChangeState(c)) { + return false; + } + String uiid = c.getUIID(); + if (uiid == null) { + return true; + } + int gen = com.codename1.ui.plaf.UIManager.getThemeGeneration(); + if (gen != neutralizeGeneration) { + NEEDS_NEUTRALIZE.clear(); + neutralizeGeneration = gen; + } + Boolean known = NEEDS_NEUTRALIZE.get(uiid); + if (known != null) { + return known.booleanValue(); + } + boolean needs; + try { + com.codename1.ui.plaf.UIManager m = c.getUIManager(); + com.codename1.ui.plaf.Style un = m.getComponentStyle(uiid); + needs = hasMargin(un) + || hasMargin(m.getComponentSelectedStyle(uiid)) + || hasMargin(m.getComponentCustomStyle(uiid, "press")) + || hasMargin(m.getComponentCustomStyle(uiid, "dis")) + || metricsDiffer(un, m.getComponentSelectedStyle(uiid)) + || metricsDiffer(un, m.getComponentCustomStyle(uiid, "dis")) + || metricsDiffer(un, m.getComponentCustomStyle(uiid, "press")); + } catch (Throwable t) { + needs = true; + } + NEEDS_NEUTRALIZE.put(uiid, Boolean.valueOf(needs)); + return needs; + } + + /// A zero margin is zero in any unit, so the unit does not have to match. + private static boolean hasMargin(com.codename1.ui.plaf.Style s) { + if (s == null) { + return false; + } + return s.getMarginTop() != 0 || s.getMarginBottom() != 0 + || s.getMarginLeftNoRTL() != 0 || s.getMarginRightNoRTL() != 0; + } + + /// Whether a UIID's state styles differ from its unselected one at all. + /// + /// Asked ONCE per UIID, not once per component. getSelectedStyle(), + /// getDisabledStyle() and getPressedStyle() do not read a shared object -- + /// UIManager documents that they "always return a new style instance" -- so + /// touching all three to unify them minted three Style objects for every + /// component mounted. At 377 components that is over eleven hundred Styles + /// built during the first frame, and it measured as the 22ms "create" half + /// of the component cost. + /// + /// Almost none of them need it: a UIID whose theme gives every state the + /// same font and padding is already state-invariant, which is what the + /// unification was there to guarantee. Deciding that from the THEME (whose + /// per-UIID styles UIManager caches) settles it for every component sharing + /// the UIID, and the ones that genuinely differ still get unified. + /// + /// Keyed by UIID and theme generation, so a theme change re-decides. + private static final java.util.HashMap STATE_METRICS_DIFFER = + new java.util.HashMap(); + private static int stateMetricsGeneration = -1; + + private static boolean statesDifferForUiid(Component c) { + String uiid = c.getUIID(); + if (uiid == null) { + return true; + } + com.codename1.ui.plaf.UIManager m = c.getUIManager(); + int gen = com.codename1.ui.plaf.UIManager.getThemeGeneration(); + if (gen != stateMetricsGeneration) { + STATE_METRICS_DIFFER.clear(); + stateMetricsGeneration = gen; + } + Boolean known = STATE_METRICS_DIFFER.get(uiid); + if (known != null) { + return known.booleanValue(); + } + boolean differs; + try { + com.codename1.ui.plaf.Style un = m.getComponentStyle(uiid); + differs = metricsDiffer(un, m.getComponentSelectedStyle(uiid)) + || metricsDiffer(un, m.getComponentCustomStyle(uiid, "dis")) + || metricsDiffer(un, m.getComponentCustomStyle(uiid, "press")); + } catch (Throwable t) { + differs = true; + } + STATE_METRICS_DIFFER.put(uiid, Boolean.valueOf(differs)); + return differs; + } + + private static boolean metricsDiffer(com.codename1.ui.plaf.Style a, + com.codename1.ui.plaf.Style b) { + if (b == null) { + return false; + } + return a.getFont() != b.getFont() + || a.getPaddingTop() != b.getPaddingTop() + || a.getPaddingBottom() != b.getPaddingBottom() + || a.getPaddingLeftNoRTL() != b.getPaddingLeftNoRTL() + || a.getPaddingRightNoRTL() != b.getPaddingRightNoRTL(); + } + + private static void unifyStateMetrics(Component c) { + if (!statesDifferForUiid(c)) { + // Already state-invariant: touching the state styles here would + // create them for nothing. + return; + } + com.codename1.ui.plaf.Style un = c.getUnselectedStyle(); + com.codename1.ui.Font font = un.getFont(); + int pt = un.getPaddingTop(); + int pb = un.getPaddingBottom(); + int pl = un.getPaddingLeftNoRTL(); + int pr = un.getPaddingRightNoRTL(); + java.util.List states = new java.util.ArrayList(); + states.add(c.getSelectedStyle()); + states.add(c.getDisabledStyle()); + if (c instanceof com.codename1.ui.Button) { + states.add(((com.codename1.ui.Button) c).getPressedStyle()); + } + for (com.codename1.ui.plaf.Style s : states) { + if (font != null) { + s.setFont(font); + } + s.setPaddingUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + s.setPadding(pt, pb, pl, pr); + s.setMarginUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + s.setMargin(0, 0, 0, 0); + } + } + + /** + * Creates the retained CN1 component for this element, or null for pure + * layout elements. Called once, on mount. + */ + protected Component createComponent() { + return null; + } + + /** + * Applies the current widget configuration to the retained component. + * Called on every widget update (the component is mutated in place). + */ + protected void updateComponent(Component c) { + } + + @Override + public void update(Widget newWidget) { + super.update(newWidget); + if (component != null) { + updateComponent(component); + } + dirty = true; + performRebuild(); + if (updateAffectsLayout()) { + markNeedsLayout(); + } else { + markNeedsPaint(); + } + } + + /** + * Whether a new configuration for THIS element can change geometry. + * + *

Flutter draws a hard line between {@code markNeedsPaint} and + * {@code markNeedsLayout}, and it is not an optimization detail — marking + * layout walks to the root, so a purely visual change to one widget would + * otherwise relayout the entire screen on every frame it animates.

+ * + *

Returning false is only safe when this element's size cannot depend on + * its own configuration. It says nothing about the CHILDREN: if a rebuild + * replaces a child, that child's own update marks layout and the walk passes + * through here as usual.

+ */ + protected boolean updateAffectsLayout() { + return true; + } + + /** + * The subtree must repaint, but every measurement stays valid. + */ + public void markNeedsPaint() { + if (component != null) { + component.repaint(); + } + } + + @Override + protected void performRebuild() { + dirty = false; + syncChildren(); + } + + /** + * Reconciles child widgets into child elements. Default: no children. + */ + protected void syncChildren() { + } + + @Override + public void unmount() { + super.unmount(); + if (host != null) { + host.detach(this); + } + } + + /** + * Theme change: re-apply the widget config (which re-derives any + * programmatic, Theme.of-based styling) to the retained component and + * invalidate the cached layout — fonts/metrics may have changed. + */ + @Override + public void themeChanged() { + if (component != null) { + updateComponent(component); + } + markNeedsLayout(); + super.themeChanged(); + } + + public Component component() { + return component; + } + + // ------------------------------------------------------------------ + // Layout protocol + // ------------------------------------------------------------------ + + /** + * Runs (or reuses the cached result of) the layout pass for this box. + */ + /// Diagnostic counters for {@link BuildOwner#traceFrames(boolean)}: how much of a layout + /// pass the constraints-to-size cache actually absorbs. A pass that misses on nearly + /// every box is re-laying out the whole tree for one changed leaf. + /// True while a dry measurement is running, so nested layout() calls measure dryly too. + private static boolean dryPass; + + /** + * Whether the pass currently running is a dry measurement. + * + *

performLayout is allowed to have side effects — it writes child + * offsets, and a text box writes the lines it wrapped — but a DRY pass runs + * against constraints that are not the ones the box will be painted at, so + * anything it publishes for the painter is wrong. Text was writing its + * wrapped lines unconditionally, so a dry measurement at unbounded width + * left the label holding one long unwrapped line and it painted straight + * past its own edge. + */ + protected static boolean isDryPass() { + return dryPass; + } + + static long layoutCalls; + static long layoutHits; + static long layoutMissDirty; + static long layoutMissConstraints; + + /// Self time inside performLayout per element class, so a slow pass names the widget + /// responsible instead of just being slow. Self time, not total: a parent's entry would + /// otherwise swallow its whole subtree and every pass would blame the root. + static final java.util.Map layoutSelfNanos = new java.util.HashMap(); + private static long childNanos; + + static void resetLayoutCounters() { + layoutCalls = 0; + layoutHits = 0; + layoutMissDirty = 0; + layoutMissConstraints = 0; + layoutSelfNanos.clear(); + childNanos = 0; + } + + /// The costliest element classes by self time, worst first. + static String hotLayoutClasses(int top) { + java.util.List> all = + new java.util.ArrayList>(layoutSelfNanos.entrySet()); + java.util.Collections.sort(all, new java.util.Comparator>() { + @Override + public int compare(java.util.Map.Entry a, java.util.Map.Entry b) { + return Long.compare(b.getValue()[0], a.getValue()[0]); + } + }); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < Math.min(top, all.size()); i++) { + if (i > 0) { + sb.append(','); + } + sb.append("{\"class\":\"").append(all.get(i).getKey()) + .append("\",\"ms\":").append(all.get(i).getValue()[0] / 1000000) + .append(",\"calls\":").append(all.get(i).getValue()[1]).append('}'); + } + return sb.append(']').toString(); + } + + /// Runs performLayout while attributing only its OWN time to this element's class. + private Size timedPerformLayout(BoxConstraints constraints) { + if (!Trace.on()) { + return performLayout(constraints); + } + long start = System.nanoTime(); + long childrenBefore = childNanos; + childNanos = 0; + Size result; + try { + result = performLayout(constraints); + } finally { + long elapsed = System.nanoTime() - start; + long self = elapsed - childNanos; + String key = getClass().getSimpleName(); + long[] slot = layoutSelfNanos.get(key); + if (slot == null) { + slot = new long[2]; + layoutSelfNanos.put(key, slot); + } + slot[0] += self; + slot[1]++; + childNanos = childrenBefore + elapsed; + } + return result; + } + + /** + * Measures this box WITHOUT making the result the authoritative layout - Flutter's dry + * layout. Codename One asks a container for its preferred size far more often than it + * lays it out (scroll extents, focus maths, revalidate), and that question arrives with + * different constraints than the real pass. With one cache slot the two alternate and + * evict each other on every box in the tree, so a single changed leaf re-measured + * everything: on the gallery home that was a 92% miss rate over 7600 layout calls. + * + *

The dry result gets its own slot and never clears {@code needsLayout}, so the real + * pass still runs. A dry HIT skips the subtree entirely, which is the whole point.

+ */ + public final Size dryLayout(BoxConstraints constraints) { + layoutCalls++; + if (!needsLayout && drySize != null && constraints.equals(lastDryConstraints)) { + layoutHits++; + return drySize; + } + if (needsLayout) { + layoutMissDirty++; + } else { + layoutMissConstraints++; + } + lastDryConstraints = constraints; + // Dryness has to propagate. performLayout measures its children through layout(), + // so without this flag a dry pass would write dry constraints into every + // descendant's REAL slot and the real pass that follows would miss on all of them - + // which is most of what made a one-element change re-measure the whole tree. + // Layout runs on the EDT, so a plain static is the whole of the bookkeeping. + boolean outer = dryPass; + dryPass = true; + try { + drySize = timedPerformLayout(constraints); + } finally { + dryPass = outer; + } + // performLayout is NOT side-effect free: it writes child offsets, and it just wrote + // them for the dry constraints. So the real pass has to recompute them - if it were + // allowed to hit its cache it would keep the dry offsets and place children wrongly + // (this put the study card's caption at the top of the card instead of the bottom). + // Drop only THIS element's real result; ancestors are untouched, so this does not + // escalate into the whole-tree invalidation the cache exists to avoid. + lastConstraints = null; + trace(true, constraints, drySize); + return drySize; + } + + /** + * Drops this element's cached layout result, so the next {@code layout} call + * runs {@link #performLayout} again even if the constraints have not changed. + * + *

For a pass that deliberately did not compute the real answer. The dry + * path above does this for itself; {@code LayoutBuilder} needs it when it + * sits out a speculative unbounded measurement, because the pass that + * follows can arrive with those same constraints and must not be handed the + * placeholder the sat-out pass returned.

+ */ + protected final void invalidateLayoutCache() { + lastConstraints = null; + } + + public final Size layout(BoxConstraints constraints) { + if (dryPass) { + return dryLayout(constraints); + } + layoutCalls++; + if (!needsLayout && constraints.equals(lastConstraints)) { + layoutHits++; + return size; + } + if (needsLayout) { + layoutMissDirty++; + } else { + layoutMissConstraints++; + } + lastConstraints = constraints; + boolean wasInLayout = inLayout; + boolean wasRemarked = remarkedDuringLayout; + inLayout = true; + remarkedDuringLayout = false; + try { + size = timedPerformLayout(constraints); + } finally { + inLayout = wasInLayout; + } + // A box invalidated while it was being computed is not clean when it finishes. + needsLayout = remarkedDuringLayout; + if (remarkedDuringLayout) { + // ...and its cached constraints must not answer for the stale pass either. + lastConstraints = null; + } + remarkedDuringLayout = wasRemarked; + trace(false, constraints, size); + return size; + } + + /// Set to a class-name substring with -Dcn1.flutter.layout.trace to print every + /// layout of the matching elements: which constraints went in, which size came out, + /// and whether the pass was dry. + /// + /// The distinction that matters is dry-vs-real. A dry measurement runs the same + /// performLayout, so it writes whatever that method keeps in fields; if the last pass + /// over an element was dry, its component can end up sized from a measurement taken + /// under constraints that were never real. + private static final String LAYOUT_TRACE = System.getProperty("cn1.flutter.layout.trace"); + + private void trace(boolean dry, BoxConstraints c, Size s) { + if (LAYOUT_TRACE == null || !getClass().getName().contains(LAYOUT_TRACE)) { + return; + } + com.codename1.io.Log.p((dry ? "[dry] " : "[lay] ") + getClass().getSimpleName() + + " in=" + c + " out=" + s); + } + + /** + * Computes this box's size under the given constraints and stores each + * render child's offset via {@link #setChildOffset}. + */ + protected abstract Size performLayout(BoxConstraints constraints); + + /** + * Invalidates the cached layout of this box and all its render ancestors + * so the next pass recomputes down this branch. + */ + /// Whether this box still owes a layout -- true when it has never been measured, or + /// when something invalidated it while it was being measured. + public boolean needsLayout() { + return needsLayout; + } + + public void markNeedsLayout() { + for (Element a = this; a != null; a = a.parent) { + if (a instanceof RenderElement) { + RenderElement r = (RenderElement) a; + r.needsLayout = true; + if (r.inLayout) { + r.remarkedDuringLayout = true; + } + // The dry measurement is just as stale as the real one. + r.drySize = null; + r.lastDryConstraints = null; + } + } + } + + /** + * Positions this box at absolute host coordinates, writes the bounds of + * the retained component (if any) and recursively positions render + * children at their stored offsets. + */ + public void position(int x, int y) { + absX = x; + absY = y; + if (component != null) { + component.setX(x); + component.setY(y); + component.setWidth((int) Math.round(size.width())); + component.setHeight((int) Math.round(size.height())); + } + positionChildren(x, y); + } + + /** + * Default child positioning: every same-host render child goes to its + * offset stored during performLayout. + */ + protected void positionChildren(int x, int y) { + for (RenderElement child : renderChildren()) { + child.position(x + (int) Math.round(child.relX), y + (int) Math.round(child.relY)); + } + } + + protected void setChildOffset(RenderElement child, double dx, double dy) { + child.relX = dx; + child.relY = dy; + } + + public double relX() { + return relX; + } + + public double relY() { + return relY; + } + + public int x() { + return absX; + } + + public int y() { + return absY; + } + + public Size size() { + return size; + } + + // ------------------------------------------------------------------ + // Render tree navigation + // ------------------------------------------------------------------ + + /** + * Descends from an element through composition elements to the first + * render element (Flutter's renderObject lookup), or null. + */ + public static RenderElement findRenderElement(Element e) { + while (e != null) { + if (e instanceof RenderElement) { + return (RenderElement) e; + } + final Element[] first = new Element[1]; + e.visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + if (first[0] == null) { + first[0] = c; + } + } + }); + e = first[0]; + } + return null; + } + + /** + * The render elements directly below this one (descending through + * composition), in tree order, excluding children routed to a different + * host (e.g. a root Scaffold's appBar living in the Toolbar). + */ + public List renderChildren() { + final List out = new ArrayList(); + visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + RenderElement r = findRenderElement(c); + if (r != null && r.host == RenderElement.this.host) { + out.add(r); + } + } + }); + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ResizeImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ResizeImage.java new file mode 100644 index 00000000000..b846e8a1b99 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ResizeImage.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An {@link ImageProvider} that wraps another provider and resizes its decoded + * image to a target {@code width}/{@code height} — Flutter's {@code ResizeImage}. + * The wrapped provider's identity is threaded into {@link #sourceKey()} so the + * framework still detects source changes across in-place updates. + */ +public class ResizeImage extends ImageProvider { + + private final ImageProvider imageProvider; + private Long width; + private Long height; + private Object policy; + private boolean allowUpscaling; + + public ResizeImage(ImageProvider imageProvider) { + this.imageProvider = imageProvider; + } + + /** Named parameter setter for the Dart {@code width:} parameter. */ + public void width(long v) { + this.width = v; + } + + /** Named parameter setter for the Dart {@code height:} parameter. */ + public void height(long v) { + this.height = v; + } + + /** Named parameter setter for the Dart {@code policy:} parameter. */ + public void policy(Object v) { + this.policy = v; + } + + /** Named parameter setter for the Dart {@code allowUpscaling:} parameter. */ + public void allowUpscaling(boolean v) { + this.allowUpscaling = v; + } + + public ImageProvider getImageProvider() { + return imageProvider; + } + + public Long getWidth() { + return width; + } + + public Long getHeight() { + return height; + } + + @Override + public String sourceKey() { + String inner = imageProvider == null ? "null" : imageProvider.sourceKey(); + return "resize:" + width + "x" + height + ":" + inner; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBool.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBool.java new file mode 100644 index 00000000000..bef812ea0bd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBool.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable non-null boolean ({@code RestorableBool} in Flutter). The value + * lives in a field; setting it notifies listeners. Restoration is not persisted. + */ +public class RestorableBool extends RestorableProperty { + + private boolean current; + + public RestorableBool(boolean defaultValue) { + this.current = defaultValue; + } + + public boolean value() { + return current; + } + + public void value(boolean v) { + if (current != v) { + current = v; + notifyListeners(); + } + } + + @Override + public Boolean createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Boolean value) { + this.current = value != null && value.booleanValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBoolN.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBoolN.java new file mode 100644 index 00000000000..d38bd2d7fc1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableBoolN.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable nullable boolean ({@code RestorableBoolN} in Flutter). Restoration + * is not persisted; the value is held in a field. + */ +public class RestorableBoolN extends RestorableProperty { + + private Boolean current; + + public RestorableBoolN(Boolean defaultValue) { + this.current = defaultValue; + } + + public Boolean value() { + return current; + } + + public void value(Boolean v) { + if (current == null ? v != null : !current.equals(v)) { + current = v; + notifyListeners(); + } + } + + @Override + public Boolean createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Boolean value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableChangeNotifier.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableChangeNotifier.java new file mode 100644 index 00000000000..66a84433d87 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableChangeNotifier.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A {@link RestorableListenable} specialised for {@code ChangeNotifier} values, + * mirroring Flutter's {@code RestorableChangeNotifier}. In Flutter this also + * disposes the held notifier; Codename One folds that into + * {@link RestorableProperty#dispose()}. + * + * @param the held ChangeNotifier value type + */ +public class RestorableChangeNotifier extends RestorableListenable { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDateTime.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDateTime.java new file mode 100644 index 00000000000..31913518296 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDateTime.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.core.DateTime; + +/** + * A restorable {@code DateTime} — Flutter's {@code RestorableDateTime} (a + * {@code RestorableValue}). The value is held as a + * {@link dart.core.DateTime} so callers can read date components (e.g. + * {@code value.millisecondsSinceEpoch}). Restoration is not persisted. + */ +public class RestorableDateTime extends RestorableProperty { + + private DateTime current; + + public RestorableDateTime(DateTime defaultValue) { + this.current = defaultValue; + } + + public DateTime value() { + return current; + } + + public void value(DateTime v) { + if (current == null ? v != null : !current.equals(v)) { + current = v; + notifyListeners(); + } + } + + @Override + public DateTime createDefaultValue() { + return current; + } + + @Override + public void initWithValue(DateTime value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDouble.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDouble.java new file mode 100644 index 00000000000..88831872af3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDouble.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable non-null double ({@code RestorableDouble} in Flutter). Restoration + * is not persisted. + */ +public class RestorableDouble extends RestorableProperty { + + private double current; + + public RestorableDouble(double defaultValue) { + this.current = defaultValue; + } + + public double value() { + return current; + } + + public void value(double v) { + if (current != v) { + current = v; + notifyListeners(); + } + } + + @Override + public Double createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Double value) { + this.current = value == null ? 0.0 : value.doubleValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDoubleN.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDoubleN.java new file mode 100644 index 00000000000..79ead256f2f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableDoubleN.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable nullable double ({@code RestorableDoubleN} in Flutter). Restoration + * is not persisted. + */ +public class RestorableDoubleN extends RestorableProperty { + + private Double current; + + public RestorableDoubleN(Double defaultValue) { + this.current = defaultValue; + } + + public Double value() { + return current; + } + + public void value(Double v) { + if (current == null ? v != null : !current.equals(v)) { + current = v; + notifyListeners(); + } + } + + @Override + public Double createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Double value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableInt.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableInt.java new file mode 100644 index 00000000000..6e96d622ce5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableInt.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable non-null integer ({@code RestorableInt} in Flutter). Dart + * {@code int} maps to Java {@code long}. Restoration is not persisted. + */ +public class RestorableInt extends RestorableProperty { + + private long current; + + public RestorableInt(long defaultValue) { + this.current = defaultValue; + } + + public long value() { + return current; + } + + public void value(long v) { + if (current != v) { + current = v; + notifyListeners(); + } + } + + @Override + public Long createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Long value) { + this.current = value == null ? 0L : value.longValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableIntN.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableIntN.java new file mode 100644 index 00000000000..2e7eb33dc81 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableIntN.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable nullable integer ({@code RestorableIntN} in Flutter). Restoration + * is not persisted; the value is held in a field. + */ +public class RestorableIntN extends RestorableProperty { + + private Long current; + + public RestorableIntN(Long defaultValue) { + this.current = defaultValue; + } + + public Long value() { + return current; + } + + public void value(Long v) { + if (current == null ? v != null : !current.equals(v)) { + current = v; + notifyListeners(); + } + } + + @Override + public Long createDefaultValue() { + return current; + } + + @Override + public void initWithValue(Long value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableListenable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableListenable.java new file mode 100644 index 00000000000..7dbdb8ed7b7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableListenable.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable whose value is a {@code Listenable} that is restored rather than + * re-created, mirroring Flutter's {@code RestorableListenable}. User code + * (studies/reply/app.dart, studies/shrine/app.dart) subclasses this directly, + * overriding {@link #createDefaultValue()} / {@link #fromPrimitives(Object)} / + * {@link #toPrimitives()}, and reads the inherited {@link #value()} getter. + * + *

The value is created lazily from {@link #createDefaultValue()} on first + * access (Codename One does not persist restoration data).

+ * + * @param the held (listenable) value type + */ +public class RestorableListenable extends RestorableProperty { + + private T current; + private boolean initialized; + + /** The restored value, created lazily from {@link #createDefaultValue()}. */ + public T value() { + if (!initialized) { + current = createDefaultValue(); + initialized = true; + } + return current; + } + + @Override + public void initWithValue(T value) { + this.current = value; + this.initialized = true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableProperty.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableProperty.java new file mode 100644 index 00000000000..75a11dc3ad7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableProperty.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for restorable state values, mirroring Flutter's + * {@code RestorableProperty} (which extends {@code ChangeNotifier}). User + * code subclasses this directly to restore custom values, overriding + * {@link #createDefaultValue()}, {@link #fromPrimitives(Object)}, + * {@link #toPrimitives()} and {@link #initWithValue(Object)} and calling + * {@link #notifyListeners()}. + * + *

Codename One does not persist restoration data, so the serialization hooks + * are inert; what matters at runtime is the {@code ChangeNotifier} behaviour and + * the value held by the {@link RestorableValue} subtypes.

+ * + * @param the restored value type + */ +public class RestorableProperty { + + private final List listeners = new ArrayList(); + private boolean registered; + private boolean disposed; + + /** The value used when no restoration data is available. */ + public T createDefaultValue() { + return null; + } + + /** Adopt {@code value} as the current value (no persistence side effects). Returns the + * adopted value — Flutter's {@code initWithValue} returns {@code T}. */ + public void initWithValue(T value) { + } + + /** Serialize the current value; inert because nothing is persisted. */ + public Object toPrimitives() { + return null; + } + + /** Deserialize a previously persisted value; never called (no persistence). */ + public T fromPrimitives(Object data) { + return createDefaultValue(); + } + + /** Whether this property has been registered with a {@link RestorationMixin}. */ + public boolean isRegistered() { + return registered; + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + listeners.remove(listener); + } + + public void notifyListeners() { + com.codename1.flutter.foundation.Listeners.notify(listeners); + } + + public void dispose() { + disposed = true; + listeners.clear(); + } + + // ------------------------------------------------------------------ + // Framework plumbing (used by RestorationMixin) + // ------------------------------------------------------------------ + + void markRegistered() { + this.registered = true; + } + + void markUnregistered() { + this.registered = false; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableString.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableString.java new file mode 100644 index 00000000000..687f9a94d34 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableString.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable non-null string ({@code RestorableString} in Flutter). Restoration + * is not persisted. + */ +public class RestorableString extends RestorableProperty { + + private String current; + + public RestorableString(String defaultValue) { + this.current = defaultValue == null ? "" : defaultValue; + } + + public String value() { + return current; + } + + public void value(String v) { + String nv = v == null ? "" : v; + if (!current.equals(nv)) { + current = nv; + notifyListeners(); + } + } + + @Override + public String createDefaultValue() { + return current; + } + + @Override + public void initWithValue(String value) { + this.current = value == null ? "" : value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableStringN.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableStringN.java new file mode 100644 index 00000000000..9e56a3915a7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableStringN.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable nullable string ({@code RestorableStringN} in Flutter). Restoration + * is not persisted; the value is held in a field. + */ +public class RestorableStringN extends RestorableProperty { + + private String current; + + public RestorableStringN(String defaultValue) { + this.current = defaultValue; + } + + public String value() { + return current; + } + + public void value(String v) { + if (current == null ? v != null : !current.equals(v)) { + current = v; + notifyListeners(); + } + } + + @Override + public String createDefaultValue() { + return current; + } + + @Override + public void initWithValue(String value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTextEditingController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTextEditingController.java new file mode 100644 index 00000000000..664519c1eb8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTextEditingController.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.material.TextEditingController; + +/** + * A restorable {@link TextEditingController} ({@code RestorableTextEditingController} + * in Flutter). It owns a live controller, exposed via {@link #value()}; the text + * itself is not persisted across launches. + */ +public class RestorableTextEditingController extends RestorableProperty { + + private final TextEditingController controller = new TextEditingController(); + + public RestorableTextEditingController() { + } + + /** Named constructor parameter {@code text:} — the initial text. */ + public void text(String v) { + controller.text(v); + } + + public TextEditingController value() { + return controller; + } + + @Override + public TextEditingController createDefaultValue() { + return controller; + } + + @Override + public void initWithValue(TextEditingController value) { + } + + @Override + public void dispose() { + super.dispose(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTimeOfDay.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTimeOfDay.java new file mode 100644 index 00000000000..48f5115bd7a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableTimeOfDay.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.material.TimeOfDay; + +/** + * A restorable {@link TimeOfDay} property ({@code RestorableTimeOfDay} in + * Flutter) — new_gallery's picker demo holds the selected time in one. The value + * lives in a field; setting it notifies listeners. Restoration is not persisted. + */ +public class RestorableTimeOfDay extends RestorableProperty { + + private TimeOfDay current; + + public RestorableTimeOfDay(TimeOfDay defaultValue) { + this.current = defaultValue; + } + + public TimeOfDay value() { + return current; + } + + public void value(TimeOfDay v) { + boolean changed = current == null ? v != null : !current.equals(v); + if (changed) { + current = v; + notifyListeners(); + } + } + + @Override + public TimeOfDay createDefaultValue() { + return current; + } + + @Override + public void initWithValue(TimeOfDay value) { + this.current = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableValue.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableValue.java new file mode 100644 index 00000000000..005ed0025e8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorableValue.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A restorable holding a single value with a read/write {@code value} accessor, + * mirroring Flutter's {@code RestorableValue}. The value is created lazily + * from {@link #createDefaultValue()} on first access; assigning it notifies + * listeners. + * + * @param the held value type + */ +public class RestorableValue extends RestorableProperty { + + private T current; + private boolean initialized; + + public T value() { + if (!initialized) { + current = createDefaultValue(); + initialized = true; + } + return current; + } + + public void value(T newValue) { + this.current = newValue; + this.initialized = true; + notifyListeners(); + } + + @Override + public void initWithValue(T value) { + this.current = value; + this.initialized = true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationBucket.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationBucket.java new file mode 100644 index 00000000000..f5a0bde0329 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationBucket.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An opaque token that, in Flutter, holds serialized restoration data for a + * subtree. Codename One does not persist restoration state, so this is an inert + * marker: it exists purely so that {@code restoreState(RestorationBucket?, bool)} + * and {@link RestorationMixin#bucket()} type-check. It carries no data. + */ +public class RestorationBucket { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationMixin.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationMixin.java new file mode 100644 index 00000000000..0d5e847cd06 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RestorationMixin.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The Codename One equivalent of Flutter's {@code RestorationMixin}, applied to + * a {@link State} subclass (Dart {@code class _FooState extends State with + * RestorationMixin}). The transpiler emits the applying class as a Java class + * that {@code implements RestorationMixin}, so the mixin's members are reached + * as inherited interface default methods. + * + *

Restoration is a no-op in Codename One: nothing is serialized or restored + * across launches. {@link #registerForRestoration} simply notes that a property + * is live (each {@link RestorableProperty} already holds its own value in a + * field), and {@link #restoreState} is invoked once after {@code initState} so + * subclasses can register their properties. The API is complete enough for the + * gallery demos to build and behave correctly within a single session.

+ */ +public interface RestorationMixin { + + /** + * A stable identifier for this state's restoration scope. Overridden by the + * applying State; the default (no scope) is null. + */ + default String restorationId() { + return null; + } + + /** The restoration bucket for this scope; always null (no persistence). */ + default RestorationBucket bucket() { + return null; + } + + /** + * Register the state's {@link RestorableProperty} instances. Called once + * after the element is mounted (and again if the bucket changes, which never + * happens here). Subclasses override this and call + * {@link #registerForRestoration} for each property. + */ + default void restoreState(RestorationBucket oldBucket, boolean initialRestore) { + } + + /** + * Wire a restorable property into this scope. With no persisted data the + * property keeps the default value it was constructed with; we only mark it + * registered so repeated registration is a no-op. + */ + default void registerForRestoration(RestorableProperty property, String restorationId) { + if (property != null) { + property.markRegistered(); + } + } + + /** Detach a property from this scope. */ + default void unregisterFromRestoration(RestorableProperty property) { + if (property != null) { + property.markUnregistered(); + } + } + + /** Bucket-change hook; a no-op because the bucket is always null. */ + default void didToggleBucket(RestorationBucket oldBucket) { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/RoundedRectangleBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RoundedRectangleBorder.java new file mode 100644 index 00000000000..095b7bce5d3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/RoundedRectangleBorder.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A rectangular border with rounded corners — Flutter's + * {@code RoundedRectangleBorder}. {@code borderRadius} accepts a + * {@link BorderRadius}/{@link BorderRadiusDirectional} (held as an opaque value + * for this milestone). + */ +public class RoundedRectangleBorder extends OutlinedBorder { + + private Object borderRadius; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public Object getBorderRadius() { + return borderRadius; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Shader.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Shader.java new file mode 100644 index 00000000000..1289d70e2e6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Shader.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * An opaque object that generates colors for painting — Flutter's dart:ui + * {@code Shader}. Concrete shaders are produced by {@link Gradient#createShader}. + */ +public abstract class Shader { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ShapeBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ShapeBorder.java new file mode 100644 index 00000000000..d7ef464b47f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ShapeBorder.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The base class for shape outlines that can paint a border and clip a shape — + * Flutter's {@code ShapeBorder}. + */ +public abstract class ShapeBorder { + + /** + * Paints this outline -- Flutter's {@code ShapeBorder.paint}. + * + *

Declared HERE so an application's own border overrides it and can be + * called back. It was absent, so a subclass that draws itself (Shrine's + * CutCornersBorder chamfers its corners with a Path) declared a paint method + * that overrode nothing and was never reached, and the runtime fell back to + * approximating the outline with a rounded rectangle. Nothing about the + * chamfer is beyond Codename One -- it is a polygon, and GraphicsCanvas + * already turns a Flutter Path into one; the hook to ask for it was what was + * missing.

+ * + *

The default draws nothing, so a border with no opinion is unchanged.

+ */ + public void paint(Canvas canvas, Rect rect, Double gapStart, double gapExtent, + double gapPercentage, TextDirection textDirection) { + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/SingleChildRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/SingleChildRenderElement.java new file mode 100644 index 00000000000..d8beefd763e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/SingleChildRenderElement.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.Funcs; + +/** + * Convenience base for render elements holding a single (possibly null) + * child widget from their configuration. + */ +public abstract class SingleChildRenderElement extends RenderElement { + + private Element child; + + protected SingleChildRenderElement(Widget widget) { + super(widget); + } + + /** + * The child widget from the current configuration (may be null). + */ + protected abstract Widget childWidget(); + + @Override + protected void syncChildren() { + child = updateChild(child, childWidget(), 0); + } + + public Element childElement() { + return child; + } + + /** + * The render element of the child, descending through composition. + */ + protected RenderElement renderChild() { + return findRenderElement(child); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (child != null) { + visitor.call(child); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StackFit.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StackFit.java new file mode 100644 index 00000000000..f94d15c94a1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StackFit.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How a {@code Stack} sizes its non-positioned children — Flutter's + * {@code StackFit}. + */ +public enum StackFit { + loose, expand, passthrough +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StadiumBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StadiumBorder.java new file mode 100644 index 00000000000..ae5893f385a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StadiumBorder.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A border that fits a stadium (pill) shape — Flutter's {@code StadiumBorder}. */ +public class StadiumBorder extends OutlinedBorder { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/State.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/State.java new file mode 100644 index 00000000000..a798c85c0e0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/State.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.Funcs; + +/** + * Mutable state for a {@link StatefulWidget}. Owned by a + * {@link StatefulElement}: created on mount, retargeted at new widget + * instances on update (with {@link #didUpdateWidget}), disposed on unmount. + */ +public abstract class State { + + StatefulElement element; + private T widgetValue; + + /** + * The current widget configuration for this state. + */ + public T widget() { + return widgetValue; + } + + /** + * The location of this state's widget in the element tree. + */ + public BuildContext context() { + return element; + } + + /** + * Runs {@code fn} (which mutates fields of this state) and schedules a + * rebuild of this element for the next frame. + */ + public void setState(Funcs.VoidFunc0 fn) { + FlutterUI.assertEdt(); + if (fn != null) { + fn.call(); + } + if (element != null) { + element.markNeedsBuild(); + } + } + + /** + * Called once when the element is first mounted, before the first build. + */ + public void initState() { + } + + /** + * Called immediately after {@link #initState} and again whenever an + * inherited widget this state depends on changes. No-op by default. + */ + public void didChangeDependencies() { + } + + /** + * Whether this state is currently in the tree ({@code State.mounted}): + * true between mount and {@link #dispose()}. + */ + public boolean mounted() { + return element != null; + } + + /** + * Called when the element absorbed a new widget configuration. The new + * widget is already available via {@link #widget()}. + */ + public void didUpdateWidget(T oldWidget) { + } + + /** + * Called when the element is removed from the tree permanently. + */ + public void dispose() { + } + + public abstract Widget build(BuildContext context); + + // ------------------------------------------------------------------ + // Framework plumbing (package private) + // ------------------------------------------------------------------ + + @SuppressWarnings("unchecked") + void attach(StatefulElement element, StatefulWidget widget) { + this.element = element; + this.widgetValue = (T) widget; + } + + @SuppressWarnings("unchecked") + void updateWidget(StatefulWidget widget) { + this.widgetValue = (T) widget; + } + + @SuppressWarnings("unchecked") + void invokeDidUpdateWidget(StatefulWidget oldWidget) { + didUpdateWidget((T) oldWidget); + } + + void detach() { + this.element = null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulElement.java new file mode 100644 index 00000000000..129ff8f2aaa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulElement.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Element for a {@link StatefulWidget}. Owns the {@link State} instance: + * created (and {@code initState} run) on mount, retargeted with + * {@code didUpdateWidget} when a new widget of the same type/key arrives, + * disposed on unmount. + */ +public class StatefulElement extends ComposedElement { + + private final State state; + + public StatefulElement(StatefulWidget widget) { + super(widget); + this.state = widget.createState(); + this.state.attach(this, widget); + } + + public State state() { + return state; + } + + @Override + protected Object globalKeyState() { + return state; + } + + @Override + protected void firstBuild() { + state.initState(); + // Flutter runs didChangeDependencies right after initState and before the first + // build; widgets that create controllers there (e.g. a PageController sized from + // MediaQuery) rely on it having run before build reads them. + state.didChangeDependencies(); + super.firstBuild(); + } + + @Override + public void update(Widget newWidget) { + StatefulWidget oldWidget = (StatefulWidget) widget; + widget = newWidget; + // What Element.update does, which this override cannot call through + // ComposedElement (that would rebuild before didUpdateWidget): a GlobalKey's + // currentWidget follows the new configuration. Skipping it left + // key.currentWidget() on the first configuration for the element's life. + registerGlobalKey(); + state.updateWidget((StatefulWidget) newWidget); + state.invokeDidUpdateWidget(oldWidget); + dirty = true; + performRebuild(); + } + + @Override + public void unmount() { + super.unmount(); + state.dispose(); + state.detach(); + } + + /** Set when a dependency changed; State.didChangeDependencies runs before the next build. */ + private boolean dependenciesChanged; + + /** + * Flutter runs State.didChangeDependencies whenever an inherited widget the state read + * changes, before rebuilding it. It only ever ran after initState here, so a state that + * refreshed locale-derived data or re-created a controller from MediaQuery there kept + * stale fields after a Theme, MediaQuery or localization change. + */ + @Override + public void didChangeDependencies() { + dependenciesChanged = true; + super.didChangeDependencies(); + } + + @Override + protected Widget build() { + if (dependenciesChanged) { + dependenciesChanged = false; + state.didChangeDependencies(); + } + return state.build(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulWidget.java new file mode 100644 index 00000000000..90762343840 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatefulWidget.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A widget with mutable state. The framework creates the {@link State} object + * when the widget is first inflated into an element and keeps it alive across + * rebuilds as long as reconciliation reuses that element. + */ +public abstract class StatefulWidget extends Widget { + + public abstract State createState(); + + @Override + public Element createElement() { + return new StatefulElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessElement.java new file mode 100644 index 00000000000..94c6d7c1fc3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessElement.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Element for a {@link StatelessWidget}: rebuilding calls the widget's build + * method and reconciles the single resulting child. + */ +public class StatelessElement extends ComposedElement { + + public StatelessElement(StatelessWidget widget) { + super(widget); + } + + @Override + protected Widget build() { + return ((StatelessWidget) widget).build(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessWidget.java new file mode 100644 index 00000000000..3dceaaec825 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StatelessWidget.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A widget that describes part of the UI purely as a function of its + * configuration: {@link #build(BuildContext)} composes other widgets. + */ +public abstract class StatelessWidget extends Widget { + + public abstract Widget build(BuildContext context); + + @Override + public Element createElement() { + return new StatelessElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeCap.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeCap.java new file mode 100644 index 00000000000..4c61a38087d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeCap.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** The shape used at the ends of stroked lines — Flutter's {@code StrokeCap}. */ +public enum StrokeCap { + butt, + round, + square +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeJoin.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeJoin.java new file mode 100644 index 00000000000..8aba842d496 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/StrokeJoin.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** The shape used at the corners of a stroked path — Flutter's {@code StrokeJoin}. */ +public enum StrokeJoin { + miter, + round, + bevel +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/SweepGradient.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/SweepGradient.java new file mode 100644 index 00000000000..da7505cd9e5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/SweepGradient.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A 2D sweep (angular) gradient — Flutter's {@code SweepGradient}. */ +public class SweepGradient extends Gradient { + + double startAngle; + double endAngle = 2 * Math.PI; + + public void startAngle(double v) { + this.startAngle = v; + } + + public void endAngle(double v) { + this.endAngle = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TargetPlatform.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TargetPlatform.java new file mode 100644 index 00000000000..d2f30b3b64a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TargetPlatform.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The platform an app is running on, mirroring Flutter's {@code TargetPlatform}. + */ +public enum TargetPlatform { + android, fuchsia, iOS, linux, macOS, windows +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextAlign.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextAlign.java new file mode 100644 index 00000000000..87251f7d5b0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextAlign.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Horizontal text alignment. + */ +public enum TextAlign { + left, right, center, start, end +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextDirection.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextDirection.java new file mode 100644 index 00000000000..7c3def5f204 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextDirection.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A direction in which text flows — Flutter's {@code TextDirection}. + */ +public enum TextDirection { + rtl, ltr +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextEditingValue.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextEditingValue.java new file mode 100644 index 00000000000..910db03e671 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextEditingValue.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * The text/selection/composing snapshot a {@code TextInputFormatter} transforms + * ({@code TextEditingValue} in Flutter). new_gallery's phone-number formatter + * reads {@link #text()} / {@link #selection()} and returns a new value built with + * a collapsed selection. + * + *

Transpiler surface: the named {@code text:}/{@code selection:}/ + * {@code composing:} constructor parameters map to same-named setters; the Dart + * getters map to the zero-arg accessors.

+ */ +public class TextEditingValue { + + /** The empty value (Dart's {@code TextEditingValue.empty}). */ + public static final TextEditingValue empty = new TextEditingValue(); + + private String text = ""; + private TextSelection selection = TextSelection.collapsed(-1); + private TextRange composing = TextRange.empty; + + public TextEditingValue() { + } + + // Named-parameter setters. + public void text(String v) { + this.text = v == null ? "" : v; + } + + public void selection(TextSelection v) { + if (v != null) { + this.selection = v; + } + } + + public void composing(TextRange v) { + if (v != null) { + this.composing = v; + } + } + + public String text() { + return text; + } + + public TextSelection selection() { + return selection; + } + + public TextRange composing() { + return composing; + } + + /** Returns a copy with the supplied fields overridden (null keeps current). */ + public TextEditingValue copyWith(String text, TextSelection selection, TextRange composing) { + TextEditingValue v = new TextEditingValue(); + v.text = text != null ? text : this.text; + v.selection = selection != null ? selection : this.selection; + v.composing = composing != null ? composing : this.composing; + return v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextOverflow.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextOverflow.java new file mode 100644 index 00000000000..7fc65d1da8a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextOverflow.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How overflowing text is handled — Flutter's {@code TextOverflow}. + */ +public enum TextOverflow { + clip, fade, ellipsis, visible +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextRange.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextRange.java new file mode 100644 index 00000000000..cd8f2f8b684 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextRange.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A range of characters in a string of text ({@code TextRange} in Flutter). The + * base type for {@link TextSelection}; {@link TextEditingValue#composing()} is a + * bare {@code TextRange}. A collapsed range has {@code start == end}; an invalid + * range uses {@code -1} for both. + * + *

Transpiler surface: the named {@code start:}/{@code end:} constructor + * parameters map to the {@link #start(int)}/{@link #end(int)} setters, the Dart + * getters to the zero-arg {@link #start()}/{@link #end()} accessors.

+ */ +public class TextRange { + + /** An invalid, empty range (Dart's {@code TextRange.empty}). */ + public static final TextRange empty = new TextRange(-1, -1); + + private long start = -1; + private long end = -1; + + public TextRange() { + } + + public TextRange(long start, long end) { + this.start = start; + this.end = end; + } + + // Named-parameter setters. + public void start(long v) { + this.start = v; + } + + public void end(long v) { + this.end = v; + } + + // Named constructor / static getter. + public static TextRange collapsed(long offset) { + return new TextRange(offset, offset); + } + + // Getters. + public long start() { + return start; + } + + public long end() { + return end; + } + + public boolean isValid() { + return start >= 0 && end >= 0; + } + + public boolean isCollapsed() { + return start == end; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextSelection.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextSelection.java new file mode 100644 index 00000000000..88a009709ec --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextSelection.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A selected range within editable text ({@code TextSelection} in Flutter), + * extending {@link TextRange} with a base/extent (anchor/caret) pair. The + * new_gallery phone-number formatter reads {@code selection.end} and produces a + * collapsed selection via {@link #collapsed(int)}. + */ +public class TextSelection extends TextRange { + + private long baseOffset; + private long extentOffset; + + public TextSelection() { + } + + // Named-parameter setters. + public void baseOffset(long v) { + this.baseOffset = v; + syncRange(); + } + + public void extentOffset(long v) { + this.extentOffset = v; + syncRange(); + } + + /** {@code TextSelection.collapsed(offset: ...)} — a zero-length selection. */ + public static TextSelection collapsed(long offset) { + TextSelection s = new TextSelection(); + s.baseOffset = offset; + s.extentOffset = offset; + s.syncRange(); + return s; + } + + public long baseOffset() { + return baseOffset; + } + + public long extentOffset() { + return extentOffset; + } + + private void syncRange() { + start(Math.min(baseOffset, extentOffset)); + end(Math.max(baseOffset, extentOffset)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextStyle.java new file mode 100644 index 00000000000..d4fd412e917 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TextStyle.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Text styling configuration. Like the widgets, named Dart parameters become + * void setter methods; unset properties stay null and inherit the CN1 + * default style. {@link #copyWith} and {@link #apply} return derived copies + * (the P2 cascade fixers) so member access on a text style stays statically + * typed rather than collapsing to dynamic. + */ +public class TextStyle { + + private Double fontSize; + private FontWeight fontWeight; + private Color color; + private String fontFamily; + private Double letterSpacing; + private Double height; + + public void fontSize(double v) { + this.fontSize = v; + } + + public void fontWeight(FontWeight v) { + this.fontWeight = v; + } + + public void color(Color v) { + this.color = v; + } + + public void fontFamily(String v) { + this.fontFamily = v; + } + + public void letterSpacing(double v) { + this.letterSpacing = v; + } + + public Double getLetterSpacing() { + return letterSpacing; + } + + public void height(double v) { + this.height = v; + } + + // ------------------------------------------------------------------ + // Dart getters -> no-arg methods + // ------------------------------------------------------------------ + + public Color color() { + return color; + } + + public Double fontSize() { + return fontSize; + } + + public FontWeight fontWeight() { + return fontWeight; + } + + public String fontFamily() { + return fontFamily; + } + + public Double letterSpacing() { + return letterSpacing; + } + + public Double height() { + return height; + } + + // ------------------------------------------------------------------ + // Legacy accessors used by the render elements + // ------------------------------------------------------------------ + + /** + * Font size in logical pixels, or null when inherited. + */ + public Double getFontSize() { + return fontSize; + } + + public FontWeight getFontWeight() { + return fontWeight; + } + + public Color getColor() { + return color; + } + + public String getFontFamily() { + return fontFamily; + } + + private TextStyle shallowClone() { + TextStyle t = new TextStyle(); + t.fontSize = fontSize; + t.fontWeight = fontWeight; + t.color = color; + t.fontFamily = fontFamily; + t.letterSpacing = letterSpacing; + t.height = height; + return t; + } + + /** + * Returns a copy with the supplied (non-null) values overridden. Parameter + * order matches the Dart stub. Properties this pass does not model + * (fontStyle, wordSpacing, background, foreground, decoration) are accepted + * for API shape and ignored. + */ + /** + * This style with {@code other}'s set properties layered on top — Flutter's + * {@code TextStyle.merge}. A null {@code other} returns this style. + */ + public TextStyle merge(TextStyle other) { + if (other == null) { + return this; + } + TextStyle t = shallowClone(); + if (other.color != null) { + t.color = other.color; + } + if (other.fontSize != null) { + t.fontSize = other.fontSize; + } + if (other.fontWeight != null) { + t.fontWeight = other.fontWeight; + } + if (other.fontFamily != null) { + t.fontFamily = other.fontFamily; + } + if (other.letterSpacing != null) { + t.letterSpacing = other.letterSpacing; + } + if (other.height != null) { + t.height = other.height; + } + return t; + } + + public TextStyle copyWith(Boolean inherit, Color color, Color backgroundColor, String fontFamily, + Double fontSize, FontWeight fontWeight, Object fontStyle, Double letterSpacing, + Double wordSpacing, Double height, Object background, Object foreground, + Object decoration) { + TextStyle t = shallowClone(); + if (color != null) t.color = color; + if (fontFamily != null) t.fontFamily = fontFamily; + if (fontSize != null) t.fontSize = fontSize; + if (fontWeight != null) t.fontWeight = fontWeight; + if (letterSpacing != null) t.letterSpacing = letterSpacing; + if (height != null) t.height = height; + return t; + } + + /** + * Returns a copy with a foreground {@code color} override and the font size + * scaled by {@code fontSizeFactor} then offset by {@code fontSizeDelta} + * (both default to no-op when null). Mirrors Flutter's {@code TextStyle.apply}. + */ + public TextStyle apply(Color color, Color backgroundColor, String fontFamily, + Double fontSizeFactor, Double fontSizeDelta, Object decoration) { + TextStyle t = shallowClone(); + if (color != null) t.color = color; + if (fontFamily != null) t.fontFamily = fontFamily; + if (t.fontSize != null) { + double factor = fontSizeFactor != null ? fontSizeFactor : 1.0; + double delta = fontSizeDelta != null ? fontSizeDelta : 0.0; + t.fontSize = t.fontSize * factor + delta; + } + return t; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ThemeMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ThemeMode.java new file mode 100644 index 00000000000..ce0539306a7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ThemeMode.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Which of a {@link com.codename1.flutter.material.MaterialApp MaterialApp}'s + * themes to use: {@code system} follows the platform dark-mode setting + * (defaulting to light when the platform can't report it), {@code light} and + * {@code dark} force one theme regardless of the platform. + */ +public enum ThemeMode { + system, light, dark +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/TileMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TileMode.java new file mode 100644 index 00000000000..3b072d0e7b3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/TileMode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** How a gradient (or shader) tiles outside its defined region — Flutter's {@code TileMode}. */ +public enum TileMode { + clamp, + repeated, + mirror, + decal +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Trace.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Trace.java new file mode 100644 index 00000000000..855d092dd21 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Trace.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Whether the runtime's first-frame attribution counters are collecting. + * + *

The counters — per-class build time, per-class layout self time, component + * creation cost, glyph rasterisation cost — are what turned "the app takes + * 250ms to start" into a list of things to fix. But they are not free: layout + * attribution alone costs two {@code nanoTime} reads, a + * {@code getClass().getSimpleName()} (which allocates a String) and a map + * lookup on EVERY box laid out, and a screen lays out a few thousand boxes per + * frame. Left always-on they measure themselves into the number they report, + * and every application that never asks for the trace pays for it.

+ * + *

Resolved once, from {@code cn1.flutter.startupTrace}, so the check itself + * is a static field read.

+ */ +public final class Trace { + + private Trace() { + } + + private static Boolean enabled; + + /** Whether attribution counters should collect. */ + public static boolean on() { + Boolean e = enabled; + if (e == null) { + e = Boolean.FALSE; + try { + if (com.codename1.ui.Display.isInitialized()) { + e = Boolean.valueOf("true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.startupTrace", "false"))); + } + } catch (Throwable t) { + e = Boolean.FALSE; + } + enabled = e; + } + return e.booleanValue(); + } + + /** Test hook: forces the flag, or restores lazy resolution with null. */ + public static void force(Boolean value) { + enabled = value; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/UnderlineInputBorder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UnderlineInputBorder.java new file mode 100644 index 00000000000..237e55fac34 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UnderlineInputBorder.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** A single underline drawn beneath a Material text field — Flutter's {@code UnderlineInputBorder}. */ +public class UnderlineInputBorder extends InputBorder { + + private Object borderRadius; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public Object getBorderRadius() { + return borderRadius; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/UniqueKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UniqueKey.java new file mode 100644 index 00000000000..bebfaf25a85 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/UniqueKey.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * A key that is unique across the entire application — Flutter's + * {@code UniqueKey}. It is never equal to any other key (identity equality is + * intentional: {@link Object}'s default {@code equals}/{@code hashCode}), so a + * widget carrying a UniqueKey always forces the framework to inflate a fresh + * element rather than update an existing one. + */ +public class UniqueKey extends Key { + + public UniqueKey() { + } + + @Override + public String toString() { + return "UniqueKey#" + Integer.toHexString(System.identityHashCode(this)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueHash.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueHash.java new file mode 100644 index 00000000000..cdf18c6523d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueHash.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Hashing for the double-valued value types -- Offset, Size, Rect, EdgeInsets, + * Alignment, Radius, BoxConstraints. + * + *

Their equals compares with {@code ==}, under which 0.0 and -0.0 are equal, + * while Double.doubleToLongBits tells them apart. Equal values then hashed + * differently, so an Offset(-0.0, y) could not find an entry stored under + * Offset(0.0, y) in a map or set.

+ */ +public final class ValueHash { + + private ValueHash() { + } + + /** The bits to hash for {@code d}, with -0.0 folded onto 0.0 to agree with {@code ==}. */ + public static long bits(double d) { + return Double.doubleToLongBits(d == 0.0 ? 0.0 : d); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueKey.java new file mode 100644 index 00000000000..033f684b902 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/ValueKey.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.runtime.DartRuntime; + +/** + * A key that uses value equality of the wrapped value, mirroring Flutter's + * {@code ValueKey}. Two ValueKeys are equal when they have the same + * runtime class and equal values. + */ +public class ValueKey extends Key { + private final T value; + + public ValueKey(T value) { + this.value = value; + } + + public T value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || o.getClass() != getClass()) { + return false; + } + ValueKey other = (ValueKey) o; + // Dart's ==, not Java's equals: ValueKey(1) and ValueKey(1.0) are + // the same key, and two NaN keys are not. Java's equals said the opposite + // on both, so a rebuild discarded and recreated a subtree it should keep. + return DartRuntime.eq(value, other.value); + } + + @Override + public int hashCode() { + return DartRuntime.hashOf(value); // consistent with DartRuntime.eq above + } + + @Override + public String toString() { + return "ValueKey(" + value + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/VertexMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/VertexMode.java new file mode 100644 index 00000000000..aa19faac3f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/VertexMode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How a {@link Vertices} mesh strings its points into triangles — dart:ui's + * {@code VertexMode}. + */ +public enum VertexMode { + triangles, triangleStrip, triangleFan +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Vertices.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Vertices.java new file mode 100644 index 00000000000..5b0a62647b8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Vertices.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import dart.core.DartList; + +/** + * A raw triangle mesh handed to {@code Canvas.drawVertices} — dart:ui's + * {@code Vertices}. The 2D-transformations demo builds one per hexagon of its + * board. + */ +public class Vertices { + + private final VertexMode mode; + private final DartList positions; + private DartList colors; + private DartList indices; + private DartList textureCoordinates; + + public Vertices(VertexMode mode, DartList positions) { + this.mode = mode; + this.positions = positions; + } + + public void colors(DartList v) { + this.colors = v; + } + + public void indices(DartList v) { + this.indices = v; + } + + public void textureCoordinates(DartList v) { + this.textureCoordinates = v; + } + + public VertexMode getMode() { + return mode; + } + + public DartList getPositions() { + return positions; + } + + public DartList getColors() { + return colors; + } + + /** The index buffer, or null when the positions are used in order. */ + public DartList getIndices() { + return indices; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/Widget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Widget.java new file mode 100644 index 00000000000..302fa86a0ff --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/Widget.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * Base class of the widget hierarchy. Widgets are write-once configuration + * objects: transpiled Dart code allocates a widget, calls its named-parameter + * setter methods, and hands it to the framework. The element tree (see + * {@link Element}) is the retained structure; widgets are cheap descriptions + * that are diffed against the previous configuration on every rebuild. + */ +public abstract class Widget { + private Key key; + + /** + * Named parameter setter for the Dart {@code key:} parameter. + */ + public void key(Key v) { + this.key = v; + } + + public Key getKey() { + return key; + } + + /** + * Flutter's Widget.canUpdate: an existing element can absorb a new widget + * when the runtime type and key both match. + */ + public static boolean canUpdate(Widget oldWidget, Widget newWidget) { + if (oldWidget == null || newWidget == null) { + return false; + } + return oldWidget.getClass() == newWidget.getClass() + && eq(oldWidget.getKey(), newWidget.getKey()); + } + + static boolean eq(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Inflates this widget's configuration into an element. Framework widget + * subclasses supply this; application widgets inherit it from + * StatelessWidget/StatefulWidget. + */ + public abstract Element createElement(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/WidgetPreview.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WidgetPreview.java new file mode 100644 index 00000000000..c78f2197021 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WidgetPreview.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.widgets.HasChild; +import com.codename1.flutter.widgets.LayoutBuilder; + +import com.codename1.ui.Display; + +/** + * Asks what a widget would render, without mounting it. + * + *

A few places have to look THROUGH a composed widget rather than mount it — + * a button consumes its icon instead of hosting it, for one — and each of them + * used to guess at the shapes it knew. The gallery hands every demo page's + * options button an {@code IconButton(icon: FeatureDiscovery(child: Icon(...)))}, + * and FeatureDiscovery builds a {@code LayoutBuilder}: a shape none of those + * guesses covered, so the button drew nothing and the options icon was missing + * from every demo screen in the app.

+ * + *

Preview building is best-effort by nature — the widget is built outside the + * tree, with no element behind it — so every step is guarded and a failure just + * says "cannot tell" rather than propagating.

+ */ +public final class WidgetPreview { + + private WidgetPreview() { + } + + /** + * One step of composition: the child of a wrapper, or the result of building + * a composed widget. Null when {@code w} renders itself (a Text, an Icon, a + * render widget) or when the step could not be taken. + */ + public static Widget step(Widget w, BuildContext context) { + if (w == null) { + return null; + } + try { + if (w instanceof HasChild) { + return ((HasChild) w).getChild(); + } + if (w instanceof LayoutBuilder) { + dart.runtime.Funcs.Func2 b = + ((LayoutBuilder) w).getBuilder(); + return b == null ? null : b.call(context, viewport()); + } + if (w instanceof StatelessWidget) { + return ((StatelessWidget) w).build(context); + } + if (w instanceof StatefulWidget) { + // A throwaway state, purely to see what the widget renders. It is + // attached to its widget first: a State's build almost always reads + // `widget.something`, and an unattached one throws on the first read. + State s = ((StatefulWidget) w).createState(); + if (s == null) { + return null; + } + s.attach(null, (StatefulWidget) w); + return s.build(context); + } + } catch (Throwable t) { + return null; + } + return null; + } + + /** + * Follows {@link #step} until {@code type} turns up, or the walk runs out. + * Bounded, because a preview build is not a mounted tree and a cycle here + * would be a hang with no frame to show for it. + */ + public static T findLeaf(Widget from, Class type, BuildContext context) { + Widget cur = from; + for (int depth = 0; depth < 6 && cur != null; depth++) { + if (type.isInstance(cur)) { + return type.cast(cur); + } + cur = step(cur, context); + } + return null; + } + + /** The viewport, in logical pixels — what a LayoutBuilder is asked about. */ + private static BoxConstraints viewport() { + double w = 400; + double h = 800; + if (Display.isInitialized()) { + double scale = Dp.scale(); + if (scale > 0) { + w = Display.getInstance().getDisplayWidth() / scale; + h = Display.getInstance().getDisplayHeight() / scale; + } + } + return new BoxConstraints(0, w, 0, h); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapAlignment.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapAlignment.java new file mode 100644 index 00000000000..be205d6e54a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapAlignment.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How {@code Wrap} positions children within a run and runs within the + * cross axis — Flutter's {@code WrapAlignment}. + */ +public enum WrapAlignment { + start, end, center, spaceBetween, spaceAround, spaceEvenly +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapCrossAlignment.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapCrossAlignment.java new file mode 100644 index 00000000000..9e2656375ff --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/WrapCrossAlignment.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +/** + * How {@code Wrap} positions children within a run on the cross axis — + * Flutter's {@code WrapCrossAlignment}. + */ +public enum WrapCrossAlignment { + start, end, center +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AlwaysStoppedAnimation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AlwaysStoppedAnimation.java new file mode 100644 index 00000000000..ad16f78da2b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AlwaysStoppedAnimation.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * An {@link Animation} that is permanently stopped at a single value — + * Flutter's {@code AlwaysStoppedAnimation}. Its listeners never fire. + */ +public class AlwaysStoppedAnimation extends Animation { + + private final T value; + + public AlwaysStoppedAnimation(T value) { + this.value = value; + } + + @Override + public T value() { + return value; + } + + @Override + public AnimationStatus status() { + return AnimationStatus.forward; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animatable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animatable.java new file mode 100644 index 00000000000..4c9588f2d8b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animatable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A mapping from a {@code double} (typically an animation's 0..1 value) to a + * value of type {@code T} — Flutter's {@code Animatable}, the supertype of + * {@link Tween} and {@link CurveTween}. + */ +public abstract class Animatable { + + /** Maps the parametric value {@code t} to a {@code T}. */ + public abstract T transform(double t); + + /** {@code transform(animation.value)}. */ + public T evaluate(Animation animation) { + return transform(animation.value()); + } + + /** Returns an {@link Animation} whose value is {@code transform(parent.value)}. */ + public Animation animate(Animation parent) { + return new AnimatedEvaluation(parent, this); + } + + /** Chains this after {@code parent}: {@code transform(parent.transform(t))}. */ + public Animatable chain(Animatable parent) { + return new ChainedEvaluation(parent, this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilder.java new file mode 100644 index 00000000000..252eeb9e1b6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Rebuilds via a {@code builder} callback whenever its {@code animation} + * notifies — Flutter's {@code AnimatedBuilder}. The optional {@code child} is + * an optimization handed back to the builder unchanged. + */ +public class AnimatedBuilder extends Widget { + + private com.codename1.flutter.foundation.Listenable animation; + private Funcs.Func2 builder; + private Widget child; + + public void animation(com.codename1.flutter.foundation.Listenable v) { + this.animation = v; + } + + public void builder(Funcs.Func2 v) { + this.builder = v; + } + + public void child(Widget v) { + this.child = v; + } + + public com.codename1.flutter.foundation.Listenable getAnimation() { + return animation; + } + + public Funcs.Func2 getBuilder() { + return builder; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new AnimatedBuilderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilderElement.java new file mode 100644 index 00000000000..d0ab00d11b6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedBuilderElement.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.ComposedElement; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Element for {@link AnimatedBuilder}: subscribes to the widget's animation on + * mount and rebuilds (re-invoking the builder) on every notification, mirroring + * Flutter's {@code AnimatedWidget}/{@code _AnimatedState} listen-and-rebuild. + */ +public class AnimatedBuilderElement extends ComposedElement { + + private com.codename1.flutter.foundation.Listenable listened; + private final Funcs.VoidFunc0 handler = new Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + } + }; + + public AnimatedBuilderElement(AnimatedBuilder widget) { + super(widget); + } + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + } + + @Override + public void update(Widget newWidget) { + unsubscribe(); + super.update(newWidget); + subscribe(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + private void subscribe() { + listened = ((AnimatedBuilder) widget()).getAnimation(); + if (listened != null) { + listened.addListener(handler); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(handler); + listened = null; + } + } + + @Override + protected Widget build() { + AnimatedBuilder w = (AnimatedBuilder) widget(); + Funcs.Func2 b = w.getBuilder(); + if (b == null) { + return null; + } + return b.call(this, w.getChild()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedChildWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedChildWidget.java new file mode 100644 index 00000000000..753fb2090d7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedChildWidget.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +/** + * Shared base for the transition and implicitly-animated widgets that wrap a single + * {@code child} (FadeTransition, ScaleTransition, AnimatedContainer, ...). + * + *

It is an {@link AnimatedWidget}, so a subclass that names its driving animation + * through {@link #listenable(com.codename1.flutter.foundation.Listenable)} is rebuilt on + * every tick. Subclasses override {@link #build} to wrap the child in the effect they + * describe — {@code Opacity} for a fade, {@code Transform} for a scale or a rotation — and + * the default is the child unchanged, which is right for the implicitly-animated widgets + * that have no Animation of their own.

+ * + *

Until now the whole family rendered the child through with no effect at all: a + * FadeTransition never faded, a ScaleTransition never scaled, and every page transition in + * the app was a cut.

+ */ +public abstract class AnimatedChildWidget extends AnimatedWidget { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + return getChild(); + } + + /** The animation's current value, or {@code fallback} before it has one. */ + protected static double valueOf(Animation animation, double fallback) { + if (animation == null) { + return fallback; + } + Double v = animation.value(); + return v == null ? fallback : v.doubleValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedContainer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedContainer.java new file mode 100644 index 00000000000..a2cb7bf5738 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedContainer.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; + +import dart.core.Duration; + +/** + * A Container whose properties animate to new values over a {@link Duration} + * when the widget rebuilds — Flutter's {@code AnimatedContainer}. This pass + * stores the properties and hosts the child; the tweened transitions are + * deferred. + */ +public class AnimatedContainer extends AnimatedChildWidget { + + private Duration duration; + private Curve curve; + private Double width; + private Double height; + private Color color; + private EdgeInsets padding; + private EdgeInsets margin; + private Alignment alignment; + private Object decoration; + + public void duration(Duration v) { + this.duration = v; + } + + public void curve(Curve v) { + this.curve = v; + } + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void color(Color v) { + this.color = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void margin(EdgeInsets v) { + this.margin = v; + } + + public void alignment(Alignment v) { + this.alignment = v; + } + + public void decoration(Object v) { + this.decoration = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedEvaluation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedEvaluation.java new file mode 100644 index 00000000000..dc5cc6986b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedEvaluation.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.runtime.Funcs; + +/** + * The {@link Animation} produced by {@code animatable.animate(parent)}: its + * value is {@code animatable.transform(parent.value)} and it forwards status + * and listener registration straight to {@code parent}. + */ +public class AnimatedEvaluation extends Animation { + + private final Animation parent; + private final Animatable evaluatable; + + public AnimatedEvaluation(Animation parent, Animatable evaluatable) { + this.parent = parent; + this.evaluatable = evaluatable; + } + + @Override + public T value() { + return evaluatable.transform(parent.value()); + } + + @Override + public AnimationStatus status() { + return parent.status(); + } + + @Override + public void addListener(Funcs.VoidFunc0 listener) { + parent.addListener(listener); + } + + @Override + public void removeListener(Funcs.VoidFunc0 listener) { + parent.removeListener(listener); + } + + @Override + public void addStatusListener(Funcs.VoidFunc1 listener) { + parent.addStatusListener(listener); + } + + @Override + public void removeStatusListener(Funcs.VoidFunc1 listener) { + parent.removeStatusListener(listener); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedOpacity.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedOpacity.java new file mode 100644 index 00000000000..d7f32b7a70d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedOpacity.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.core.Duration; + +/** + * Animates its child's opacity to a target value over a {@link Duration} — + * Flutter's {@code AnimatedOpacity}. This pass hosts the child; opacity + * compositing is deferred. + */ +public class AnimatedOpacity extends AnimatedChildWidget { + + private double opacity = 1.0; + private Duration duration; + private Curve curve; + + public void opacity(double v) { + this.opacity = v; + } + + public void duration(Duration v) { + this.duration = v; + } + + public void curve(Curve v) { + this.curve = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedPadding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedPadding.java new file mode 100644 index 00000000000..1910502c8f1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedPadding.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.EdgeInsets; + +import dart.core.Duration; + +/** + * Animates changes to its padding over a {@link Duration} — Flutter's + * {@code AnimatedPadding}. This pass hosts the child; the padding is applied + * without the tween (final value). + */ +public class AnimatedPadding extends AnimatedChildWidget { + + private EdgeInsets padding; + private Duration duration; + private Curve curve; + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void duration(Duration v) { + this.duration = v; + } + + public void curve(Curve v) { + this.curve = v; + } + + public EdgeInsets getPadding() { + return padding; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSize.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSize.java new file mode 100644 index 00000000000..3d3c1893700 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSize.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Alignment; + +import dart.core.Duration; + +/** + * Animates its own size to fit its child when the child changes size — + * Flutter's {@code AnimatedSize}. This pass hosts the child and sizes to it + * immediately; the size tween is deferred. + */ +public class AnimatedSize extends AnimatedChildWidget { + + private Duration duration; + private Curve curve; + private Alignment alignment; + + public void duration(Duration v) { + this.duration = v; + } + + public void curve(Curve v) { + this.curve = v; + } + + public void alignment(Alignment v) { + this.alignment = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSwitcher.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSwitcher.java new file mode 100644 index 00000000000..3c1ceee9d57 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedSwitcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.core.Duration; + +/** + * Cross-fades between successive children over a {@link Duration} — Flutter's + * {@code AnimatedSwitcher}. This pass shows the current child directly; the + * in/out transition is deferred. + */ +public class AnimatedSwitcher extends AnimatedChildWidget { + + private Duration duration; + private Duration reverseDuration; + private Curve switchInCurve; + private Curve switchOutCurve; + + public void duration(Duration v) { + this.duration = v; + } + + public void reverseDuration(Duration v) { + this.reverseDuration = v; + } + + public void switchInCurve(Curve v) { + this.switchInCurve = v; + } + + public void switchOutCurve(Curve v) { + this.switchOutCurve = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidget.java new file mode 100644 index 00000000000..8a418c286d8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidget.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Element; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.foundation.Listenable; + +/** + * A widget that rebuilds when a {@link Listenable} it is given notifies its + * listeners — Flutter's {@code AnimatedWidget}. Subclasses implement + * {@code build(BuildContext)} and read {@link #listenable()} (usually an + * {@code Animation}) to derive the current frame. Modelled on top of + * {@link StatelessWidget}: its build runs as a pure function of the current + * listenable value. + */ +public abstract class AnimatedWidget extends StatelessWidget { + + private Listenable listenable; + + /** Named parameter setter for the Dart {@code listenable:} parameter. */ + public void listenable(Listenable v) { + this.listenable = v; + } + + /** Getter for the driving {@link Listenable}. */ + public Listenable listenable() { + return listenable; + } + + /** + * An element that LISTENS. The whole point of the type is that a notification rebuilds + * the widget, so the plain {@code StatelessElement} a StatelessWidget would otherwise + * get leaves every subclass frozen on its first frame. + */ + @Override + public Element createElement() { + return new AnimatedWidgetElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidgetElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidgetElement.java new file mode 100644 index 00000000000..0c5c683d01b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimatedWidgetElement.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.StatelessElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.Listenable; + +import dart.runtime.Funcs; + +/** + * Element for {@link AnimatedWidget}: listens to the widget's {@code listenable} and rebuilds + * on every notification — Flutter's {@code _AnimatedState}. + * + *

Without this an AnimatedWidget is a plain StatelessWidget that happens to read an + * animation: it samples the value once, paints a correct first frame, and never moves again. + * The gallery's settings button is exactly that shape — it renders + * {@code SettingsIcon(animationController.value)}, so a frozen subscription leaves the icon + * stuck on whichever glyph it was built with while the panel behind it opens and closes.

+ * + *

{@link AnimatedBuilderElement} already did this for the builder-callback form; the + * subclass form went without, which is why three of the gallery's widgets — this icon, + * Shrine's backdrop title and Rally's pie chart — were all still.

+ */ +public class AnimatedWidgetElement extends StatelessElement { + + private Listenable listened; + private final Funcs.VoidFunc0 handler = new Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + } + }; + + public AnimatedWidgetElement(AnimatedWidget widget) { + super(widget); + } + + @Override + public void mount(com.codename1.flutter.Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + } + + @Override + public void update(Widget newWidget) { + // Resubscribed around the swap: a rebuilt widget may carry a DIFFERENT listenable, + // and holding the old one leaks a listener onto a controller that outlives us. + unsubscribe(); + super.update(newWidget); + subscribe(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + private void subscribe() { + Widget w = widget(); + if (!(w instanceof AnimatedWidget)) { + return; + } + listened = ((AnimatedWidget) w).listenable(); + if (listened != null) { + listened.addListener(handler); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(handler); + listened = null; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animation.java new file mode 100644 index 00000000000..bf7cf5167b8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Animation.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.foundation.Listenable; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * A value of type {@code T} that changes over the lifetime of an animation, + * plus a {@link AnimationStatus} and listener registration — Flutter's + * {@code Animation}. + * + *

Concrete subclasses ({@link AnimationController}, {@link CurvedAnimation}, + * {@link AlwaysStoppedAnimation}, and the tween-driven evaluations) supply the + * current {@link #value()} and {@link #status()}. This base class owns the + * value- and status-listener bookkeeping so subclasses can broadcast changes + * with {@link #notifyListeners()} / {@link #notifyStatusListeners}.

+ */ +public abstract class Animation implements Listenable { + + private final List listeners = new ArrayList(); + private final List> statusListeners = + new ArrayList>(); + + /** The current animated value. */ + public abstract T value(); + + /** The current phase; the default is {@link AnimationStatus#dismissed}. */ + public AnimationStatus status() { + return AnimationStatus.dismissed; + } + + public boolean isCompleted() { + return status() == AnimationStatus.completed; + } + + public boolean isDismissed() { + return status() == AnimationStatus.dismissed; + } + + public boolean isAnimating() { + AnimationStatus s = status(); + return s == AnimationStatus.forward || s == AnimationStatus.reverse; + } + + public boolean isForwardOrCompleted() { + AnimationStatus s = status(); + return s == AnimationStatus.forward || s == AnimationStatus.completed; + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + boolean first = !hasAnyListeners(); + listeners.add(listener); + if (first) { + didStartListening(); + } + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + boolean had = hasAnyListeners(); + listeners.remove(listener); + if (had && !hasAnyListeners()) { + didStopListening(); + } + } + + public void addStatusListener(Funcs.VoidFunc1 listener) { + if (listener != null) { + boolean first = !hasAnyListeners(); + statusListeners.add(listener); + if (first) { + didStartListening(); + } + } + } + + public void removeStatusListener(Funcs.VoidFunc1 listener) { + boolean had = hasAnyListeners(); + statusListeners.remove(listener); + if (had && !hasAnyListeners()) { + didStopListening(); + } + } + + /** Whether anyone listens to this animation's value or status. */ + protected final boolean hasAnyListeners() { + return !listeners.isEmpty() || !statusListeners.isEmpty(); + } + + /** + * The first listener arrived: an animation derived from another subscribes to it + * here, as Flutter's AnimationLazyListenerMixin does, so it forwards changes only + * while someone is listening and never holds its parent's listener list otherwise. + */ + protected void didStartListening() { + } + + /** The last listener left: undo {@link #didStartListening()}. */ + protected void didStopListening() { + } + + protected void notifyListeners() { + com.codename1.flutter.foundation.Listeners.notify(listeners); + } + + protected void notifyStatusListeners(AnimationStatus s) { + com.codename1.flutter.foundation.Listeners.notify(statusListeners, s); + } + + /** Removes every value and status listener, as Flutter's dispose does. */ + protected void clearListeners() { + boolean had = hasAnyListeners(); + listeners.clear(); + statusListeners.clear(); + if (had) { + didStopListening(); + } + } + + /** + * Drives {@code child} from this animation (which must produce doubles): + * {@code controller.drive(tween)} == {@code tween.animate(controller)}. + */ + @SuppressWarnings("unchecked") + public Animation drive(Animatable child) { + return child.animate((Animation) (Animation) this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationBehavior.java new file mode 100644 index 00000000000..1f9ffadb996 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationBehavior.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Configures how an {@link AnimationController} behaves when animation is + * disabled (e.g. by the platform's "reduce motion" accessibility setting) — + * Flutter's {@code AnimationBehavior}. {@link #normal} lets the controller obey + * the platform setting, while {@link #preserve} forces it to animate anyway + * (the progress-indicator demo passes {@code preserve} so its spinner keeps + * turning). + */ +public enum AnimationBehavior { + normal, + preserve +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationController.java new file mode 100644 index 00000000000..f6d4929eb21 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationController.java @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.ui.CN; +import com.codename1.ui.Display; + +import dart.core.Duration; + +/** + * Drives an animation value between {@code lowerBound} and {@code upperBound} + * over a {@link Duration} — Flutter's {@code AnimationController}. The + * controller joins the shared {@link FrameDriver} clock, which rides Codename + * One's own animation loop, and advances once per frame — firing value + * listeners each frame and status listeners at the transitions. The + * {@code vsync} TickerProvider is accepted for API shape but not otherwise + * used, because the frame clock already is the vsync. + * + *

A run is timed from its FIRST tick, not from the call that started it, + * which is what Flutter's {@code Ticker} does; see {@code beginRun}.

+ * + *

When CN1's Display is not initialized (headless), animations complete + * synchronously so logic that awaits {@code forward()} still progresses.

+ */ +public class AnimationController extends Animation { + + private double lowerBound = 0.0; + private double upperBound = 1.0; + private double currentValue; + private long durationMs = 300; + private long reverseDurationMs = -1; + private AnimationStatus status = AnimationStatus.dismissed; + private AnimationBehavior animationBehavior = AnimationBehavior.normal; + + // Active run state. + private boolean running; + private int generation; + /** {@link #runStartTime} before the first tick has established the run's zero point. */ + private static final long UNSTARTED = Long.MIN_VALUE; + private long runStartTime = UNSTARTED; + private long runDurationMs; + private double runStartValue; + private double runTargetValue; + private AnimationStatus runStatus; + /** + * The curve this run eases along, or null for a linear run. + * + *

{@code animateTo}/{@code animateBack} take a curve in Flutter and it shapes the + * controller's own progress; {@code forward}, {@code reverse} and {@code fling} are + * linear. Dropping it made every eased call linear, which is not a subtle difference: + * Reply opens its drawer with {@code animateTo(0.4, curve: Easing.legacy)}, and a + * linear run starts and stops abruptly where the real one eases in and settles.

+ */ + private Curve runCurve; + + // Repeat config. + private boolean repeating; + private boolean repeatReverse; + private double repeatMin; + private double repeatMax; + + public AnimationController() { + } + + // ------------------------------------------------------------------ + // Named-parameter setters (constructor arguments) + // ------------------------------------------------------------------ + + public void duration(Duration v) { + if (v != null) { + this.durationMs = v.inMilliseconds(); + } + } + + public void reverseDuration(Duration v) { + if (v != null) { + this.reverseDurationMs = v.inMilliseconds(); + } + } + + /** + * The Dart {@code value} setter — both the constructor {@code value:} + * argument and the imperative {@code controller.value = v} assignment map + * here (the transpiler emits the overloaded {@code value(v)} method). Stops + * any running animation, clamps to the bounds, and notifies listeners. + */ + public void value(double v) { + // Canceled, as Flutter's value setter, reset and dispose are: the interrupted run's + // future stays pending rather than resolving as if its target had been reached. + stop(Boolean.TRUE); + requestedValue = v; + double clamped = clamp(v); + this.currentValue = clamped; + AnimationStatus newStatus = statusForValue(clamped); + boolean statusChanged = newStatus != status; + this.status = newStatus; + notifyListeners(); + if (statusChanged) { + notifyStatusListeners(status); + } + } + + public void lowerBound(double v) { + this.lowerBound = v; + rebaseOnBounds(); + } + + public void upperBound(double v) { + this.upperBound = v; + rebaseOnBounds(); + } + + /// The value last asked for through value(), or null if none was. + private Double requestedValue; + + /// Re-derives the value after a bound changes -- which in Flutter only happens + /// at construction, where the bounds are final. + /// + /// Flutter starts a controller at its lower bound unless `value:` is given. + /// The no-argument Java controller starts at 0 and the bound setters only + /// stored the bound, so AnimationController(lowerBound: 10, upperBound: 20) + /// reported and animated from 0 -- outside its own range -- with the wrong + /// status. The transpiler also applies named arguments as setters in source + /// order, so `value: 15` given before the bounds had already been clamped + /// against the default 0..1 and stored as 1. Keeping the requested value and + /// re-clamping it here makes the result independent of that order. + private void rebaseOnBounds() { + currentValue = requestedValue != null ? clamp(requestedValue) : lowerBound; + status = statusForValue(currentValue); + } + + public void vsync(TickerProvider v) { + // self-driven; provider unused + } + + public void debugLabel(String v) { + // ignored + } + + /** + * How the controller behaves when animation is disabled by the platform — + * Flutter's {@code AnimationController.animationBehavior}. Captured for API + * shape; this runtime always animates (it does not consult a + * reduce-motion setting), matching {@link AnimationBehavior#preserve}. + */ + public void animationBehavior(AnimationBehavior v) { + this.animationBehavior = v; + } + + // ------------------------------------------------------------------ + // Getters + // ------------------------------------------------------------------ + + @Override + public Double value() { + return currentValue; + } + + @Override + public AnimationStatus status() { + return status; + } + + public Duration duration() { + return Duration.ofMicroseconds(durationMs * 1000); + } + + /** Flutter's {@code controller.view}: the controller is its own view. */ + public Animation view() { + return this; + } + + // ------------------------------------------------------------------ + // Playback controls + // ------------------------------------------------------------------ + + public dart.async.Future forward(Double from) { + if (from != null) { + // Through the value setter, as Flutter's forward(from:) does: it stops the + // previous run and notifies value and status listeners now. Assigning the + // field left dependants showing the old value until the next frame. + value(from.doubleValue()); + } + repeating = false; + dart.async.Future done = newRun(); + beginRun(upperBound, durationMs, AnimationStatus.forward); + return done; + } + + /** No-argument {@code forward()} — usable as a bare {@code VoidCallback} tear-off. */ + public dart.async.Future forward() { + return forward(null); + } + + /** No-argument {@code reverse()} — usable as a bare {@code VoidCallback} tear-off. */ + public dart.async.Future reverse() { + return reverse(null); + } + + public dart.async.Future reverse(Double from) { + if (from != null) { + value(from.doubleValue()); // the setter, as forward(from:) -- see there + } + repeating = false; + long d = reverseDurationMs >= 0 ? reverseDurationMs : durationMs; + dart.async.Future done = newRun(); + beginRun(lowerBound, d, AnimationStatus.reverse); + return done; + } + + /** + * Drives the controller with a fling toward the bound implied by the sign + * of {@code velocity} — Flutter's {@code AnimationController.fling}. A + * positive velocity flings toward {@code upperBound}, a negative one toward + * {@code lowerBound}. The {@code springDescription} (the spring modeling the + * fling's settle) and {@code animationBehavior} are captured for API shape; + * this runtime plays a plain timed run to the target bound. + */ + public dart.async.Future fling(double velocity, Object springDescription, + AnimationBehavior animationBehavior) { + repeating = false; + dart.async.Future done = newRun(); + if (velocity < 0.0) { + beginRun(lowerBound, durationMs, AnimationStatus.reverse); + } else { + beginRun(upperBound, durationMs, AnimationStatus.forward); + } + return done; + } + + public dart.async.Future animateTo(double target, Duration duration, Curve curve) { + repeating = false; + AnimationStatus dir = target >= currentValue + ? AnimationStatus.forward : AnimationStatus.reverse; + dart.async.Future done = newRun(); + beginRun(clamp(target), simulationMillis(target, duration, dir), dir, curve); + return done; + } + + /** + * How long an {@code animateTo}/{@code animateBack} run lasts. + * + *

A duration given explicitly is that duration. Without one, the controller's + * duration describes crossing the WHOLE range, and the run gets the fraction of it + * this move actually covers -- Flutter's {@code _animateToInternal}: + * {@code directionDuration * remainingFraction}.

+ * + *

Not a detail. Reply opens its mailbox drawer with {@code animateTo(0.4)} on a + * 300ms controller, which is a 120ms animation; running the full 300 made the drawer + * take two and a half times as long to arrive as the reference, and the same error + * slowed the drop arrow beside it and the search page behind it. Measured against the + * reference frame by frame, ours had barely moved at the point the reference was + * finished.

+ */ + /// Seam for AnimateToDurationTest: the arithmetic, without needing a frame clock. + long simulationMillisForTest(double target, Duration explicit, AnimationStatus dir) { + return simulationMillis(target, explicit, dir); + } + + private long simulationMillis(double target, Duration explicit, AnimationStatus dir) { + if (explicit != null) { + // Flutter does not animate at all when asked to go where it already is. + return target == currentValue ? 0 : explicit.inMilliseconds(); + } + double range = upperBound - lowerBound; + double remaining = range > 0 ? Math.abs(target - currentValue) / range : 1.0; + long base = dir == AnimationStatus.reverse && reverseDurationMs >= 0 + ? reverseDurationMs : durationMs; + return Math.round(base * remaining); + } + + public dart.async.Future animateBack(double target, Duration duration, Curve curve) { + repeating = false; + dart.async.Future done = newRun(); + beginRun(clamp(target), + simulationMillis(target, duration, AnimationStatus.reverse), + AnimationStatus.reverse, curve); + return done; + } + + /// The future the CURRENT run completes, as Flutter's TickerFuture: it + /// completes when this run reaches its target, and never when the run is + /// stopped or replaced by another -- Flutter's plain TickerFuture does not + /// complete on cancellation either. + /// + /// These controls were void, so `await controller.forward()` returned at once + /// and `.then(...)` ran before the first frame: "reverse, then remove the + /// widget" removed it while the reverse was still to play. Created BEFORE the + /// run begins, because a zero-length run finishes inside beginRun. + private dart.async.Completer runCompleter; + + private dart.async.Future newRun() { + dart.async.Completer c = new dart.async.Completer(); + runCompleter = c; + return c.future(); + } + + public void repeat(Double min, Double max, Boolean reverse, Duration period) { + repeating = true; + repeatReverse = reverse != null && reverse; + repeatMin = min != null ? min : lowerBound; + repeatMax = max != null ? max : upperBound; + long d = period != null ? period.inMilliseconds() : durationMs; + currentValue = repeatMin; + beginRun(repeatMax, d, AnimationStatus.forward); + } + + public void stop(Boolean canceled) { + // A canceled run's future never completes, as in Flutter -- but + // stop(canceled: false) completes it, and ignoring the flag left every + // `await controller.forward()` pending for good. + dart.async.Completer c = runCompleter; + runCompleter = null; + settleStoppedRun(c, canceled); + running = false; + generation++; + // Leave the frame clock immediately; it stops itself once nothing is running. + FrameDriver.remove(this); + } + + /** + * What stop does with the stopped run's future: completes it for + * {@code canceled: false}, and leaves it pending otherwise -- Flutter's default is + * canceled, and a canceled TickerFuture never completes. + */ + static void settleStoppedRun(dart.async.Completer run, Boolean canceled) { + if (run != null && canceled != null && !canceled.booleanValue()) { + run.complete(null); + } + } + + public void reset() { + // Canceled, as Flutter's value setter, reset and dispose are: the interrupted run's + // future stays pending rather than resolving as if its target had been reached. + stop(Boolean.TRUE); + currentValue = lowerBound; + AnimationStatus newStatus = statusForValue(currentValue); + boolean changed = newStatus != status; + status = newStatus; + notifyListeners(); + if (changed) { + notifyStatusListeners(status); + } + } + + public void dispose() { + // Canceled, as Flutter's value setter, reset and dispose are: the interrupted run's + // future stays pending rather than resolving as if its target had been reached. + stop(Boolean.TRUE); + // Flutter's dispose clears both listener lists, so a listener an earlier one + // disposed this controller from is not called in the same notification. + clearListeners(); + } + + // ------------------------------------------------------------------ + // Driving + // ------------------------------------------------------------------ + + private void beginRun(double target, long dMs, AnimationStatus phase) { + beginRun(target, dMs, phase, null); + } + + private void beginRun(double target, long dMs, AnimationStatus phase, Curve curve) { + generation++; + final int gen = generation; + running = true; + runStartValue = currentValue; + runTargetValue = target; + runDurationMs = Math.max(0, dMs); + runStatus = phase; + runCurve = curve; + // NOT now(): the run is timed from its FIRST tick, which is what Flutter's Ticker + // does (it records _startTime inside the first frame callback). The gap between + // "start the animation" and "the clock reaches it" is setup - the setState that + // starts it, the build it triggers, the paint of that build - and charging it to + // the curve is what makes a short animation snap. Measured on the gallery's + // category expand that gap was 223ms against a 200ms duration, so the first tick + // already had t >= 1 and the animation only ever showed its end state. + runStartTime = UNSTARTED; + + if (status != phase) { + status = phase; + notifyStatusListeners(status); + } + + if (runDurationMs == 0 || runStartValue == runTargetValue) { + finishRun(gen); + return; + } + if (!Display.isInitialized()) { + // Headless: there is no frame clock to advance us, so the run collapses to its + // end state. It still gets its first tick, so a listener that reacts to the + // start of a run behaves the same way here as on a device -- which is the kind + // of difference a headless test exists to rule out. + firstTick(); + finishRun(gen); + return; + } + scheduleTick(gen); + } + + /** + * The opening frame of a run. It defines t = 0, so the value is still + * {@code runStartValue} and nothing moves -- but the listeners are notified anyway. + * + *

Flutter's Ticker calls its callback on the first frame with an elapsed of zero + * and {@code AnimationController._tick} notifies unconditionally, so a listener is + * guaranteed one call while the run is still at the value it started from. Apps build + * on that: Reply's bottom drawer animates a controller up from 0, and the rebuild that + * makes the drawer visible comes from a listener that only calls setState while the + * value is below 0.01. Suppressing this notification -- on the reasonable-sounding + * grounds that the value has not moved yet -- meant the first call a listener ever saw + * was already past that threshold. The drawer's state flipped and its arrow turned, + * and no panel was ever built.

+ */ + private void firstTick() { + FrameDriver.noteAdvance(0); + notifyListeners(); + } + + private void scheduleTick(final int gen) { + // Join the shared frame clock rather than chaining a timer of our own: N + // animations then cost one wakeup and one build flush per frame between them. + FrameDriver.add(this); + } + + /** + * How far through a run of {@code durationMs} an elapsed time is, under the + * scheduler's current time dilation. + * + *

{@code scheduler.timeDilation} stretches every animation in the app. It was + * declared here and never read, so the gallery's own "Slow motion" switch -- a + * control whose entire purpose is to let a motion design be inspected -- moved + * nothing at all. Flutter divides the frame timestamp by it; dividing the run's + * duration is the same thing and keeps the run's zero point where the first tick + * put it.

+ * + *

A dilation that is zero, negative or NaN would divide the animation by zero or + * run it backwards. Flutter asserts on it; here it means real time, so a bad value + * cannot wedge the UI.

+ * + *

Package-private and static so it can be asserted directly -- the controller's + * clock is the system clock, and a test that reimplements this arithmetic to check + * it would be testing its own copy.

+ */ + static double progress(long elapsedMs, long durationMs) { + double dilation = com.codename1.flutter.scheduler.SchedulerLib.timeDilation; + if (!(dilation > 0)) { + dilation = 1.0; + } + double effective = durationMs * dilation; + return effective <= 0 ? 1.0 : elapsedMs / effective; + } + + /** + * Advances this animation to the current time. Called once per frame by + * {@link FrameDriver}; finishing removes it from the clock. + */ + void advance() { + if (!running) { + FrameDriver.remove(this); + return; + } + int gen = generation; + if (runStartTime == UNSTARTED) { + runStartTime = now(); + firstTick(); + return; + } + long elapsed = now() - runStartTime; + FrameDriver.noteAdvance(elapsed); + double t = progress(elapsed, runDurationMs); + if (t >= 1.0) { + finishRun(gen); + if (!running) { + FrameDriver.remove(this); + } + return; + } + if (runCurve != null) { + t = runCurve.transform(t); + } + currentValue = runStartValue + (runTargetValue - runStartValue) * t; + notifyListeners(); + } + + private void finishRun(int gen) { + currentValue = runTargetValue; + running = false; + notifyListeners(); + AnimationStatus terminal = statusForValue(currentValue); + if (terminal != status) { + status = terminal; + notifyStatusListeners(status); + } + // Completed after the listeners have seen the final value, so code that + // awaited the run observes the finished state. A repeating run never + // completes -- it has no end -- which is also Flutter's behaviour. + if (!repeating && runCompleter != null) { + dart.async.Completer c = runCompleter; + runCompleter = null; + c.complete(null); + } + if (repeating && Display.isInitialized()) { + if (repeatReverse) { + double nextTarget = currentValue >= repeatMax ? repeatMin : repeatMax; + AnimationStatus phase = nextTarget >= currentValue + ? AnimationStatus.forward : AnimationStatus.reverse; + beginRun(nextTarget, runDurationMs, phase); + } else { + currentValue = repeatMin; + beginRun(repeatMax, runDurationMs, AnimationStatus.forward); + } + } + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private AnimationStatus statusForValue(double v) { + if (v <= lowerBound) { + return AnimationStatus.dismissed; + } + if (v >= upperBound) { + return AnimationStatus.completed; + } + return status == AnimationStatus.reverse ? AnimationStatus.reverse : AnimationStatus.forward; + } + + private double clamp(double v) { + if (v < lowerBound) { + return lowerBound; + } + if (v > upperBound) { + return upperBound; + } + return v; + } + + private static long now() { + // Not System.currentTimeMillis() directly: a harness comparing this runtime's + // motion against another stack's has to be able to ask both for the same + // animation time. See MotionClock -- released, which is every application, this + // IS the wall clock. + return MotionClock.now(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatus.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatus.java new file mode 100644 index 00000000000..979cb8a0042 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatus.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The lifecycle phase of an {@link Animation}, mirroring Flutter's + * {@code AnimationStatus} enum so {@code switch (controller.status)} and + * {@code status == AnimationStatus.completed} transpile directly. + */ +public enum AnimationStatus { + /** Stopped at the beginning (lowerBound). */ + dismissed, + /** Running from beginning toward end. */ + forward, + /** Running from end back toward beginning. */ + reverse, + /** Stopped at the end (upperBound). */ + completed +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatusExtensions.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatusExtensions.java new file mode 100644 index 00000000000..44f2c589b5e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationStatusExtensions.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The getter extensions Flutter defines on {@link AnimationStatus} + * ({@code isDismissed} / {@code isCompleted} / {@code isAnimating} / + * {@code isForwardOrCompleted}). Supplied as static helpers because the + * transpiler resolves Dart extension getters to static calls of the form + * {@code AnimationStatusExtensions.isDismissed(status)}. + */ +public final class AnimationStatusExtensions { + + private AnimationStatusExtensions() { + } + + /** Whether the animation is stopped at the beginning. */ + public static boolean isDismissed(AnimationStatus status) { + return status == AnimationStatus.dismissed; + } + + /** Whether the animation is stopped at the end. */ + public static boolean isCompleted(AnimationStatus status) { + return status == AnimationStatus.completed; + } + + /** Whether the animation is currently running (forward or reverse). */ + public static boolean isAnimating(AnimationStatus status) { + return status == AnimationStatus.forward || status == AnimationStatus.reverse; + } + + /** Whether the animation is running forward or has completed. */ + public static boolean isForwardOrCompleted(AnimationStatus status) { + return status == AnimationStatus.forward || status == AnimationStatus.completed; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationTrace.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationTrace.java new file mode 100644 index 00000000000..7bd2d5e3a4b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/AnimationTrace.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Public window onto the frame clock's counters, for the diagnostics in + * {@code BuildOwner.frameStats()}. {@link FrameDriver} itself stays package-private: + * nothing outside this package should be able to reach the clock, only to read it. + */ +public final class AnimationTrace { + + private AnimationTrace() { + } + + /** Starts or stops recording clock ticks; resets the counters either way. */ + public static void trace(boolean on) { + FrameDriver.trace(on); + } + + /** The recorded numbers as JSON object members, without the enclosing braces. */ + public static String stats() { + return FrameDriver.stats(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/BorderRadiusTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/BorderRadiusTween.java new file mode 100644 index 00000000000..e83e3ca803c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/BorderRadiusTween.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A {@link Tween} over {@code BorderRadius} - Flutter's {@code BorderRadiusTween}. + * + *

A corner that steps is a corner that changes shape in one frame, halfway + * through whatever motion it was meant to accompany.

+ */ +public class BorderRadiusTween extends Tween { + + @Override + public com.codename1.flutter.BorderRadius lerp(double t) { + return com.codename1.flutter.BorderRadius.lerp(begin(), end(), t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ChainedEvaluation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ChainedEvaluation.java new file mode 100644 index 00000000000..beb01873fbc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ChainedEvaluation.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The {@link Animatable} produced by {@code evaluatable.chain(parent)}: + * {@code transform(t) == evaluatable.transform(parent.transform(t))}. Used to + * compose a {@link CurveTween} in front of another tween. + */ +public class ChainedEvaluation extends Animatable { + + private final Animatable parent; + private final Animatable evaluatable; + + public ChainedEvaluation(Animatable parent, Animatable evaluatable) { + this.parent = parent; + this.evaluatable = evaluatable; + } + + @Override + public T transform(double t) { + return evaluatable.transform(parent.transform(t)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ColorTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ColorTween.java new file mode 100644 index 00000000000..a7deb6599f2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ColorTween.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Color; + +/** + * A {@link Tween} that interpolates ARGB {@link Color}s channel by channel — + * Flutter's {@code ColorTween}. + */ +public class ColorTween extends Tween { + + @Override + public Color lerp(double t) { + Color b = begin(); + Color e = end(); + if (b == null && e == null) { + return null; + } + if (b == null) { + return scaleAlpha(e, t); + } + if (e == null) { + return scaleAlpha(b, 1.0 - t); + } + int a = lerpChannel(b.alpha(), e.alpha(), t); + int r = lerpChannel(b.red(), e.red(), t); + int g = lerpChannel(b.green(), e.green(), t); + int bl = lerpChannel(b.blue(), e.blue(), t); + return new Color((a << 24) | (r << 16) | (g << 8) | bl); + } + + private static Color scaleAlpha(Color c, double t) { + int a = lerpChannel(0, c.alpha(), t); + return new Color((a << 24) | (c.value() & 0xFFFFFFL)); + } + + private static int lerpChannel(int a, int b, double t) { + int v = (int) Math.round(a + (b - a) * t); + return v < 0 ? 0 : (v > 255 ? 255 : v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Cubic.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Cubic.java new file mode 100644 index 00000000000..f237d498a75 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Cubic.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A cubic Bezier easing curve through control points (a, b) and (c, d) — + * Flutter's {@code Cubic}. Solves for the x parameter by bisection (the same + * approach Flutter uses) then evaluates y. + */ +public class Cubic extends Curve { + + private static final double CUBIC_ERROR_BOUND = 0.001; + + private final double a; + private final double b; + private final double c; + private final double d; + + public Cubic(double a, double b, double c, double d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + private static double evaluateCubic(double a, double b, double m) { + return 3 * a * (1 - m) * (1 - m) * m + + 3 * b * (1 - m) * m * m + + m * m * m; + } + + @Override + protected double transformInternal(double t) { + double start = 0.0; + double end = 1.0; + while (true) { + double midpoint = (start + end) / 2; + double estimate = evaluateCubic(a, c, midpoint); + if (Math.abs(t - estimate) < CUBIC_ERROR_BOUND) { + return evaluateCubic(b, d, midpoint); + } + if (estimate < t) { + start = midpoint; + } else { + end = midpoint; + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curve.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curve.java new file mode 100644 index 00000000000..ff05176a22f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curve.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A mapping of the unit interval to itself — Flutter's {@code Curve}. Concrete + * curves ({@link Cubic}, {@link Interval}, and the {@link Curves} constants) + * override {@link #transform}. {@code t} is clamped to [0, 1]. + */ +public abstract class Curve { + + /** Maps {@code t} (0..1) to an eased value; endpoints are pinned to 0 and 1. */ + public double transform(double t) { + if (t <= 0.0) { + return 0.0; + } + if (t >= 1.0) { + return 1.0; + } + return transformInternal(t); + } + + /** The eased value strictly inside (0, 1). */ + protected abstract double transformInternal(double t); + + /** The curve that runs this one in reverse ({@code 1 - curve(1 - t)}). */ + public Curve flipped() { + return new FlippedCurve(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurveTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurveTween.java new file mode 100644 index 00000000000..8c636f94a88 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurveTween.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * An {@link Animatable} that maps its parametric value through a {@link Curve} + * — Flutter's {@code CurveTween}, typically chained in front of another tween + * ({@code tween.chain(CurveTween(curve: Curves.easeOut))}). + */ +public class CurveTween extends Animatable { + + private Curve curve = Curves.linear; + + /** Named-parameter setter for the Dart {@code curve:} argument. */ + public void curve(Curve v) { + this.curve = v == null ? Curves.linear : v; + } + + public Curve curve() { + return curve; + } + + @Override + public Double transform(double t) { + return curve.transform(t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurvedAnimation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurvedAnimation.java new file mode 100644 index 00000000000..920ac0964f2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/CurvedAnimation.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.runtime.Funcs; + +/** + * An {@link Animation} that runs its {@code parent}'s value through a + * {@link Curve} (and an optional {@code reverseCurve} while the parent is + * running backward) — Flutter's {@code CurvedAnimation}. Status and listener + * registration forward to the parent so dependents rebuild on every tick. + */ +public class CurvedAnimation extends Animation { + + private Animation parent; + private Curve curve = Curves.linear; + private Curve reverseCurve; + + /// Whether we have already subscribed to {@link #parent}. + private boolean following; + + /** + * Named-parameter setter for {@code parent:}. + * + *

Subscribing here is what makes this animation tick. A CurvedAnimation only + * reshapes another animation's value; it is never driven by a clock of its own, so + * unless it follows its parent it never notifies anybody and every widget listening + * to it sits at the value it happened to be built with.

+ * + *

This class always claimed to forward registration to the parent and never did: + * {@code addListener} was inherited unchanged, so it filed the listener on an object + * that nothing ever notified. Anything animated through a curve was therefore + * motionless -- which in the mail study is the bottom bar that should fold away as you + * read down the list, driven by a controller behind {@code Easing.legacy}.

+ */ + public void parent(Animation v) { + this.parent = v; + if (v == null || following) { + return; + } + following = true; + v.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + notifyListeners(); + } + }); + v.addStatusListener(new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + notifyStatusListeners(s); + } + }); + } + + /** Named-parameter setter for {@code curve:}. */ + public void curve(Curve v) { + this.curve = v == null ? Curves.linear : v; + } + + /** Named-parameter setter for {@code reverseCurve:}. */ + public void reverseCurve(Curve v) { + this.reverseCurve = v; + } + + @Override + public Double value() { + double t = parent == null ? 0.0 : parent.value(); + Curve active = curve; + if (reverseCurve != null && parent != null && parent.status() == AnimationStatus.reverse) { + active = reverseCurve; + } + return active.transform(t); + } + + @Override + public AnimationStatus status() { + return parent == null ? AnimationStatus.dismissed : parent.status(); + } + + @Override + public void addListener(Funcs.VoidFunc0 listener) { + if (parent != null) { + parent.addListener(listener); + } + } + + @Override + public void removeListener(Funcs.VoidFunc0 listener) { + if (parent != null) { + parent.removeListener(listener); + } + } + + @Override + public void addStatusListener(Funcs.VoidFunc1 listener) { + if (parent != null) { + parent.addStatusListener(listener); + } + } + + @Override + public void removeStatusListener(Funcs.VoidFunc1 listener) { + if (parent != null) { + parent.removeStatusListener(listener); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curves.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curves.java new file mode 100644 index 00000000000..0eccc59d856 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Curves.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The standard easing {@link Curve} constants — Flutter's {@code Curves}. The + * Bezier-based curves use the same control points as Flutter; the analytic + * ones (linear, decelerate, bounce, elastic) are computed directly. + */ +public final class Curves { + + private Curves() { + } + + public static final Curve linear = new Curve() { + @Override + public double transform(double t) { + return t; + } + + @Override + protected double transformInternal(double t) { + return t; + } + }; + + public static final Curve decelerate = new Curve() { + @Override + protected double transformInternal(double t) { + double u = 1.0 - t; + return 1.0 - u * u; + } + }; + + public static final Cubic ease = new Cubic(0.25, 0.1, 0.25, 1.0); + public static final Cubic easeIn = new Cubic(0.42, 0.0, 1.0, 1.0); + public static final Cubic easeOut = new Cubic(0.0, 0.0, 0.58, 1.0); + public static final Cubic easeInOut = new Cubic(0.42, 0.0, 0.58, 1.0); + public static final Cubic easeInOutCubic = new Cubic(0.645, 0.045, 0.355, 1.0); + public static final Cubic easeInCubic = new Cubic(0.55, 0.055, 0.675, 0.19); + public static final Cubic easeOutCubic = new Cubic(0.215, 0.61, 0.355, 1.0); + public static final Cubic easeInSine = new Cubic(0.47, 0.0, 0.745, 0.715); + public static final Cubic easeOutSine = new Cubic(0.39, 0.575, 0.565, 1.0); + public static final Cubic easeInOutSine = new Cubic(0.445, 0.05, 0.55, 0.95); + public static final Cubic fastOutSlowIn = new Cubic(0.4, 0.0, 0.2, 1.0); + public static final Cubic slowMiddle = new Cubic(0.15, 0.85, 0.85, 0.15); + public static final Cubic fastLinearToSlowEaseIn = new Cubic(0.18, 1.0, 0.04, 1.0); + + public static final Curve bounceIn = new Curve() { + @Override + protected double transformInternal(double t) { + return 1.0 - bounce(1.0 - t); + } + }; + + public static final Curve bounceOut = new Curve() { + @Override + protected double transformInternal(double t) { + return bounce(t); + } + }; + + public static final Curve bounceInOut = new Curve() { + @Override + protected double transformInternal(double t) { + if (t < 0.5) { + return (1.0 - bounce(1.0 - t * 2.0)) * 0.5; + } + return bounce(t * 2.0 - 1.0) * 0.5 + 0.5; + } + }; + + public static final Curve elasticIn = new Curve() { + @Override + protected double transformInternal(double t) { + double p = 0.4; + double s = p / 4.0; + double m = t - 1.0; + return -Math.pow(2.0, 10.0 * m) * Math.sin((m - s) * (2.0 * Math.PI) / p); + } + }; + + public static final Curve elasticOut = new Curve() { + @Override + protected double transformInternal(double t) { + double p = 0.4; + double s = p / 4.0; + return Math.pow(2.0, -10.0 * t) * Math.sin((t - s) * (2.0 * Math.PI) / p) + 1.0; + } + }; + + private static double bounce(double t) { + if (t < 1.0 / 2.75) { + return 7.5625 * t * t; + } + if (t < 2.0 / 2.75) { + double u = t - 1.5 / 2.75; + return 7.5625 * u * u + 0.75; + } + if (t < 2.5 / 2.75) { + double u = t - 2.25 / 2.75; + return 7.5625 * u * u + 0.9375; + } + double u = t - 2.625 / 2.75; + return 7.5625 * u * u + 0.984375; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Easing.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Easing.java new file mode 100644 index 00000000000..24e546e7f59 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Easing.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The Material 3 named easing curves — Flutter's {@code Easing}. Each is a + * static {@link Curve} (the {@code legacy}/{@code standard}/{@code emphasized} + * families are Bezier {@link Cubic}s using the same control points as Flutter); + * the reply study reads {@code Easing.legacy} and {@code Easing.legacy.flipped}. + */ +public final class Easing { + + private Easing() { + } + + public static final Curve linear = Curves.linear; + public static final Curve legacy = new Cubic(0.4, 0.0, 0.2, 1.0); + public static final Curve legacyDecelerate = new Cubic(0.0, 0.0, 0.2, 1.0); + public static final Curve legacyAccelerate = new Cubic(0.4, 0.0, 1.0, 1.0); + public static final Curve standard = new Cubic(0.2, 0.0, 0.0, 1.0); + public static final Curve standardAccelerate = new Cubic(0.3, 0.0, 1.0, 1.0); + public static final Curve standardDecelerate = new Cubic(0.0, 0.0, 0.0, 1.0); + public static final Curve emphasized = new Cubic(0.2, 0.0, 0.0, 1.0); + public static final Curve emphasizedAccelerate = new Cubic(0.3, 0.0, 0.8, 0.15); + public static final Curve emphasizedDecelerate = new Cubic(0.05, 0.7, 0.1, 1.0); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/EdgeInsetsGeometryTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/EdgeInsetsGeometryTween.java new file mode 100644 index 00000000000..424a5861607 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/EdgeInsetsGeometryTween.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A {@link Tween} over {@code EdgeInsetsGeometry} - Flutter's {@code EdgeInsetsGeometryTween}. + * + *

Insets that step relayout the subtree in one jump at the midpoint instead of + * easing it.

+ */ +public class EdgeInsetsGeometryTween extends Tween { + + @Override + public com.codename1.flutter.EdgeInsetsGeometry lerp(double t) { + com.codename1.flutter.EdgeInsetsGeometry b = begin(); + com.codename1.flutter.EdgeInsetsGeometry e = end(); + if (b instanceof com.codename1.flutter.EdgeInsets + || e instanceof com.codename1.flutter.EdgeInsets) { + return com.codename1.flutter.EdgeInsets.lerp( + (com.codename1.flutter.EdgeInsets) (b instanceof com.codename1.flutter.EdgeInsets + ? b : null), + (com.codename1.flutter.EdgeInsets) (e instanceof com.codename1.flutter.EdgeInsets + ? e : null), + t); + } + return super.lerp(t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FadeTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FadeTransition.java new file mode 100644 index 00000000000..72292c0ed51 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FadeTransition.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Opacity; + +/** + * Animates the opacity of its child from an {@link Animation} — Flutter's + * {@code FadeTransition}. The child is wrapped in an {@link Opacity}, which composites the + * whole subtree as one layer, so overlapping children fade together rather than each + * showing through the others. + */ +public class FadeTransition extends AnimatedChildWidget { + + private Animation opacity; + + public void opacity(Animation v) { + this.opacity = v; + listenable(v); + } + + public Animation getOpacity() { + return opacity; + } + + @Override + public Widget build(BuildContext context) { + Opacity o = new Opacity(); + o.opacity(valueOf(opacity, 1.0)); + o.child(getChild()); + return o; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FlippedCurve.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FlippedCurve.java new file mode 100644 index 00000000000..e27ebfd8cd7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FlippedCurve.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Runs a {@link Curve} in reverse: {@code transform(t) == 1 - curve(1 - t)} — + * Flutter's {@code FlippedCurve} / {@code Curve.flipped}. + */ +public class FlippedCurve extends Curve { + + private final Curve curve; + + public FlippedCurve(Curve curve) { + this.curve = curve; + } + + @Override + protected double transformInternal(double t) { + return 1.0 - curve.transform(1.0 - t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FrameDriver.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FrameDriver.java new file mode 100644 index 00000000000..feb4720abfe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/FrameDriver.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; + +import java.util.ArrayList; +import java.util.List; + +/** + * One clock for every running animation, driven by Codename One's animation loop. + * + *

This registers a single {@link com.codename1.ui.animations.Animation} on the current + * Form. That is the EDT's own frame clock: a Form with a registered animation does not let + * the EDT sleep ({@code Display.shouldEDTSleep} consults {@code Form.hasAnimations}), so + * {@code animate()} is called once per frame for as long as anything is animating, and the + * driver deregisters itself the moment the last animation finishes.

+ * + *

It used to chain {@code CN.setTimeout(16)} per frame instead, which was wrong twice + * over. {@code Display.setTimeout} allocates a whole {@code java.util.Timer} thread per + * call, so this created and abandoned one thread per animation frame; and measured in the + * simulator that path delivered a tick every ~300ms whatever delay was asked for - 1ms and + * 16ms both came back at ~290ms. At ~3fps a 300ms curve reaches t >= 1 on its FIRST tick, + * so every animation snapped straight to its end value instead of tweening, and anything + * driven by the same clock stuttered.

+ */ +final class FrameDriver { + + private static final List RUNNING = + new ArrayList(); + + /** The form the clock is currently registered on, or null when detached. */ + private static Form registeredOn; + private static boolean attachPending; + + private static final com.codename1.ui.animations.Animation CLOCK = + new com.codename1.ui.animations.Animation() { + @Override + public boolean animate() { + frame(); + // TRUE while anything is running, because that is what keeps the + // clock being called. + // + // Form.loopAnimations only calls Display.repaint(this) for a + // non-Component animation when animate() returns true, and the EDT + // only keeps looping while something is pending. Returning false + // left the clock dependent on some OTHER component repainting, so an + // animation on an otherwise still page advanced a few frames a + // second: measured, a 500ms feature-discovery open took over two + // seconds and had barely started before its route was gone, against + // Flutter's fully-open at 0.4s. An animation that is running is by + // definition about to change something, so asking for the frame is + // not waste. + // FALSE: a true here makes Form.loopAnimations call + // Display.repaint(this), and for a bare Animation the port turns + // that into a FULL-FORM repaint every frame -- measured, that alone + // held the whole app to 14-19fps. The listeners a controller + // notifies already repaint exactly the elements that changed; what + // was missing was only something to keep the EDT looping, and a + // pending serial call does that for free (see requestFrame). + return false; + } + + @Override + public void paint(Graphics g) { + } + }; + + private FrameDriver() { + } + + // ------------------------------------------------------------------ + // Diagnostics + // ------------------------------------------------------------------ + // + // "The animation snaps to its end state" and "the animation is choppy" are the same + // question asked twice: how many times did the clock tick between the start of a run + // and its end? A 200ms curve wants ~12 ticks. One tick means the first frame already + // measured t >= 1. So the counter that matters is ticks-per-run, not ticks-per-second. + + private static boolean trace; + private static long ticks; + private static long lastTickTime; + private static long gapSum; + private static long gaps; + private static long worstGap; + private static long runs; + private static long ticksThisRun; + private static long minTicksPerRun = Long.MAX_VALUE; + private static long worstFirstTickLag; + + static void trace(boolean on) { + trace = on; + ticks = 0; + gapSum = 0; + gaps = 0; + worstGap = 0; + runs = 0; + ticksThisRun = 0; + lastTickTime = 0; + minTicksPerRun = Long.MAX_VALUE; + worstFirstTickLag = 0; + } + + /// How stale a controller already was the first time the clock reached it. An animation + /// that snaps has nothing wrong with its curve - it was simply handed a first frame + /// whose elapsed time already exceeded its duration. + static void noteAdvance(long elapsedMs) { + if (trace && ticksThisRun == 1) { + worstFirstTickLag = Math.max(worstFirstTickLag, elapsedMs); + } + } + + /// The clock's own numbers, as JSON members (no braces) for embedding in a larger report. + static String stats() { + return "\"animTicks\":" + ticks + + ",\"animRuns\":" + runs + + ",\"animMinTicksPerRun\":" + (minTicksPerRun == Long.MAX_VALUE ? 0 : minTicksPerRun) + + ",\"animWorstFirstTickLagMs\":" + worstFirstTickLag + + ",\"animMeanGapMs\":" + (gaps == 0 ? 0 : gapSum / gaps) + + ",\"animWorstGapMs\":" + worstGap; + } + + private static void traceTick() { + long now = System.currentTimeMillis(); + ticks++; + ticksThisRun++; + if (lastTickTime != 0) { + long gap = now - lastTickTime; + gapSum += gap; + gaps++; + worstGap = Math.max(worstGap, gap); + } + lastTickTime = now; + } + + private static void traceRunEnded() { + if (ticksThisRun > 0) { + runs++; + minTicksPerRun = Math.min(minTicksPerRun, ticksThisRun); + ticksThisRun = 0; + } + lastTickTime = 0; + } + + /** Adds a controller to the frame loop, starting the clock if it was idle. */ + static synchronized void add(AnimationController c) { + if (!RUNNING.contains(c)) { + RUNNING.add(c); + } + attach(); + // ...and start the chain here rather than waiting for the Form to + // deliver the first tick. An animation that starts DURING a route push + // registers on the form that is still current at that instant, so that + // first tick never arrives, the self-scheduling chain never starts, and + // the controller sits frozen at its start value for good. Measured on + // the feature-discovery overlay: its open stalled around a quarter of + // the way through, which is why the coach mark came out at roughly half + // the radius Flutter draws. + requestFrame(); + } + + /** Removes a controller; the clock stops once none are left. */ + static synchronized void remove(AnimationController c) { + RUNNING.remove(c); + if (RUNNING.isEmpty()) { + detach(); + } + } + + private static synchronized void attach() { + if (!Display.isInitialized()) { + return; + } + Form f = Display.getInstance().getCurrent(); + if (f == null) { + // Animations normally start inside a mounted form; if one somehow starts first, + // retry on the next EDT pass rather than spinning up a timer. + retryAttach(); + return; + } + if (registeredOn == f) { + return; + } + detach(); + f.registerAnimated(CLOCK); + registeredOn = f; + } + + private static void retryAttach() { + if (attachPending) { + return; + } + attachPending = true; + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + synchronized (FrameDriver.class) { + attachPending = false; + if (!RUNNING.isEmpty()) { + attach(); + } + } + } + }); + } + + private static synchronized void detach() { + if (registeredOn != null) { + registeredOn.deregisterAnimated(CLOCK); + registeredOn = null; + if (trace) { + traceRunEnded(); + } + } + } + + /** + * Runs one animation frame now, instead of waiting for the clock to come round. + * + *

Reached through {@link MotionClock#advanceAndPump}: freeze the clock, advance + * it, pump. The controllers then see exactly the elapsed time the harness named. Does + * nothing an ordinary tick would not do -- it IS the ordinary tick.

+ */ + static void pump() { + frame(); + } + + private static boolean framePending; + + /** + * Queues the next animation frame without painting anything. + * + *

Display.shouldEDTSleep refuses to park while a serial call is pending, + * so this keeps the loop turning at whatever rate the EDT can manage while + * leaving the painting to the elements that actually changed.

+ */ + private static void requestFrame() { + if (framePending || !Display.isInitialized()) { + return; + } + framePending = true; + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + synchronized (FrameDriver.class) { + framePending = false; + } + frame(); + } + }); + } + + private static void frame() { + AnimationController[] due; + synchronized (FrameDriver.class) { + if (RUNNING.isEmpty()) { + detach(); + return; + } + // A form switch mid-animation would otherwise leave the clock on the form that + // is no longer being animated, and it would never tick again. + if (Display.isInitialized() && Display.getInstance().getCurrent() != registeredOn) { + attach(); + } + due = RUNNING.toArray(new AnimationController[RUNNING.size()]); + if (trace) { + traceTick(); + } + } + // Advance every animation before anything rebuilds: the build owner coalesces the + // dirty elements, so the whole frame costs one flush. + for (int i = 0; i < due.length; i++) { + try { + due[i].advance(); + } catch (Throwable t) { + // One misbehaving animation must not stop the clock for the others. + com.codename1.flutter.FlutterErrorReport.record(t); + remove(due[i]); + } + } + synchronized (FrameDriver.class) { + if (RUNNING.isEmpty()) { + detach(); + } else { + requestFrame(); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/IntTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/IntTween.java new file mode 100644 index 00000000000..41689f3d1ea --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/IntTween.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A {@link Tween} that interpolates integers, ROUNDING to the nearest like Flutter's + * {@code IntTween} ({@code (begin + (end - begin) * t).round()}). It truncated, which + * is neither IntTween nor StepTween (that one floors), so a 0 to 1 tween stayed 0 until + * the very end and every threshold landed late. + */ +public class IntTween extends Tween { + + @Override + public Integer lerp(double t) { + Integer b = begin(); + Integer e = end(); + int bi = b == null ? 0 : b.intValue(); + int ei = e == null ? 0 : e.intValue(); + return (int) dart.runtime.DartRuntime.round(bi + (ei - bi) * t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Interval.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Interval.java new file mode 100644 index 00000000000..d2c090f897a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Interval.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A curve that is 0 until {@code begin}, 1 after {@code end}, and applies an + * inner {@code curve} across [begin, end] — Flutter's {@code Interval}. Used to + * stagger sub-animations off a single controller. + */ +public class Interval extends Curve { + + private final double begin; + private final double end; + private Curve curve = Curves.linear; + + public Interval(double begin, double end) { + this.begin = begin; + this.end = end; + } + + /** Named-parameter setter for the Dart {@code curve:} argument. */ + public void curve(Curve v) { + this.curve = v == null ? Curves.linear : v; + } + + @Override + protected double transformInternal(double t) { + double span = end - begin; + double p = span <= 0.0 ? (t < begin ? 0.0 : 1.0) : (t - begin) / span; + if (p < 0.0) { + p = 0.0; + } else if (p > 1.0) { + p = 1.0; + } + return curve.transform(p); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Matrix4Tween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Matrix4Tween.java new file mode 100644 index 00000000000..05022329bb1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Matrix4Tween.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A {@link Tween} over {@code Matrix4} - Flutter's {@code Matrix4Tween}. + * + *

A matrix that steps teleports its subtree, which is the whole of what the + * animation was for.

+ */ +public class Matrix4Tween extends Tween { + + @Override + public com.codename1.flutter.vectormath.Matrix4 lerp(double t) { + return com.codename1.flutter.vectormath.Matrix4.lerp(begin(), end(), t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/MotionClock.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/MotionClock.java new file mode 100644 index 00000000000..ccf8c701961 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/MotionClock.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * The clock every animation reads, and a way to take it off the wall. + * + *

Comparing motion between two stacks only means something if both are asked for the + * SAME animation time. Against the wall clock they never are: a frame lands when it + * lands, and two recordings of one gesture are two different samplings of it. Flutter's + * own widget tests solve this by owning the clock -- {@code tester.pump(d)} advances + * animations by exactly {@code d} -- and the reference frames this port is measured + * against are captured that way.

+ * + *

This is a thin face over Codename One's own {@code AnimationTime}, the framework's + * pluggable animation clock, which exists for exactly this. Going through it rather than + * keeping a clock of our own is the point: a Flutter animation and the Codename One FORM + * TRANSITION carrying the page it lives on then advance together off one time source. + * With a separate clock, freezing it held the widgets still while the transition around + * them ran on regardless, so a captured frame was half of one moment and half of another + * -- and a page push, which is entirely a form transition, did not hold still at all.

+ * + *

Released, which is how an application always runs, this is + * {@code System.currentTimeMillis()} and costs one boolean test per tick.

+ */ +public final class MotionClock { + + private MotionClock() { + } + + /** The current animation time: the frozen one, or the wall clock. */ + public static long now() { + return com.codename1.ui.animations.AnimationTime.now(); + } + + /** + * Takes the clock off the wall, starting at the current wall time. + * + *

Starting from the wall clock rather than zero keeps any run already in flight + * consistent: a controller that recorded its start time a moment ago would otherwise + * see the clock jump backwards and measure a negative elapsed.

+ */ + public static void freeze() { + com.codename1.ui.animations.AnimationTime.setTime(System.currentTimeMillis()); + } + + /** Moves the frozen clock forward by {@code deltaMs}. Ignored when not frozen. */ + public static void advance(long deltaMs) { + if (isFrozen() && deltaMs > 0) { + com.codename1.ui.animations.AnimationTime.setTime(now() + deltaMs); + } + } + + /** + * Advances the frozen clock and runs one animation frame at the new time. + * + *

The single call a harness needs: FrameDriver is package private and staying that + * way, because an application has no business pumping the frame clock by hand. A form + * transition needs no pumping here -- Codename One's own painting loop drives it, and + * that loop now reads the same clock.

+ */ + public static void advanceAndPump(long deltaMs) { + advance(deltaMs); + FrameDriver.pump(); + } + + /** Hands the clock back to the wall. */ + public static void release() { + com.codename1.ui.animations.AnimationTime.reset(); + } + + /** Whether the clock is currently frozen. */ + public static boolean isFrozen() { + return com.codename1.ui.animations.AnimationTime.isOverridden(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcher.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcher.java new file mode 100644 index 00000000000..b892703b47f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcher.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.core.Duration; + +/** + * Runs a supplied transition when its {@code child} is replaced -- the {@code animations} + * package's {@code PageTransitionSwitcher}. The {@code transitionBuilder} is a + * three-argument closure {@code (child, primaryAnimation, secondaryAnimation)}. + * + *

It used to host the current child directly and run nothing, which made every switch + * a cut. That is not a small omission where the gallery uses it: Reply's search is not a + * pushed route at all, it is this widget swapping the search page in for the mail + * navigator, so the whole animation of opening search lived here and there was none.

+ */ +public class PageTransitionSwitcher extends AnimatedChildWidget { + + private Duration duration; + private boolean reverse; + private Object transitionBuilder; + + public void duration(Duration v) { + this.duration = v; + } + + public void reverse(boolean v) { + this.reverse = v; + } + + public void transitionBuilder(dart.runtime.Funcs.Func3, Animation, com.codename1.flutter.Widget> v) { + this.transitionBuilder = v; + } + + public Object getTransitionBuilder() { + return transitionBuilder; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new PageTransitionSwitcherElement(this); + } + + @SuppressWarnings("unchecked") + @Override + public com.codename1.flutter.Widget build(com.codename1.flutter.BuildContext context) { + com.codename1.flutter.Widget child = getChild(); + if (child == null || !(transitionBuilder instanceof dart.runtime.Funcs.Func3) + || !(context instanceof PageTransitionSwitcherElement)) { + return child; + } + PageTransitionSwitcherElement e = (PageTransitionSwitcherElement) context; + e.noteChild(child, duration); + dart.runtime.Funcs.Func3, + Animation, com.codename1.flutter.Widget> build = + (dart.runtime.Funcs.Func3, + Animation, com.codename1.flutter.Widget>) transitionBuilder; + com.codename1.flutter.Widget incoming = + build.call(child, e.primary(), e.secondary()); + com.codename1.flutter.Widget out = e.leaving(); + if (out == null) { + return incoming; + } + // Both at once, the outgoing one UNDER the incoming one. The transition itself + // decides what that looks like: it is handed the outgoing child as arrived + // (primary complete) and leaving (secondary running), which is how a shared axis + // fades one out while the other comes in. + com.codename1.flutter.widgets.Stack stack = + new com.codename1.flutter.widgets.Stack(); + dart.core.DartList kids = + new dart.core.DartList(); + kids.add(build.call(out, e.arrived(), e.primary())); + kids.add(incoming); + stack.children(kids); + return stack; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcherElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcherElement.java new file mode 100644 index 00000000000..8c12509e697 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PageTransitionSwitcherElement.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Widget; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** + * Holds the run a {@link PageTransitionSwitcher} plays when its child is replaced. + * + *

The state has to live here rather than on the widget: a widget is rebuilt every time + * anything above it changes, so a controller kept there would be a fresh one on every + * frame and the transition would restart forever. The element survives those rebuilds, + * which is what lets it notice that THIS child is not the one it had.

+ */ +public class PageTransitionSwitcherElement extends AnimatedWidgetElement { + + /// The animations package's own default, and what the gallery relies on. + private static final int DEFAULT_MS = 300; + + private final AnimationController controller = new AnimationController(); + private final AlwaysStoppedAnimation still = + new AlwaysStoppedAnimation(Double.valueOf(0)); + private Widget shown; + /// The child on its way out, kept mounted until it has finished leaving. + private Widget leaving; + private boolean subscribed; + + public PageTransitionSwitcherElement(PageTransitionSwitcher widget) { + super(widget); + } + + /** + * Notes the child being built, starting a run when it is a different one. + * + *

"Different" is Flutter's own {@code canUpdate}: same type and same key updates in + * place and is not a transition, anything else replaces the subtree and is.

+ */ + void noteChild(Widget child, Duration duration) { + if (!subscribed) { + subscribed = true; + // Settled, not starting. The first child was not switched TO -- it was always + // there -- and a transition that reads its animation at zero draws the page + // fully transparent and scaled away. Every screen built through a switcher + // would come up blank and stay blank, which is what a first run of this + // measured: the bottom-navigation demo went from 4.6% wrong to 39%. + controller.value(1.0); + controller.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + } + }); + } + controller.duration(duration != null ? duration + : Duration.of(0, 0, 0, 0, DEFAULT_MS, 0)); + if (shown != null && !Widget.canUpdate(shown, child)) { + leaving = shown; + controller.forward(Double.valueOf(0)); + } + if (controller.value().doubleValue() >= 1.0) { + leaving = null; + } + shown = child; + } + + /** The incoming child's animation: 0 when it arrives, 1 when it has settled. */ + Animation primary() { + return controller; + } + + /** The incoming child's secondary animation: it is not on its way anywhere. */ + Animation secondary() { + return still; + } + + /** The child on its way out, or null when nothing is leaving. */ + Widget leaving() { + return leaving; + } + + /** + * The outgoing child's primary animation: arrived, and staying arrived. + * + *

A shared-axis transition fades the outgoing child out over the first part of the + * run and the incoming one in over the rest, so with only the incoming child built + * there is nothing on screen at all until the run is a third done. That is a blank + * flash where the reference shows one page dissolving into the other, and it is the + * most visible thing about opening search.

+ */ + Animation arrived() { + return done; + } + + private final AlwaysStoppedAnimation done = + new AlwaysStoppedAnimation(Double.valueOf(1)); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PassthroughRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PassthroughRenderElement.java new file mode 100644 index 00000000000..af096a92f73 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PassthroughRenderElement.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * The render element backing an {@link AnimatedChildWidget}: it owns no CN1 + * component and lays out its single child with the incoming constraints, + * reporting the child's size (or the smallest allowed size when there is no + * child). Visual transforms (opacity, scale, slide) are not yet applied. + */ +public class PassthroughRenderElement extends SingleChildRenderElement { + + public PassthroughRenderElement(AnimatedChildWidget widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((AnimatedChildWidget) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(Size.ZERO); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return cs; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransition.java new file mode 100644 index 00000000000..6a1c6451775 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransition.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RelativeRect; +import com.codename1.flutter.widgets.Positioned; +import com.codename1.flutter.widgets.PositionedRenderElement; + +/** + * Animates the position/size (a {@code RelativeRect}) of a child within a + * Stack — Flutter's {@code PositionedTransition}. It resolves the animation's + * current {@code RelativeRect} and hosts the child as a {@link Positioned} + * (LTRB insets from the stack edges) so the Stack lays it out in place. The + * gallery's Backdrop drives two of these to slide the home/settings panels. + * + *

{@link PositionedTransitionElement} follows the animation frame by frame; the + * widget only carries the configuration.

+ */ +public class PositionedTransition extends AnimatedChildWidget { + + private Animation rect; + + public void rect(Animation v) { + this.rect = v; + } + + public Animation getRect() { + return rect; + } + + @Override + public Element createElement() { + Positioned p = new Positioned(); + Object v = rect != null ? rect.value() : null; + RelativeRect r = v instanceof RelativeRect ? (RelativeRect) v : RelativeRect.fill; + p.left(r.left()); + p.top(r.top()); + p.right(r.right()); + p.bottom(r.bottom()); + p.child(getChild()); + PositionedTransitionElement e = new PositionedTransitionElement(p); + e.transition(this); + return e; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransitionElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransitionElement.java new file mode 100644 index 00000000000..baeb74036e6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/PositionedTransitionElement.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RelativeRect; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Positioned; +import com.codename1.flutter.widgets.PositionedRenderElement; + +import dart.runtime.Funcs; + +/** + * Element for {@link PositionedTransition}: re-reads the animation's + * {@code RelativeRect} on every notification and re-lays the child out at the new + * insets, mirroring Flutter's {@code AnimatedWidget} rebuild. + * + *

It used to resolve the rect once when the element was created. That is fine while the + * animation is at rest at its END value and catastrophic when it rests at its START value: + * the gallery's settings panel begins one full screen-height ABOVE the viewport and slides + * down, so a panel that never moved simply stayed off-screen. Tapping the settings button + * ran the whole toggle — the notifier flipped, the controllers animated, the subtree + * rebuilt — and produced no visible change, which reads as a dead button.

+ * + *

It stays a {@link PositionedRenderElement} rather than a composed element that builds + * one, because {@code StackRenderElement} decides what is positioned by looking for this + * type; wrapping it would make the Stack treat the panel as a non-positioned child and + * size the whole Stack to it.

+ */ +public class PositionedTransitionElement extends PositionedRenderElement { + + private com.codename1.flutter.foundation.Listenable listened; + + private final Funcs.VoidFunc0 handler = new Funcs.VoidFunc0() { + @Override + public void call() { + applyRect(); + } + }; + + public PositionedTransitionElement(Positioned positioned) { + super(positioned); + } + + /// The transition this element animates. Held separately from {@code widget()}, + /// which is the synthesised Positioned carrying the current insets. + private PositionedTransition transition; + + void transition(PositionedTransition t) { + this.transition = t; + } + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + applyRect(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + private void subscribe() { + Animation a = transition == null ? null : transition.getRect(); + if (a instanceof com.codename1.flutter.foundation.Listenable) { + listened = (com.codename1.flutter.foundation.Listenable) a; + listened.addListener(handler); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(handler); + listened = null; + } + } + + /** Copies the animation's current rect onto the Positioned and re-lays it out. */ + private void applyRect() { + if (transition == null) { + return; + } + Animation a = transition.getRect(); + Object v = a == null ? null : a.value(); + RelativeRect r = v instanceof RelativeRect ? (RelativeRect) v : RelativeRect.fill; + Positioned p = positioned(); + p.left(r.left()); + p.top(r.top()); + p.right(r.right()); + p.bottom(r.bottom()); + // Insets are geometry: the Stack resolves them into this box's constraints and + // offset, so a changed rect is a layout change, not a repaint. + markNeedsLayout(); + RenderElement host = this; + while (host != null && host.parent() instanceof RenderElement) { + host = (RenderElement) host.parent(); + } + if (host != null && host.host() != null) { + host.host().revalidate(); + } + } + + @Override + public void update(Widget newWidget) { + super.update(newWidget); + applyRect(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ProxyAnimation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ProxyAnimation.java new file mode 100644 index 00000000000..07163bb27b3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ProxyAnimation.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.runtime.Funcs; + +/** + * An {@link Animation} that delegates to a swappable inner animation — + * Flutter's {@code ProxyAnimation}. When {@link #parent(Animation)} is null the + * proxy holds the last value/status it saw. Reassigning the parent redirects + * value and status queries to the new animation. + */ +public class ProxyAnimation extends Animation { + + private Animation parent; + private double cachedValue; + private AnimationStatus cachedStatus = AnimationStatus.dismissed; + + public ProxyAnimation() { + } + + public ProxyAnimation(Animation animation) { + parent(animation); + } + + /** + * Forwards the parent's changes to this proxy's listeners, as Flutter's does. They + * were stored and never notified, so a widget listening to the proxy froze. + */ + private final Funcs.VoidFunc0 valueRelay = new Funcs.VoidFunc0() { + @Override + public void call() { + notifyListeners(); + } + }; + + private final Funcs.VoidFunc1 statusRelay = new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + notifyStatusListeners(s); + } + }; + + public void parent(Animation v) { + if (v == parent) { + return; + } + double oldValue = value(); + AnimationStatus oldStatus = status(); + if (parent != null) { + cachedValue = oldValue; + cachedStatus = oldStatus; + if (hasAnyListeners()) { + unsubscribe(parent); + } + } + this.parent = v; + if (parent != null) { + if (hasAnyListeners()) { + subscribe(parent); + } + // Swapping the parent is itself a change, when it moves the value or status. + if (value() != oldValue) { + notifyListeners(); + } + if (status() != oldStatus) { + notifyStatusListeners(status()); + } + } + } + + @Override + protected void didStartListening() { + if (parent != null) { + subscribe(parent); + } + } + + @Override + protected void didStopListening() { + if (parent != null) { + unsubscribe(parent); + } + } + + private void subscribe(Animation p) { + p.addListener(valueRelay); + p.addStatusListener(statusRelay); + } + + private void unsubscribe(Animation p) { + p.removeListener(valueRelay); + p.removeStatusListener(statusRelay); + } + + public Animation getParent() { + return parent; + } + + @Override + public Double value() { + if (parent == null) { + return cachedValue; + } + Double v = parent.value(); + return v == null ? 0.0 : v; + } + + @Override + public AnimationStatus status() { + return parent == null ? cachedStatus : parent.status(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RelativeRectTween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RelativeRectTween.java new file mode 100644 index 00000000000..046a12ed317 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RelativeRectTween.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * A {@link Tween} over {@code RelativeRect} - Flutter's {@code RelativeRectTween}. + * + *

The gallery's settings panel is animated by one of these: it begins a full + * screen-height above the viewport and slides down over 80ms. Stepping meant it + * hung off-screen and then appeared already in place, which is a menu that pops + * rather than one that slides.

+ */ +public class RelativeRectTween extends Tween { + + @Override + public com.codename1.flutter.RelativeRect lerp(double t) { + return com.codename1.flutter.RelativeRect.lerp(begin(), end(), t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ReverseAnimation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ReverseAnimation.java new file mode 100644 index 00000000000..27d461d833b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ReverseAnimation.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.runtime.Funcs; + +/** + * Runs a parent animation in reverse — Flutter's {@code ReverseAnimation}. Its + * {@link #value()} is {@code 1 - parent.value()} and its status is the parent's + * status with {@code forward}/{@code reverse} swapped. The backdrop title uses + * it to fade its front-layer title out as the parent reveal animates in. + */ +public class ReverseAnimation extends Animation { + + private final Animation parent; + + /** + * Forwards the parent's changes to this animation's own listeners. Listeners were + * stored on the wrapper and nothing ever notified them, so a fade driven by a + * ReverseAnimation stayed at the value it first rendered with. + */ + private final Funcs.VoidFunc0 valueRelay = new Funcs.VoidFunc0() { + @Override + public void call() { + notifyListeners(); + } + }; + + private final Funcs.VoidFunc1 statusRelay = new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + notifyStatusListeners(reverse(s)); + } + }; + + public ReverseAnimation(Animation parent) { + this.parent = parent; + } + + @Override + protected void didStartListening() { + if (parent != null) { + parent.addListener(valueRelay); + parent.addStatusListener(statusRelay); + } + } + + @Override + protected void didStopListening() { + if (parent != null) { + parent.removeListener(valueRelay); + parent.removeStatusListener(statusRelay); + } + } + + public Animation getParent() { + return parent; + } + + @Override + public Double value() { + double v = parent == null || parent.value() == null ? 0.0 : parent.value(); + return 1.0 - v; + } + + @Override + public AnimationStatus status() { + if (parent == null) { + return AnimationStatus.dismissed; + } + return reverse(parent.status()); + } + + /** The parent's status as seen running backward. */ + private static AnimationStatus reverse(AnimationStatus s) { + switch (s) { + case forward: + return AnimationStatus.reverse; + case reverse: + return AnimationStatus.forward; + case completed: + return AnimationStatus.dismissed; + case dismissed: + return AnimationStatus.completed; + default: + return s; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RotationTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RotationTransition.java new file mode 100644 index 00000000000..2d881c9fa60 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/RotationTransition.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Transform; + +/** + * Rotates its child about its centre from an {@link Animation} measured in TURNS — + * Flutter's {@code RotationTransition}, where 1.0 is a full revolution. + */ +public class RotationTransition extends AnimatedChildWidget { + + private Animation turns; + private Alignment alignment; + + public void turns(Animation v) { + this.turns = v; + listenable(v); + } + + public void alignment(Alignment v) { + this.alignment = v; + } + + public Animation getTurns() { + return turns; + } + + @Override + public Widget build(BuildContext context) { + double radians = valueOf(turns, 0.0) * 2 * Math.PI; + return Transform.rotate(null, radians, null, alignment, null, null, getChild()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ScaleTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ScaleTransition.java new file mode 100644 index 00000000000..6502cec95e8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/ScaleTransition.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Transform; + +/** + * Scales its child about its centre from an {@link Animation} — Flutter's + * {@code ScaleTransition}. + */ +public class ScaleTransition extends AnimatedChildWidget { + + private Animation scale; + private Alignment alignment; + + public void scale(Animation v) { + this.scale = v; + listenable(v); + } + + public void alignment(Alignment v) { + this.alignment = v; + } + + public Animation getScale() { + return scale; + } + + @Override + public Widget build(BuildContext context) { + return Transform.scale(null, Double.valueOf(valueOf(scale, 1.0)), + null, null, null, alignment, null, null, getChild()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SingleTickerProviderStateMixin.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SingleTickerProviderStateMixin.java new file mode 100644 index 00000000000..d991fd2d71f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SingleTickerProviderStateMixin.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Java surface of Flutter's {@code SingleTickerProviderStateMixin}. A Dart + * {@code State with SingleTickerProviderStateMixin} transpiles to a Java class + * that {@code implements} this interface, which extends {@link TickerProvider} + * so {@code AnimationController(vsync: this)} type-checks. No members are + * needed — controllers self-drive from a CN1 timer. + */ +public interface SingleTickerProviderStateMixin extends TickerProvider { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SizeTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SizeTransition.java new file mode 100644 index 00000000000..892b163b93a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SizeTransition.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Animates its own size along one axis, clipping its {@code child} — Flutter's + * {@code SizeTransition}. The {@code sizeFactor} animation drives the visible + * fraction (0..1) and {@code axisAlignment} anchors the reveal. This pass hosts + * the child at full size; the animated clip is deferred (see + * {@link AnimatedChildWidget}). + */ +public class SizeTransition extends AnimatedChildWidget { + + private Object axis; + private Animation sizeFactor; + private Double axisAlignment; + + public void axis(Object v) { + this.axis = v; + } + + public void axisAlignment(double v) { + this.axisAlignment = v; + } + + public Animation getSizeFactor() { + return sizeFactor; + } + + public void sizeFactor(Animation v) { + this.sizeFactor = v; + listenable(v); + } + + /** + * Flutter's own composition: a ClipRect over an Align whose factor along the axis is + * the animation's value. Align already scales its box to a FRACTION of its child, and + * the ClipRect hides the part that does not fit yet. + * + *

This used to hand the child through at full size -- the note said the clip was + * "deferred" -- so nothing driven by one ever grew or shrank. The mail study's bottom + * bar is a SizeTransition, which is why it appeared and vanished instead of sliding.

+ */ + @Override + public com.codename1.flutter.Widget build(com.codename1.flutter.BuildContext context) { + com.codename1.flutter.Widget child = getChild(); + if (child == null || sizeFactor == null) { + return child; + } + double factor = Math.max(0, valueOf(sizeFactor, 1)); + if (factor >= 1) { + // Fully revealed: hand the child straight through. Align shrink-wraps to its + // child once given a factor, which throws away a tight height the parent + // meant it to fill -- a scaffold stretches its bottom bar over the display's + // bottom inset, and wrapping it unconditionally left that strip unpainted. + // At rest this widget should change nothing, and now it does not. + return child; + } + boolean horizontal = axis == com.codename1.flutter.Axis.horizontal; + double along = axisAlignment != null ? axisAlignment.doubleValue() : 0; + com.codename1.flutter.widgets.Align align = new com.codename1.flutter.widgets.Align(); + align.alignment(horizontal + ? new com.codename1.flutter.Alignment(along, -1) + : new com.codename1.flutter.Alignment(-1, along)); + if (horizontal) { + align.widthFactor(Double.valueOf(factor)); + } else { + align.heightFactor(Double.valueOf(factor)); + } + align.child(child); + com.codename1.flutter.widgets.ClipRect clip = + new com.codename1.flutter.widgets.ClipRect(); + clip.child(align); + return clip; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SlideTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SlideTransition.java new file mode 100644 index 00000000000..2c235fad870 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/SlideTransition.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.FractionalTranslationRenderElement; + +/** + * Slides its child by an animated offset given as a FRACTION of the child's own size — + * Flutter's {@code SlideTransition}. + * + *

It is its own render element rather than a wrapper around FractionalTranslation + * because the fraction has to be read at PAINT time: the animation moves every frame, and + * rebuilding a wrapper widget per frame to carry the new value would relayout the subtree + * for what is only a change of where it is drawn.

+ */ +public class SlideTransition extends AnimatedChildWidget + implements FractionalTranslationRenderElement.FractionSource { + + private Animation position; + + public void position(Animation v) { + this.position = v; + listenable(v); + } + + public Animation getPosition() { + return position; + } + + @Override + public Offset fraction() { + if (position == null) { + return null; + } + Object v = position.value(); + return v instanceof Offset ? (Offset) v : null; + } + + @Override + public Widget child() { + return getChild(); + } + + @Override + public com.codename1.flutter.foundation.Listenable driver() { + return position; + } + + @Override + public Element createElement() { + return new FractionalTranslationRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProvider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProvider.java new file mode 100644 index 00000000000..0a764d5d11e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Flutter's TickerProvider: the object that vends the frame ticks an + * {@link AnimationController} uses to advance. In this runtime the controller + * self-drives from a CN1 timer, so the provider is a marker type — passing a + * State (mixed with {@link SingleTickerProviderStateMixin}) as {@code vsync} + * simply satisfies the API shape. + */ +public interface TickerProvider { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProviderStateMixin.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProviderStateMixin.java new file mode 100644 index 00000000000..de22eb99f99 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TickerProviderStateMixin.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Java surface of Flutter's {@code TickerProviderStateMixin} (the multi-ticker + * variant of {@link SingleTickerProviderStateMixin}). Marker interface — see + * {@link TickerProvider}. + */ +public interface TickerProviderStateMixin extends TickerProvider { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Tween.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Tween.java new file mode 100644 index 00000000000..55d1cad83d5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/Tween.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * Linearly interpolates between a {@code begin} and {@code end} value — + * Flutter's {@code Tween}. The base class handles the numeric case + * ({@code begin + (end - begin) * t}) for {@link Number} values; typed + * subclasses ({@link ColorTween}, {@link IntTween}) override {@link #lerp} for + * their own interpolation. For opaque value types (border radius, matrices) + * where no interpolation is wired this pass, {@link #lerp} steps at the + * midpoint — a correct-shape, minimal fallback. + */ +public class Tween extends Animatable { + + private T beginValue; + private T endValue; + + public Tween() { + } + + /** Named-parameter setter for the Dart {@code begin:} argument. */ + public void begin(T v) { + this.beginValue = v; + } + + /** Named-parameter setter for the Dart {@code end:} argument. */ + public void end(T v) { + this.endValue = v; + } + + /** Dart getter {@code tween.begin}. */ + public T begin() { + return beginValue; + } + + /** Dart getter {@code tween.end}. */ + public T end() { + return endValue; + } + + /** Interpolates at {@code t} (0..1). Override for typed interpolation. */ + @SuppressWarnings("unchecked") + public T lerp(double t) { + if (beginValue instanceof Number && endValue instanceof Number) { + double b = ((Number) beginValue).doubleValue(); + double e = ((Number) endValue).doubleValue(); + return (T) Double.valueOf(b + (e - b) * t); + } + if (t < 0.5) { + return beginValue; + } + return endValue; + } + + @Override + public T transform(double t) { + if (t == 0.0) { + return beginValue; + } + if (t == 1.0) { + return endValue; + } + return lerp(t); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequence.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequence.java new file mode 100644 index 00000000000..9a463d02f6b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequence.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import java.util.ArrayList; +import java.util.List; + +/** + * An {@link Animatable} that pieces together a series of weighted + * {@link TweenSequenceItem}s over the 0..1 interval — Flutter's + * {@code TweenSequence}. {@link #transform} locates the active segment by + * cumulative weight and evaluates that item's tween over its local range. + */ +public class TweenSequence extends Animatable { + + private final List> items = new ArrayList>(); + private double totalWeight; + + @SuppressWarnings("unchecked") + public TweenSequence(Object items) { + if (items instanceof Iterable) { + for (Object o : (Iterable) items) { + add((TweenSequenceItem) o); + } + } + } + + private void add(TweenSequenceItem item) { + if (item != null) { + items.add(item); + totalWeight += item.weight(); + } + } + + @Override + public T transform(double t) { + if (items.isEmpty()) { + return null; + } + if (t <= 0.0) { + return items.get(0).tween().transform(0.0); + } + if (t >= 1.0) { + TweenSequenceItem last = items.get(items.size() - 1); + return last.tween().transform(1.0); + } + double start = 0.0; + for (TweenSequenceItem item : items) { + double span = item.weight() / totalWeight; + double end = start + span; + if (t < end || item == items.get(items.size() - 1)) { + double local = span == 0.0 ? 0.0 : (t - start) / span; + return item.tween().transform(local); + } + start = end; + } + return items.get(items.size() - 1).tween().transform(1.0); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequenceItem.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequenceItem.java new file mode 100644 index 00000000000..25d21414255 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animation/TweenSequenceItem.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +/** + * One weighted segment of a {@link TweenSequence}: the {@link Animatable} to + * evaluate over this segment and its relative {@code weight}. + */ +public class TweenSequenceItem { + + private Animatable tween; + private double weight = 1.0; + + public void tween(Animatable v) { + this.tween = v; + } + + public Animatable tween() { + return tween; + } + + public void weight(double v) { + this.weight = v; + } + + public double weight() { + return weight; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/Animations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/Animations.java new file mode 100644 index 00000000000..df36c25de59 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/Animations.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Dialogs; + +import dart.runtime.Funcs; + +/** + * Top-level entry points of the {@code animations} package. Currently hosts + * {@code showModal}, which shows a modal route with an {@code animations}-package + * transition (fade-scale by default). This pass presents the modal via the + * Material {@link Dialogs#showDialog} plumbing; the package's custom + * fade/scale reveal is deferred. + */ +public final class Animations { + + private Animations() { + } + + /** + * Shows a modal built by {@code builder} over the current route — the + * {@code animations} package's top-level {@code showModal}. The + * {@code configuration}, {@code useRootNavigator} and {@code filter} + * parameters are captured for API shape. + * + * @return the route's completion result (a future value); ignored by + * callers that do not await the dismissal + */ + public static Object showModal(BuildContext context, Object configuration, + Object useRootNavigator, + Funcs.Func1 builder, + Object filter) { + if (builder != null) { + return Dialogs.showDialog(context, builder); + } + return dart.async.Future.value(null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/CloseContainerBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/CloseContainerBuilder.java new file mode 100644 index 00000000000..65334f757db --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/CloseContainerBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Signature for the {@code closedBuilder} of an {@link OpenContainer} — the + * animations package's {@code CloseContainerBuilder} + * ({@code Widget Function(BuildContext, VoidCallback openContainer)}). The + * second argument is the callback that triggers the open transition. A + * single-abstract-method interface so transpiled Dart closures bind as Java + * lambdas. + */ +public interface CloseContainerBuilder { + Widget call(BuildContext context, Funcs.VoidFunc0 openContainer); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/ContainerTransitionType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/ContainerTransitionType.java new file mode 100644 index 00000000000..a4da98ddfab --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/ContainerTransitionType.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +/** + * Whether an {@code OpenContainer} morphs its closed and open states with a + * fade or a fade-through transition — the {@code animations} package's + * {@code ContainerTransitionType}. + */ +public enum ContainerTransitionType { + fade, fadeThrough +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeScaleTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeScaleTransition.java new file mode 100644 index 00000000000..87ebd547514 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeScaleTransition.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.animation.AnimatedWidget; +import com.codename1.flutter.widgets.Opacity; +import com.codename1.flutter.widgets.Transform; + +/** + * Fades and scales its child in and out for modal reveals — the {@code animations} + * package's {@code FadeScaleTransition}. + * + *

Follows the package's own curves: the fade runs over the first 30% of the animation + * and the scale grows from 80% to full over the first 40%, so the child arrives already + * visible and settles rather than popping in at the end.

+ */ +public class FadeScaleTransition extends AnimatedWidget { + + private Animation animation; + private Widget child; + + public void animation(Animation v) { + this.animation = v; + listenable(v); + } + + public void child(Widget v) { + this.child = v; + } + + public Animation getAnimation() { + return animation; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + double t = 1; + if (animation != null && animation.value() != null) { + t = animation.value().doubleValue(); + } + double fade = interval(t, 0.0, 0.3); + double scale = 0.80 + 0.20 * interval(t, 0.0, 0.4); + + Opacity fadeLayer = new Opacity(); + fadeLayer.opacity(fade); + fadeLayer.child(Transform.scale(null, Double.valueOf(scale), null, null, null, null, + null, null, child)); + return fadeLayer; + } + + /** {@code t} remapped onto [begin, end] and clamped — Flutter's Interval curve. */ + static double interval(double t, double begin, double end) { + if (end <= begin) { + return t >= end ? 1 : 0; + } + double v = (t - begin) / (end - begin); + return v < 0 ? 0 : (v > 1 ? 1 : v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeThroughTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeThroughTransition.java new file mode 100644 index 00000000000..fd25eb73aef --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/FadeThroughTransition.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.animation.AnimatedWidget; +import com.codename1.flutter.widgets.Opacity; +import com.codename1.flutter.widgets.Transform; + +/** + * Fades the outgoing child out, then the incoming one in while it grows slightly — the + * Material "fade through" motion, from the {@code animations} package. + * + *

The two halves do not overlap, which is the whole point of the pattern: the incoming + * content waits until the outgoing has gone rather than cross-dissolving with it. The + * secondary animation drives the outgoing half.

+ */ +public class FadeThroughTransition extends AnimatedWidget { + + private Animation animation; + private Animation secondaryAnimation; + private Color fillColor; + private Widget child; + + public void animation(Animation v) { + this.animation = v; + listenable(v); + } + + public void secondaryAnimation(Animation v) { + this.secondaryAnimation = v; + } + + public void fillColor(Color v) { + this.fillColor = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + /// The pattern's own curves: it leaves on one and arrives on the other. + private static final com.codename1.flutter.animation.Cubic IN_CURVE = + new com.codename1.flutter.animation.Cubic(0.0, 0.0, 0.2, 1.0); + private static final com.codename1.flutter.animation.Cubic OUT_CURVE = + new com.codename1.flutter.animation.Cubic(0.4, 0.0, 1.0, 1.0); + + @Override + public Widget build(BuildContext context) { + double in = value(animation, 1); + double out = value(secondaryAnimation, 0); + + // Incoming: nothing for the first 30%, then fade up while scaling 92% -> 100%. + // Outgoing: fade away over that first 30%. + // + // Both eased, and on DIFFERENT curves -- the pattern leaves fast and arrives + // slow, and running either leg linearly is most of what makes it read as a cut + // rather than as a dissolve. The old page is the one you actually watch: it + // carries the whole screen for the first 30% while the new one is still + // invisible, and linearly it is only 44% gone a third of the way in where the + // curve has it at 58%. + double enter = IN_CURVE.transform( + FadeScaleTransition.interval(in, 0.3, 1.0)); + double opacity = enter; + double scale = 0.92 + 0.08 * enter; + opacity *= 1 - OUT_CURVE.transform( + FadeScaleTransition.interval(out, 0.0, 0.3)); + + Opacity layer = new Opacity(); + layer.opacity(opacity); + layer.child(Transform.scale(null, Double.valueOf(scale), null, null, null, null, + null, null, child)); + return layer; + } + + private static double value(Animation a, double fallback) { + return a == null || a.value() == null ? fallback : a.value().doubleValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/OpenContainer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/OpenContainer.java new file mode 100644 index 00000000000..595d5289893 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/OpenContainer.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.Duration; + +/** + * A container that expands (a "container transform") from a closed state to a + * full page — the {@code animations} package's {@code OpenContainer}. The + * {@code closedBuilder} paints the resting state and {@code openBuilder} the + * opened page; each is a closure {@code (context, action)} where {@code action} + * opens/closes the container. This pass renders the closed state via the + * closedBuilder (falling back to an empty box); the expand transition is + * deferred. + */ +public class OpenContainer extends StatelessWidget { + + private Object onClosed; + private CloseContainerBuilder closedBuilder; + private CloseContainerBuilder openBuilder; + private boolean tappable = true; + private Duration transitionDuration; + private Color closedColor; + private Color openColor; + private Color middleColor; + private Double closedElevation; + private Double openElevation; + private Object closedShape; + private Object openShape; + + public void onClosed(Object v) { + this.onClosed = v; + } + + public void closedBuilder(CloseContainerBuilder v) { + this.closedBuilder = v; + } + + public void openBuilder(CloseContainerBuilder v) { + this.openBuilder = v; + } + + public void tappable(boolean v) { + this.tappable = v; + } + + public void transitionDuration(Duration v) { + this.transitionDuration = v; + } + + public void transitionType(Object v) { + } + + public void closedColor(Color v) { + this.closedColor = v; + } + + public void openColor(Color v) { + this.openColor = v; + } + + public void middleColor(Color v) { + this.middleColor = v; + } + + public void closedElevation(double v) { + this.closedElevation = v; + } + + public void openElevation(double v) { + this.openElevation = v; + } + + public void closedShape(Object v) { + this.closedShape = v; + } + + public void openShape(Object v) { + this.openShape = v; + } + + public void routeSettings(String v) { + } + + public void useRootNavigator(boolean v) { + } + + public Object getClosedBuilder() { + return closedBuilder; + } + + /** + * The closed state: the {@code closedBuilder}'s widget on a Material surface, tappable + * to open. + * + *

It rendered NOTHING before — an empty box — which took the whole motion demo with + * it, eighteen containers' worth of it. The transform itself (the closed card growing + * into the page) is a compositing effect we do not have; opening pushes the built page + * as a route instead, so the demo is navigable and shows both of its states even though + * the growth between them is a cut rather than a morph.

+ */ + @Override + public Widget build(final BuildContext context) { + if (closedBuilder == null) { + return new SizedBox(); + } + Widget closed = closedBuilder.call(context, new dart.runtime.Funcs.VoidFunc0() { + @Override + public void call() { + open(context); + } + }); + com.codename1.flutter.material.Material surface = + new com.codename1.flutter.material.Material(); + if (closedColor != null) { + surface.color(closedColor); + } + if (closedElevation != null) { + surface.elevation(closedElevation.doubleValue()); + } + if (closedShape != null) { + surface.shape(closedShape); + } + surface.clipBehavior(com.codename1.flutter.Clip.antiAlias); + if (!tappable) { + surface.child(closed); + return surface; + } + com.codename1.flutter.material.InkWell tap = + new com.codename1.flutter.material.InkWell(); + tap.child(closed); + tap.onTap(new dart.runtime.Funcs.VoidFunc0() { + @Override + public void call() { + open(context); + } + }); + surface.child(tap); + return surface; + } + + /// Distinguishes one open container's surface from every other one on screen. + private static int surfaceSerial; + + /** + * Gives this container's closed surface a name the transition can find it by, and + * returns that name (null when it has no component of its own to grow from). + */ + private static String nameClosedSurface(BuildContext context) { + if (!(context instanceof com.codename1.flutter.Element)) { + return null; + } + com.codename1.flutter.RenderElement r = com.codename1.flutter.RenderElement + .findRenderElement((com.codename1.flutter.Element) context); + com.codename1.ui.Component c = r == null ? null : r.component(); + if (c == null) { + return null; + } + String name = "cn1-open-container-" + (++surfaceSerial); + c.setName(name); + return name; + } + + /** Pushes the opened page; closing it pops back and reports through {@code onClosed}. */ + private void open(BuildContext context) { + if (openBuilder == null) { + return; + } + // The route carries the container transform's identity and duration, so the + // Navigator animates it as an expanding surface rather than as a page push. + // Named at TAP time, not at build time. The name has to identify the one surface + // the user actually touched -- a mail list is a column of these -- and a widget is + // rebuilt often enough that a name assigned during build belongs to whichever + // instance built last. The element under this context is stable and is the one in + // front of the user right now. + final String source = nameClosedSurface(context); + com.codename1.flutter.navigation.MaterialPageRoute route = + new com.codename1.flutter.navigation.MaterialPageRoute() { + @Override + public boolean isContainerTransform() { + return true; + } + + @Override + public String containerTransformSource() { + return source; + } + + @Override + public int transitionMillis() { + return transitionDuration == null + ? -1 : (int) transitionDuration.inMilliseconds(); + } + }; + route.builder(new dart.runtime.Funcs.Func1() { + @Override + public Widget call(BuildContext routeContext) { + Widget page = openBuilder.call(routeContext, new dart.runtime.Funcs.VoidFunc0() { + @Override + public void call() { + close(routeContext); + } + }); + if (openColor == null) { + return page; + } + com.codename1.flutter.material.Material surface = + new com.codename1.flutter.material.Material(); + surface.color(openColor); + if (openElevation != null) { + surface.elevation(openElevation.doubleValue()); + } + surface.child(page); + return surface; + } + }); + com.codename1.flutter.navigation.Navigator.push(context, route); + } + + @SuppressWarnings("unchecked") + private void close(BuildContext context) { + com.codename1.flutter.navigation.Navigator.pop(context); + if (onClosed instanceof dart.runtime.Funcs.VoidFunc1) { + ((dart.runtime.Funcs.VoidFunc1) onClosed).call(null); + } else if (onClosed instanceof dart.runtime.Funcs.VoidFunc0) { + ((dart.runtime.Funcs.VoidFunc0) onClosed).call(); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisPageTransitionsBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisPageTransitionsBuilder.java new file mode 100644 index 00000000000..5656ea42d6f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisPageTransitionsBuilder.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.Color; + +/** + * The {@code PageTransitionsBuilder} for the shared-axis (X/Y/Z) motion pattern + * — the {@code animations} package's {@code SharedAxisPageTransitionsBuilder}. + * Installed into a {@code PageTransitionsTheme} so route pushes animate along + * the configured {@link SharedAxisTransitionType}. The optional {@code fillColor} + * paints behind the transitioning pages. Configuration only in this pass; the + * builder emits a {@link SharedAxisTransition} when the navigation renderer + * lands. + */ +public class SharedAxisPageTransitionsBuilder + extends com.codename1.flutter.material.PageTransitionsBuilder { + + private SharedAxisTransitionType transitionType; + private Color fillColor; + + public void transitionType(SharedAxisTransitionType v) { + this.transitionType = v; + } + + public void fillColor(Color v) { + this.fillColor = v; + } + + public SharedAxisTransitionType getTransitionType() { + return transitionType; + } + + public Color getFillColor() { + return fillColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransition.java new file mode 100644 index 00000000000..26a24a0a062 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransition.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.animation.AnimatedWidget; +import com.codename1.flutter.widgets.FractionalTranslationRenderElement; +import com.codename1.flutter.widgets.Opacity; +import com.codename1.flutter.widgets.Transform; + +/** + * Slides and cross-fades between two pages along a shared axis — the {@code animations} + * package's {@code SharedAxisTransition}. + * + *

Horizontal and vertical variants slide by 30% of the page; the scaled (Z) variant + * grows from 80% instead. In every case the incoming page fades in over the last 70% of the + * run while the outgoing one fades out over the first 30%, so the two never overlap at full + * strength.

+ */ +public class SharedAxisTransition extends AnimatedWidget + implements FractionalTranslationRenderElement.FractionSource { + + private Animation animation; + private Animation secondaryAnimation; + private SharedAxisTransitionType transitionType; + private Color fillColor; + private Widget child; + + public void animation(Animation v) { + this.animation = v; + listenable(v); + } + + public void secondaryAnimation(Animation v) { + this.secondaryAnimation = v; + } + + public void transitionType(SharedAxisTransitionType v) { + this.transitionType = v; + } + + public void fillColor(Color v) { + this.fillColor = v; + } + + public void child(Widget v) { + this.child = v; + } + + public SharedAxisTransitionType getTransitionType() { + return transitionType; + } + + public Widget getChild() { + return child; + } + + /// The pattern's three curves: arrive, leave, and the one both scales run on. + private static final com.codename1.flutter.animation.Cubic DECELERATE = + new com.codename1.flutter.animation.Cubic(0.0, 0.0, 0.2, 1.0); + private static final com.codename1.flutter.animation.Cubic ACCELERATE = + new com.codename1.flutter.animation.Cubic(0.4, 0.0, 1.0, 1.0); + private static final com.codename1.flutter.animation.Cubic STANDARD = + new com.codename1.flutter.animation.Cubic(0.4, 0.0, 0.2, 1.0); + + @Override + public Widget build(BuildContext context) { + double in = value(animation, 1); + double out = value(secondaryAnimation, 0); + + // Every leg is EASED, and on three different curves: the incoming content + // decelerates in, the outgoing accelerates out, and both scales run on the + // standard curve. Running them linearly -- which this did -- is most of what + // makes a shared axis read as a cut with a dissolve bolted on rather than as one + // surface handing over to another. + double opacity = DECELERATE.transform( + FadeScaleTransition.interval(in, 0.3, 1.0)) + * (1 - ACCELERATE.transform( + FadeScaleTransition.interval(out, 0.0, 0.3))); + + Opacity layer = new Opacity(); + layer.opacity(opacity); + if (transitionType == SharedAxisTransitionType.scaled) { + // The child on its way OUT keeps growing, to 110%; only the one arriving + // comes up from 80%. Ours held the outgoing child at 100% for the whole + // run, so the surface being replaced simply faded where it should have + // continued through the screen. + double scale = out > 0 + ? 1.00 + 0.10 * STANDARD.transform(out) + : 0.80 + 0.20 * STANDARD.transform(in); + layer.child(Transform.scale(null, Double.valueOf(scale), null, null, null, null, + null, null, child)); + return layer; + } + // Horizontal/vertical: the slide is a fraction of the page, so it rides the + // fractional-translation element, which reads the offset at paint time. + Slide slide = new Slide(this); + slide.setChild(layer); + layer.child(child); + return slide; + } + + /** The current slide offset, as a fraction of the page. */ + @Override + public Offset fraction() { + double in = value(animation, 1); + double out = value(secondaryAnimation, 0); + // Incoming slides in from +30%, outgoing continues to -30%. + double f = (1 - in) * 0.3 - out * 0.3; + if (transitionType == SharedAxisTransitionType.vertical) { + return new Offset(0, f); + } + return new Offset(f, 0); + } + + @Override + public Widget child() { + return child; + } + + @Override + public com.codename1.flutter.foundation.Listenable driver() { + return animation; + } + + private static double value(Animation a, double fallback) { + return a == null || a.value() == null ? fallback : a.value().doubleValue(); + } + + /** + * Carries the fraction from the transition to a paint-time translation, wrapping + * whatever layer the build produced. + */ + static final class Slide extends Widget + implements FractionalTranslationRenderElement.FractionSource { + + private final SharedAxisTransition owner; + private Widget wrapped; + + Slide(SharedAxisTransition owner) { + this.owner = owner; + } + + void setChild(Widget w) { + this.wrapped = w; + } + + @Override + public Offset fraction() { + return owner.fraction(); + } + + @Override + public Widget child() { + return wrapped; + } + + @Override + public com.codename1.flutter.foundation.Listenable driver() { + return owner.driver(); + } + + @Override + public Element createElement() { + return new FractionalTranslationRenderElement(this); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransitionType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransitionType.java new file mode 100644 index 00000000000..2e09fcbcc46 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/animations/SharedAxisTransitionType.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animations; + +/** + * The axis a {@link SharedAxisTransition} slides along while cross-fading — the + * {@code animations} package's {@code SharedAxisTransitionType}. + */ +public enum SharedAxisTransitionType { + horizontal, + vertical, + scaled +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheet.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheet.java new file mode 100644 index 00000000000..0f85e5e2739 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheet.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; + +/** + * An iOS bottom action sheet — Flutter's {@code CupertinoActionSheet}: an + * optional title / message, a list of {@link CupertinoActionSheetAction}s and + * an optional cancel button. Laid out as a vertical {@link Column} of those + * parts this pass (the sheet chrome / slide-up is approximate). + */ +public class CupertinoActionSheet extends StatelessWidget { + + private Widget title; + private Widget message; + private DartList actions; + private Widget cancelButton; + + public void title(Widget v) { + this.title = v; + } + + public void message(Widget v) { + this.message = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public void messageScrollController(Object v) { + } + + public void actionScrollController(Object v) { + } + + public void cancelButton(Widget v) { + this.cancelButton = v; + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (title != null) { + kids.add(title); + } + if (message != null) { + kids.add(message); + } + if (actions != null) { + kids.addAll(actions); + } + if (cancelButton != null) { + kids.add(cancelButton); + } + Column col = new Column(); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheetAction.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheetAction.java new file mode 100644 index 00000000000..ea7473664e0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActionSheetAction.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.TextButton; + +import dart.runtime.Funcs; + +/** + * A row of a {@link CupertinoActionSheet} — Flutter's + * {@code CupertinoActionSheetAction}. Composed onto a material + * {@link TextButton} (default / destructive styling approximate this pass). + */ +public class CupertinoActionSheetAction extends StatelessWidget { + + private Funcs.VoidFunc0 onPressed; + private Widget child; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void isDefaultAction(boolean v) { + } + + public void isDestructiveAction(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + TextButton b = new TextButton(); + b.onPressed(onPressed); + b.child(child); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActivityIndicator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActivityIndicator.java new file mode 100644 index 00000000000..38a519107b0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoActivityIndicator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +/** + * The iOS spinner — Flutter's {@code CupertinoActivityIndicator}. The animated + * ticks are not drawn this pass; it reserves the correct footprint (a + * {@code 2 * radius} box, default radius 10) so surrounding layout matches. + */ +public class CupertinoActivityIndicator extends StatelessWidget { + + private Double radius; + + public void color(Color v) { + } + + public void animating(boolean v) { + } + + public void radius(double v) { + this.radius = v; + } + + @Override + public Widget build(BuildContext context) { + double diameter = (radius != null ? radius : 10.0) * 2.0; + SizedBox b = new SizedBox(); + b.width(diameter); + b.height(diameter); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoAlertDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoAlertDialog.java new file mode 100644 index 00000000000..4c971f3318d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoAlertDialog.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.AlertDialog; + +import dart.core.DartList; + +/** + * An iOS-style alert dialog — Flutter's {@code CupertinoAlertDialog}: a title, + * optional content and a set of {@link CupertinoDialogAction} buttons. + * Composed onto the material {@link AlertDialog} (shown through the same + * {@code CupertinoDialogRoute} / dialog surface; visually approximate). + */ +public class CupertinoAlertDialog extends StatelessWidget { + + private Widget title; + private Widget content; + private DartList actions; + + public void title(Widget v) { + this.title = v; + } + + public void content(Widget v) { + this.content = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public void scrollController(Object v) { + } + + public void actionScrollController(Object v) { + } + + @Override + public Widget build(BuildContext context) { + AlertDialog d = new AlertDialog(); + if (title != null) { + d.title(title); + } + if (content != null) { + d.content(content); + } + if (actions != null) { + d.actions(actions); + } + return d; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoButton.java new file mode 100644 index 00000000000..5876e93b70b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoButton.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Key; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.ButtonBase; +import com.codename1.flutter.material.ElevatedButton; +import com.codename1.flutter.material.TextButton; + +import dart.runtime.Funcs; + +/** + * An iOS-style button — Flutter's {@code CupertinoButton}. The default variant + * is a borderless tinted-text button; {@code CupertinoButton.filled} has a + * solid background. A null {@code onPressed} disables it. Composed onto the + * material {@link TextButton} (default) / {@link ElevatedButton} (filled). + */ +public class CupertinoButton extends StatelessWidget { + + private Funcs.VoidFunc0 onPressed; + private Widget child; + private boolean filled; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void child(Widget v) { + this.child = v; + } + + public void padding(Object v) { + } + + public void color(Color v) { + } + + public void disabledColor(Color v) { + } + + public void minSize(double v) { + } + + public void pressedOpacity(double v) { + } + + public void borderRadius(Object v) { + } + + public void alignment(Object v) { + } + + /** + * Dart's {@code CupertinoButton.filled} named constructor in canonical + * positional form. + */ + public static CupertinoButton filled(Key key, Funcs.VoidFunc0 onPressed, Widget child) { + CupertinoButton b = new CupertinoButton(); + b.key(key); + b.onPressed = onPressed; + b.child = child; + b.filled = true; + return b; + } + + @Override + public Widget build(BuildContext context) { + ButtonBase b = filled ? new ElevatedButton() : new TextButton(); + b.onPressed(onPressed); + b.child(child); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoColors.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoColors.java new file mode 100644 index 00000000000..e261898a796 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoColors.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +/** + * The iOS system colors, mirroring Flutter's {@code CupertinoColors}. Each is + * a {@link CupertinoDynamicColor}; this single-appearance runtime uses the + * light-mode value (the demos resolve them via {@code resolveFrom(context)} + * but never switch appearance). + */ +public final class CupertinoColors { + + private CupertinoColors() { + } + + public static final CupertinoDynamicColor systemBackground = new CupertinoDynamicColor(0xFFFFFFFFL); + public static final CupertinoDynamicColor label = new CupertinoDynamicColor(0xFF000000L); + public static final CupertinoDynamicColor inactiveGray = new CupertinoDynamicColor(0xFF999999L); + public static final CupertinoDynamicColor systemBlue = new CupertinoDynamicColor(0xFF007AFFL); + public static final CupertinoDynamicColor systemGrey = new CupertinoDynamicColor(0xFF8E8E93L); + public static final CupertinoDynamicColor activeBlue = new CupertinoDynamicColor(0xFF007AFFL); + public static final CupertinoDynamicColor activeGreen = new CupertinoDynamicColor(0xFF34C759L); + public static final CupertinoDynamicColor destructiveRed = new CupertinoDynamicColor(0xFFFF3B30L); + public static final CupertinoDynamicColor white = new CupertinoDynamicColor(0xFFFFFFFFL); + public static final CupertinoDynamicColor black = new CupertinoDynamicColor(0xFF000000L); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenu.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenu.java new file mode 100644 index 00000000000..fc4ef97fec7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenu.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Dialogs; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.GestureDetector; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A long-press context menu — Flutter's {@code CupertinoContextMenu}. + * + *

Press and hold reveals the actions. Flutter blurs the page and floats a scaled preview + * of the child above the list; here the actions are presented as a modal sheet, so the menu + * is reachable and its actions run even though the reveal is plainer than iOS's.

+ */ +public class CupertinoContextMenu extends StatelessWidget { + + private DartList actions; + private Widget child; + + public void actions(DartList v) { + this.actions = v; + } + + public void child(Widget v) { + this.child = v; + } + + public void previewBuilder(Object v) { + } + + public DartList getActions() { + return actions; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(final BuildContext context) { + if (actions == null || actions.isEmpty()) { + return child; + } + GestureDetector press = new GestureDetector(); + press.child(child); + press.onLongPress(new Funcs.VoidFunc0() { + @Override + public void call() { + Dialogs.showDialog(context, new Funcs.Func1() { + @Override + public Widget call(BuildContext dialogContext) { + Column list = new Column(); + list.mainAxisSize(com.codename1.flutter.MainAxisSize.min); + list.children(actions); + return list; + } + }); + } + }); + return press; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenuAction.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenuAction.java new file mode 100644 index 00000000000..0a8961c8bd2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoContextMenuAction.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.TextButton; + +import dart.runtime.Funcs; + +/** + * An entry of a {@link CupertinoContextMenu} — Flutter's + * {@code CupertinoContextMenuAction}. Composed onto a material + * {@link TextButton} this pass. + */ +public class CupertinoContextMenuAction extends StatelessWidget { + + private Funcs.VoidFunc0 onPressed; + private Widget child; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void isDefaultAction(boolean v) { + } + + public void isDestructiveAction(boolean v) { + } + + public void trailingIcon(Widget v) { + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + TextButton b = new TextButton(); + b.onPressed(onPressed); + b.child(child); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePicker.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePicker.java new file mode 100644 index 00000000000..62235360e6c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePicker.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.core.DateTime; +import dart.runtime.Funcs; + +/** + * The iOS date/time wheel — Flutter's {@code CupertinoDatePicker}. The picker + * wheel is not modeled this pass; it composes an empty {@link Container} + * placeholder while keeping the correct API shape (mode / initial value / the + * change callback are captured). + */ +public class CupertinoDatePicker extends Widget { + + private CupertinoDatePickerMode mode; + private DateTime initialDateTime; + private Funcs.VoidFunc1 onDateTimeChanged; + + public void backgroundColor(Color v) { + } + + public void mode(CupertinoDatePickerMode v) { + this.mode = v; + } + + public void initialDateTime(DateTime v) { + this.initialDateTime = v; + } + + public void minimumDate(DateTime v) { + } + + public void maximumDate(DateTime v) { + } + + public void minimumYear(long v) { + } + + public void maximumYear(long v) { + } + + public void minuteInterval(long v) { + } + + public void use24hFormat(boolean v) { + } + + public void onDateTimeChanged(Funcs.VoidFunc1 v) { + this.onDateTimeChanged = v; + } + + CupertinoDatePickerMode getMode() { + return mode; + } + + DateTime getInitialDateTime() { + return initialDateTime; + } + + Funcs.VoidFunc1 getOnDateTimeChanged() { + return onDateTimeChanged; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new CupertinoDatePickerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerMode.java new file mode 100644 index 00000000000..297c80b130b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerMode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +/** + * The columns shown in a {@link CupertinoDatePicker} — Flutter's + * {@code CupertinoDatePickerMode}. + */ +public enum CupertinoDatePickerMode { + time, date, dateAndTime, monthYear +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerRenderElement.java new file mode 100644 index 00000000000..1cab3f1bd7b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDatePickerRenderElement.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.spinner.DateSpinner3D; +import com.codename1.ui.spinner.DateTimeSpinner3D; +import com.codename1.ui.spinner.InternalPickerWidget; +import com.codename1.ui.spinner.TimeSpinner3D; + +import dart.core.DateTime; + +import java.util.Calendar; +import java.util.Date; + +/** + * {@link CupertinoDatePicker}: one of Codename One's three date/time wheels, chosen by + * the mode. {@code date} is a day-month-year spinner, {@code time} an hour-minute one, + * and the default {@code dateAndTime} the combined wheel. + * + *

{@code monthYear} has no spinner of its own and uses the date wheel, which shows one + * column more than it was asked for. That is a visible extra column rather than a missing + * control, and the gallery does not ask for the mode.

+ */ +class CupertinoDatePickerRenderElement extends CupertinoWheelRenderElement { + + CupertinoDatePickerRenderElement(Widget widget) { + super(widget); + } + + private CupertinoDatePicker picker() { + return (CupertinoDatePicker) widget(); + } + + @Override + protected Container createWheel() { + CupertinoDatePickerMode mode = picker().getMode(); + Date initial = toDate(picker().getInitialDateTime()); + if (mode == CupertinoDatePickerMode.time) { + TimeSpinner3D t = new TimeSpinner3D(); + if (initial != null) { + Calendar c = Calendar.getInstance(); + c.setTime(initial); + t.setValue(Integer.valueOf( + c.get(Calendar.HOUR_OF_DAY) * 60 + c.get(Calendar.MINUTE))); + } + return t; + } + if (mode == CupertinoDatePickerMode.dateAndTime || mode == null) { + DateTimeSpinner3D d = new DateTimeSpinner3D(); + if (initial != null) { + d.setValue(initial); + } + return d; + } + DateSpinner3D d = new DateSpinner3D(); + if (initial != null) { + d.setValue(initial); + } + return d; + } + + private static Date toDate(DateTime dt) { + return dt == null ? null : new Date(dt.millisecondsSinceEpoch()); + } + + @Override + protected void report(Object value) { + dart.runtime.Funcs.VoidFunc1 f = picker().getOnDateTimeChanged(); + if (f == null) { + return; + } + Date d = asDate(value); + if (d != null) { + f.call(DateTime.fromMillisecondsSinceEpoch(d.getTime(), false)); + } + } + + /** + * The spinners answer in their own currency: a Date from the date wheels, minutes + * past midnight from the time one. Both become the instant the Dart expects. + */ + private Date asDate(Object value) { + if (value instanceof Date) { + return (Date) value; + } + if (value instanceof Integer) { + Calendar c = Calendar.getInstance(); + Date base = toDate(picker().getInitialDateTime()); + if (base != null) { + c.setTime(base); + } + int minutes = ((Integer) value).intValue(); + c.set(Calendar.HOUR_OF_DAY, minutes / 60); + c.set(Calendar.MINUTE, minutes % 60); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTime(); + } + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogAction.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogAction.java new file mode 100644 index 00000000000..24ec5e41cf5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogAction.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.TextButton; + +import dart.runtime.Funcs; + +/** + * A button in a {@link CupertinoAlertDialog} — Flutter's + * {@code CupertinoDialogAction}. The default / destructive styling is + * approximate this pass; composed onto a material {@link TextButton}. + */ +public class CupertinoDialogAction extends StatelessWidget { + + private Funcs.VoidFunc0 onPressed; + private Widget child; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void isDefaultAction(boolean v) { + } + + public void isDestructiveAction(boolean v) { + } + + public void textStyle(TextStyle v) { + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + TextButton b = new TextButton(); + b.onPressed(onPressed); + b.child(child); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogRoute.java new file mode 100644 index 00000000000..0cac4da8d4d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogRoute.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; +import com.codename1.flutter.navigation.Route; + +import dart.runtime.Funcs; + +/** + * A route that shows an iOS alert dialog — Flutter's {@code CupertinoDialogRoute}. + * Configuration only (the page {@code builder} and barrier options); it is + * handed to the navigator's {@code restorablePush}, which drives presentation. + * + * @param the value type the route completes with when popped + */ +public class CupertinoDialogRoute extends Route { + + private BuildContext context; + private Funcs.Func1 builder; + + public void context(BuildContext v) { + this.context = v; + } + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void settings(Object v) { + } + + public void barrierDismissible(boolean v) { + } + + public void barrierColor(Color v) { + } + + public void barrierLabel(String v) { + } + + public Funcs.Func1 getBuilder() { + return builder; + } + + public BuildContext getContext() { + return context; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogs.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogs.java new file mode 100644 index 00000000000..fff21386fa6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDialogs.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Dialogs; + +import dart.runtime.Funcs; + +/** + * Host class for Dart's top-level {@code showCupertinoDialog} and + * {@code showCupertinoModalPopup} functions. Both present the built widget + * tree through the shared modeless dialog surface (see + * {@link Dialogs#showDialog}) — the iOS-specific barrier / slide-up chrome is + * approximate this pass. + */ +public final class CupertinoDialogs { + + private CupertinoDialogs() { + } + + public static dart.async.Future showCupertinoDialog(BuildContext context, + Funcs.Func1 builder, + Boolean barrierDismissible, Color barrierColor, + String barrierLabel, Boolean useRootNavigator, + Object routeSettings) { + return Dialogs.showDialog(context, builder); + } + + public static dart.async.Future showCupertinoModalPopup(BuildContext context, + Funcs.Func1 builder, + Color barrierColor, Boolean barrierDismissible, + Boolean useRootNavigator, Object semanticsDismissible, + Object routeSettings) { + return Dialogs.showModalPopup(context, builder); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDynamicColor.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDynamicColor.java new file mode 100644 index 00000000000..67ed72122d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoDynamicColor.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; + +/** + * A {@link Color} that, in Flutter, resolves to a different concrete color + * depending on the ambient {@code CupertinoTheme} brightness, accessibility + * contrast and elevation. This runtime is single-appearance, so + * {@link #resolveFrom(BuildContext)} returns this same color — enough for the + * demos, which call {@code resolveFrom(context)} purely to obtain a plain + * Color. + */ +public class CupertinoDynamicColor extends Color { + + public CupertinoDynamicColor(long value) { + super(value); + } + + /** + * Resolves against the given context; this single-appearance runtime + * returns the color unchanged. + */ + public Color resolveFrom(BuildContext context) { + return this; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoIcons.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoIcons.java new file mode 100644 index 00000000000..1eda84961ec --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoIcons.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.IconData; +import com.codename1.ui.FontImage; + +/** + * iOS-style icons named as in Flutter's {@code CupertinoIcons}. There is no + * Cupertino icon font in this runtime, so each maps to the closest CN1 + * material icon-font glyph (visually approximate this pass). + */ +public final class CupertinoIcons { + + private CupertinoIcons() { + } + + public static final IconData home = new IconData(FontImage.MATERIAL_HOME); + public static final IconData conversation_bubble = new IconData(FontImage.MATERIAL_CHAT_BUBBLE); + public static final IconData profile_circled = new IconData(FontImage.MATERIAL_ACCOUNT_CIRCLE); + public static final IconData padlock_solid = new IconData(FontImage.MATERIAL_LOCK); + public static final IconData search = new IconData(FontImage.MATERIAL_SEARCH); + public static final IconData settings = new IconData(FontImage.MATERIAL_SETTINGS); + public static final IconData share = new IconData(FontImage.MATERIAL_SHARE); + public static final IconData add = new IconData(FontImage.MATERIAL_ADD); + public static final IconData clear = new IconData(FontImage.MATERIAL_CLOSE); + public static final IconData back = new IconData(FontImage.MATERIAL_ARROW_BACK); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoModalPopupRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoModalPopupRoute.java new file mode 100644 index 00000000000..51df31a0991 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoModalPopupRoute.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; +import com.codename1.flutter.navigation.Route; + +import dart.runtime.Funcs; + +/** + * A route that slides an iOS modal popup / action sheet up from the bottom — + * Flutter's {@code CupertinoModalPopupRoute}. Configuration only (the popup + * {@code builder} and barrier options). + * + * @param the value type the route completes with when popped + */ +public class CupertinoModalPopupRoute extends Route { + + private Funcs.Func1 builder; + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void settings(Object v) { + } + + public void barrierColor(Color v) { + } + + public void barrierDismissible(boolean v) { + } + + public void barrierLabel(String v) { + } + + public Funcs.Func1 getBuilder() { + return builder; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoNavigationBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoNavigationBar.java new file mode 100644 index 00000000000..ad107067577 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoNavigationBar.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.material.AppBar; + +import dart.core.DartList; + +/** + * The iOS top navigation bar — Flutter's {@code CupertinoNavigationBar}: a + * centered middle title with optional leading/trailing widgets. Composed onto + * the material {@link AppBar} with a centered title (visually approximate this + * pass). + */ +public class CupertinoNavigationBar extends StatelessWidget { + + private Widget leading; + private Widget middle; + private Widget trailing; + private Color backgroundColor; + private boolean automaticallyImplyLeading = true; + + /** + * The iOS bar's own defaults, so the material {@code AppBarTheme} never + * decides how a Cupertino bar looks. + * + *

This bar composes onto the material {@link AppBar}, which resolves an + * unset background and foreground through the ambient {@code AppBarTheme}. + * Inside a {@code MaterialApp} that themes its bars purple-on-white — as the + * gallery does — an unset Cupertino bar would inherit it and stop looking + * like iOS at all. Naming both colours here keeps that resolution from ever + * running.

+ */ + private static final long BAR_BACKGROUND = 0xFFF9F9F9L; + private static final long BAR_FOREGROUND = 0xFF000000L; + /** CupertinoTheme's navTitleTextStyle: 17pt semibold label. */ + private static final double TITLE_SIZE = 17; + + public void leading(Widget v) { + this.leading = v; + } + + public void automaticallyImplyLeading(boolean v) { + this.automaticallyImplyLeading = v; + } + + public void automaticallyImplyMiddle(boolean v) { + } + + public void previousPageTitle(String v) { + } + + public void middle(Widget v) { + this.middle = v; + } + + public void trailing(Widget v) { + this.trailing = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void brightness(Object v) { + } + + public void padding(Object v) { + } + + public void border(Object v) { + } + + public void transitionBetweenRoutes(Object v) { + } + + @Override + public Widget build(BuildContext context) { + AppBar bar = new AppBar(); + if (middle != null) { + bar.title(middle); + } + bar.centerTitle(true); + bar.backgroundColor(backgroundColor != null + ? backgroundColor : new Color(BAR_BACKGROUND)); + bar.foregroundColor(new Color(BAR_FOREGROUND)); + TextStyle title = new TextStyle(); + title.fontSize(TITLE_SIZE); + title.fontWeight(com.codename1.flutter.FontWeight.w600); + bar.titleTextStyle(title); + // leading and trailing used to be stored and never passed on, so an iOS + // bar rendered its title and nothing else. + bar.automaticallyImplyLeading(automaticallyImplyLeading); + if (leading != null) { + bar.leading(leading); + } + if (trailing != null) { + DartList actions = new DartList(); + actions.add(trailing); + bar.actions(actions); + } + return bar; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageRoute.java new file mode 100644 index 00000000000..f144a467b4e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageRoute.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.navigation.Route; + +import dart.runtime.Funcs; + +/** + * A route that presents its page with the iOS slide-in transition — Flutter's + * {@code CupertinoPageRoute}. The transition itself is not animated this pass; + * the route carries the page {@code builder}, optional {@code settings} and + * {@code title}. Subclassable (a demo overrides {@link #buildTransitions} to + * disable the animation). + * + * @param the value type the route completes with when popped + */ +public class CupertinoPageRoute extends Route { + + private Funcs.Func1 builder; + private Object settings; + private String title; + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void settings(Object v) { + this.settings = v; + } + + public void title(String v) { + this.title = v; + } + + public void maintainState(boolean v) { + } + + public void fullscreenDialog(boolean v) { + } + + public Funcs.Func1 getBuilder() { + return builder; + } + + public String getTitle() { + return title; + } + + /** + * Wraps the page in its transition; the default is the identity (no + * animation) — overridable by subclasses. + */ + public Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + return child; + } + + @Override + public Widget buildPage(BuildContext context) { + Funcs.Func1 b = getBuilder(); + return b == null ? null : b.call(context); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageScaffold.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageScaffold.java new file mode 100644 index 00000000000..3634c06f796 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPageScaffold.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Scaffold; + +/** + * The basic iOS page layout — Flutter's {@code CupertinoPageScaffold}: an + * optional navigation bar above a full-bleed body. Composed onto the material + * {@link Scaffold} (its app-bar slot renders the navigation bar as the top + * strip), which is faithful to the structure. + */ +public class CupertinoPageScaffold extends StatelessWidget { + + private Widget navigationBar; + private Color backgroundColor; + private Widget child; + + public void navigationBar(Widget v) { + this.navigationBar = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void resizeToAvoidBottomInset(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + Scaffold s = new Scaffold(); + if (navigationBar != null) { + s.appBar(navigationBar); + } + s.body(child); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPicker.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPicker.java new file mode 100644 index 00000000000..d1336276f39 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPicker.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * The iOS spinning-wheel picker — Flutter's {@code CupertinoPicker}. The 3D + * wheel is not modeled this pass; the item widgets are laid out as a vertical + * {@link Column} (approximate). The selection callback is captured. + */ +public class CupertinoPicker extends Widget { + + private DartList children; + private Funcs.VoidFunc1 onSelectedItemChanged; + + public void backgroundColor(Color v) { + } + + public void itemExtent(double v) { + } + + public void diameterRatio(double v) { + } + + public void magnification(double v) { + } + + public void squeeze(double v) { + } + + public void useMagnifier(boolean v) { + } + + public void scrollController(Object v) { + } + + public void onSelectedItemChanged(Funcs.VoidFunc1 v) { + this.onSelectedItemChanged = v; + } + + public void children(DartList v) { + this.children = v; + } + + DartList getChildren() { + return children; + } + + Funcs.VoidFunc1 getOnSelectedItemChanged() { + return onSelectedItemChanged; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new CupertinoPickerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPickerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPickerRenderElement.java new file mode 100644 index 00000000000..710132fc755 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoPickerRenderElement.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Text; +import com.codename1.ui.Container; +import com.codename1.ui.list.DefaultListModel; +import com.codename1.ui.spinner.Spinner3D; + +import dart.core.DartList; + +/** + * {@link CupertinoPicker}: a wheel of the strings its children carry. + * + *

The children are widgets, and Flutter draws each one. This draws the TEXT in each, + * because a Codename One spinner is a list of strings -- which covers what a picker is + * for and what the gallery asks of it, a column of day names. A child with no text in it + * contributes an empty row rather than disappearing, so the wheel keeps its indices and + * {@code onSelectedItemChanged} still reports the right one.

+ */ +class CupertinoPickerRenderElement extends CupertinoWheelRenderElement { + + CupertinoPickerRenderElement(Widget widget) { + super(widget); + } + + private CupertinoPicker picker() { + return (CupertinoPicker) widget(); + } + + @Override + protected Container createWheel() { + DartList kids = picker().getChildren(); + int n = kids == null ? 0 : kids.size(); + String[] rows = new String[Math.max(1, n)]; + for (int iter = 0; iter < n; iter++) { + rows[iter] = labelOf(kids.get(iter)); + } + if (n == 0) { + rows[0] = ""; + } + return new Spinner3D(new DefaultListModel(rows)); + } + + /// The text inside a child, looking through one level of wrapping, or "". + private static String labelOf(Widget w) { + Widget cur = w; + for (int depth = 0; depth < 6 && cur != null; depth++) { + if (cur instanceof Text) { + String d = ((Text) cur).getData(); + return d == null ? "" : d; + } + Widget next = com.codename1.flutter.WidgetPreview.step(cur, null); + if (next == null) { + return ""; + } + cur = next; + } + return ""; + } + + @Override + protected void report(Object value) { + dart.runtime.Funcs.VoidFunc1 f = picker().getOnSelectedItemChanged(); + if (f == null) { + return; + } + com.codename1.ui.Component c = component(); + if (c instanceof Spinner3D) { + f.call(Long.valueOf(((Spinner3D) c).getSelectedIndex())); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoScrollbar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoScrollbar.java new file mode 100644 index 00000000000..931025df816 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoScrollbar.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * An iOS-style scrollbar wrapper — Flutter's {@code CupertinoScrollbar}. CN1 + * scrollables draw their own scrollbar, so this is a pass-through: it composes + * its child directly. + */ +public class CupertinoScrollbar extends StatelessWidget { + + private Widget child; + + public void controller(Object v) { + } + + public void thumbVisibility(boolean v) { + } + + public void thickness(double v) { + } + + public void thicknessWhileDragging(double v) { + } + + public void radius(Object v) { + } + + public void radiusWhileDragging(Object v) { + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + // CN1's scrollables draw their own scrollbar, so wrapping one adds nothing. This + // is a genuine pass-through rather than a missing feature. + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSearchTextField.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSearchTextField.java new file mode 100644 index 00000000000..dfc7c4039c4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSearchTextField.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.InputDecoration; +import com.codename1.flutter.material.TextEditingController; +import com.codename1.flutter.material.TextField; + +import dart.runtime.Funcs; + +/** + * An iOS-style search field — Flutter's {@code CupertinoSearchTextField}. + * Composed onto the material {@link TextField} with the placeholder as the CN1 + * hint; the magnifier/clear affordances are approximate this pass. + */ +public class CupertinoSearchTextField extends StatelessWidget { + + private Object controller; + private String placeholder; + private Funcs.VoidFunc1 onChanged; + private Funcs.VoidFunc1 onSubmitted; + + public void controller(Object v) { + this.controller = v; + } + + public void placeholder(String v) { + this.placeholder = v; + } + + public void decoration(Object v) { + } + + public void padding(Object v) { + } + + public void restorationId(String v) { + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void onSubmitted(Funcs.VoidFunc1 v) { + this.onSubmitted = v; + } + + public void onSuffixTap(Funcs.VoidFunc0 v) { + } + + @Override + public Widget build(BuildContext context) { + TextField tf = new TextField(); + if (controller instanceof TextEditingController) { + tf.controller((TextEditingController) controller); + } + if (placeholder != null) { + InputDecoration d = new InputDecoration(); + d.hintText(placeholder); + tf.decoration(d); + } + if (onChanged != null) { + tf.onChanged(onChanged); + } + if (onSubmitted != null) { + tf.onSubmitted(onSubmitted); + } + return tf; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSegmentedControl.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSegmentedControl.java new file mode 100644 index 00000000000..aa66e873a58 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSegmentedControl.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.runtime.Funcs; + +/** + * A horizontal iOS segmented control — Flutter's {@code CupertinoSegmentedControl}. + * The segment children and selection callback are captured; the segmented + * visual (a bordered row of tappable segments) is not laid out this pass, so + * it composes an empty {@link Container} placeholder holding the correct API + * shape. + * + * @param the segment key type + */ +public class CupertinoSegmentedControl extends StatelessWidget { + + private Object children; + private Funcs.VoidFunc1 onValueChanged; + private Object groupValue; + + public void children(Object v) { + this.children = v; + } + + public void onValueChanged(Funcs.VoidFunc1 v) { + this.onValueChanged = v; + } + + public void groupValue(Object v) { + this.groupValue = v; + } + + public void unselectedColor(Color v) { + } + + public void selectedColor(Color v) { + } + + public void borderColor(Color v) { + } + + public void pressedColor(Color v) { + } + + public void padding(Object v) { + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlider.java new file mode 100644 index 00000000000..722d431d9d7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlider.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Slider; + +import dart.runtime.Funcs; + +/** + * An iOS-style slider — Flutter's {@code CupertinoSlider}. Same controlled + * double-range semantics as the material slider; composed onto material + * {@link Slider} (visually approximate this pass). + */ +public class CupertinoSlider extends StatelessWidget { + + private double value; + private Double min; + private Double max; + private Long divisions; + private Funcs.VoidFunc1 onChanged; + + public void value(double v) { + this.value = v; + } + + public void min(double v) { + this.min = v; + } + + public void max(double v) { + this.max = v; + } + + public void divisions(long v) { + this.divisions = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void onChangeStart(Object v) { + } + + public void onChangeEnd(Object v) { + } + + public void activeColor(Color v) { + } + + public void thumbColor(Color v) { + } + + @Override + public Widget build(BuildContext context) { + Slider s = new Slider(); + s.value(value); + if (min != null) { + s.min(min); + } + if (max != null) { + s.max(max); + } + if (divisions != null) { + s.divisions(divisions); + } + s.onChanged(onChanged); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlidingSegmentedControl.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlidingSegmentedControl.java new file mode 100644 index 00000000000..7d0d2d1a06d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSlidingSegmentedControl.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.runtime.Funcs; + +/** + * The iOS-13 sliding segmented control — Flutter's + * {@code CupertinoSlidingSegmentedControl}. Like {@link CupertinoSegmentedControl}, + * captures the segments and selection callback but composes an empty + * {@link Container} placeholder this pass. + * + * @param the segment key type + */ +public class CupertinoSlidingSegmentedControl extends StatelessWidget { + + private Object children; + private Funcs.VoidFunc1 onValueChanged; + private Object groupValue; + + public void children(Object v) { + this.children = v; + } + + public void onValueChanged(Funcs.VoidFunc1 v) { + this.onValueChanged = v; + } + + public void groupValue(Object v) { + this.groupValue = v; + } + + public void thumbColor(Color v) { + } + + public void backgroundColor(Color v) { + } + + public void padding(Object v) { + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSliverNavigationBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSliverNavigationBar.java new file mode 100644 index 00000000000..5d5103c1807 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSliverNavigationBar.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.AppBar; + +/** + * The large-title iOS navigation bar used inside a scroll view — Flutter's + * {@code CupertinoSliverNavigationBar}. The collapsing large-title behavior is + * not modeled this pass; it composes a static {@link AppBar} using the large + * title (or middle) as its title. + */ +public class CupertinoSliverNavigationBar extends StatelessWidget { + + private Widget largeTitle; + private Widget leading; + private Widget middle; + private Widget trailing; + private Color backgroundColor; + + public void largeTitle(Widget v) { + this.largeTitle = v; + } + + public void leading(Widget v) { + this.leading = v; + } + + private boolean automaticallyImplyLeading = true; + + public void automaticallyImplyLeading(boolean v) { + this.automaticallyImplyLeading = v; + } + + public void automaticallyImplyTitle(boolean v) { + } + + public void previousPageTitle(String v) { + } + + public void middle(Widget v) { + this.middle = v; + } + + public void trailing(Widget v) { + this.trailing = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void border(Object v) { + } + + public void stretch(boolean v) { + } + + @Override + public Widget build(BuildContext context) { + AppBar bar = new AppBar(); + Widget title = largeTitle != null ? largeTitle : middle; + if (title != null) { + bar.title(title); + } + // The iOS bar's own defaults, so the material AppBarTheme never reaches it. This + // composes onto AppBar, which resolves an unset background and foreground through + // that theme -- and the gallery gives every demo page a purple one, so an iOS bar + // rendered as a purple material bar with a white back arrow where the reference + // has a white bar and a large black title. Its sibling CupertinoNavigationBar + // already sets these; this one did not. + bar.backgroundColor(backgroundColor != null ? backgroundColor + : new Color(BAR_BACKGROUND)); + bar.foregroundColor(new Color(BAR_FOREGROUND)); + // A large title is iOS's 34pt, and it is left aligned, not centred. + TextStyle large = new TextStyle(); + large.fontSize(LARGE_TITLE_SIZE); + large.fontWeight(com.codename1.flutter.FontWeight.bold); + bar.titleTextStyle(large); + bar.centerTitle(false); + bar.automaticallyImplyLeading(automaticallyImplyLeading); + if (leading != null) { + bar.leading(leading); + } + if (trailing != null) { + dart.core.DartList actions = new dart.core.DartList(); + actions.add(trailing); + bar.actions(actions); + } + return bar; + } + + /** The iOS bar's own background, matching {@link CupertinoNavigationBar}. */ + private static final long BAR_BACKGROUND = 0xFFF9F9F9L; + private static final long BAR_FOREGROUND = 0xFF000000L; + + /** iOS's large-title size. */ + private static final double LARGE_TITLE_SIZE = 34; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSwitch.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSwitch.java new file mode 100644 index 00000000000..460df6a0fbd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoSwitch.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Switch; + +import dart.runtime.Funcs; + +/** + * An iOS-style on/off toggle — Flutter's {@code CupertinoSwitch}. Same + * controlled semantics as the material switch; composed onto material + * {@link Switch} (visually approximate this pass). + */ +public class CupertinoSwitch extends StatelessWidget { + + private boolean value; + private Funcs.VoidFunc1 onChanged; + + public void value(boolean v) { + this.value = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void activeColor(Color v) { + } + + public void trackColor(Color v) { + } + + public void thumbColor(Color v) { + } + + @Override + public Widget build(BuildContext context) { + Switch s = new Switch(); + s.value(value); + s.onChanged(onChanged); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabBar.java new file mode 100644 index 00000000000..9910957a888 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabBar.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.BottomNavigationBarItem; +import com.codename1.flutter.widgets.Container; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * The iOS bottom tab bar — Flutter's {@code CupertinoTabBar}. Used to + * configure a {@link CupertinoTabScaffold}; the bar itself composes an empty + * {@link Container} placeholder this pass (the tab items are captured). + */ +public class CupertinoTabBar extends StatelessWidget { + + private DartList items; + private Funcs.VoidFunc1 onTap; + + public void items(DartList v) { + this.items = v; + } + + public void onTap(Funcs.VoidFunc1 v) { + this.onTap = v; + } + + public void currentIndex(long v) { + } + + public void backgroundColor(Color v) { + } + + public void activeColor(Color v) { + } + + public void inactiveColor(Color v) { + } + + public void iconSize(double v) { + } + + public void border(Object v) { + } + + public DartList getItems() { + return items; + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabScaffold.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabScaffold.java new file mode 100644 index 00000000000..5bfdb76d38a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabScaffold.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A tabbed iOS page — Flutter's {@code CupertinoTabScaffold}: a bottom + * {@link CupertinoTabBar} above a body produced per-tab by {@code tabBuilder}. + * Tab switching is not wired this pass, so it builds and shows the first tab's + * content (index 0). + */ +public class CupertinoTabScaffold extends StatelessWidget { + + private CupertinoTabBar tabBar; + private Funcs.Func2 tabBuilder; + + public void tabBar(CupertinoTabBar v) { + this.tabBar = v; + } + + public void tabBuilder(Funcs.Func2 v) { + this.tabBuilder = v; + } + + public void controller(Object v) { + } + + public void backgroundColor(Color v) { + } + + public void resizeToAvoidBottomInset(boolean v) { + } + + public void restorationId(String v) { + } + + @Override + public Widget build(BuildContext context) { + return tabBuilder == null ? null : tabBuilder.call(context, 0L); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabView.java new file mode 100644 index 00000000000..d9e126847bd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTabView.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A single tab's navigation root — Flutter's {@code CupertinoTabView}. Runs + * its {@code builder} to produce the tab content (the per-tab navigator stack + * is not modeled this pass). + */ +public class CupertinoTabView extends StatelessWidget { + + private Funcs.Func1 builder; + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void restorationScopeId(String v) { + } + + public void defaultTitle(String v) { + } + + public void routes(Object v) { + } + + public void onGenerateRoute(Object v) { + } + + public void onUnknownRoute(Object v) { + } + + public void navigatorObservers(Object v) { + } + + @Override + public Widget build(BuildContext context) { + return builder == null ? null : builder.call(context); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextField.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextField.java new file mode 100644 index 00000000000..1b5471d8274 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextField.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.InputDecoration; +import com.codename1.flutter.material.TextEditingController; +import com.codename1.flutter.material.TextField; + +import dart.runtime.Funcs; + +/** + * An iOS-style single-line text input — Flutter's {@code CupertinoTextField}. + * Composed onto the material {@link TextField}: the {@code placeholder} maps to + * the CN1 hint, and controller / obscured / enabled state carry over (visually + * approximate this pass). + */ +public class CupertinoTextField extends StatelessWidget { + + private Object controller; + private String placeholder; + private Boolean obscureText; + private Boolean enabled; + private Funcs.VoidFunc1 onChanged; + private Funcs.VoidFunc1 onSubmitted; + + public void controller(Object v) { + this.controller = v; + } + + public void decoration(Object v) { + } + + public void padding(Object v) { + } + + public void placeholder(String v) { + this.placeholder = v; + } + + public void placeholderStyle(Object v) { + } + + public void prefix(Widget v) { + } + + public void prefixMode(Object v) { + } + + public void suffix(Widget v) { + } + + public void suffixMode(Object v) { + } + + public void clearButtonMode(Object v) { + } + + public void keyboardType(Object v) { + } + + public void textInputAction(Object v) { + } + + public void obscureText(boolean v) { + this.obscureText = v; + } + + public void autocorrect(boolean v) { + } + + public void enabled(boolean v) { + this.enabled = v; + } + + public void restorationId(String v) { + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void onSubmitted(Funcs.VoidFunc1 v) { + this.onSubmitted = v; + } + + @Override + public Widget build(BuildContext context) { + TextField tf = new TextField(); + if (controller instanceof TextEditingController) { + tf.controller((TextEditingController) controller); + } + if (placeholder != null) { + InputDecoration d = new InputDecoration(); + d.hintText(placeholder); + tf.decoration(d); + } + if (obscureText != null) { + tf.obscureText(obscureText); + } + if (enabled != null) { + tf.enabled(enabled); + } + if (onChanged != null) { + tf.onChanged(onChanged); + } + if (onSubmitted != null) { + tf.onSubmitted(onSubmitted); + } + return tf; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextThemeData.java new file mode 100644 index 00000000000..024bd09b657 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTextThemeData.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.TextStyle; + +/** + * The iOS default text styles, mirroring Flutter's {@code CupertinoTextThemeData}. + * Each getter returns a fresh {@link TextStyle} carrying the default logical + * size for that role (TextStyle is a mutable write-once config, so fresh + * instances avoid leaking a call site's mutation). + */ +public class CupertinoTextThemeData { + + private static TextStyle sized(double size) { + TextStyle t = new TextStyle(); + t.fontSize(size); + return t; + } + + public TextStyle textStyle() { + return sized(17); + } + + public TextStyle actionTextStyle() { + return sized(17); + } + + public TextStyle navTitleTextStyle() { + return sized(17); + } + + public TextStyle navLargeTitleTextStyle() { + return sized(34); + } + + public TextStyle tabLabelTextStyle() { + return sized(10); + } + + public TextStyle pickerTextStyle() { + return sized(21); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTheme.java new file mode 100644 index 00000000000..0dff89ad930 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTheme.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Applies a {@link CupertinoThemeData} to its subtree — Flutter's + * {@code CupertinoTheme}. {@link #of(BuildContext)} walks up to the nearest + * CupertinoTheme ancestor and returns its data, falling back to a default. + * As a widget it is a pass-through: it composes its child directly (this + * runtime does not yet thread Cupertino styling through the element tree). + */ +public class CupertinoTheme extends StatelessWidget { + + private CupertinoThemeData data; + private Widget child; + + public void data(CupertinoThemeData v) { + this.data = v; + } + + public void child(Widget v) { + this.child = v; + } + + public CupertinoThemeData getData() { + return data; + } + + public static CupertinoThemeData of(BuildContext context) { + if (context != null) { + CupertinoTheme t = context.findAncestorWidgetOfExactType(CupertinoTheme.class); + if (t != null && t.data != null) { + return t.data; + } + } + return new CupertinoThemeData(); + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoThemeData.java new file mode 100644 index 00000000000..e1309276d10 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoThemeData.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.Color; + +/** + * The resolved iOS theme, mirroring Flutter's {@code CupertinoThemeData}: + * brightness, a few key colors and the {@link CupertinoTextThemeData}. Named + * constructor parameters and {@link #copyWith} arrive as setters / a + * positional copy respectively. + */ +public class CupertinoThemeData { + + private Brightness brightness; + private Color primaryColor; + private Color primaryContrastingColor; + private Color scaffoldBackgroundColor; + private Color barBackgroundColor; + private CupertinoTextThemeData textTheme; + + public void brightness(Brightness v) { + this.brightness = v; + } + + public void primaryColor(Color v) { + this.primaryColor = v; + } + + public void primaryContrastingColor(Color v) { + this.primaryContrastingColor = v; + } + + public void scaffoldBackgroundColor(Color v) { + this.scaffoldBackgroundColor = v; + } + + public void barBackgroundColor(Color v) { + this.barBackgroundColor = v; + } + + public void textTheme(CupertinoTextThemeData v) { + this.textTheme = v; + } + + public Brightness brightness() { + return brightness; + } + + public Color primaryColor() { + return primaryColor != null ? primaryColor : CupertinoColors.systemBlue; + } + + public Color scaffoldBackgroundColor() { + return scaffoldBackgroundColor != null ? scaffoldBackgroundColor : CupertinoColors.systemBackground; + } + + public Color barBackgroundColor() { + return barBackgroundColor != null ? barBackgroundColor : CupertinoColors.systemBackground; + } + + public CupertinoTextThemeData textTheme() { + return textTheme != null ? textTheme : new CupertinoTextThemeData(); + } + + /** + * Returns a copy with the supplied (non-null) fields overridden; + * parameters follow the order declared in the Dart stub. + */ + public CupertinoThemeData copyWith(Brightness brightness, Color primaryColor, + Color primaryContrastingColor, Color scaffoldBackgroundColor, + Color barBackgroundColor, CupertinoTextThemeData textTheme) { + CupertinoThemeData c = new CupertinoThemeData(); + c.brightness = brightness != null ? brightness : this.brightness; + c.primaryColor = primaryColor != null ? primaryColor : this.primaryColor; + c.primaryContrastingColor = primaryContrastingColor != null ? primaryContrastingColor : this.primaryContrastingColor; + c.scaffoldBackgroundColor = scaffoldBackgroundColor != null ? scaffoldBackgroundColor : this.scaffoldBackgroundColor; + c.barBackgroundColor = barBackgroundColor != null ? barBackgroundColor : this.barBackgroundColor; + c.textTheme = textTheme != null ? textTheme : this.textTheme; + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPicker.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPicker.java new file mode 100644 index 00000000000..fcfb1744770 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPicker.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** + * The iOS countdown-timer wheel — Flutter's {@code CupertinoTimerPicker}. Like + * {@link CupertinoDatePicker}, the wheel is not modeled this pass; it composes + * an empty {@link Container} placeholder and captures the change callback. + */ +public class CupertinoTimerPicker extends Widget { + + private Duration initialTimerDuration; + private Funcs.VoidFunc1 onTimerDurationChanged; + + public void backgroundColor(Color v) { + } + + public void mode(Object v) { + } + + public void initialTimerDuration(Duration v) { + this.initialTimerDuration = v; + } + + public void minuteInterval(long v) { + } + + public void secondInterval(long v) { + } + + public void onTimerDurationChanged(Funcs.VoidFunc1 v) { + this.onTimerDurationChanged = v; + } + + Duration getInitialTimerDuration() { + return initialTimerDuration; + } + + Funcs.VoidFunc1 getOnTimerDurationChanged() { + return onTimerDurationChanged; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new CupertinoTimerPickerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPickerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPickerRenderElement.java new file mode 100644 index 00000000000..398f29fd671 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoTimerPickerRenderElement.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.spinner.DurationSpinner3D; + +import dart.core.Duration; + +/** + * {@link CupertinoTimerPicker}: Codename One's duration wheel, in hours, minutes and + * seconds -- which is what the Cupertino timer picker's default mode shows. + */ +class CupertinoTimerPickerRenderElement extends CupertinoWheelRenderElement { + + private static final long MILLIS_PER_MICRO = 1000; + + CupertinoTimerPickerRenderElement(Widget widget) { + super(widget); + } + + private CupertinoTimerPicker picker() { + return (CupertinoTimerPicker) widget(); + } + + @Override + protected Container createWheel() { + DurationSpinner3D d = new DurationSpinner3D(DurationSpinner3D.FIELD_HOUR + | DurationSpinner3D.FIELD_MINUTE | DurationSpinner3D.FIELD_SECOND); + Duration initial = picker().getInitialTimerDuration(); + if (initial != null) { + d.setValue(Long.valueOf(initial.inMicroseconds() / MILLIS_PER_MICRO)); + } + return d; + } + + @Override + protected void report(Object value) { + dart.runtime.Funcs.VoidFunc1 f = picker().getOnTimerDurationChanged(); + if (f == null || !(value instanceof Long)) { + return; + } + f.call(Duration.ofMicroseconds(((Long) value).longValue() * MILLIS_PER_MICRO)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoWheelRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoWheelRenderElement.java new file mode 100644 index 00000000000..e7429544c0e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/CupertinoWheelRenderElement.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.events.ScrollListener; +import com.codename1.ui.spinner.InternalPickerWidget; + +/** + * Base for the Cupertino wheels, each of which is one of Codename One's 3D spinners. + * + *

They are the same control: a column of rows that scrolls under a fixed selection + * band, with the rows away from the band foreshortened. Building a faithful one from + * scratch -- item extent, magnification, squeeze, momentum, selection tracking -- would + * have been a piece of work in its own right, and it already exists in + * {@code com.codename1.ui.spinner}. The only thing missing was that those classes were + * package private; they are public now.

+ * + *

A wheel FILLS the box it is given. Flutter's does, and the gallery relies on it: + * every picker in the demo is a wheel inside a SizedBox of a stated height.

+ */ +abstract class CupertinoWheelRenderElement extends RenderElement { + + /** Flutter's default wheel height when nothing constrains it, in logical pixels. */ + private static final double DEFAULT_HEIGHT_LP = 216; + + private Object lastReported; + + CupertinoWheelRenderElement(Widget widget) { + super(widget); + } + + /** The spinner this wheel is. */ + protected abstract Container createWheel(); + + /** Hands the current value to whatever the Dart asked to be told. */ + protected abstract void report(Object value); + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Container wheel = createWheel(); + if (wheel == null) { + return null; + } + listenForChanges(wheel); + if (wheel instanceof InternalPickerWidget) { + lastReported = ((InternalPickerWidget) wheel).getValue(); + } + return wheel; + } + + /** + * Reports the wheel's value as it turns. + * + *

A spinner is a set of scrollable columns and has no value event of its own, so + * the scroll is what is listened to and the VALUE is what is compared -- it only + * changes as a row crosses the selection band, which is exactly when Flutter's + * {@code onSelectedItemChanged} fires. Without the comparison this would report on + * every pixel of every drag.

+ */ + private void listenForChanges(Container wheel) { + final Container root = wheel; + ScrollListener l = new ScrollListener() { + @Override + public void scrollChanged(int scrollX, int scrollY, int oldX, int oldY) { + if (!(root instanceof InternalPickerWidget)) { + return; + } + Object now = ((InternalPickerWidget) root).getValue(); + if (now == null ? lastReported == null : now.equals(lastReported)) { + return; + } + lastReported = now; + report(now); + } + }; + attachScrollListeners(wheel, l); + } + + private void attachScrollListeners(Container c, ScrollListener l) { + if (c.isScrollableY()) { + c.addScrollListener(l); + } + int count = c.getComponentCount(); + for (int iter = 0; iter < count; iter++) { + Component child = c.getComponentAt(iter); + if (child instanceof Container) { + attachScrollListeners((Container) child, l); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double w = constraints.hasBoundedWidth() + ? constraints.maxWidth() : com.codename1.flutter.rendering.Dp.px(320); + double h = constraints.hasBoundedHeight() + ? constraints.maxHeight() + : com.codename1.flutter.rendering.Dp.px(DEFAULT_HEIGHT_LP); + return constraints.constrain(new Size(w, h)); + } + + @Override + public void position(int x, int y) { + super.position(x, y); + // The spinner lays ITSELF out. Everything else in this runtime is a leaf whose + // bounds the Flutter pass writes directly, and writing only the outer bounds of a + // control with columns inside it leaves every column at whatever size it had when + // it was created -- which is none. + Component c = component(); + if (c instanceof Container) { + ((Container) c).layoutContainer(); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/MouseCursor.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/MouseCursor.java new file mode 100644 index 00000000000..13817aadba8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/MouseCursor.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +/** + * A pointer cursor kind — Flutter's {@code MouseCursor}. CN1 does not retarget + * the desktop cursor per widget, so this is an opaque marker carried by + * {@code MouseRegion(cursor:)} and never acted upon this pass. + */ +public class MouseCursor { + + /** + * {@code MouseCursor.defer}: defers the cursor decision to the region behind + * this one. An opaque marker in this runtime. + */ + public static final MouseCursor defer = new MouseCursor("defer"); + + private final String kind; + + public MouseCursor(String kind) { + this.kind = kind; + } + + public String kind() { + return kind; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/OverlayVisibilityMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/OverlayVisibilityMode.java new file mode 100644 index 00000000000..d9ae3815e11 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/OverlayVisibilityMode.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +/** + * When an overlay widget (clear button, prefix, suffix) of a + * {@link CupertinoTextField} is shown — Flutter's {@code OverlayVisibilityMode}. + */ +public enum OverlayVisibilityMode { + never, editing, notEditing, always +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/SystemMouseCursors.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/SystemMouseCursors.java new file mode 100644 index 00000000000..d30b29426c4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/cupertino/SystemMouseCursors.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.cupertino; + +/** + * The system-provided {@link MouseCursor} constants, mirroring Flutter's + * {@code SystemMouseCursors}. Opaque markers in this runtime (see + * {@link MouseCursor}). + */ +public final class SystemMouseCursors { + + private SystemMouseCursors() { + } + + public static final MouseCursor none = new MouseCursor("none"); + public static final MouseCursor basic = new MouseCursor("basic"); + public static final MouseCursor click = new MouseCursor("click"); + public static final MouseCursor forbidden = new MouseCursor("forbidden"); + public static final MouseCursor wait = new MouseCursor("wait"); + public static final MouseCursor progress = new MouseCursor("progress"); + public static final MouseCursor text = new MouseCursor("text"); + public static final MouseCursor grab = new MouseCursor("grab"); + public static final MouseCursor grabbing = new MouseCursor("grabbing"); + public static final MouseCursor move = new MouseCursor("move"); + public static final MouseCursor resizeUpDown = new MouseCursor("resizeUpDown"); + public static final MouseCursor resizeLeftRight = new MouseCursor("resizeLeftRight"); + public static final MouseCursor resizeColumn = new MouseCursor("resizeColumn"); + public static final MouseCursor resizeRow = new MouseCursor("resizeRow"); + public static final MouseCursor copy = new MouseCursor("copy"); + public static final MouseCursor alias = new MouseCursor("alias"); + public static final MouseCursor cell = new MouseCursor("cell"); + public static final MouseCursor precise = new MouseCursor("precise"); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/FontResolver.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/FontResolver.java new file mode 100644 index 00000000000..6cb2fe39137 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/FontResolver.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.fonts; + +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.FontWeight; +import com.codename1.ui.Font; + +/** + * Resolves a Flutter {@code fontFamily} to a real Codename One font loaded from + * a bundled TrueType file. + * + *

Flutter's {@code google_fonts} package downloads a face at runtime or reads + * one the app bundled; either way the text is painted in the family the design + * asks for. This runtime used to discard {@code fontFamily} entirely and paint + * every string in the platform default, which is the single largest visual + * difference on any screen with a designed typeface -- a study whose whole look + * is Work Sans or Libre Franklin renders in Helvetica and every glyph is the + * wrong shape, the wrong width and on the wrong baseline.

+ * + *

Faces are looked up among the app's flattened Flutter assets, under the + * folders {@link #assetFolders()} lists. An app that bundles its faces + * elsewhere adds its own folder rather than renaming its assets.

+ */ +public final class FontResolver { + + private FontResolver() { + } + + /** + * Asset folders searched for a face, in order. + * + *

The last entry is where the Flutter gallery's asset package keeps the + * Google Fonts it ships; it is a default, not a special case, and an app + * with its own layout appends to this list.

+ */ + private static final java.util.List FOLDERS = new java.util.ArrayList(); + + static { + FOLDERS.add("fonts/"); + FOLDERS.add("assets/fonts/"); + FOLDERS.add("fonts/google_fonts/"); + FOLDERS.add("packages/flutter_gallery_assets/fonts/google_fonts/"); + } + + /** The mutable search path; add a folder before the first text is painted. */ + public static java.util.List assetFolders() { + return FOLDERS; + } + + /** + * The google_fonts file-name suffix for each weight, heaviest last. A face + * rarely ships every weight, so a miss falls back to the nearest one that + * exists rather than to the platform font. + */ + private static final String[] VARIANTS = { + "Thin", "ExtraLight", "Light", "Regular", "Medium", + "SemiBold", "Bold", "ExtraBold", "Black", + }; + + private static final java.util.Map CACHE = + new java.util.HashMap(); + + /** + * The face for {@code family} at {@code weight}, or null when the app + * bundles no such file (in which case the caller keeps the platform font). + * + *

Cached including the misses -- a family with no bundled face is asked + * for on every build of every Text that names it, and probing the asset + * folders each time is a filesystem walk per string.

+ */ + public static Font resolve(String family, FontWeight weight, boolean italic) { + if (family == null || family.length() == 0) { + return null; + } + String key = family + '|' + (weight == null ? "w400" : weight.name()) + '|' + italic; + if (CACHE.containsKey(key)) { + return CACHE.get(key); + } + Font f = load(family, weight, italic); + CACHE.put(key, f); + return f; + } + + /** Test hook: forgets what has been resolved so far. */ + public static void clearCache() { + CACHE.clear(); + } + + private static Font load(String family, FontWeight weight, boolean italic) { + String base = compact(family); + int want = index(weight); + // Nearest-weight order: the exact one, then outwards, so a family that + // ships only Regular and Bold still answers a request for Medium. + for (int distance = 0; distance < VARIANTS.length; distance++) { + for (int sign = 0; sign < 2; sign++) { + int i = sign == 0 ? want + distance : want - distance; + if (i < 0 || i >= VARIANTS.length || (distance == 0 && sign == 1)) { + continue; + } + Font f = tryFile(base + '-' + VARIANTS[i] + (italic ? "Italic" : "")); + if (f != null) { + return f; + } + } + } + // A face bundled without a variant suffix at all ("Foo.ttf"). + return tryFile(base); + } + + /** Flutter family names carry spaces ("Work Sans"); the files do not. */ + private static String compact(String family) { + StringBuilder sb = new StringBuilder(family.length()); + for (int i = 0; i < family.length(); i++) { + char c = family.charAt(i); + if (c != ' ') { + sb.append(c); + } + } + return sb.toString(); + } + + private static int index(FontWeight weight) { + if (weight == null) { + return 3; // Regular + } + switch (weight) { + case w100: return 0; + case w200: return 1; + case w300: return 2; + case w400: return 3; + case w500: return 4; + case w600: return 5; + case w700: return 6; + case w800: return 7; + case w900: return 8; + default: return 3; + } + } + + /// Cached line-height ratios, keyed by the same name tryFile() looks a face up by. + private static final java.util.HashMap RATIOS = + new java.util.HashMap(); + + /** + * The line height this face should lay out at, as a multiple of the font size, or 0 + * when it cannot be read. + * + *

A text style that names no height gets the FONT's line height, and which of a + * font's several vertical metrics that means is not settled between platforms. + * Codename One reports the typographic pair (sTypoAscender + sTypoDescender), which + * for Work Sans is 1.17 em. Flutter lays the same face out at about 1.35, and the + * difference is not cosmetic: the mail study's card is three lines of text, so each + * card came out 26 device pixels short and the list drifted further out of place the + * further down it went -- 11.9% of that screen wrong, the worst route in the sweep, + * for a reason that has nothing to do with the widgets.

+ * + *

The pair that reproduces it is the WINDOW ascent with the typographic descent, + * which is what the reference measures to within a pixel over three lines. Read from + * the face itself rather than assumed, so a font with different metrics gets its own + * answer instead of this one's.

+ */ + public static double lineHeightRatio(String family, FontWeight weight, boolean italic) { + if (family == null || family.length() == 0) { + return 0; + } + String key = family + '|' + (weight == null ? "w400" : weight.name()) + '|' + italic; + synchronized (RATIOS) { + Double cached = RATIOS.get(key); + if (cached != null) { + return cached.doubleValue(); + } + } + // The same candidate order load() uses, so the metrics come from the FACE that + // was actually resolved rather than from whichever file happens to be found first. + double ratio = 0; + String base = compact(family); + int want = index(weight); + outer: + for (int distance = 0; distance < VARIANTS.length; distance++) { + for (int sign = 0; sign < 2; sign++) { + int i = sign == 0 ? want + distance : want - distance; + if (i < 0 || i >= VARIANTS.length || (distance == 0 && sign == 1)) { + continue; + } + ratio = readRatio(base + '-' + VARIANTS[i] + (italic ? "Italic" : "")); + if (ratio > 0) { + break outer; + } + } + } + if (ratio <= 0) { + ratio = readRatio(base); + } + synchronized (RATIOS) { + RATIOS.put(key, Double.valueOf(ratio)); + } + return ratio; + } + + private static double readRatio(String baseName) { + for (int i = 0; i < FOLDERS.size(); i++) { + for (int e = 0; e < EXTENSIONS.length; e++) { + String flat = FlutterAssets.flatName( + FOLDERS.get(i) + baseName + EXTENSIONS[e]); + byte[] data = readAll(flat); + if (data != null) { + double r = metrics(data); + if (r > 0) { + return r; + } + } + } + } + return 0; + } + + private static byte[] readAll(String flatName) { + java.io.InputStream in = null; + try { + in = com.codename1.ui.Display.getInstance() + .getResourceAsStream(FontResolver.class, "/" + flatName); + if (in == null) { + return null; + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } catch (Throwable t) { + return null; + } finally { + try { + if (in != null) { + in.close(); + } + } catch (java.io.IOException ignore) { + // closing a font we already read is not worth failing over + } + } + } + + private static int u16(byte[] d, int o) { + return ((d[o] & 0xff) << 8) | (d[o + 1] & 0xff); + } + + private static int s16(byte[] d, int o) { + int v = u16(d, o); + return v > 0x7fff ? v - 0x10000 : v; + } + + /// The face's own line height as a multiple of the em, or 0 when the tables + /// are absent. + /// + /// This is the rule the reference stack uses, and it is NOT a free choice of + /// metrics: a font carries two competing pairs, and bit 7 of the OS/2 + /// `fsSelection` field (`USE_TYPO_METRICS`) is the face author stating which + /// one is authoritative. Set, the typographic pair wins, line gap included; + /// clear, the `hhea` pair does. + /// + /// Mixing the two -- pairing `usWinAscent` with `sTypoDescender`, as this + /// once did -- produces a box taller than either pair describes, because + /// `usWinAscent` is the ink bound of the tallest glyph rather than an + /// ascent. The error only shows on text whose style states no height, so it + /// stayed invisible while the type scale supplied one for nearly every role. + private static double metrics(byte[] d) { + try { + int tables = u16(d, 4); + int head = -1; + int os2 = -1; + int hhea = -1; + for (int i = 0; i < tables; i++) { + int rec = 12 + 16 * i; + String tag = new String(d, rec, 4, "ISO-8859-1"); + int off = (u16(d, rec + 8) << 16) | u16(d, rec + 10); + if ("head".equals(tag)) { + head = off; + } else if ("OS/2".equals(tag)) { + os2 = off; + } else if ("hhea".equals(tag)) { + hhea = off; + } + } + if (head < 0) { + return 0; + } + int upem = u16(d, head + 18); + if (upem <= 0) { + return 0; + } + // fsSelection bit 7 is USE_TYPO_METRICS. + boolean useTypo = os2 >= 0 && (u16(d, os2 + 62) & 0x80) != 0; + if (useTypo) { + int ascender = s16(d, os2 + 68); + int descender = s16(d, os2 + 70); + int lineGap = s16(d, os2 + 72); + int height = ascender - descender + lineGap; + if (height > 0) { + return height / (double) upem; + } + } + if (hhea >= 0) { + int ascender = s16(d, hhea + 4); + int descender = s16(d, hhea + 6); + int lineGap = s16(d, hhea + 8); + int height = ascender - descender + lineGap; + if (height > 0) { + return height / (double) upem; + } + } + return 0; + } catch (Throwable t) { + return 0; + } + } + + private static final String[] EXTENSIONS = {".ttf", ".otf"}; + + private static Font tryFile(String baseName) { + for (int i = 0; i < FOLDERS.size(); i++) { + for (int e = 0; e < EXTENSIONS.length; e++) { + String asset = FOLDERS.get(i) + baseName + EXTENSIONS[e]; + String flat = FlutterAssets.flatName(asset); + if (!exists(flat)) { + continue; + } + try { + // The FILE name is flattened; the FONT name is the face's + // own, because iOS resolves a bundled font by name rather + // than by path and will not find "cn1f_...ttf". + Font f = Font.createTrueTypeFont(baseName, flat); + if (f != null) { + return f; + } + } catch (Throwable ignore) { + // an unreadable or unsupported face is a miss, not a crash + } + } + } + return null; + } + + private static boolean exists(String flatName) { + try { + java.io.InputStream in = com.codename1.ui.Display.getInstance() + .getResourceAsStream(FontResolver.class, "/" + flatName); + if (in == null) { + return false; + } + in.close(); + return true; + } catch (Throwable t) { + return false; + } + } + + /** + * A derivable face for a style that names no family, or names one the app + * does not bundle. + * + *

Codename One can only size a SYSTEM font in three steps, and + * {@link Font#derive} on one silently does nothing -- it returns a font of + * the original size and reports no error. Every Flutter style states its + * size in logical pixels, so a style with no family had its size dropped on + * the floor: the text field demo asked for 16sp and painted a label half + * again too tall, and the overflow pushed everything under it down the + * page.

+ * + *

The {@code native:} TrueType scheme is the way out. It hands back the + * PLATFORM's own face -- San Francisco on iOS, Roboto on Android and in the + * simulator, which is what the reference renders in -- as a true type font, + * and a true type font derives to any pixel size. So the letterforms stay + * the platform's and the size finally means something.

+ * + *

Returns null where the scheme is unsupported, and the caller keeps + * whatever the theme left on the component -- the previous behaviour.

+ */ + public static Font platformFace(FontWeight weight, boolean italic) { + String name = "native:" + (italic ? "Italic" : "Main") + nativeWeight(weight); + if (CACHE.containsKey(name)) { + return CACHE.get(name); + } + Font f; + try { + f = Font.createTrueTypeFont(name, name); + } catch (Throwable unsupported) { + f = null; + } + CACHE.put(name, f); + return f; + } + + /** + * The nearest {@code native:} weight. The scheme offers five faces against + * Flutter's nine, and a request that falls between them rounds to the + * heavier neighbour the way a real family's fallback does. + */ + private static String nativeWeight(FontWeight weight) { + if (weight == null) { + return "Regular"; + } + switch (weight) { + case w100: + return "Thin"; + case w200: + case w300: + return "Light"; + case w400: + // "Normal", not "Regular": the native scheme's Regular is a + // MEDIUM face on both iOS and the desktop port, which is one + // step heavier than Flutter's weight 400 and is what made every + // run of ordinary body text read too heavy against the + // reference. MainNormal is the true regular weight. + return "Normal"; + case w500: + // ...and w500 IS Medium, so the old alias is exactly right here. + return "Regular"; + case w600: + case w700: + return "Bold"; + default: + return "Black"; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFonts.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFonts.java new file mode 100644 index 00000000000..54bde8c1240 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFonts.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.fonts; + +import com.codename1.flutter.Color; +import com.codename1.flutter.FontWeight; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.material.TextTheme; + +/** + * A no-op equivalent of the {@code google_fonts} package. Flutter's GoogleFonts + * downloads/registers web fonts and returns a styled {@link TextStyle}; here we + * return a TextStyle carrying the requested size/weight/color and fall back to + * the platform font (faithful web-font loading is a later pass). The + * {@code *TextTheme} helpers pass the supplied theme straight through. + */ +public abstract class GoogleFonts { + + /** {@code GoogleFonts.config} — a static getter, hence a static field here. */ + public static final GoogleFontsConfig config = new GoogleFontsConfig(); + + private GoogleFonts() { + } + + /// {@code textStyle:} is the BASE the rest are layered onto - google_fonts copies the + /// given style and overrides only what was named. Ignoring it dropped whichever theme + /// style the caller was extending, so the text kept the font and lost everything else. + private static TextStyle style(String family, double fontSize, FontWeight fontWeight, + Color color, Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + TextStyle t = textStyle != null + ? textStyle.copyWith(null, null, null, null, null, null, null, null, null, null, + null, null, null) + : new TextStyle(); + if (fontSize > 0) { + t.fontSize(fontSize); + } + if (fontWeight != null) { + t.fontWeight(fontWeight); + } + if (color != null) { + t.color(color); + } + if (letterSpacing != null) { + t.letterSpacing(letterSpacing.doubleValue()); + } + if (height != null) { + t.height(height.doubleValue()); + } + // NAMING the family is the whole point of the call. It used to be + // dropped, so every google_fonts style resolved to the platform face + // and the studies rendered in the wrong typeface throughout. + t.fontFamily(family); + return t; + } + + public static TextStyle eczar(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("Eczar", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle libreFranklin(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("LibreFranklin", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle merriweather(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("Merriweather", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle montserrat(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("Montserrat", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle oswald(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("Oswald", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle robotoCondensed(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("RobotoCondensed", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle robotoMono(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("RobotoMono", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + public static TextStyle workSans(double fontSize, FontWeight fontWeight, Color color, + Double letterSpacing, Double height, + com.codename1.flutter.TextStyle textStyle, Object fontStyle, Object decoration, + Double wordSpacing) { + return style("WorkSans", fontSize, fontWeight, color, letterSpacing, height, textStyle, fontStyle, + decoration, wordSpacing); + } + + /** + * {@code GoogleFonts.TextTheme(theme)} — the given text theme with + * every role re-pointed at that family, which is how a study sets its + * typeface once for a whole app. Passing the theme through unchanged (what + * this used to do) left every one of those roles on the platform face. + */ + private static TextTheme themed(String family, TextTheme textTheme) { + TextTheme base = textTheme != null ? textTheme : new TextTheme(); + return base.apply(family, null, null, null, null, null, null); + } + + public static TextTheme ralewayTextTheme(TextTheme textTheme) { + return themed("Raleway", textTheme); + } + + public static TextTheme rubikTextTheme(TextTheme textTheme) { + return themed("Rubik", textTheme); + } + + public static TextTheme workSansTextTheme(TextTheme textTheme) { + return themed("WorkSans", textTheme); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFontsConfig.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFontsConfig.java new file mode 100644 index 00000000000..38c6d341e52 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/fonts/GoogleFontsConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.fonts; + +/** + * Mirror of {@code GoogleFonts.config}. Only {@code allowRuntimeFetching} is + * modelled; runtime font fetching is never performed here so the flag is + * inert. + */ +public class GoogleFontsConfig { + + public boolean allowRuntimeFetching = true; + + public boolean allowRuntimeFetching() { + return allowRuntimeFetching; + } + + public void allowRuntimeFetching(boolean v) { + this.allowRuntimeFetching = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ChangeNotifier.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ChangeNotifier.java new file mode 100644 index 00000000000..907c4b10005 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ChangeNotifier.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import java.util.List; + +import dart.runtime.Funcs; + +/** + * Flutter's ChangeNotifier. Applied in Dart as a mixin + * ({@code class EmailStore with ChangeNotifier}); the transpiler maps a stub + * mixin to an implemented Java interface, so the notifier state (the listener + * list) lives in an identity-keyed side table rather than in an instance field. + * Listeners are {@code VoidCallback}s ({@link Funcs.VoidFunc0}). + * + *

A ChangeNotifier IS a {@link Listenable}, as in Flutter — so a model mixing it in can + * drive an {@code AnimatedBuilder} or an {@code AnimatedWidget} directly.

+ */ +public interface ChangeNotifier extends Listenable { + + /** Listener lists, one per notifier that currently has listeners. */ + NotifierListeners LISTENERS = new NotifierListeners(); + + /** + * The implementing object's own listener list, or null to use {@link #LISTENERS}. + * + *

A class should provide one. The shared map holds its notifier only weakly, but a + * listener that closes over the notifier -- a bound method, a closure reading the + * model -- is held strongly by the map's value and leads back to the key, so such a + * notifier could never be collected until someone called dispose. A list held by the + * notifier itself goes when the notifier does. The transpiler adds one to every class + * that mixes this in; the map remains for any implementation that does not.

+ */ + default List changeNotifierListeners$() { + return null; + } + + default void addListener(Funcs.VoidFunc0 listener) { + List own = changeNotifierListeners$(); + if (own != null) { + own.add(listener); + return; + } + LISTENERS.forAdding(this).add(listener); + } + + default void removeListener(Funcs.VoidFunc0 listener) { + List own = changeNotifierListeners$(); + if (own != null) { + own.remove(listener); + return; + } + LISTENERS.remove(this, listener); + } + + default void notifyListeners() { + List own = changeNotifierListeners$(); + List l = own != null ? own : LISTENERS.get(this); + if (l != null) { + Listeners.notify(l); + } + } + + default void dispose() { + List own = changeNotifierListeners$(); + if (own != null) { + own.clear(); // emptied, so a notification already running stops (see Listeners) + return; + } + LISTENERS.clear(this); + } + + default boolean hasListeners() { + List own = changeNotifierListeners$(); + List l = own != null ? own : LISTENERS.get(this); + return l != null && !l.isEmpty(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Characters.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Characters.java new file mode 100644 index 00000000000..e8e602d45f1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Characters.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +/** + * User-perceived characters (grapheme clusters), which is what Flutter counts for a + * text field's {@code maxLength} and its counter -- not UTF-16 units. Counting units + * cut an emoji in half at a limit and reported a flag as four characters. + * + *

The segmentation covers what a text field meets in practice: surrogate pairs, + * combining marks, variation selectors, emoji skin-tone modifiers, zero-width-joiner + * sequences (families, professions), regional-indicator flag pairs, tag sequences + * (subdivision flags) and CR LF. It is not the full Unicode UAX #29 table.

+ */ +public final class Characters { + + private Characters() { + } + + /** How many user-perceived characters {@code s} holds. */ + public static int count(String s) { + if (s == null) { + return 0; + } + int n = 0; + int i = 0; + while (i < s.length()) { + i = next(s, i); + n++; + } + return n; + } + + /** {@code s} cut to at most {@code max} user-perceived characters, never mid-character. */ + public static String take(String s, long max) { + if (s == null || max < 0) { + return s; + } + int i = 0; + long n = 0; + while (i < s.length() && n < max) { + i = next(s, i); + n++; + } + return s.substring(0, i); + } + + /** The index just past the cluster that starts at {@code i}. */ + static int next(String s, int i) { + int len = s.length(); + int cp = codePointAt(s, i); + int j = i + Character.charCount(cp); + if (cp == '\r' && j < len && s.charAt(j) == '\n') { + return j + 1; + } + boolean regional = isRegionalIndicator(cp); + if (regional && j < len && isRegionalIndicator(codePointAt(s, j))) { + j += Character.charCount(codePointAt(s, j)); // a flag is two indicators + } + while (j < len) { + int c = codePointAt(s, j); + if (isExtender(c)) { + j += Character.charCount(c); + } else if (c == 0x200D && j + 1 < len) { + // Zero-width joiner: the next character belongs to this cluster too. + j += 1; + j += Character.charCount(codePointAt(s, j)); + } else { + break; + } + } + return j; + } + + /** + * The code point at {@code i}, decoding a surrogate pair by hand: String.codePointAt + * is not in ParparVM's JavaAPI or CLDC11. + */ + private static int codePointAt(String s, int i) { + char c = s.charAt(i); + if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length()) { + char d = s.charAt(i + 1); + if (d >= 0xDC00 && d <= 0xDFFF) { + return ((c - 0xD800) << 10) + (d - 0xDC00) + 0x10000; + } + } + return c; + } + + private static boolean isRegionalIndicator(int c) { + return c >= 0x1F1E6 && c <= 0x1F1FF; + } + + /** + * Characters that never start a cluster: they modify the one before them. Every + * combining mark (general categories Mn, Mc and Me) extends the cluster, from + * {@link #MARKS}; a handful of Latin-script blocks did not cover Hebrew points, + * Arabic harakat or the Indic vowel signs, so maxLength split those characters. + */ + private static boolean isExtender(int c) { + return (c >= 0x1F3FB && c <= 0x1F3FF) // emoji skin-tone modifiers + || (c >= 0xE0020 && c <= 0xE007F) // tag characters (subdivision flags) + || isMark(c); + } + + private static boolean isMark(int c) { + int lo = 0; + int hi = MARKS.length / 2 - 1; + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + if (c < MARKS[2 * mid]) { + hi = mid - 1; + } else if (c > MARKS[2 * mid + 1]) { + lo = mid + 1; + } else { + return true; + } + } + return false; + } + + /** + * Inclusive [start, end] ranges of the code points that extend a grapheme cluster: + * the combining marks (Mn, Mc, Me) of Unicode 16.0, the Hangul vowel and final jamo + * that join a leading one into a syllable, and ZWNJ. Generated from Python's + * unicodedata; ParparVM's Character.getType carries no table above Latin-1, and + * CLDC11 has no getType at all, so the table lives here. + */ + private static final int[] MARKS = { + 0x300, 0x36F, 0x483, 0x489, 0x591, 0x5BD, 0x5BF, 0x5BF, + 0x5C1, 0x5C2, 0x5C4, 0x5C5, 0x5C7, 0x5C7, 0x610, 0x61A, + 0x64B, 0x65F, 0x670, 0x670, 0x6D6, 0x6DC, 0x6DF, 0x6E4, + 0x6E7, 0x6E8, 0x6EA, 0x6ED, 0x711, 0x711, 0x730, 0x74A, + 0x7A6, 0x7B0, 0x7EB, 0x7F3, 0x7FD, 0x7FD, 0x816, 0x819, + 0x81B, 0x823, 0x825, 0x827, 0x829, 0x82D, 0x859, 0x85B, + 0x897, 0x89F, 0x8CA, 0x8E1, 0x8E3, 0x903, 0x93A, 0x93C, + 0x93E, 0x94F, 0x951, 0x957, 0x962, 0x963, 0x981, 0x983, + 0x9BC, 0x9BC, 0x9BE, 0x9C4, 0x9C7, 0x9C8, 0x9CB, 0x9CD, + 0x9D7, 0x9D7, 0x9E2, 0x9E3, 0x9FE, 0x9FE, 0xA01, 0xA03, + 0xA3C, 0xA3C, 0xA3E, 0xA42, 0xA47, 0xA48, 0xA4B, 0xA4D, + 0xA51, 0xA51, 0xA70, 0xA71, 0xA75, 0xA75, 0xA81, 0xA83, + 0xABC, 0xABC, 0xABE, 0xAC5, 0xAC7, 0xAC9, 0xACB, 0xACD, + 0xAE2, 0xAE3, 0xAFA, 0xAFF, 0xB01, 0xB03, 0xB3C, 0xB3C, + 0xB3E, 0xB44, 0xB47, 0xB48, 0xB4B, 0xB4D, 0xB55, 0xB57, + 0xB62, 0xB63, 0xB82, 0xB82, 0xBBE, 0xBC2, 0xBC6, 0xBC8, + 0xBCA, 0xBCD, 0xBD7, 0xBD7, 0xC00, 0xC04, 0xC3C, 0xC3C, + 0xC3E, 0xC44, 0xC46, 0xC48, 0xC4A, 0xC4D, 0xC55, 0xC56, + 0xC62, 0xC63, 0xC81, 0xC83, 0xCBC, 0xCBC, 0xCBE, 0xCC4, + 0xCC6, 0xCC8, 0xCCA, 0xCCD, 0xCD5, 0xCD6, 0xCE2, 0xCE3, + 0xCF3, 0xCF3, 0xD00, 0xD03, 0xD3B, 0xD3C, 0xD3E, 0xD44, + 0xD46, 0xD48, 0xD4A, 0xD4D, 0xD57, 0xD57, 0xD62, 0xD63, + 0xD81, 0xD83, 0xDCA, 0xDCA, 0xDCF, 0xDD4, 0xDD6, 0xDD6, + 0xDD8, 0xDDF, 0xDF2, 0xDF3, 0xE31, 0xE31, 0xE34, 0xE3A, + 0xE47, 0xE4E, 0xEB1, 0xEB1, 0xEB4, 0xEBC, 0xEC8, 0xECE, + 0xF18, 0xF19, 0xF35, 0xF35, 0xF37, 0xF37, 0xF39, 0xF39, + 0xF3E, 0xF3F, 0xF71, 0xF84, 0xF86, 0xF87, 0xF8D, 0xF97, + 0xF99, 0xFBC, 0xFC6, 0xFC6, 0x102B, 0x103E, 0x1056, 0x1059, + 0x105E, 0x1060, 0x1062, 0x1064, 0x1067, 0x106D, 0x1071, 0x1074, + 0x1082, 0x108D, 0x108F, 0x108F, 0x109A, 0x109D, 0x1160, 0x11FF, + 0x135D, 0x135F, 0x1712, 0x1715, 0x1732, 0x1734, 0x1752, 0x1753, + 0x1772, 0x1773, 0x17B4, 0x17D3, 0x17DD, 0x17DD, 0x180B, 0x180D, + 0x180F, 0x180F, 0x1885, 0x1886, 0x18A9, 0x18A9, 0x1920, 0x192B, + 0x1930, 0x193B, 0x1A17, 0x1A1B, 0x1A55, 0x1A5E, 0x1A60, 0x1A7C, + 0x1A7F, 0x1A7F, 0x1AB0, 0x1ACE, 0x1B00, 0x1B04, 0x1B34, 0x1B44, + 0x1B6B, 0x1B73, 0x1B80, 0x1B82, 0x1BA1, 0x1BAD, 0x1BE6, 0x1BF3, + 0x1C24, 0x1C37, 0x1CD0, 0x1CD2, 0x1CD4, 0x1CE8, 0x1CED, 0x1CED, + 0x1CF4, 0x1CF4, 0x1CF7, 0x1CF9, 0x1DC0, 0x1DFF, 0x200C, 0x200C, + 0x20D0, 0x20F0, 0x2CEF, 0x2CF1, 0x2D7F, 0x2D7F, 0x2DE0, 0x2DFF, + 0x302A, 0x302F, 0x3099, 0x309A, 0xA66F, 0xA672, 0xA674, 0xA67D, + 0xA69E, 0xA69F, 0xA6F0, 0xA6F1, 0xA802, 0xA802, 0xA806, 0xA806, + 0xA80B, 0xA80B, 0xA823, 0xA827, 0xA82C, 0xA82C, 0xA880, 0xA881, + 0xA8B4, 0xA8C5, 0xA8E0, 0xA8F1, 0xA8FF, 0xA8FF, 0xA926, 0xA92D, + 0xA947, 0xA953, 0xA980, 0xA983, 0xA9B3, 0xA9C0, 0xA9E5, 0xA9E5, + 0xAA29, 0xAA36, 0xAA43, 0xAA43, 0xAA4C, 0xAA4D, 0xAA7B, 0xAA7D, + 0xAAB0, 0xAAB0, 0xAAB2, 0xAAB4, 0xAAB7, 0xAAB8, 0xAABE, 0xAABF, + 0xAAC1, 0xAAC1, 0xAAEB, 0xAAEF, 0xAAF5, 0xAAF6, 0xABE3, 0xABEA, + 0xABEC, 0xABED, 0xFB1E, 0xFB1E, 0xFE00, 0xFE0F, 0xFE20, 0xFE2F, + 0x101FD, 0x101FD, 0x102E0, 0x102E0, 0x10376, 0x1037A, 0x10A01, 0x10A03, + 0x10A05, 0x10A06, 0x10A0C, 0x10A0F, 0x10A38, 0x10A3A, 0x10A3F, 0x10A3F, + 0x10AE5, 0x10AE6, 0x10D24, 0x10D27, 0x10D69, 0x10D6D, 0x10EAB, 0x10EAC, + 0x10EFC, 0x10EFF, 0x10F46, 0x10F50, 0x10F82, 0x10F85, 0x11000, 0x11002, + 0x11038, 0x11046, 0x11070, 0x11070, 0x11073, 0x11074, 0x1107F, 0x11082, + 0x110B0, 0x110BA, 0x110C2, 0x110C2, 0x11100, 0x11102, 0x11127, 0x11134, + 0x11145, 0x11146, 0x11173, 0x11173, 0x11180, 0x11182, 0x111B3, 0x111C0, + 0x111C9, 0x111CC, 0x111CE, 0x111CF, 0x1122C, 0x11237, 0x1123E, 0x1123E, + 0x11241, 0x11241, 0x112DF, 0x112EA, 0x11300, 0x11303, 0x1133B, 0x1133C, + 0x1133E, 0x11344, 0x11347, 0x11348, 0x1134B, 0x1134D, 0x11357, 0x11357, + 0x11362, 0x11363, 0x11366, 0x1136C, 0x11370, 0x11374, 0x113B8, 0x113C0, + 0x113C2, 0x113C2, 0x113C5, 0x113C5, 0x113C7, 0x113CA, 0x113CC, 0x113D0, + 0x113D2, 0x113D2, 0x113E1, 0x113E2, 0x11435, 0x11446, 0x1145E, 0x1145E, + 0x114B0, 0x114C3, 0x115AF, 0x115B5, 0x115B8, 0x115C0, 0x115DC, 0x115DD, + 0x11630, 0x11640, 0x116AB, 0x116B7, 0x1171D, 0x1172B, 0x1182C, 0x1183A, + 0x11930, 0x11935, 0x11937, 0x11938, 0x1193B, 0x1193E, 0x11940, 0x11940, + 0x11942, 0x11943, 0x119D1, 0x119D7, 0x119DA, 0x119E0, 0x119E4, 0x119E4, + 0x11A01, 0x11A0A, 0x11A33, 0x11A39, 0x11A3B, 0x11A3E, 0x11A47, 0x11A47, + 0x11A51, 0x11A5B, 0x11A8A, 0x11A99, 0x11C2F, 0x11C36, 0x11C38, 0x11C3F, + 0x11C92, 0x11CA7, 0x11CA9, 0x11CB6, 0x11D31, 0x11D36, 0x11D3A, 0x11D3A, + 0x11D3C, 0x11D3D, 0x11D3F, 0x11D45, 0x11D47, 0x11D47, 0x11D8A, 0x11D8E, + 0x11D90, 0x11D91, 0x11D93, 0x11D97, 0x11EF3, 0x11EF6, 0x11F00, 0x11F01, + 0x11F03, 0x11F03, 0x11F34, 0x11F3A, 0x11F3E, 0x11F42, 0x11F5A, 0x11F5A, + 0x13440, 0x13440, 0x13447, 0x13455, 0x1611E, 0x1612F, 0x16AF0, 0x16AF4, + 0x16B30, 0x16B36, 0x16F4F, 0x16F4F, 0x16F51, 0x16F87, 0x16F8F, 0x16F92, + 0x16FE4, 0x16FE4, 0x16FF0, 0x16FF1, 0x1BC9D, 0x1BC9E, 0x1CF00, 0x1CF2D, + 0x1CF30, 0x1CF46, 0x1D165, 0x1D169, 0x1D16D, 0x1D172, 0x1D17B, 0x1D182, + 0x1D185, 0x1D18B, 0x1D1AA, 0x1D1AD, 0x1D242, 0x1D244, 0x1DA00, 0x1DA36, + 0x1DA3B, 0x1DA6C, 0x1DA75, 0x1DA75, 0x1DA84, 0x1DA84, 0x1DA9B, 0x1DA9F, + 0x1DAA1, 0x1DAAF, 0x1E000, 0x1E006, 0x1E008, 0x1E018, 0x1E01B, 0x1E021, + 0x1E023, 0x1E024, 0x1E026, 0x1E02A, 0x1E08F, 0x1E08F, 0x1E130, 0x1E136, + 0x1E2AE, 0x1E2AE, 0x1E2EC, 0x1E2EF, 0x1E4EC, 0x1E4EF, 0x1E5EE, 0x1E5EF, + 0x1E8D0, 0x1E8D6, 0x1E944, 0x1E94A, 0xE0100, 0xE01EF, + }; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FlutterError.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FlutterError.java new file mode 100644 index 00000000000..94bda940803 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FlutterError.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +/** + * The error type the Flutter framework (and app assertions) throw — Flutter's + * {@code FlutterError}. Modelled as a {@link RuntimeException} so transpiled + * {@code throw FlutterError(...)} statements compile and propagate like any Dart + * throw. {@link #reportError(Object)} routes a caught error to the current + * handler; this pass logs it. + */ +public class FlutterError extends RuntimeException { + + public FlutterError(String message) { + super(message); + } + + /** Dart's {@code FlutterError.reportError(details)}. */ + public static void reportError(Object details) { + if (details != null) { + System.err.println("FlutterError.reportError: " + details); + } + } + + @Override + public String toString() { + return "FlutterError: " + getMessage(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationConstants.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationConstants.java new file mode 100644 index 00000000000..906fef5bef4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationConstants.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +/** + * Top-level {@code const} values of Flutter's + * {@code package:flutter/foundation.dart} that new_gallery references directly, + * mirrored as Java statics. + */ +public final class FoundationConstants { + + private FoundationConstants() { + } + + /** + * Flutter's {@code kIsWeb}: true only in a web (dart2js/dartdevc) build. + * Codename One never targets the Flutter web backend, so this is always + * {@code false}; the app uses it to gate web-only code paths. + */ + public static final boolean kIsWeb = false; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationLib.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationLib.java new file mode 100644 index 00000000000..899b4abcc83 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/FoundationLib.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import com.codename1.flutter.TargetPlatform; + +/** + * Top-level members of Flutter's {@code package:flutter/foundation.dart} that + * the app references directly, mirrored as Java statics. + * + *

{@code defaultTargetPlatform} reports the platform the app is running on. + * It is resolved once from the Codename One runtime and cached.

+ */ +public final class FoundationLib { + + private FoundationLib() { + } + + /** Flutter's {@code defaultTargetPlatform}. */ + public static final TargetPlatform defaultTargetPlatform = detect(); + + /** + * Overrides the detected platform, for a simulator standing in for a device. + * + *

{@code defaultTargetPlatform} decides a great deal of what the gallery draws -- + * the back chevron against the arrow, page transitions, switches, scrollbars -- so it + * has to describe the device being SIMULATED, not the machine simulating it. The + * desktop simulator reports "SE", which falls through to android, while the skin it + * is wearing and the reference it is measured against are both an iPhone. Every one + * of those adaptive widgets then disagreed, and none of the disagreements were + * defects.

+ */ + public static final String PLATFORM_PROPERTY = "cn1.flutter.targetPlatform"; + + private static TargetPlatform detect() { + String forced = null; + try { + forced = System.getProperty(PLATFORM_PROPERTY); + } catch (Throwable t) { + // no system properties on this platform + } + if (forced == null || forced.length() == 0) { + try { + forced = com.codename1.ui.Display.getInstance() + .getProperty(PLATFORM_PROPERTY, null); + } catch (Throwable t) { + // no Display yet + } + } + if (forced != null) { + TargetPlatform named = byName(forced); + if (named != null) { + return named; + } + } + try { + String p = com.codename1.ui.Display.getInstance().getPlatformName(); + if (p != null) { + p = p.toLowerCase(); + if (p.startsWith("ios")) { + return TargetPlatform.iOS; + } + if (p.startsWith("and")) { + return TargetPlatform.android; + } + if (p.startsWith("mac")) { + return TargetPlatform.macOS; + } + if (p.startsWith("win")) { + return TargetPlatform.windows; + } + } + } catch (Throwable t) { + // fall through to a sensible default when no runtime is available + } + return TargetPlatform.android; + } + + /// Flutter's own spelling of each platform, or null when the name is not one. + private static TargetPlatform byName(String name) { + String n = name.trim(); + if ("ios".equalsIgnoreCase(n) || "iOS".equals(n)) { + return TargetPlatform.iOS; + } + if ("android".equalsIgnoreCase(n)) { + return TargetPlatform.android; + } + if ("macos".equalsIgnoreCase(n) || "macOS".equals(n)) { + return TargetPlatform.macOS; + } + if ("windows".equalsIgnoreCase(n)) { + return TargetPlatform.windows; + } + if ("linux".equalsIgnoreCase(n)) { + return TargetPlatform.linux; + } + if ("fuchsia".equalsIgnoreCase(n)) { + return TargetPlatform.fuchsia; + } + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listenable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listenable.java new file mode 100644 index 00000000000..5e877655a0c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listenable.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.runtime.Funcs; + +/** + * The root of Flutter's observable protocol ({@code Listenable}): an object + * that maintains a list of listeners and notifies them when it changes. + * Implemented by {@link ChangeNotifier} and by the animation {@code Animation} + * types. {@code AnimatedWidget} is driven by one. + */ +public interface Listenable { + + void addListener(Funcs.VoidFunc0 listener); + + void removeListener(Funcs.VoidFunc0 listener); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listeners.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listeners.java new file mode 100644 index 00000000000..ab90bfdf526 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/Listeners.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Listener dispatch shared by every notifier in the runtime -- Animation's + * value and status listeners, ChangeNotifier, ValueNotifier and the + * controllers. + * + *

Each call is isolated, as Flutter's own notifyListeners does: a listener + * that throws is reported and the rest still run. Dispatching in a bare loop + * let one faulty listener starve every listener after it; on an animation it + * was worse, because the throw reached the frame clock, which drops the whole + * controller -- a failing analytics listener froze the visual animation.

+ * + *

The list is copied first, so a listener may add or remove listeners + * during dispatch -- one added is not called until the next notification, as in + * Flutter. One REMOVED is not called either: each is checked against the live + * collection before it runs. Calling the copy blindly ran a listener after an + * earlier one had disposed its owner, and its state or controller code ran on + * a disposed object.

+ */ +public final class Listeners { + + private Listeners() { + } + + /** Calls every listener in {@code listeners}, reporting any that throws. */ + public static void notify(Collection listeners) { + for (Funcs.VoidFunc0 l : new ArrayList(listeners)) { + if (!listeners.contains(l)) { + continue; + } + try { + l.call(); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + } + + /** Calls every listener in {@code listeners} with {@code value}, reporting any that throws. */ + public static void notify(Collection> listeners, T value) { + for (Funcs.VoidFunc1 l : new ArrayList>(listeners)) { + if (!listeners.contains(l)) { + continue; + } + try { + l.call(value); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/NotifierListeners.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/NotifierListeners.java new file mode 100644 index 00000000000..93047814898 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/NotifierListeners.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.runtime.Funcs; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * The listener lists of ChangeNotifier mixins, which cannot hold them in a field (a + * stub mixin becomes a Java interface). + * + *

This was a static IdentityHashMap that owned every notifier ever touched: reading + * the list created an entry, emptying it kept the entry, and the strong key kept an + * otherwise-unreachable model -- and through leftover listener closures, its widget + * tree -- alive for good. Flutter keeps the list in the notifier, so it goes when the + * notifier does. Here an entry exists only while it holds listeners, and it is keyed by + * a weak identity reference, purged once its notifier has been collected.

+ */ +public final class NotifierListeners { + + /** A notifier by identity, held weakly. */ + private static final class Key { + final WeakReference ref; + final int hash; + + Key(Object o) { + ref = new WeakReference(o); + hash = System.identityHashCode(o); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Key)) { + return false; + } + Object mine = ref.get(); + return mine != null && mine == ((Key) other).ref.get(); + } + + @Override + public int hashCode() { + return hash; + } + } + + private final Map> lists = new HashMap>(); + private int addsSincePurge; + + /** The list for {@code notifier}, or null when it has no listeners. */ + List get(Object notifier) { + return lists.get(new Key(notifier)); + } + + /** The list for {@code notifier}, created if needed, to add to. */ + List forAdding(Object notifier) { + if (++addsSincePurge >= 64) { + addsSincePurge = 0; + purge(); + } + Key k = new Key(notifier); + List l = lists.get(k); + if (l == null) { + l = new ArrayList(); + lists.put(k, l); + } + return l; + } + + /** Removes one listener, and the entry with its last one. */ + void remove(Object notifier, Funcs.VoidFunc0 listener) { + Key k = new Key(notifier); + List l = lists.get(k); + if (l != null) { + l.remove(listener); + if (l.isEmpty()) { + lists.remove(k); + } + } + } + + void clear(Object notifier) { + // Emptied as well as dropped: a notification already running holds this list + // and checks each listener against it before calling, so a notifier disposed + // by one of its own listeners must stop there. + List l = lists.remove(new Key(notifier)); + if (l != null) { + l.clear(); + } + } + + /** How many notifiers currently have an entry; for tests. */ + int size() { + return lists.size(); + } + + /** Drops the entries whose notifier has been collected. */ + void purge() { + Iterator it = lists.keySet().iterator(); + while (it.hasNext()) { + if (it.next().ref.get() == null) { + it.remove(); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/SynchronousFuture.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/SynchronousFuture.java new file mode 100644 index 00000000000..c4ddd810e68 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/SynchronousFuture.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.async.Future; + +/** + * A {@link Future} that is already complete and invokes its listeners + * synchronously — Flutter foundation's {@code SynchronousFuture}. Localization + * delegates return one from {@code load} so the app can obtain its strings + * without an asynchronous frame. Because the value is available at construction, + * {@code then}/{@code whenComplete}/{@code catchError} (inherited from + * {@link Future}) run immediately. + */ +public class SynchronousFuture extends Future { + + public SynchronousFuture(T value) { + super(value); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueListenable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueListenable.java new file mode 100644 index 00000000000..6bc0e4b96bc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueListenable.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.runtime.Funcs; + +/** + * An object exposing a value that changes over time and can be listened to + * ({@code ValueListenable} in Flutter). {@code ValueListenableBuilder} + * rebuilds whenever the value changes. Implemented by {@link ValueNotifier}. + * + *

It IS a {@link Listenable}, as in Flutter, so a notifier can drive anything that takes + * one — {@code AnimatedWidget(listenable: notifier)}, {@code AnimatedBuilder(animation: + * notifier)}. Leaving the two hierarchies unrelated made those a compile error in transpiled + * code for no reason the Dart could explain.

+ * + * @param the value type + */ +public abstract class ValueListenable implements Listenable { + + public abstract T value(); + + public abstract void addListener(Funcs.VoidFunc0 listener); + + public abstract void removeListener(Funcs.VoidFunc0 listener); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueNotifier.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueNotifier.java new file mode 100644 index 00000000000..70d6c4395a3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/foundation/ValueNotifier.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@code ChangeNotifier} that holds a single value ({@code ValueNotifier} + * in Flutter); assigning {@link #value(Object)} notifies listeners when the value + * actually changes. new_gallery drives {@code ValueListenableBuilder} from + * these (the settings sheet's open flag, the extended nav-rail flag). + * + *

Transpiler surface: the Dart {@code value} getter maps to {@link #value()}, + * {@code notifier.value = v} to {@link #value(Object)}.

+ * + * @param the value type + */ +public class ValueNotifier extends ValueListenable { + + private T current; + private final List listeners = new ArrayList(); + + public ValueNotifier(T value) { + this.current = value; + } + + @Override + public T value() { + return current; + } + + public void value(T newValue) { + // Dart's ==, not Java equals: 1 and 1.0 are the same value and NaN is never + // equal to itself, so a num notifier notified for 1 -> 1.0 and stayed quiet + // for NaN -> NaN. + boolean changed = !dart.runtime.DartRuntime.eq(current, newValue); + if (changed) { + current = newValue; + notifyListeners(); + } + } + + @Override + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public void removeListener(Funcs.VoidFunc0 listener) { + listeners.remove(listener); + } + + public void notifyListeners() { + Listeners.notify(listeners); + } + + public void dispose() { + listeners.clear(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragEndDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragEndDetails.java new file mode 100644 index 00000000000..9567a2a9da9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragEndDetails.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * The details at the end of a drag, carrying the fling velocity — Flutter's + * {@code DragEndDetails}. The home splash and reply drawer read + * {@code velocity.pixelsPerSecond}. + */ +public final class DragEndDetails { + + private Velocity velocity = Velocity.zero; + private Double primaryVelocity; + + public DragEndDetails() { + } + + public DragEndDetails(Velocity velocity, Double primaryVelocity) { + this.velocity = velocity == null ? Velocity.zero : velocity; + this.primaryVelocity = primaryVelocity; + } + + public Velocity velocity() { + return velocity; + } + + public void velocity(Velocity v) { + this.velocity = v; + } + + public Double primaryVelocity() { + return primaryVelocity; + } + + public void primaryVelocity(Double v) { + this.primaryVelocity = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragStartDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragStartDetails.java new file mode 100644 index 00000000000..696deb2a998 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragStartDetails.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The details at the start of a drag — Flutter's {@code DragStartDetails}. + */ +public final class DragStartDetails { + + private Offset globalPosition = Offset.zero; + private Offset localPosition = Offset.zero; + + public DragStartDetails() { + } + + public DragStartDetails(Offset globalPosition, Offset localPosition) { + this.globalPosition = globalPosition == null ? Offset.zero : globalPosition; + this.localPosition = localPosition == null ? Offset.zero : localPosition; + } + + public Offset globalPosition() { + return globalPosition; + } + + public void globalPosition(Offset v) { + this.globalPosition = v; + } + + public Offset localPosition() { + return localPosition; + } + + public void localPosition(Offset v) { + this.localPosition = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragUpdateDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragUpdateDetails.java new file mode 100644 index 00000000000..df8425a5d7e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/DragUpdateDetails.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The incremental details of a drag — Flutter's {@code DragUpdateDetails}. The + * reply bottom-drawer reads {@link #primaryDelta()} to drive its + * AnimationController. + */ +public final class DragUpdateDetails { + + private Offset globalPosition = Offset.zero; + private Offset localPosition = Offset.zero; + private Offset delta = Offset.zero; + private Double primaryDelta; + + public DragUpdateDetails() { + } + + public DragUpdateDetails(Offset globalPosition, Offset localPosition, Offset delta, Double primaryDelta) { + this.globalPosition = globalPosition == null ? Offset.zero : globalPosition; + this.localPosition = localPosition == null ? Offset.zero : localPosition; + this.delta = delta == null ? Offset.zero : delta; + this.primaryDelta = primaryDelta; + } + + public Offset delta() { + return delta; + } + + public void delta(Offset v) { + this.delta = v; + } + + public Double primaryDelta() { + return primaryDelta; + } + + public void primaryDelta(Double v) { + this.primaryDelta = v; + } + + public Offset globalPosition() { + return globalPosition; + } + + public void globalPosition(Offset v) { + this.globalPosition = v; + } + + public Offset localPosition() { + return localPosition; + } + + public void localPosition(Offset v) { + this.localPosition = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureBinding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureBinding.java new file mode 100644 index 00000000000..dbb50908b5b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureBinding.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.rendering.Dp; +import com.codename1.ui.Display; +import com.codename1.ui.Form; + +/** + * Flutter's {@code GestureBinding}, as far as a synthesised touch needs it. + * + *

This exists so ONE Dart script can walk both stacks. A walkthrough written + * against {@code GestureBinding.instance.handlePointerEvent} runs unchanged on + * the native app, where it is Flutter's own binding, and on the transpiled one, + * where it arrives here and becomes a Codename One pointer event. The tap then + * goes through the ordinary hit test and drives the ordinary animations, which + * is the whole reason not to drive the app by poking at its state instead.

+ * + *

Coordinates are LOGICAL pixels, as Flutter's are; Codename One works in + * device pixels, so they are converted here rather than in the script.

+ */ +public class GestureBinding { + + /// Flutter's {@code GestureBinding.instance}. A FIELD, not a method: a stub's + /// static getter is rendered as a static field, which is how every other one + /// in this runtime is written. + public static final GestureBinding instance = new GestureBinding(); + + private GestureBinding() { + } + + /** + * Routes one synthesised pointer event into the framework. + * + *

Delivered straight to the Form, not queued: the script that sends it is + * already running on the event thread, and a queued event would arrive after + * the script had moved on to the next step.

+ */ + public void handlePointerEvent(PointerEvent event) { + if (event == null || !Display.isInitialized()) { + return; + } + Form f = Display.getInstance().getCurrent(); + if (f == null) { + return; + } + int x = (int) Math.round(Dp.px(event.position().dx())); + int y = (int) Math.round(Dp.px(event.position().dy())); + if (event instanceof PointerDownEvent) { + f.pointerPressed(x, y); + } else if (event instanceof PointerMoveEvent) { + f.pointerDragged(x, y); + } else if (event instanceof PointerUpEvent) { + f.pointerReleased(x, y); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragEndCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragEndCallback.java new file mode 100644 index 00000000000..13c278d2660 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragEndCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a drag-end callback — Flutter's {@code GestureDragEndCallback} + * ({@code void Function(DragEndDetails)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + */ +public interface GestureDragEndCallback { + void call(DragEndDetails details); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragStartCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragStartCallback.java new file mode 100644 index 00000000000..a83c54d2adf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragStartCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a drag-start callback — Flutter's {@code GestureDragStartCallback} + * ({@code void Function(DragStartDetails)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + */ +public interface GestureDragStartCallback { + void call(DragStartDetails details); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragUpdateCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragUpdateCallback.java new file mode 100644 index 00000000000..2e0ecef1557 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureDragUpdateCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a drag-update callback — Flutter's {@code GestureDragUpdateCallback} + * ({@code void Function(DragUpdateDetails)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + */ +public interface GestureDragUpdateCallback { + void call(DragUpdateDetails details); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapCallback.java new file mode 100644 index 00000000000..a078478ef75 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a simple tap callback — Flutter's {@code GestureTapCallback} + * ({@code void Function()}). A single-abstract-method interface so transpiled + * Dart closures and method references bind as Java lambdas. + */ +public interface GestureTapCallback { + void call(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapDownCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapDownCallback.java new file mode 100644 index 00000000000..011221dc844 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapDownCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a tap-down callback — Flutter's {@code GestureTapDownCallback} + * ({@code void Function(TapDownDetails)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + */ +public interface GestureTapDownCallback { + void call(TapDownDetails details); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapUpCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapUpCallback.java new file mode 100644 index 00000000000..21066295f29 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/GestureTapUpCallback.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Signature for a tap-up callback — Flutter's {@code GestureTapUpCallback} + * ({@code void Function(TapUpDetails)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + */ +public interface GestureTapUpCallback { + void call(TapUpDetails details); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/LongPressStartDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/LongPressStartDetails.java new file mode 100644 index 00000000000..ef414d1f944 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/LongPressStartDetails.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The details at the start of a long-press — Flutter's + * {@code LongPressStartDetails}. + */ +public final class LongPressStartDetails { + + private Offset globalPosition = Offset.zero; + private Offset localPosition = Offset.zero; + + public LongPressStartDetails() { + } + + public LongPressStartDetails(Offset globalPosition, Offset localPosition) { + this.globalPosition = globalPosition == null ? Offset.zero : globalPosition; + this.localPosition = localPosition == null ? Offset.zero : localPosition; + } + + public Offset globalPosition() { + return globalPosition; + } + + public void globalPosition(Offset v) { + this.globalPosition = v; + } + + public Offset localPosition() { + return localPosition; + } + + public void localPosition(Offset v) { + this.localPosition = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerDownEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerDownEvent.java new file mode 100644 index 00000000000..5d16d01ec86 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerDownEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** Flutter's {@code PointerDownEvent}. */ +public class PointerDownEvent extends PointerEvent { + + public PointerDownEvent() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerEvent.java new file mode 100644 index 00000000000..dac46e6e385 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * A pointer event, as Flutter's {@code PointerEvent}. + * + *

Only what a synthesised touch needs: where it happened and which pointer + * it belongs to. The point of having these at all is that ONE Dart script can + * drive both stacks -- it hands events to {@link GestureBinding} and the + * framework underneath routes them, so a tap written once produces a real tap, + * with the real hit test and the real animations, on whichever runtime is + * executing it.

+ */ +public abstract class PointerEvent { + + private Offset position = new Offset(0, 0); + private long pointer; + + protected PointerEvent() { + } + + /** Named parameter setter for the Dart {@code position:} parameter. */ + public void position(Offset v) { + this.position = v == null ? new Offset(0, 0) : v; + } + + /** Named parameter setter for the Dart {@code pointer:} parameter. */ + public void pointer(long v) { + this.pointer = v; + } + + public Offset position() { + return position; + } + + public long pointerId() { + return pointer; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerMoveEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerMoveEvent.java new file mode 100644 index 00000000000..b84cf1c5846 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerMoveEvent.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * Flutter's {@code PointerMoveEvent}. + * + *

Carries {@code delta} as well as {@code position} because Flutter's drag + * recognizers accumulate the DELTA, not the absolute position, to decide + * whether the touch has travelled past the slop. A synthesised move with the + * default zero delta therefore scrolls nothing in Flutter while scrolling + * normally here, where the binding works from the absolute position -- which is + * exactly how one scripted drag came to scroll one of the two stacks and not + * the other.

+ */ +public class PointerMoveEvent extends PointerEvent { + + private com.codename1.flutter.Offset delta; + + public PointerMoveEvent() { + } + + /** How far the pointer moved since the previous event. */ + public void delta(com.codename1.flutter.Offset v) { + this.delta = v; + } + + /** How far the pointer moved since the previous event; null when unstated. */ + public com.codename1.flutter.Offset getDelta() { + return delta; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerUpEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerUpEvent.java new file mode 100644 index 00000000000..7b6e6450f5e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/PointerUpEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** Flutter's {@code PointerUpEvent}. */ +public class PointerUpEvent extends PointerEvent { + + public PointerUpEvent() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleEndDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleEndDetails.java new file mode 100644 index 00000000000..6b2d806500a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleEndDetails.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +/** + * The details at the end of a scale/pan gesture, carrying the fling velocity — + * Flutter's {@code ScaleEndDetails}. + */ +public final class ScaleEndDetails { + + private Velocity velocity = Velocity.zero; + private long pointerCount; + + public ScaleEndDetails() { + } + + public ScaleEndDetails(Velocity velocity, long pointerCount) { + this.velocity = velocity == null ? Velocity.zero : velocity; + this.pointerCount = pointerCount; + } + + public Velocity velocity() { + return velocity; + } + + public void velocity(Velocity v) { + this.velocity = v; + } + + public long pointerCount() { + return pointerCount; + } + + public void pointerCount(long v) { + this.pointerCount = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleStartDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleStartDetails.java new file mode 100644 index 00000000000..60a17590267 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleStartDetails.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The details at the start of a scale/pan gesture — Flutter's + * {@code ScaleStartDetails}. + */ +public final class ScaleStartDetails { + + private Offset focalPoint = Offset.zero; + private Offset localFocalPoint = Offset.zero; + private long pointerCount; + + public ScaleStartDetails() { + } + + public ScaleStartDetails(Offset focalPoint, Offset localFocalPoint, long pointerCount) { + this.focalPoint = focalPoint == null ? Offset.zero : focalPoint; + this.localFocalPoint = localFocalPoint == null ? Offset.zero : localFocalPoint; + this.pointerCount = pointerCount; + } + + public Offset focalPoint() { + return focalPoint; + } + + public void focalPoint(Offset v) { + this.focalPoint = v; + } + + public Offset localFocalPoint() { + return localFocalPoint; + } + + public void localFocalPoint(Offset v) { + this.localFocalPoint = v; + } + + public Offset globalPosition() { + return focalPoint; + } + + public Offset localPosition() { + return localFocalPoint; + } + + public long pointerCount() { + return pointerCount; + } + + public void pointerCount(long v) { + this.pointerCount = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleUpdateDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleUpdateDetails.java new file mode 100644 index 00000000000..fca320aeb82 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/ScaleUpdateDetails.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The incremental details of a scale/pan gesture — Flutter's + * {@code ScaleUpdateDetails}. + */ +public final class ScaleUpdateDetails { + + private Offset focalPoint = Offset.zero; + private Offset localFocalPoint = Offset.zero; + private double scale = 1.0; + private double horizontalScale = 1.0; + private double verticalScale = 1.0; + private double rotation; + private Offset focalPointDelta = Offset.zero; + + public ScaleUpdateDetails() { + } + + public ScaleUpdateDetails(Offset focalPoint, Offset localFocalPoint, double scale, double rotation) { + this.focalPoint = focalPoint == null ? Offset.zero : focalPoint; + this.localFocalPoint = localFocalPoint == null ? Offset.zero : localFocalPoint; + this.scale = scale; + this.rotation = rotation; + } + + public Offset focalPoint() { + return focalPoint; + } + + public void focalPoint(Offset v) { + this.focalPoint = v; + } + + public Offset localFocalPoint() { + return localFocalPoint; + } + + public void localFocalPoint(Offset v) { + this.localFocalPoint = v; + } + + public Offset globalPosition() { + return focalPoint; + } + + public Offset localPosition() { + return localFocalPoint; + } + + public Offset focalPointDelta() { + return focalPointDelta; + } + + public void focalPointDelta(Offset v) { + this.focalPointDelta = v; + } + + public double scale() { + return scale; + } + + public void scale(double v) { + this.scale = v; + } + + public double horizontalScale() { + return horizontalScale; + } + + public void horizontalScale(double v) { + this.horizontalScale = v; + } + + public double verticalScale() { + return verticalScale; + } + + public void verticalScale(double v) { + this.verticalScale = v; + } + + public double rotation() { + return rotation; + } + + public void rotation(double v) { + this.rotation = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapDownDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapDownDetails.java new file mode 100644 index 00000000000..4c43fb83a94 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapDownDetails.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The details of a tap-down event — Flutter's {@code TapDownDetails}. + */ +public final class TapDownDetails { + + private Offset globalPosition = Offset.zero; + private Offset localPosition = Offset.zero; + private Object kind; + + public TapDownDetails() { + } + + public TapDownDetails(Offset globalPosition, Offset localPosition, Object kind) { + this.globalPosition = globalPosition == null ? Offset.zero : globalPosition; + this.localPosition = localPosition == null ? Offset.zero : localPosition; + this.kind = kind; + } + + public Offset globalPosition() { + return globalPosition; + } + + public void globalPosition(Offset v) { + this.globalPosition = v; + } + + public Offset localPosition() { + return localPosition; + } + + public void localPosition(Offset v) { + this.localPosition = v; + } + + public Object kind() { + return kind; + } + + public void kind(Object v) { + this.kind = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapGestureRecognizer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapGestureRecognizer.java new file mode 100644 index 00000000000..e4c54fbe905 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapGestureRecognizer.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import dart.runtime.Funcs; + +/** + * Recognizes single taps — Flutter's {@code TapGestureRecognizer}. The about + * page wires one to each link span, so only {@code onTap} and {@code dispose} + * are consumed. + */ +public final class TapGestureRecognizer { + + private Funcs.VoidFunc0 onTap; + private Object debugOwner; + + public TapGestureRecognizer() { + } + + public TapGestureRecognizer(Object debugOwner) { + this.debugOwner = debugOwner; + } + + /** Dart's {@code set onTap(VoidCallback? handler)}. */ + public void onTap(Funcs.VoidFunc0 handler) { + this.onTap = handler; + } + + /** The registered tap handler, if any. */ + public Funcs.VoidFunc0 onTap() { + return onTap; + } + + /** Invoke the registered handler (used when the host wires a real tap). */ + public void handleTap() { + if (onTap != null) { + onTap.call(); + } + } + + public void dispose() { + this.onTap = null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapUpDetails.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapUpDetails.java new file mode 100644 index 00000000000..84e63257356 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/TapUpDetails.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * The details of a tap-up event — Flutter's {@code TapUpDetails}. The + * transformations demo reads {@link #globalPosition()} to hit-test the board. + */ +public final class TapUpDetails { + + private Offset globalPosition = Offset.zero; + private Offset localPosition = Offset.zero; + private Object kind; + + public TapUpDetails() { + } + + public TapUpDetails(Offset globalPosition, Offset localPosition, Object kind) { + this.globalPosition = globalPosition == null ? Offset.zero : globalPosition; + this.localPosition = localPosition == null ? Offset.zero : localPosition; + this.kind = kind; + } + + public Offset globalPosition() { + return globalPosition; + } + + public void globalPosition(Offset v) { + this.globalPosition = v; + } + + public Offset localPosition() { + return localPosition; + } + + public void localPosition(Offset v) { + this.localPosition = v; + } + + public Object kind() { + return kind; + } + + public void kind(Object v) { + this.kind = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/Velocity.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/Velocity.java new file mode 100644 index 00000000000..03cff636e7f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/gestures/Velocity.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.gestures; + +import com.codename1.flutter.Offset; + +/** + * A 2-D velocity in logical pixels per second — Flutter's {@code Velocity}. + * Carried by {@link DragEndDetails} so fling handlers can read + * {@code velocity.pixelsPerSecond}. + */ +public final class Velocity { + + /** {@code Velocity.zero} — no motion. */ + public static final Velocity zero = new Velocity(Offset.zero); + + private final Offset pixelsPerSecond; + + public Velocity() { + this(Offset.zero); + } + + public Velocity(Offset pixelsPerSecond) { + this.pixelsPerSecond = pixelsPerSecond == null ? Offset.zero : pixelsPerSecond; + } + + /** Dart's static {@code Velocity.zero} getter. */ + public static Velocity zero() { + return zero; + } + + public Offset pixelsPerSecond() { + return pixelsPerSecond; + } + + /** + * Dart's {@code Velocity.clampMagnitude(min, max)} — returns a velocity with + * the same direction but magnitude clamped to {@code [minValue, maxValue]}. + */ + public Velocity clampMagnitude(double minValue, double maxValue) { + double valueSquared = pixelsPerSecond.distanceSquared(); + if (valueSquared > maxValue * maxValue) { + return new Velocity(pixelsPerSecond.$div(pixelsPerSecond.distance()).$times(maxValue)); + } + if (valueSquared < minValue * minValue) { + return new Velocity(pixelsPerSecond.$div(pixelsPerSecond.distance()).$times(minValue)); + } + return this; + } + + @Override + public String toString() { + return "Velocity(" + pixelsPerSecond + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/DateFormat.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/DateFormat.java new file mode 100644 index 00000000000..e7a925ca286 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/DateFormat.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.intl; + +import com.codename1.l10n.SimpleDateFormat; +import dart.core.DateTime; + +/** + * A subset of {@code package:intl}'s DateFormat backed by CN1's + * {@link SimpleDateFormat}. The named "skeleton" constructors map to concrete + * patterns; {@code add_jm}/{@code add_jms} append a time component. + */ +public final class DateFormat { + + /** + * Skeleton "field" constants from {@code package:intl}'s DateFormat, used + * bare as a pattern, e.g. {@code DateFormat(DateFormat.WEEKDAY, locale)}. + * Their values are ICU/{@link SimpleDateFormat}-compatible pattern strings. + */ + public static final String WEEKDAY = "EEEE"; + public static final String MMM = "MMM"; + + private String pattern; + + public DateFormat(String pattern, String locale) { + this.pattern = pattern == null ? "M/d/yyyy" : pattern; + } + + private static DateFormat of(String pattern) { + return new DateFormat(pattern, null); + } + + public static DateFormat MMMd(String locale) { + return of("MMM d"); + } + + public static DateFormat jm(String locale) { + return of("h:mm a"); + } + + public static DateFormat Hm(String locale) { + return of("HH:mm"); + } + + public static DateFormat yMMM(String locale) { + return of("MMM yyyy"); + } + + public static DateFormat yMMMMd(String locale) { + return of("MMMM d, yyyy"); + } + + public static DateFormat yMMMd(String locale) { + return of("MMM d, yyyy"); + } + + public static DateFormat yMd(String locale) { + return of("M/d/yyyy"); + } + + public DateFormat add_jm() { + pattern = pattern + " h:mm a"; + return this; + } + + public DateFormat add_jms() { + pattern = pattern + " h:mm:ss a"; + return this; + } + + public String format(DateTime date) { + if (date == null) { + return ""; + } + try { + SimpleDateFormat f = new SimpleDateFormat(pattern); + if (date.isUtc()) { + // The instant itself, formatted in UTC. Moving its fields onto a LOCAL + // value first broke inside the device's daylight-saving gap, where that + // local time does not exist: 02:30 UTC printed as 03:30 in New York. + f.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + } + return f.format(date.toJavaDate()); + } catch (Throwable t) { + return date.toJavaDate().toString(); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/Intl.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/Intl.java new file mode 100644 index 00000000000..11851177654 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/Intl.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.intl; + +/** + * The {@code Intl} class from {@code package:intl}. In the gallery it is always + * reached as {@code intl.Intl.xxx(...)}; the transpiler strips the {@code intl} + * import prefix and resolves {@code Intl} to this type, invoking every member as + * a static method. All members are therefore static. + */ +public final class Intl { + + /** + * The default locale, mirroring {@code Intl.defaultLocale}. {@code null} + * means "use the runtime/system locale". + */ + public static String defaultLocale = null; + + // Package-private: the app never instantiates Intl (all members are static), + // but IntlLib retains a shared instance for the legacy `intl.` prefix path. + Intl() { + } + + /** + * Normalizes a locale identifier. A minimal implementation that maps the + * common {@code _} separator to {@code -} and returns the value unchanged + * otherwise. + */ + public static String canonicalizedLocale(String aLocale) { + if (aLocale == null || aLocale.length() == 0) { + return "und"; + } + return aLocale.replace('_', '-'); + } + + /** + * English-style plural selection. Returns the branch matching + * {@code howMany} (one for 1, zero for 0 when supplied), else {@code other}. + * Extra branches accepted for API shape. + */ + public static String pluralLogic(Object howManyValue, String locale, String zero, String one, + String two, String few, String many, String other) { + double howMany = howManyValue instanceof Number ? ((Number) howManyValue).doubleValue() : 0; + if (howMany == 0 && zero != null) { + return zero; + } + if (howMany == 1 && one != null) { + return one; + } + if (howMany == 2 && two != null) { + return two; + } + if (other != null) { + return other; + } + return one != null ? one : ""; + } + + /** + * Returns a translated message. Without a message catalog this simply + * returns the source {@code messageText}, which is the correct behaviour for + * the base (English) locale. + */ + public static String message(String messageText, String desc, String locale, String name, + Object args, String meaning) { + return messageText == null ? "" : messageText; + } + + /** + * Plural message selection, delegating to {@link #pluralLogic}. Extra + * message-metadata parameters ({@code name} / {@code args}) are accepted for + * API shape. + */ + public static String plural(Object howMany, String locale, String zero, String one, String two, + String few, String many, String other, String name, Object args) { + return pluralLogic(howMany, locale, zero, one, two, few, many, other); + } + + /** + * Selects a branch from {@code cases} by string key, falling back to the + * {@code "other"} entry. {@code cases} is expected to be a + * {@code Map}. + */ + @SuppressWarnings("unchecked") + public static String select(Object choice, Object cases, String locale, String name, Object args) { + if (cases instanceof java.util.Map) { + java.util.Map m = (java.util.Map) cases; + Object v = m.get(choice); + if (v == null && choice != null) { + v = m.get(String.valueOf(choice)); + } + if (v == null) { + v = m.get("other"); + } + return v == null ? "" : String.valueOf(v); + } + return ""; + } + + /** + * Gender-based message selection ({@code female} / {@code male} / other). + */ + public static String gender(String targetGender, String female, String male, String other, + String locale, String name, Object args) { + if ("female".equals(targetGender) && female != null) { + return female; + } + if ("male".equals(targetGender) && male != null) { + return male; + } + return other != null ? other : ""; + } + + /** + * Runs {@code function} with {@code defaultLocale} temporarily set to + * {@code locale}. {@code function} is expected to be a zero-arg callable + * ({@code T Function()}); its result is returned. + */ + public static Object withLocale(String locale, Object function) { + String prev = defaultLocale; + defaultLocale = locale; + try { + if (function instanceof dart.runtime.Funcs.Func0) { + return ((dart.runtime.Funcs.Func0) function).call(); + } + if (function instanceof dart.runtime.Funcs.VoidFunc0) { + ((dart.runtime.Funcs.VoidFunc0) function).call(); + } + return null; + } finally { + defaultLocale = prev; + } + } + + /** The current locale, i.e. {@link #defaultLocale} or the system default. */ + public static String getCurrentLocale() { + if (defaultLocale != null) { + return defaultLocale; + } + return "en_US"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/IntlLib.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/IntlLib.java new file mode 100644 index 00000000000..145740d7ab8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/IntlLib.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.intl; + +/** + * Backing class for the {@code intl} import prefix. {@code intl.Intl} in Dart + * resolves through the transpiler to the static field {@link #Intl} here. + */ +public abstract class IntlLib { + + /** The shared {@code Intl} instance reached via {@code intl.Intl}. */ + public static final Intl Intl = new Intl(); + + private IntlLib() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/NumberFormat.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/NumberFormat.java new file mode 100644 index 00000000000..8a34ce238bf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/intl/NumberFormat.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.intl; + +/** + * A subset of {@code package:intl}'s NumberFormat covering the currency and + * percent factory constructors used by the gallery. Formatting is a simple + * fixed-decimal render (no locale grouping); faithful locale output is a + * later pass. + */ +public final class NumberFormat { + + private final String prefix; + private final String suffix; + private final int digits; + private final boolean percent; + + private NumberFormat(String prefix, String suffix, int digits, boolean percent) { + this.prefix = prefix; + this.suffix = suffix; + this.digits = digits; + this.percent = percent; + } + + // decimalDigits is boxed so that an omitted argument (null) and an explicit 0 + // stay distinct: as a primitive both arrived as 0, and 0 was read as "not + // given", so a whole-unit currency printed $12.00 for 12. + + public static NumberFormat currency(String locale, String symbol, Long decimalDigits, String name) { + return new NumberFormat(symbol != null ? symbol : "$", "", + decimalDigits != null ? (int) decimalDigits.longValue() : 2, false); + } + + public static NumberFormat simpleCurrency(String locale, String name, Long decimalDigits) { + return new NumberFormat("$", "", decimalDigits != null ? (int) decimalDigits.longValue() : 2, false); + } + + public static NumberFormat decimalPercentPattern(String locale, long decimalDigits) { + return new NumberFormat("", "%", decimalDigits >= 0 ? (int) decimalDigits : 0, true); + } + + public String format(Object number) { + double v = number instanceof Number ? ((Number) number).doubleValue() : 0; + if (percent) { + v = v * 100; + } + return prefix + fixed(v, digits) + suffix; + } + + private static String fixed(double value, int digits) { + boolean neg = value < 0; + double v = neg ? -value : value; + long factor = 1; + for (int i = 0; i < digits; i++) { + factor *= 10; + } + long scaled = Math.round(v * factor); + long intPart = scaled / factor; + long fracPart = scaled % factor; + StringBuilder sb = new StringBuilder(); + if (neg) { + sb.append('-'); + } + sb.append(intPart); + if (digits > 0) { + sb.append('.'); + String f = Long.toString(fracPart); + for (int i = f.length(); i < digits; i++) { + sb.append('0'); + } + sb.append(f); + } + return sb.toString(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java new file mode 100644 index 00000000000..13823cfca0d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalCupertinoLocalizations.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +/** + * Delegate provider for Cupertino localizations, mirroring + * {@code GlobalCupertinoLocalizations} from {@code flutter_localizations}. + */ +public abstract class GlobalCupertinoLocalizations { + + /** Static getter -> static field. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + + private GlobalCupertinoLocalizations() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java new file mode 100644 index 00000000000..6b4d0b1389f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalMaterialLocalizations.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +/** + * Delegate provider for Material localizations, mirroring + * {@code GlobalMaterialLocalizations} from {@code flutter_localizations}. + */ +public abstract class GlobalMaterialLocalizations { + + /** Static getter -> static field. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + + private GlobalMaterialLocalizations() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalWidgetsLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalWidgetsLocalizations.java new file mode 100644 index 00000000000..833a34549fe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/GlobalWidgetsLocalizations.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +/** + * Delegate provider for Widgets-layer localizations, mirroring + * {@code GlobalWidgetsLocalizations} from {@code flutter_localizations}. + */ +public abstract class GlobalWidgetsLocalizations { + + /** Static getter -> static field. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + + private GlobalWidgetsLocalizations() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNames.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNames.java new file mode 100644 index 00000000000..e5d743b1e10 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNames.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +import com.codename1.flutter.BuildContext; + +/** + * The {@code flutter_localized_countries} package's {@code LocaleNames} — maps a + * locale code to its display name in the current locale. new_gallery's settings + * page reaches it via {@code LocaleNames.of(context).nameOf(code)}. + * + *

The translation table is empty at this milestone, so {@link #nameOf} + * echoes the locale code back; callers combine it with + * {@link LocaleNamesLocalizationsDelegate#nativeLocaleNames} for the native + * name.

+ */ +public final class LocaleNames { + + private static final LocaleNames INSTANCE = new LocaleNames(); + + private LocaleNames() { + } + + /** Dart's {@code LocaleNames.of(context)} — the ambient locale-name table. */ + public static LocaleNames of(BuildContext context) { + return INSTANCE; + } + + /** + * Dart's {@code nameOf(localeCode)} — the locale's display name in the + * current locale, or the code itself when no translation is available. + */ + public String nameOf(String localeCode) { + return localeCode; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNamesLocalizationsDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNamesLocalizationsDelegate.java new file mode 100644 index 00000000000..6ee655bf14c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocaleNamesLocalizationsDelegate.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +import dart.core.DartMap; + +/** + * The {@code flutter_localized_countries} package's + * {@code LocaleNamesLocalizationsDelegate}. Only the static + * {@link #nativeLocaleNames} lookup (locale-code -> the locale's own native + * display name) is consumed by new_gallery's settings page. The table is empty + * at this milestone; callers fall back to the translated name when a native + * name is absent. + */ +public class LocaleNamesLocalizationsDelegate extends LocalizationsDelegate { + + /** Locale code to the locale's native display name. */ + public static final DartMap nativeLocaleNames = new DartMap(); + + public LocaleNamesLocalizationsDelegate() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocalizationsDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocalizationsDelegate.java new file mode 100644 index 00000000000..6e92c8a8798 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/LocalizationsDelegate.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +/** + * A factory for a set of localized resources, mirroring Flutter's + * {@code LocalizationsDelegate}. Opaque marker in this runtime; the type + * parameter {@code T} (the resource type the delegate loads) exists so + * transpiled {@code LocalizationsDelegate} type arguments + * resolve. + * + * @param the localized-resources type this delegate produces + */ +public class LocalizationsDelegate { + + /** + * Loads the localized resources for {@code locale}. Generated delegates + * override this with a {@code SynchronousFuture} of the resource instance; + * the base returns null so opaque runtime delegates (material/cupertino/ + * widgets globals) are simply skipped by the MaterialApp load pass. + */ + public dart.async.Future load(com.codename1.flutter.Locale locale) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java new file mode 100644 index 00000000000..4283e145869 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/l10n/MaterialLocalizations.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.l10n; + +import com.codename1.flutter.BuildContext; + +/** + * Localized strings for Material widgets, mirroring Flutter's + * {@code MaterialLocalizations}. English defaults; a single shared instance is + * returned by {@link #of(BuildContext)}. + */ +public class MaterialLocalizations { + + private static final MaterialLocalizations INSTANCE = new MaterialLocalizations(); + + /** {@code MaterialLocalizations.delegate} — a static getter, hence a field. */ + public static final LocalizationsDelegate delegate = new LocalizationsDelegate(); + + public static MaterialLocalizations of(BuildContext context) { + return INSTANCE; + } + + public String backButtonTooltip() { + return "Back"; + } + + public String closeButtonTooltip() { + return "Close"; + } + + public String closeButtonLabel() { + return "CLOSE"; + } + + public String viewLicensesButtonLabel() { + return "VIEW LICENSES"; + } + + public String nextPageTooltip() { + return "Next page"; + } + + public String previousPageTooltip() { + return "Previous page"; + } + + public String openAppDrawerTooltip() { + return "Open navigation menu"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveBreakpoints.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveBreakpoints.java new file mode 100644 index 00000000000..253f5f5590c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveBreakpoints.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.layout; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.rendering.Dp; +import com.codename1.ui.Display; + +/** + * Window-size breakpoint helpers — the {@code adaptive_breakpoints} package. + * {@link #getWindowType} buckets the current window by its LOGICAL width (CN1 + * device pixels / {@link Dp#scale()}), matching the package's Material window + * size classes. Apps gate responsive layouts on this — the gallery treats + * {@code >= medium} as "desktop", so a phone-sized window must report a small + * bucket to get the mobile layout. + */ +public final class AdaptiveBreakpoints { + + private AdaptiveBreakpoints() { + } + + public static AdaptiveWindowType getWindowType(BuildContext context) { + double width = 360; + try { + if (Display.isInitialized()) { + double scale = Dp.scale(); + if (scale > 0) { + width = Display.getInstance().getDisplayWidth() / scale; + } + } + } catch (Throwable t) { + // headless / no display: fall back to a phone-sized default + } + if (width < 600) { + return AdaptiveWindowType.xsmall; + } + if (width < 1024) { + return AdaptiveWindowType.small; + } + if (width < 1440) { + return AdaptiveWindowType.medium; + } + if (width < 1920) { + return AdaptiveWindowType.large; + } + return AdaptiveWindowType.xlarge; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveWindowType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveWindowType.java new file mode 100644 index 00000000000..3d3cec3bfb9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/layout/AdaptiveWindowType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.layout; + +/** + * The Material breakpoint bucket for the current window — the + * {@code adaptive_breakpoints} package's {@code AdaptiveWindowType}. + */ +public enum AdaptiveWindowType { + xsmall, small, medium, large, xlarge +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ActionChip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ActionChip.java new file mode 100644 index 00000000000..734f17e1c11 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ActionChip.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A chip that triggers an action when pressed — Flutter's {@code ActionChip}. + * This milestone renders {@code avatar} + {@code label} in a {@link Row}; the + * {@code onPressed} tap is deferred. + */ +public class ActionChip extends StatelessWidget { + + private Widget avatar; + private Widget label; + private Color backgroundColor; + private TextStyle labelStyle; + private Funcs.VoidFunc0 onPressed; + + public void avatar(Widget v) { + this.avatar = v; + } + + public void label(Widget v) { + this.label = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + } + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void pressElevation(Object v) { + } + + public void tooltip(Object v) { + } + + public void side(Object v) { + } + + public void shape(Object v) { + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void padding(Object v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (avatar != null) { + kids.add(avatar); + } + if (label != null) { + kids.add(label); + } + if (kids.size() == 0) { + return new SizedBox(); + } + Row row = new Row(); + row.mainAxisSize(MainAxisSize.min); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(kids); + + // An ActionChip is a chip. Built as a bare row it was an icon and some + // text loose on the page, with none of the outline that IS a chip's + // appearance -- Chip itself had already been given the capsule and this + // one had not. Material's default is an outline over the surface rather + // than a fill. + double h = 32; + com.codename1.flutter.BoxDecoration d = + new com.codename1.flutter.BoxDecoration(); + d.borderRadius(com.codename1.flutter.BorderRadius.circular(h / 2)); + com.codename1.flutter.BorderSide side = new com.codename1.flutter.BorderSide(); + side.color(outlineColor(context)); + side.width(1); + d.border(com.codename1.flutter.Border.all( + side.color(), 1, null, null)); + + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.decoration(d); + box.padding(com.codename1.flutter.EdgeInsets.symmetric(12, 0)); + box.constraints(new com.codename1.flutter.rendering.BoxConstraints( + 0, Double.POSITIVE_INFINITY, h, h)); + box.child(row); + return box; + } + + /** The chip's outline: the scheme's outline, or a mid grey without a theme. */ + private com.codename1.flutter.Color outlineColor(BuildContext context) { + try { + ThemeData t = Theme.of(context); + if (t != null && t.colorScheme() != null + && t.colorScheme().outline() != null) { + return t.colorScheme().outline(); + } + } catch (Throwable noTheme) { + // an unthemed chip still needs an outline + } + return new com.codename1.flutter.Color(0xFF79747EL); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialog.java new file mode 100644 index 00000000000..59b3b4ed652 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialog.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * A material alert dialog body: a title (rendered bold when it is a plain + * {@code Text}), optional content and a right-aligned actions row. Shown via + * {@link Dialogs#showDialog}; the CN1 Dialog supplies the surface chrome, so + * this widget is pure layout. + */ +public class AlertDialog extends Widget { + + private Widget title; + private Widget content; + private DartList actions; + private com.codename1.flutter.ShapeBorder shape; + private com.codename1.flutter.Color backgroundColor; + + public void shape(com.codename1.flutter.ShapeBorder v) { + this.shape = v; + } + + public void backgroundColor(com.codename1.flutter.Color v) { + this.backgroundColor = v; + } + + public void title(Widget v) { + this.title = v; + } + + public void content(Widget v) { + this.content = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public Widget getTitle() { + return title; + } + + public Widget getContent() { + return content; + } + + public DartList getActions() { + return actions; + } + + @Override + public Element createElement() { + return new AlertDialogRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialogRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialogRenderElement.java new file mode 100644 index 00000000000..3f31c2cd218 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AlertDialogRenderElement.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.widgets.Text; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Render element for {@link AlertDialog}: pure layout (the CN1 Dialog + * provides the surface), Material metrics — 24lp content padding, a bold + * 20lp title (synthesized when the title is a plain {@link Text}), the + * content below it, and a right-aligned actions row with 8lp spacing. + */ +public class AlertDialogRenderElement extends RenderElement { + + /** Dialog content padding in logical pixels. */ + public static final double PAD_LP = 24; + /** Gap between title and content in logical pixels. */ + public static final double TITLE_GAP_LP = 16; + /** Actions row padding / spacing in logical pixels. */ + public static final double ACTION_PAD_LP = 8; + /** Material minimum dialog width in logical pixels. */ + public static final double MIN_WIDTH_LP = 280; + /** Synthesized title font size in logical pixels. */ + public static final double TITLE_FONT_LP = 20; + + private Element titleChild; + private Element contentChild; + private List actionChildren = new ArrayList(); + + public AlertDialogRenderElement(AlertDialog widget) { + super(widget); + } + + private AlertDialog dialog() { + return (AlertDialog) widget(); + } + + /** + * A plain (styleless) Text title gets the Material bold headline style; + * everything else mounts as-is. Reconciliation keeps this cheap: the + * synthesized Text updates the same element in place on rebuilds. + */ + private Widget titleWidget() { + Widget t = dialog().getTitle(); + if (t instanceof Text && ((Text) t).getStyle() == null) { + Text styled = new Text(((Text) t).getData()); + TextStyle ts = new TextStyle(); + ts.fontSize(TITLE_FONT_LP); + ts.fontWeight(com.codename1.flutter.FontWeight.bold); + styled.style(ts); + return styled; + } + return t; + } + + @Override + protected void syncChildren() { + titleChild = updateChild(titleChild, titleWidget(), 0); + contentChild = updateChild(contentChild, dialog().getContent(), 1); + List actionWidgets = new ArrayList(); + if (dialog().getActions() != null) { + for (Widget w : dialog().getActions()) { + if (w != null) { + actionWidgets.add(w); + } + } + } + actionChildren = updateChildren(actionChildren, actionWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (titleChild != null) { + visitor.call(titleChild); + } + if (contentChild != null) { + visitor.call(contentChild); + } + for (Element a : actionChildren) { + if (a != null) { + visitor.call(a); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double pad = Dp.px(PAD_LP); + double titleGap = Dp.px(TITLE_GAP_LP); + double actionPad = Dp.px(ACTION_PAD_LP); + + double innerMax = constraints.hasBoundedWidth() + ? Math.max(0, constraints.maxWidth() - pad * 2) + : Double.POSITIVE_INFINITY; + BoxConstraints childConstraints = + new BoxConstraints(0, innerMax, 0, Double.POSITIVE_INFINITY); + + RenderElement title = RenderElement.findRenderElement(titleChild); + RenderElement content = RenderElement.findRenderElement(contentChild); + List actions = new ArrayList(); + for (Element a : actionChildren) { + RenderElement r = RenderElement.findRenderElement(a); + if (r != null) { + actions.add(r); + } + } + + double innerWidth = 0; + Size titleSize = Size.ZERO; + if (title != null) { + titleSize = title.layout(childConstraints); + innerWidth = Math.max(innerWidth, titleSize.width()); + } + Size contentSize = Size.ZERO; + if (content != null) { + contentSize = content.layout(childConstraints); + innerWidth = Math.max(innerWidth, contentSize.width()); + } + double actionsW = 0; + double actionsH = 0; + for (RenderElement a : actions) { + Size as = a.layout(childConstraints); + actionsW += as.width() + (actionsW > 0 ? actionPad : 0); + actionsH = Math.max(actionsH, as.height()); + } + innerWidth = Math.max(innerWidth, actionsW); + + double width = constraints.hasBoundedWidth() + ? constraints.maxWidth() + : Math.max(Dp.px(MIN_WIDTH_LP), innerWidth + pad * 2); + + double y = pad; + if (title != null) { + setChildOffset(title, pad, y); + y += titleSize.height() + (content != null ? titleGap : 0); + } + if (content != null) { + setChildOffset(content, pad, y); + y += contentSize.height(); + } + if (!actions.isEmpty()) { + y += actionPad * 2; + // right-aligned, last action flush with the right padding + double x = width - actionPad; + for (int i = actions.size() - 1; i >= 0; i--) { + RenderElement a = actions.get(i); + x -= a.size().width(); + setChildOffset(a, x, y + (actionsH - a.size().height()) / 2); + x -= actionPad; + } + y += actionsH + actionPad; + } else { + y += pad; + } + + return constraints.constrain(new Size(width, y)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBar.java new file mode 100644 index 00000000000..fab0b8580ed --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBar.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.services.SystemUiOverlayStyle; + +import dart.core.DartList; + +/** + * A material app bar. Under a root Scaffold it renders into the CN1 Form's + * Toolbar (title component + toolbar background color); elsewhere it renders + * as a strip at the top of the Flutter canvas. + */ +public class AppBar extends Widget { + + /** Flutter's default toolbar height in logical pixels. */ + public static final double DEFAULT_TOOLBAR_HEIGHT = 56; + + private Widget title; + private Color backgroundColor; + private boolean centerTitle; + private boolean centerTitleSet; + private DartList actions; + private Widget leading; + private boolean automaticallyImplyLeading = true; + private Widget bottom; + private Double elevation; + private SystemUiOverlayStyle systemOverlayStyle; + private Double titleSpacing; + private Double toolbarHeight; + private Widget flexibleSpace; + private com.codename1.flutter.TextStyle titleTextStyle; + private boolean primary = true; + + public void title(Widget v) { + this.title = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public void leading(Widget v) { + this.leading = v; + } + + public void automaticallyImplyLeading(boolean v) { + this.automaticallyImplyLeading = v; + } + + public void bottom(Widget v) { + this.bottom = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void systemOverlayStyle(SystemUiOverlayStyle v) { + this.systemOverlayStyle = v; + } + + public void titleSpacing(double v) { + this.titleSpacing = Double.valueOf(v); + } + + public void toolbarHeight(double v) { + this.toolbarHeight = Double.valueOf(v); + } + + /** The gap either side of the title, or null for NavigationToolbar's 16lp default. */ + public Double getTitleSpacing() { + return titleSpacing; + } + + /** The bar's height, or null for the 56lp Material default. */ + public Double getToolbarHeight() { + return toolbarHeight; + } + + private IconThemeData iconTheme; + private Color foregroundColor; + + public void iconTheme(IconThemeData v) { + this.iconTheme = v; + } + + /** {@code AppBar.iconTheme} — the style for the bar's leading and action icons. */ + public IconThemeData getIconTheme() { + return iconTheme; + } + + public void foregroundColor(Color v) { + this.foregroundColor = v; + } + + /** + * {@code AppBar.foregroundColor} — the colour of the title and of any icon + * that has no icon theme of its own. + * + *

Does NOT fall back to {@code iconTheme}: an icon theme colours icons, + * and reading it as the bar's foreground tints the title with it too. The + * icons resolve their own colour through {@code IconTheme}.

+ */ + public Color getForegroundColor() { + return foregroundColor; + } + + /** Flutter's {@code AppBar.flexibleSpace} — a widget stacked behind the toolbar. */ + public void flexibleSpace(Widget v) { + this.flexibleSpace = v; + } + + /** + * {@code AppBar.flexibleSpace} — the widget that fills the bar behind the + * leading/title/actions row. + * + *

This used to be accepted and dropped, which is a quiet way to lose a + * whole screen: Crane builds its entire bar — logo and FLY/SLEEP/EAT tabs — + * as {@code AppBar(flexibleSpace: CraneAppBar(...))} with no title at all, + * so the bar rendered empty.

+ */ + public Widget getFlexibleSpace() { + return flexibleSpace; + } + + /** + * {@code AppBar.primary} — whether this bar sits at the top of the screen + * and must therefore clear the status bar itself. + */ + public void primary(boolean v) { + this.primary = v; + } + + public boolean isPrimary() { + return primary; + } + + /** {@code AppBar.titleTextStyle} — the style for the title, over the theme's. */ + public void titleTextStyle(com.codename1.flutter.TextStyle v) { + this.titleTextStyle = v; + } + + public com.codename1.flutter.TextStyle getTitleTextStyle() { + return titleTextStyle; + } + + public DartList getActions() { + return actions; + } + + public Widget getLeading() { + return leading; + } + + public boolean getAutomaticallyImplyLeading() { + return automaticallyImplyLeading; + } + + public Widget getBottom() { + return bottom; + } + + public Double getElevation() { + return elevation; + } + + /** {@code PreferredSizeWidget.preferredSize}: the bar's height. */ + public Size preferredSize() { + return new Size(Double.POSITIVE_INFINITY, + toolbarHeight == null ? DEFAULT_TOOLBAR_HEIGHT : toolbarHeight.doubleValue()); + } + + public void centerTitle(boolean v) { + this.centerTitle = v; + this.centerTitleSet = true; + } + + public Widget getTitle() { + return title; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + public boolean getCenterTitle() { + return centerTitle; + } + + public boolean isCenterTitleSet() { + return centerTitleSet; + } + + @Override + public Element createElement() { + return new AppBarRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java new file mode 100644 index 00000000000..188d2c27a2f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarRenderElement.java @@ -0,0 +1,661 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Render element for {@link AppBar}: Flutter's {@code NavigationToolbar} arrangement of + * leading, title and actions across the bar, in two modes: + *
    + *
  • Toolbar mode (host is a root Scaffold's toolbar title host): + * owns no strip component and applies the backgroundColor to the CN1 Toolbar's + * style. The Form's Toolbar draws the bar chrome; the row is laid out inside the + * Toolbar's title component, which spans its full width.
  • + *
  • Strip mode (embedded/non-root): owns a background Container + * (UIID "FlutterAppBar") covering a 56lp-high strip.
  • + *
+ * + *

Only the title used to be laid out. {@code leading} and {@code actions} were + * read into the widget and then dropped on the floor, which is a quiet way to lose a lot + * of an app: every gallery demo page carries its back button as + * {@code AppBar.leading} and its options/info/code/documentation buttons as + * {@code actions}, so each one rendered as a bare title with no way out and no controls — + * a page that looked like a stub of itself.

+ */ +public class AppBarRenderElement extends RenderElement { + + /** Material toolbar height in logical pixels. */ + public static final double TOOLBAR_HEIGHT_LP = 56; + /** NavigationToolbar.kMiddleSpacing — the gap either side of the title. */ + private static final double TITLE_SPACING_LP = 16; + /** Flutter's _kLeadingWidth: the leading slot is a square the height of the bar. */ + private static final double LEADING_WIDTH_LP = 56; + + private List children = new ArrayList(); + /** Index into {@link #children} of each slot, or -1 when absent. */ + private int flexibleSpaceIndex = -1; + private int leadingIndex = -1; + private int titleIndex = -1; + private int firstActionIndex = -1; + private int bottomIndex = -1; + + public AppBarRenderElement(AppBar widget) { + super(widget); + } + + private AppBar appBar() { + return (AppBar) widget(); + } + + private boolean toolbarMode() { + return host() != null && host().isToolbarTitleHost(); + } + + // ------------------------------------------------------------------ + // Children: leading, title, actions - in that order + // ------------------------------------------------------------------ + + @Override + protected void syncChildren() { + List slots = new ArrayList(); + flexibleSpaceIndex = -1; + leadingIndex = -1; + titleIndex = -1; + firstActionIndex = -1; + bottomIndex = -1; + + // First, so it mounts and paints underneath the row: flexibleSpace is + // Flutter's background layer for the bar, not a fourth slot in it. + if (appBar().getFlexibleSpace() != null) { + flexibleSpaceIndex = slots.size(); + slots.add(appBar().getFlexibleSpace()); + } + + Widget leading = effectiveLeading(); + if (leading != null) { + leadingIndex = slots.size(); + slots.add(styled(leading, false)); + } + if (appBar().getTitle() != null) { + titleIndex = slots.size(); + slots.add(styled(appBar().getTitle(), true)); + } + if (appBar().getActions() != null) { + for (Widget a : appBar().getActions()) { + if (a == null) { + continue; + } + if (firstActionIndex < 0) { + firstActionIndex = slots.size(); + } + slots.add(styled(a, false)); + } + } + // Last, and below the row: Flutter's AppBar.bottom is a band under the + // toolbar, not a slot in it. It was read into the widget and dropped, + // which is how the colors demo lost its whole palette tab bar and the + // tabs demo lost its tabs. + if (appBar().getBottom() != null) { + bottomIndex = slots.size(); + slots.add(appBar().getBottom()); + } + children = updateChildren(children, slots); + } + + /** + * Wraps a toolbar slot in the bar's ambient icon theme and text style. + * + *

This is how Flutter tints an app bar's contents, and why it works + * without every {@code Text} and {@code Icon} naming a colour: the bar + * publishes one {@code IconTheme} and one {@code DefaultTextStyle} and the + * subtree reads them. Setting a foreground on the strip container instead + * does nothing, because a Codename One style does not inherit its ink from + * an ancestor — which is why themed bars rendered black glyphs on purple.

+ * + *

Deliberately NOT applied to {@code flexibleSpace}: in Flutter the + * flexible space sits in a Stack beneath the toolbar, outside these two + * wrappers, so it keeps whatever style its own subtree establishes. Crane's + * bar depends on that — its tab labels are white by its own theme.

+ */ + private Widget styled(Widget slot, boolean isTitle) { + Widget out = slot; + com.codename1.flutter.TextStyle text = isTitle ? titleTextStyle() : toolbarTextStyle(); + if (text != null) { + out = com.codename1.flutter.widgets.DefaultTextStyle.wrap(text, out); + } + if (!isTitle) { + IconThemeData icons = effectiveIconTheme(); + if (icons != null) { + IconTheme t = new IconTheme(); + t.data(icons); + t.child(out); + out = t; + } + } + return out; + } + + /** + * The icon styling for the bar's glyphs: {@code AppBar.iconTheme}, then the + * ambient {@code AppBarTheme}'s, then the bar's foreground colour. + * + *

One-directional on purpose. An {@code AppBarTheme.iconTheme} colours + * the ICONS and nothing else — reading it as the bar's foreground turns the + * title white too, which is wrong wherever a theme tints its glyphs against + * a bar whose title is meant to stay default ink. The gallery's demo pages + * are exactly that case: white icons, black title, on purple.

+ */ + private IconThemeData effectiveIconTheme() { + if (appBar().getIconTheme() != null) { + return appBar().getIconTheme(); + } + try { + AppBarTheme bar = Theme.of(this).appBarTheme(); + if (bar != null && bar.iconTheme() != null) { + return bar.iconTheme(); + } + } catch (Throwable t) { + // no ambient theme + } + com.codename1.flutter.Color fg = effectiveForeground(); + if (fg == null) { + return null; + } + IconThemeData d = new IconThemeData(); + d.color(fg); + return d; + } + + /** The style for the title: {@code AppBarTheme.titleTextStyle}, tinted with the foreground. */ + private com.codename1.flutter.TextStyle titleTextStyle() { + if (appBar().getTitleTextStyle() != null) { + return tinted(appBar().getTitleTextStyle()); + } + com.codename1.flutter.TextStyle fromBarTheme = null; + TextTheme textTheme = null; + try { + ThemeData theme = Theme.of(this); + AppBarTheme bar = theme.appBarTheme(); + if (bar != null) { + fromBarTheme = bar.titleTextStyle(); + } + textTheme = theme.textTheme(); + } catch (Throwable t) { + // no ambient theme + } + return tinted(chooseTitleStyle(fromBarTheme, textTheme)); + } + + /** + * Flutter's chain for the title style: + * {@code AppBar.titleTextStyle ?? AppBarTheme.titleTextStyle ?? + * textTheme.titleLarge}. The bar's own style is handled by the caller, + * which returns before reaching here. + * + *

The last link was missing, so a bar whose theme names no title style + * -- which is most of them -- fell through to whatever size a bare + * {@code Text} picks. That is about 16 logical pixels against titleLarge's + * 22, and every title in the gallery rendered at roughly seven tenths of + * its size.

+ */ + static com.codename1.flutter.TextStyle chooseTitleStyle( + com.codename1.flutter.TextStyle fromBarTheme, TextTheme textTheme) { + if (fromBarTheme != null) { + return fromBarTheme; + } + return textTheme == null ? null : textTheme.titleLarge(); + } + + /** The style for everything else on the bar (Flutter's toolbarTextStyle). */ + private com.codename1.flutter.TextStyle toolbarTextStyle() { + com.codename1.flutter.TextStyle themed = null; + try { + AppBarTheme bar = Theme.of(this).appBarTheme(); + if (bar != null) { + themed = bar.toolbarTextStyle(); + } + } catch (Throwable t) { + // no ambient theme + } + return tinted(themed); + } + + /** {@code base} with the bar's foreground applied when it names no colour of its own. */ + private com.codename1.flutter.TextStyle tinted(com.codename1.flutter.TextStyle base) { + com.codename1.flutter.Color fg = effectiveForeground(); + if (base == null) { + if (fg == null) { + return null; + } + com.codename1.flutter.TextStyle t = new com.codename1.flutter.TextStyle(); + t.color(fg); + return t; + } + if (base.getColor() != null || fg == null) { + return base; + } + return base.copyWith(null, fg, null, null, null, null, null, null, + null, null, null, null, null); + } + + /** + * The leading widget, or the back button Flutter would imply in its place. + * + *

{@code automaticallyImplyLeading} defaults to true, and a route that can be popped + * gets a back button for free — which is how most Flutter pages get theirs. Without it + * a Scaffold that never names a leading is a page with no way back.

+ */ + private Widget effectiveLeading() { + if (appBar().getLeading() != null) { + return appBar().getLeading(); + } + if (!appBar().getAutomaticallyImplyLeading()) { + return null; + } + return canPop() ? new BackButton() : null; + } + + private boolean canPop() { + try { + com.codename1.flutter.navigation.NavigatorState nav = + com.codename1.flutter.navigation.Navigator.of(this, Boolean.FALSE); + return nav != null && nav.canPop(); + } catch (Throwable t) { + return false; + } + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : children) { + if (c != null) { + visitor.call(c); + } + } + } + + private RenderElement renderAt(int index) { + if (index < 0 || index >= children.size()) { + return null; + } + return findRenderElement(children.get(index)); + } + + // ------------------------------------------------------------------ + // Component and styling + // ------------------------------------------------------------------ + + @Override + protected Component createComponent() { + if (toolbarMode()) { + applyToolbarStyle(); + return null; + } + if (!com.codename1.ui.Display.isInitialized()) { + // headless unit tests: the layout is exercised without any CN1 components + return null; + } + Container strip = new Container(); + strip.setUIID("FlutterAppBar"); + strip.getAllStyles().setPadding(0, 0, 0, 0); + strip.getAllStyles().setMargin(0, 0, 0, 0); + applyStripStyle(strip); + return strip; + } + + @Override + protected void updateComponent(Component c) { + applyStripStyle(c); + } + + @Override + public void update(Widget newWidget) { + super.update(newWidget); + if (toolbarMode()) { + applyToolbarStyle(); + } + } + + /** + * The bar background actually in effect — Flutter's resolution order: + * {@code AppBar.backgroundColor}, then the ambient {@code AppBarTheme}, + * then the M3 default of colorScheme.surface (an AppBar sitting on the + * surface with an elevation tint rather than a saturated fill). + * + *

The AppBarTheme step was missing, and the gallery leans on it hard: it + * themes every demo page's bar with {@code AppBarTheme(color: primary)} and + * its own chrome with {@code AppBarTheme(backgroundColor: background)}, so + * both came out the same default grey and the demos lost their purple bar.

+ */ + private com.codename1.flutter.Color effectiveBackground() { + if (appBar().getBackgroundColor() != null) { + return appBar().getBackgroundColor(); + } + try { + ThemeData theme = Theme.of(this); + AppBarTheme bar = theme.appBarTheme(); + if (bar != null && bar.backgroundColor() != null) { + return bar.backgroundColor(); + } + return theme.colorScheme().surface(); + } catch (Throwable t) { + return null; + } + } + + /** + * The colour for the bar's title and icons: {@code AppBar.foregroundColor}, + * then the ambient {@code AppBarTheme}'s icon theme, then null for the + * default. The gallery pairs its purple bar with white icons this way. + */ + com.codename1.flutter.Color effectiveForeground() { + if (appBar().getForegroundColor() != null) { + return appBar().getForegroundColor(); + } + try { + AppBarTheme bar = Theme.of(this).appBarTheme(); + if (bar != null && bar.foregroundColor() != null) { + return bar.foregroundColor(); + } + return Theme.of(this).colorScheme().onSurface(); + } catch (Throwable t) { + return null; + } + } + + private void applyStripStyle(Component strip) { + com.codename1.flutter.Color bg = effectiveBackground(); + if (bg != null) { + ThemeDataAdapter.paintColor(strip.getAllStyles(), bg); + } + // The default ink for anything in the bar that does not pick its own. + // A purple bar with black-by-default glyphs on it is unreadable, and + // that is exactly what the demo pages' AppBarTheme asks for. + com.codename1.flutter.Color fg = effectiveForeground(); + if (fg != null) { + strip.getAllStyles().setFgColor((int) (fg.value() & 0xFFFFFFL)); + } + } + + /** + * Root mode: style the CN1 Toolbar itself. BACKGROUND_NONE (inside + * paintSolid) is essential — the Material base theme's Toolbar style can + * carry a background image/gradient that paints OVER a bare setBgColor, + * which is why AppBar.backgroundColor used to be ignored here. Applied + * to getAllStyles so focus/scroll state changes can't swap the color + * back (state-metric invariance). + */ + private void applyToolbarStyle() { + com.codename1.ui.Toolbar tb = host() == null ? null : host().toolbar(); + if (tb == null) { + return; + } + com.codename1.flutter.Color bg = effectiveBackground(); + if (bg != null) { + ThemeDataAdapter.paintColor(tb.getAllStyles(), bg); + } + } + + /** + * Theme change: recompute the ThemeData-driven default background in + * BOTH modes (the base class only re-applies the strip component's + * config; the Toolbar is not our component). + */ + @Override + public void themeChanged() { + if (toolbarMode()) { + applyToolbarStyle(); + } + super.themeChanged(); + } + + // ------------------------------------------------------------------ + // Layout - Flutter's NavigationToolbar + // ------------------------------------------------------------------ + + /** + * The status-bar strip this bar has to clear, in pixels. + * + *

Flutter's app bar is {@code toolbarHeight + MediaQuery.padding.top} + * tall and puts its row below the inset, while the flexible space fills the + * whole thing. Ours was just {@code toolbarHeight}, so a bar at the top of + * the screen came out a notch short and any safe area inside its flexible + * space pushed that content clean out of the bar — which is why Crane's + * logo and tab bar rendered below their own app bar.

+ */ + private double topInset() { + if (!appBar().isPrimary() || toolbarMode()) { + return 0; + } + try { + return Dp.px(com.codename1.flutter.MediaQuery.of(this).padding().top()); + } catch (Throwable t) { + return 0; + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double totalHeight = barHeight(constraints); + double topInset = Math.min(topInset(), totalHeight); + // The row occupies the toolbar band; `bottom` takes the rest. + RenderElement bottom = renderAt(bottomIndex); + double bottomHeight = bottomHeight(constraints, bottom); + double barHeight = Math.max(0, totalHeight - bottomHeight - topInset); + double spacing = Dp.px(titleSpacing()); + + // Leading first: it fixes where the title may start. + RenderElement leading = renderAt(leadingIndex); + double leadingWidth = 0; + Size leadingSize = null; + if (leading != null) { + leadingSize = leading.layout(BoxConstraints.loose( + Math.min(Dp.px(LEADING_WIDTH_LP), maxWidthFor(constraints)), barHeight)); + leadingWidth = leadingSize.width(); + } + + // Actions next, packed at the end. Each is laid out against what is still free, so + // a long row degrades by shrinking rather than by overflowing the bar. + List actions = new ArrayList(); + List actionSizes = new ArrayList(); + double actionsWidth = 0; + if (firstActionIndex >= 0) { + int lastAction = bottomIndex >= 0 ? bottomIndex : children.size(); + for (int i = firstActionIndex; i < lastAction; i++) { + RenderElement a = renderAt(i); + if (a == null) { + continue; + } + double free = Math.max(0, maxWidthFor(constraints) - leadingWidth - actionsWidth); + Size as = a.layout(BoxConstraints.loose(free, barHeight)); + actions.add(a); + actionSizes.add(as); + actionsWidth += as.width(); + } + } + + RenderElement title = renderAt(titleIndex); + Size titleSize = null; + + double width; + if (constraints.hasBoundedWidth()) { + width = constraints.maxWidth(); + if (title != null) { + double avail = Math.max(0, + width - leadingWidth - actionsWidth - spacing * 2); + titleSize = title.layout(BoxConstraints.loose(avail, barHeight)); + } + } else { + // Unbounded (the dry pass that yields a preferred size): the bar is as wide as + // its contents, so the title is measured free and everything is summed. + if (title != null) { + titleSize = title.layout(BoxConstraints.loose(Double.POSITIVE_INFINITY, barHeight)); + } + width = leadingWidth + actionsWidth + + (titleSize == null ? 0 : titleSize.width() + spacing * 2); + // A bar whose only content is its background layer still has a width. + // Crane's bar has no title, leading or actions at all, so summing the + // row alone would measure it as zero and collapse the strip. + RenderElement flexDry = renderAt(flexibleSpaceIndex); + if (flexDry != null) { + width = Math.max(width, flexDry.layout( + BoxConstraints.loose(Double.POSITIVE_INFINITY, barHeight)).width()); + } + } + + // The background layer last, once the bar's width is settled: it fills the + // whole bar rather than taking a share of it, so it must not contribute to + // the width the row was measured against (that is what makes it a + // background and not a fourth slot). + RenderElement flexible = renderAt(flexibleSpaceIndex); + if (flexible != null) { + flexible.layout(BoxConstraints.tight(width, totalHeight)); + setChildOffset(flexible, 0, 0); + } + if (bottom != null) { + bottom.layout(BoxConstraints.tight(width, bottomHeight)); + setChildOffset(bottom, 0, topInset + barHeight); + } + + // Place: leading at the start, actions flush to the end, title between. + if (leading != null) { + setChildOffset(leading, 0, topInset + centreY(leadingSize, barHeight)); + } + double actionX = width - actionsWidth; + for (int i = 0; i < actions.size(); i++) { + Size as = actionSizes.get(i); + setChildOffset(actions.get(i), actionX, topInset + centreY(as, barHeight)); + actionX += as.width(); + } + if (title != null) { + double tx; + if (centerTitle()) { + tx = (width - titleSize.width()) / 2; + // A centred title still may not slide under the leading or the actions. + tx = Math.max(leadingWidth + spacing, + Math.min(tx, width - actionsWidth - spacing - titleSize.width())); + tx = Math.max(0, tx); + } else { + tx = leadingWidth + spacing; + } + setChildOffset(title, tx, topInset + centreY(titleSize, barHeight)); + } + + return constraints.constrain(new Size(width, totalHeight)); + } + + /** Vertical centring of one slot within the bar. */ + private static double centreY(Size child, double barHeight) { + if (child == null) { + return 0; + } + return Math.max(0, (barHeight - child.height()) / 2); + } + + private static double maxWidthFor(BoxConstraints constraints) { + return constraints.hasBoundedWidth() ? constraints.maxWidth() : Double.POSITIVE_INFINITY; + } + + /** + * The bar's height. In toolbar mode the CN1 Toolbar owns the chrome and has already + * been given a height, so the row fills whatever box it was handed rather than forcing + * a second 56lp on top of it. + */ + /** + * The height {@code bottom} wants, measured against an UNBOUNDED height. + * + *

Offering it the bar's height instead invites a greedy child to take + * all of it — a scrollable tab strip does exactly that — which leaves the + * toolbar row nothing and stacks the two on top of each other. It also has + * to be the same number {@link #barHeight} used, or the two passes disagree + * about where the row ends.

+ */ + private double bottomHeight(BoxConstraints constraints, RenderElement bottom) { + if (bottom == null) { + return 0; + } + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() + : Double.POSITIVE_INFINITY; + return bottom.layout(BoxConstraints.loose(w, Double.POSITIVE_INFINITY)).height(); + } + + private double barHeight(BoxConstraints constraints) { + double preferred = Dp.px(appBar().getToolbarHeight() == null + ? TOOLBAR_HEIGHT_LP : appBar().getToolbarHeight().doubleValue()) + + topInset(); + RenderElement bottom = renderAt(bottomIndex); + if (bottom != null && !(toolbarMode() && constraints.hasBoundedHeight() + && constraints.maxHeight() > 0)) { + preferred += bottomHeight(constraints, bottom); + } + if (toolbarMode() && constraints.hasBoundedHeight() && constraints.maxHeight() > 0) { + return constraints.maxHeight(); + } + return constraints.constrainHeight(preferred); + } + + /** + * Whether the title is centred — {@code AppBar.centerTitle}, then the + * ambient theme's, then the platform default. + * + *

Flutter centres app bar titles on iOS and macOS and left-aligns them + * everywhere else. Defaulting to left on every platform puts the title in + * the wrong place on every iOS screen in the app.

+ */ + private boolean centerTitle() { + if (appBar().isCenterTitleSet()) { + return appBar().getCenterTitle(); + } + try { + AppBarTheme bar = Theme.of(this).appBarTheme(); + if (bar != null && bar.centerTitle() != null) { + return bar.centerTitle().booleanValue(); + } + } catch (Throwable t) { + // no ambient theme + } + com.codename1.flutter.TargetPlatform p = + com.codename1.flutter.foundation.FoundationLib.defaultTargetPlatform; + return p == com.codename1.flutter.TargetPlatform.iOS + || p == com.codename1.flutter.TargetPlatform.macOS; + } + + private double titleSpacing() { + Double s = appBar().getTitleSpacing(); + return s == null ? TITLE_SPACING_LP : s.doubleValue(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarTheme.java new file mode 100644 index 00000000000..0cd849327e9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AppBarTheme.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code AppBarTheme}: write-once app-bar styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class AppBarTheme { + + private Color backgroundColor; + private Color foregroundColor; + private Color color; + private Color shadowColor; + private Color surfaceTintColor; + private Double elevation; + private Double scrolledUnderElevation; + private IconThemeData iconTheme; + private IconThemeData actionsIconTheme; + private TextStyle titleTextStyle; + private TextStyle toolbarTextStyle; + private Boolean centerTitle; + private Double titleSpacing; + private Double toolbarHeight; + private Object systemOverlayStyle; + private Object shape; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void foregroundColor(Color v) { + this.foregroundColor = v; + } + + public void color(Color v) { + this.color = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void scrolledUnderElevation(double v) { + this.scrolledUnderElevation = v; + } + + public void iconTheme(IconThemeData v) { + this.iconTheme = v; + } + + public void actionsIconTheme(IconThemeData v) { + this.actionsIconTheme = v; + } + + public void titleTextStyle(TextStyle v) { + this.titleTextStyle = v; + } + + public void toolbarTextStyle(TextStyle v) { + this.toolbarTextStyle = v; + } + + public void centerTitle(boolean v) { + this.centerTitle = v; + } + + public void titleSpacing(double v) { + this.titleSpacing = v; + } + + public void toolbarHeight(double v) { + this.toolbarHeight = v; + } + + public void systemOverlayStyle(Object v) { + this.systemOverlayStyle = v; + } + + public void shape(Object v) { + this.shape = v; + } + + /** + * The bar's fill. {@code color} is Flutter's older name for the same slot and + * apps still use it — the gallery themes every demo page's bar with + * {@code AppBarTheme(color: colorScheme.primary)}, so reading only + * {@code backgroundColor} left every demo bar the default grey instead of + * Material purple. + */ + public Color backgroundColor() { + return backgroundColor != null ? backgroundColor : color; + } + + public Double elevation() { + return elevation; + } + + public IconThemeData iconTheme() { + return iconTheme; + } + + /** {@code AppBarTheme.actionsIconTheme}, falling back to the bar's icon theme. */ + public IconThemeData actionsIconTheme() { + return actionsIconTheme != null ? actionsIconTheme : iconTheme; + } + + public Color foregroundColor() { + return foregroundColor; + } + + public TextStyle titleTextStyle() { + return titleTextStyle; + } + + public TextStyle toolbarTextStyle() { + return toolbarTextStyle; + } + + public Object shape() { + return shape; + } + + public Boolean centerTitle() { + return centerTitle; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AutovalidateMode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AutovalidateMode.java new file mode 100644 index 00000000000..229a69824fa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/AutovalidateMode.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import dart.core.DartList; + +/** + * When a Form (or FormField) auto-validates its fields — Flutter's + * {@code AutovalidateMode}. Modelled as a class (not a Java enum) because the + * text-field demo reads {@link #index()} off a value and indexes {@link #values()} + * to round-trip the choice through a RestorableInt. + */ +public final class AutovalidateMode { + + public static final AutovalidateMode disabled = new AutovalidateMode(0); + public static final AutovalidateMode always = new AutovalidateMode(1); + public static final AutovalidateMode onUserInteraction = new AutovalidateMode(2); + + /** The enum-like value list, indexable like Dart's {@code AutovalidateMode.values}. */ + public static final DartList values = buildValues(); + + private static DartList buildValues() { + DartList v = new DartList(); + v.add(disabled); + v.add(always); + v.add(onUserInteraction); + return v; + } + + private final int index; + + private AutovalidateMode(int index) { + this.index = index; + } + + public int index() { + return index; + } + + public static AutovalidateMode disabled() { return disabled; } + public static AutovalidateMode always() { return always; } + public static AutovalidateMode onUserInteraction() { return onUserInteraction; } + + public static DartList values() { + DartList v = new DartList(); + v.add(disabled); + v.add(always); + v.add(onUserInteraction); + return v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButton.java new file mode 100644 index 00000000000..1d0e82f6ea4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButton.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Icons; +import com.codename1.flutter.widgets.Icon; + +import dart.runtime.Funcs; + +/** + * A material back button: an {@link IconButton} showing the platform back + * chevron that, when pressed, pops the current route (Flutter's + * {@code BackButton}). {@code onPressed} overrides the default pop. + */ +public class BackButton extends IconButton { + + public BackButton() { + icon(new Icon(Icons.arrow_back)); + } + + public BackButton(com.codename1.flutter.Key key, Color color, Funcs.VoidFunc0 onPressed) { + this(); + if (color != null) { + color(color); + } + if (onPressed != null) { + onPressed(onPressed); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButtonIcon.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButtonIcon.java new file mode 100644 index 00000000000..bb2d3abeed5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BackButtonIcon.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Icons; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TargetPlatform; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Icon; + +/** + * The platform-appropriate back-arrow glyph, decoupled from its button — + * Flutter's {@code BackButtonIcon}: a chevron on iOS/macOS, an arrow elsewhere. + * + *

It rendered NOTHING until now, which is a costlier omission than it sounds: the + * gallery builds every demo page's back button as {@code IconButton(icon: BackButtonIcon())}, + * so each of those pages had an invisible — though still tappable — way back.

+ */ +public class BackButtonIcon extends StatelessWidget + implements com.codename1.flutter.widgets.HasIcon { + + /** + * The glyph, for callers that want it without building — see + * {@link com.codename1.flutter.widgets.HasIcon}. + * + *

There is no context here to read the ambient theme's platform from, so this asks + * the one the app is RUNNING on, which is what {@code ThemeData.platform} defaults to + * anyway. It used to answer the material arrow unconditionally, and the shortcut is + * the path an extended FloatingActionButton takes for its icon -- so the gallery's + * "Back to gallery" button wore a long arrow on iOS where the reference wears the + * chevron. The glyph was not merely a different shape; it was the wrong platform's.

+ */ + @Override + public com.codename1.flutter.IconData iconData() { + return isApplePlatform() ? Icons.arrow_back_ios : Icons.arrow_back; + } + + /** Whether the platform the app is running on uses the chevron. */ + private static boolean isApplePlatform() { + try { + TargetPlatform p = com.codename1.flutter.foundation.FoundationLib + .defaultTargetPlatform; + return p == TargetPlatform.iOS || p == TargetPlatform.macOS; + } catch (Throwable noPlatform) { + return false; + } + } + + + @Override + public Widget build(BuildContext context) { + return new Icon(isApplePlatform(context) ? Icons.arrow_back_ios : Icons.arrow_back); + } + + /** Whether the ambient theme targets a platform that uses the chevron. */ + private static boolean isApplePlatform(BuildContext context) { + try { + Object p = Theme.of(context).platform(); + return p == TargetPlatform.iOS || p == TargetPlatform.macOS; + } catch (Throwable t) { + return false; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Banner.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Banner.java new file mode 100644 index 00000000000..08dd44632c6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Banner.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +/** + * A diagonal ribbon banner drawn over a corner of its child — Flutter's + * {@code Banner}. This milestone renders the {@code child}; the diagonal + * {@code message} ribbon is deferred. + */ +public class Banner extends StatelessWidget { + + private Widget child; + private String message; + private Object location; + private Color color; + private TextStyle textStyle; + + public void child(Widget v) { + this.child = v; + } + + public void message(String v) { + this.message = v; + } + + public void textDirection(Object v) { + } + + public void location(Object v) { + this.location = v; + } + + public void layoutDirection(Object v) { + } + + public void color(Color v) { + this.color = v; + } + + public void textStyle(TextStyle v) { + this.textStyle = v; + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BannerLocation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BannerLocation.java new file mode 100644 index 00000000000..d6fb602731d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BannerLocation.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Where a diagonal {@link Banner} is drawn on its child — Flutter's + * {@code BannerLocation}. + */ +public enum BannerLocation { + topStart, topEnd, bottomStart, bottomEnd +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBar.java new file mode 100644 index 00000000000..96485091735 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBar.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +/** + * A material bottom app bar: a bar docked to the bottom of a {@link Scaffold}, + * typically hosting a row of actions and (with a {@code shape}) a notch for a + * docked FloatingActionButton. The notch geometry is retained as configuration + * but not yet cut. + * + *

It used to build a bare container with neither the theme's colour nor the + * Material height, so a bar that names no colour of its own — which is the + * usual case, since the theme supplies it — came out invisible.

+ */ +public class BottomAppBar extends StatelessWidget { + + private Color color; + private Double elevation; + private Object shape; + private Double notchMargin; + private Clip clipBehavior; + private Widget child; + + public void color(Color v) { + this.color = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void notchMargin(double v) { + this.notchMargin = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Color getColor() { + return color; + } + + public Double getElevation() { + return elevation; + } + + public Object getShape() { + return shape; + } + + public Widget getChild() { + return child; + } + + /** Material 3's bottom app bar height in logical pixels -- 80, not M2's 56. */ + public static final double HEIGHT_LP = 80; + + @Override + public Widget build(BuildContext context) { + Container c = new Container(); + Color fill = color != null ? color : themedColor(context); + NotchedShape notch = shape instanceof NotchedShape ? (NotchedShape) shape : null; + c.height(HEIGHT_LP); + c.alignment(com.codename1.flutter.Alignment.topCenter); + c.child(child); + // The bar CARRIES the display's bottom inset; it is not something the scaffold + // paints behind it. Flutter's BottomAppBar wraps its child in a SafeArea, so the + // bar measures its Material height plus the inset and everything applied to the + // bar applies to both. + // + // That distinction is invisible at rest and decides what happens when the bar + // animates. Reply folds its bar away as you read down the list, and a scaffold + // that adds the inset separately keeps painting a band of bar colour across the + // bottom of the screen after the bar itself has collapsed to nothing -- the bar + // does not go away, it shrinks to a stripe. Owning the inset means the fold takes + // it along and the bar vanishes, which is what the reference does. + Widget body = withBottomInset(context, c); + if (fill != null && notch != null) { + // Painted through the shape rather than coloured: a Container fills its box, + // and the whole point of a notched shape is that the box is not what should + // be filled. + NotchedSurface surface = new NotchedSurface(); + surface.shape(notch); + surface.color(fill); + surface.notchMargin(notchMargin != null ? notchMargin.doubleValue() : 4.0); + surface.child(body); + return surface; + } + if (fill == null) { + return body; + } + Container filled = new Container(); + filled.color(fill); + filled.child(body); + return filled; + } + + /// The bar's content plus the display's bottom inset below it, as Flutter's + /// {@code SafeArea} inside BottomAppBar does. + private static Widget withBottomInset(BuildContext context, Widget content) { + double bottom = 0; + try { + com.codename1.flutter.EdgeInsets p = + com.codename1.flutter.MediaQuery.paddingOf(context); + if (p != null) { + bottom = p.bottom(); + } + } catch (Throwable t) { + // no MediaQuery in reach: the bar is simply its Material height + } + if (bottom <= 0) { + return content; + } + com.codename1.flutter.widgets.Padding pad = new com.codename1.flutter.widgets.Padding(); + pad.padding(com.codename1.flutter.EdgeInsets.only(0, 0, 0, bottom)); + pad.child(content); + return pad; + } + + /** {@code BottomAppBarTheme.color}, then the surface the bar sits on. */ + private static Color themedColor(BuildContext context) { + try { + ThemeData theme = Theme.of(context); + BottomAppBarThemeData bar = theme.bottomAppBarTheme(); + if (bar != null && bar.color() != null) { + return bar.color(); + } + return theme.colorScheme().surface(); + } catch (Throwable t) { + return null; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBarThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBarThemeData.java new file mode 100644 index 00000000000..15fa91449f2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomAppBarThemeData.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; + +/** + * Material {@code BottomAppBarThemeData}: write-once bottom-app-bar styling. + */ +public class BottomAppBarThemeData { + + private Color color; + private Color surfaceTintColor; + private Color shadowColor; + private Double elevation; + private Double height; + private EdgeInsets padding; + private Object shape; + + public void color(Color v) { + this.color = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void height(double v) { + this.height = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public Color color() { + return color; + } + + public Double elevation() { + return elevation; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBar.java new file mode 100644 index 00000000000..784661c2ff4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBar.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A material bottom navigation bar: items rendered icon-above-label in + * equal-width slots, the {@code currentIndex} item tinted with the theme's + * primary color, {@code onTap(index)} fired on press. 80lp tall (M3 + * navigation bar height). As a root Scaffold's {@code bottomNavigationBar} + * it renders into the Form's SOUTH region; embedded Scaffolds lay it out as + * a bottom strip. + */ +public class BottomNavigationBar extends Widget { + + private DartList items; + private Long currentIndex; + private Funcs.VoidFunc1 onTap; + private BottomNavigationBarType type; + private Color backgroundColor; + private Color selectedItemColor; + private Color unselectedItemColor; + private Double selectedFontSize; + private Double unselectedFontSize; + + public void type(BottomNavigationBarType v) { + this.type = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void selectedItemColor(Color v) { + this.selectedItemColor = v; + } + + public void unselectedItemColor(Color v) { + this.unselectedItemColor = v; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + public Color getSelectedItemColor() { + return selectedItemColor; + } + + public Color getUnselectedItemColor() { + return unselectedItemColor; + } + + public void selectedFontSize(double v) { + this.selectedFontSize = v; + } + + public void unselectedFontSize(double v) { + this.unselectedFontSize = v; + } + + public void showUnselectedLabels(boolean v) { + } + + public void showSelectedLabels(boolean v) { + } + + public void elevation(double v) { + } + + public void iconSize(double v) { + } + + public void items(DartList v) { + this.items = v; + } + + public void currentIndex(long v) { + this.currentIndex = v; + } + + public void onTap(Funcs.VoidFunc1 v) { + this.onTap = v; + } + + public DartList getItems() { + return items; + } + + /** Flutter default: 0. */ + public long getCurrentIndex() { + return currentIndex == null ? 0 : currentIndex; + } + + public Funcs.VoidFunc1 getOnTap() { + return onTap; + } + + @Override + public Element createElement() { + return new BottomNavigationBarRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarItem.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarItem.java new file mode 100644 index 00000000000..63b63c009da --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +/** + * One destination of a {@link BottomNavigationBar}: an icon widget and an + * optional text label. Configuration only — not itself a widget. + */ +public class BottomNavigationBarItem { + + private Widget icon; + private String label; + + public void icon(Widget v) { + this.icon = v; + } + + public void label(String v) { + this.label = v; + } + + public Widget getIcon() { + return icon; + } + + public String getLabel() { + return label; + } + + /** Dart {@code item.icon} getter. */ + public Widget icon() { + return icon; + } + + /** Dart {@code item.label} getter. */ + public String label() { + return label; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarRenderElement.java new file mode 100644 index 00000000000..a529c3172cc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarRenderElement.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.widgets.Text; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Render element for {@link BottomNavigationBar}. Owns a background + * Container (UIID "FlutterBottomNavigationBar", surface-colored) painted + * behind the items, mounts per item a synthesized icon and (when a label is + * set) a synthesized 12lp {@link Text}, and a transparent tap overlay LAST + * that maps the release x-coordinate to an item index for {@code onTap}. + * + *

Selected-item tinting: when an item's icon is a plain {@link Icon} + * without an explicit color, a tinted copy is mounted (primary for the + * currentIndex item, onSurface otherwise); custom icon widgets mount + * unchanged. Bar height: 80lp (M3 navigation bar).

+ */ +public class BottomNavigationBarRenderElement extends RenderElement { + + /** M3 navigation bar height in logical pixels. */ + public static final double HEIGHT_LP = 80; + /** Top padding above the icons in logical pixels. */ + public static final double TOP_PAD_LP = 12; + /** Gap between icon and label in logical pixels. */ + public static final double LABEL_GAP_LP = 4; + /** Label font size in logical pixels. */ + public static final double LABEL_FONT_LP = 12; + /** Fallback slot width when the incoming width is unbounded. */ + public static final double FALLBACK_SLOT_WIDTH_LP = 80; + + private final List iconChildren = new ArrayList(); + private final List labelChildren = new ArrayList(); + private Element overlayChild; + + public BottomNavigationBarRenderElement(BottomNavigationBar widget) { + super(widget); + } + + private BottomNavigationBar bar() { + return (BottomNavigationBar) widget(); + } + + private List items() { + List out = new ArrayList(); + if (bar().getItems() != null) { + for (BottomNavigationBarItem i : bar().getItems()) { + if (i != null) { + out.add(i); + } + } + } + return out; + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Container c = new Container(); + c.setUIID("FlutterBottomNavigationBar"); + c.getAllStyles().setPadding(0, 0, 0, 0); + c.getAllStyles().setMargin(0, 0, 0, 0); + style(c); + return c; + } + + @Override + protected void updateComponent(Component c) { + style(c); + } + + /** + * The bar's own {@code backgroundColor} first, the theme's surface second. + * + *

The three colours a caller can set -- background, selected item, + * unselected item -- were all accepted and discarded, so a bar that names + * them (the bottom-navigation demo asks for a primary-coloured bar with + * white labels) came out surface-coloured with dark ink: the right shape in + * entirely the wrong palette.

+ */ + private void style(Component c) { + try { + Color bg = bar().getBackgroundColor(); + if (bg == null) { + bg = Theme.of(this).colorScheme().surface(); + } + c.getAllStyles().setBgColor(bg.rgb()); + c.getAllStyles().setBgTransparency(255); + } catch (Exception err) { + // styling is best-effort; the base theme look remains + } + } + + private Widget iconWidgetFor(BottomNavigationBarItem item, boolean selected) { + Widget w = item.getIcon(); + if (w instanceof Icon && ((Icon) w).getColor() == null) { + Icon src = (Icon) w; + Icon tinted = new Icon(src.getIcon()); + if (src.getSize() != null) { + tinted.size(src.getSize()); + } + tinted.color(tintFor(selected)); + return tinted; + } + return w; + } + + private Widget labelWidgetFor(BottomNavigationBarItem item, boolean selected) { + if (item.getLabel() == null) { + return null; + } + Text t = new Text(item.getLabel()); + TextStyle ts = new TextStyle(); + ts.fontSize(LABEL_FONT_LP); + ts.color(tintFor(selected)); + t.style(ts); + return t; + } + + private Color tintFor(boolean selected) { + Color own = selected ? bar().getSelectedItemColor() : bar().getUnselectedItemColor(); + if (own != null) { + return own; + } + ColorScheme cs = Theme.of(this).colorScheme(); + return selected ? cs.primary() : cs.onSurface(); + } + + @Override + protected void syncChildren() { + List items = items(); + int n = items.size(); + // shrink leftover slots when the item count drops + while (iconChildren.size() > n) { + Element leftover = iconChildren.remove(iconChildren.size() - 1); + if (leftover != null) { + deactivateChild(leftover); + } + } + while (labelChildren.size() > n) { + Element leftover = labelChildren.remove(labelChildren.size() - 1); + if (leftover != null) { + deactivateChild(leftover); + } + } + while (iconChildren.size() < n) { + iconChildren.add(null); + } + while (labelChildren.size() < n) { + labelChildren.add(null); + } + long current = bar().getCurrentIndex(); + for (int i = 0; i < n; i++) { + boolean selected = i == current; + BottomNavigationBarItem item = items.get(i); + iconChildren.set(i, updateChild(iconChildren.get(i), iconWidgetFor(item, selected), 2 * i)); + labelChildren.set(i, updateChild(labelChildren.get(i), labelWidgetFor(item, selected), 2 * i + 1)); + } + overlayChild = updateChild(overlayChild, new NavOverlay(), 2 * n); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + int n = Math.max(iconChildren.size(), labelChildren.size()); + for (int i = 0; i < n; i++) { + if (i < iconChildren.size() && iconChildren.get(i) != null) { + visitor.call(iconChildren.get(i)); + } + if (i < labelChildren.size() && labelChildren.get(i) != null) { + visitor.call(labelChildren.get(i)); + } + } + if (overlayChild != null) { + visitor.call(overlayChild); + } + } + + /** + * The render element of item {@code i}'s icon (test hook). + */ + public RenderElement iconRenderElement(int i) { + return RenderElement.findRenderElement(iconChildren.get(i)); + } + + /** + * Fires onTap with the item index (public so tests / the overlay can + * drive it). + */ + public void userTapped(int index) { + int n = items().size(); + if (index < 0 || index >= n) { + return; + } + Funcs.VoidFunc1 f = bar().getOnTap(); + if (f != null) { + f.call((long) index); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + int n = Math.max(1, items().size()); + double height = constraints.constrainHeight(Dp.px(HEIGHT_LP)); + double width = constraints.hasBoundedWidth() + ? constraints.maxWidth() + : n * Dp.px(FALLBACK_SLOT_WIDTH_LP); + double slotW = width / n; + double topPad = Dp.px(TOP_PAD_LP); + double labelGap = Dp.px(LABEL_GAP_LP); + + for (int i = 0; i < items().size(); i++) { + RenderElement icon = RenderElement.findRenderElement( + i < iconChildren.size() ? iconChildren.get(i) : null); + RenderElement label = RenderElement.findRenderElement( + i < labelChildren.size() ? labelChildren.get(i) : null); + BoxConstraints slotLoose = BoxConstraints.loose(slotW, height); + Size is = icon != null ? icon.layout(slotLoose) : Size.ZERO; + Size ls = label != null ? label.layout(slotLoose) : Size.ZERO; + double slotX = i * slotW; + if (icon != null) { + double iconY = label != null + ? topPad + : (height - is.height()) / 2; + setChildOffset(icon, slotX + (slotW - is.width()) / 2, iconY); + } + if (label != null) { + setChildOffset(label, slotX + (slotW - ls.width()) / 2, + topPad + is.height() + labelGap); + } + } + RenderElement overlay = RenderElement.findRenderElement(overlayChild); + if (overlay != null) { + overlay.layout(BoxConstraints.tight(width, height)); + setChildOffset(overlay, 0, 0); + } + return constraints.constrain(new Size(width, height)); + } + + // ------------------------------------------------------------------ + // Tap overlay (synthesized, mounts after the items) + // ------------------------------------------------------------------ + + static class NavOverlay extends Widget { + @Override + public Element createElement() { + return new NavOverlayElement(this); + } + } + + static class NavOverlayElement extends RenderElement { + + NavOverlayElement(NavOverlay widget) { + super(widget); + } + + private BottomNavigationBarRenderElement barElement() { + Element p = parent(); + return p instanceof BottomNavigationBarRenderElement + ? (BottomNavigationBarRenderElement) p : null; + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + return new OverlayComponent(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + // the parent hands us tight constraints matching the bar bounds + return constraints.smallest(); + } + + class OverlayComponent extends Component { + + OverlayComponent() { + setUIID("FlutterGesture"); + setGrabsPointerEvents(true); + setFocusable(false); + getAllStyles().setBgTransparency(0); + getAllStyles().setPadding(0, 0, 0, 0); + getAllStyles().setMargin(0, 0, 0, 0); + } + + @Override + public void paint(Graphics g) { + // paints nothing — pure hit area + } + + @Override + public void pointerReleased(int x, int y) { + boolean wasDrag = isDragActivated(); + super.pointerReleased(x, y); + if (wasDrag || !contains(x, y)) { + return; + } + BottomNavigationBarRenderElement bar = barElement(); + if (bar == null || getWidth() <= 0) { + return; + } + int n = bar.items().size(); + if (n == 0) { + return; + } + int index = (x - getAbsoluteX()) * n / getWidth(); + bar.userTapped(Math.max(0, Math.min(n - 1, index))); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarType.java new file mode 100644 index 00000000000..20a3b48f4a5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomNavigationBarType.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Defines the layout and behavior of a {@link BottomNavigationBar} — Flutter's + * {@code BottomNavigationBarType}. {@link #fixed} keeps all destinations the + * same size and always labelled; {@link #shifting} enlarges the selected + * destination and hides the unselected labels. + */ +public enum BottomNavigationBarType { + fixed, shifting +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheet.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheet.java new file mode 100644 index 00000000000..ec9a15339e1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheet.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +import dart.runtime.Funcs; + +/** + * A material bottom sheet surface — Flutter's {@code BottomSheet}. Renders the + * widget produced by {@code builder(context)}; the drag-to-dismiss gesture that + * fires {@code onClosing} is deferred. + */ +public class BottomSheet extends StatelessWidget { + + private Funcs.Func1 builder; + private Funcs.VoidFunc0 onClosing; + private boolean enableDrag = true; + private Color backgroundColor; + + public void animationController(Object v) { + } + + public void enableDrag(boolean v) { + this.enableDrag = v; + } + + public void onClosing(Funcs.VoidFunc0 v) { + this.onClosing = v; + } + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void elevation(double v) { + } + + public void shape(Object v) { + } + + public void clipBehavior(Clip v) { + } + + public void constraints(Object v) { + } + + @Override + public Widget build(BuildContext context) { + if (builder != null) { + Widget w = builder.call(context); + if (w != null) { + return w; + } + } + return new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheetThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheetThemeData.java new file mode 100644 index 00000000000..6f1785841a6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheetThemeData.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +/** + * The theme overrides for bottom sheets — Flutter's {@code BottomSheetThemeData}. + * Configuration only; consumed when a bottom sheet is shown. + */ +public class BottomSheetThemeData { + + private Color backgroundColor; + private Color modalBackgroundColor; + private Double elevation; + private Double modalElevation; + private Object shape; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void modalBackgroundColor(Color v) { + this.modalBackgroundColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void modalElevation(double v) { + this.modalElevation = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public Color backgroundColor() { + return backgroundColor; + } + + public Color modalBackgroundColor() { + return modalBackgroundColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheets.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheets.java new file mode 100644 index 00000000000..acd5d89e6fa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/BottomSheets.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; + +import dart.async.Future; +import dart.runtime.Funcs; + +/** + * Host class for Dart's top-level {@code showModalBottomSheet} function. This + * milestone is a bookkeeping stub: it returns an already-completed + * {@link Future} (the modal sheet is dismissed immediately) so a non-awaited + * {@code showModalBottomSheet(...)} call transpiles and runs to completion. The + * modal presentation of {@code builder}'s widget tree is deferred; wiring it to + * a CN1 {@code Dialog} the way {@link Dialogs} does is a follow-up. + */ +public final class BottomSheets { + + private BottomSheets() { + } + + public static Future showModalBottomSheet(BuildContext context, + Funcs.Func1 builder, + Color backgroundColor, + Double elevation, + Object shape, + Clip clipBehavior, + Object constraints, + Color barrierColor, + Boolean isScrollControlled, + Boolean useRootNavigator, + Boolean isDismissible, + Boolean enableDrag, + Boolean showDragHandle, + Object routeSettings, + Object transitionAnimationController) { + return Future.value(null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonBase.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonBase.java new file mode 100644 index 00000000000..68dab3a5466 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonBase.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Shared configuration of the material buttons ({@link ElevatedButton}, + * {@link TextButton}, {@link OutlinedButton}): a press callback (null means + * disabled) and a content child consumed as the button's label or icon. + */ +public abstract class ButtonBase extends Widget { + + private Funcs.VoidFunc0 onPressed; + private Widget child; + private ButtonStyle style; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void child(Widget v) { + this.child = v; + } + + public void style(ButtonStyle v) { + this.style = v; + } + + public Funcs.VoidFunc0 getOnPressed() { + return onPressed; + } + + public Widget getChild() { + return child; + } + + public ButtonStyle getStyle() { + return style; + } + + @Override + public Element createElement() { + return new ButtonRenderElement(this); + } + + /** + * The icon a {@code .icon} factory was given, beside the label. + * + *

Both were accepted and only one kept, so every icon button in the app + * was a plain label -- the button demo's "+ BUTTON" pair came up as two more + * "BUTTON"s. It is held HERE rather than composed into a Row with the label + * because a button whose child is a row is mounted as a subtree, and the + * button's own foreground then never reaches the text inside it: the label + * came out black where the reference paints it in the button's colour. + * Codename One's Button draws a glyph beside its text natively.

+ */ + private com.codename1.flutter.Widget leadingIcon; + + public void leadingIcon(com.codename1.flutter.Widget v) { + this.leadingIcon = v; + } + + public com.codename1.flutter.Widget getLeadingIcon() { + return leadingIcon; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonRenderElement.java new file mode 100644 index 00000000000..efd73895d64 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonRenderElement.java @@ -0,0 +1,620 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.widgets.Text; +import com.codename1.io.Log; +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.FontImage; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.plaf.Border; +import com.codename1.ui.plaf.RoundBorder; + +/** + * Shared leaf render box for the material buttons (ElevatedButton, + * TextButton, OutlinedButton, IconButton), owning a CN1 {@link Button}. The + * content widget is consumed as configuration rather than mounted: a + * {@link Text} child becomes the button label, an {@link Icon} child the + * material icon; any other widget falls back to its {@code toString()} with + * a log warning. A null {@code onPressed} disables the button. + * + *

Styling is programmatic Material 3 on top of the base theme, with + * colors derived from the nearest MaterialApp ThemeData's ColorScheme + * (Theme.of): ElevatedButton is a primary-filled capsule, TextButton + * borderless primary text, OutlinedButton a 1lp-outline capsule, IconButton + * a bare icon.

+ */ +public class ButtonRenderElement extends RenderElement { + + /** Flutter's default icon-button glyph size in logical pixels. */ + public static final double DEFAULT_ICON_SIZE_LP = 24; + private static final double CAPSULE_HPAD_LP = 24; + private static final double CAPSULE_VPAD_LP = 10; + + public ButtonRenderElement(Widget widget) { + super(widget); + } + + // ------------------------------------------------------------------ + // Configuration accessors (per widget kind) + // ------------------------------------------------------------------ + + /** Test hook: the handler a press would run, or null when the button is disabled. */ + public dart.runtime.Funcs.VoidFunc0 pressHandler() { + return onPressed(); + } + + /** What a press does. Overridden by buttons that act rather than call back. */ + protected dart.runtime.Funcs.VoidFunc0 onPressed() { + Widget w = widget(); + if (w instanceof ButtonBase) { + return ((ButtonBase) w).getOnPressed(); + } + return ((IconButton) w).getOnPressed(); + } + + /** The widget the button draws. Overridden by buttons with their own trigger. */ + protected Widget contentWidget() { + return unwrapToLeaf(rawContentWidget()); + } + + /** + * The content child exactly as written, without the walk to a leaf. Overridden -- not + * {@link #contentWidget()} -- by buttons with a trigger of their own, so both the + * consumed form and the mounted form come from one place. + */ + protected Widget rawContentWidget() { + Widget w = widget(); + return w instanceof ButtonBase + ? ((ButtonBase) w).getChild() + : ((IconButton) w).getIcon(); + } + + /** + * Whether this button's content is a real widget subtree rather than something the + * button can swallow into its own text or glyph. + * + *

A button draws a label and an icon, so a child that is one of those is CONSUMED: + * cheaper, and it lets the button's own style reach the text. Anything else has to be + * mounted and laid out like any other subtree, and that is not an exotic case -- the + * compose page's account row is a PopupMenuButton whose child is a Row of an address + * and a caret. Consumed, it could be reduced to neither a string nor a glyph, so the + * button drew nothing at all and the row was a blank band on the page.

+ */ + protected boolean isCompositeContent() { + Widget raw = rawContentWidget(); + if (raw == null) { + return false; + } + Widget leaf = contentWidget(); + if (!(leaf instanceof Text) && !(leaf instanceof Icon)) { + return true; + } + // ...and a leaf reached THROUGH a widget that has its own behaviour has + // to be mounted too, however drawable it is. + // + // Consuming the glyph skips the wrapper entirely: it is never mounted, + // so its State never initialises and whatever it exists to do never + // happens. The gallery's demo pages wrap the options icon in a + // FeatureDiscovery whose whole job is to insert a coach mark on first + // view; the button rendered its icon perfectly and the coach mark was + // never built, because the widget that builds it was never alive. + return hasStatefulWrapper(rawContentWidget(), leaf); + } + + /** + * Whether reaching {@code leaf} from {@code raw} passes through a widget + * with a State of its own. A pure layout wrapper (Padding, Center, a + * SizedBox) has no behaviour to lose and is still consumed. + */ + private boolean hasStatefulWrapper(Widget raw, Widget leaf) { + // The SAME walk unwrapToLeaf makes, so the two cannot disagree about + // what lies between the button and its glyph. + Widget w = raw; + for (int depth = 0; depth < 6 && w != null && w != leaf; depth++) { + if (w instanceof com.codename1.flutter.StatefulWidget) { + return true; + } + w = com.codename1.flutter.WidgetPreview.step(w, this); + } + return false; + } + + /** + * Looks THROUGH wrapper and composed widgets for the Text or Icon a button can actually + * render. + * + *

A button consumes its content rather than mounting it, so anything that is not + * literally a Text or an Icon used to fall through to {@code toString()} and be drawn as + * a label — the gallery's back button ({@code IconButton(icon: BackButtonIcon())}) + * rendered the string "com.codename1.flutter.material.BackButtonIcon@1a2b3c", clipped by + * the bar to a baffling "com.c". The same applied to the demo pages' options button, + * whose icon is wrapped in a FeatureDiscovery.

+ * + *

Composed widgets are built here to see what they produce. That is safe for the + * icon-shaped widgets this reaches — they are pure {@code build} methods returning a + * glyph — and it is bounded: the walk gives up after a few levels and any failure + * returns the original widget, restoring the previous behaviour.

+ */ + private Widget unwrapToLeaf(Widget content) { + Widget cur = content; + for (int depth = 0; depth < 6 && cur != null; depth++) { + if (cur instanceof Text || cur instanceof Icon) { + return cur; + } + Widget next = com.codename1.flutter.WidgetPreview.step(cur, this); + if (next == null) { + return content; + } + cur = next; + } + return cur == null ? content : cur; + } + + /** + * Whether this renders as a bare glyph rather than a capsule with a label — true for + * IconButton and for anything else whose trigger is an icon (a popup menu button). + */ + protected boolean isIconButton() { + return !(widget() instanceof ButtonBase); + } + + /** + * The label text consumed from a {@link Text} content child, a + * {@code toString()} fallback for unsupported content (with a log + * warning), or null when the content is an icon or absent. + */ + /// The colour the consumed Text asked for, or null when it asked for none. + private com.codename1.flutter.Color consumedLabelColor() { + Widget c = contentWidget(); + if (!(c instanceof Text)) { + return null; + } + com.codename1.flutter.TextStyle ts = ((Text) c).getStyle(); + return ts == null ? null : ts.getColor(); + } + + /// The colour the consumed Icon asked for, or null when it asked for none. + /// + /// The icon's own colour was never consulted -- only the button's, then the + /// ambient IconTheme -- so an Icon that states one lost it and wore the + /// bar's instead. The demo pages' settings glyph is the case that shows: + /// it asks for the same near-black as the info and code glyphs beside it + /// and came out in the primary red of the back chevron. + private com.codename1.flutter.Color consumedIconColor() { + Widget c = contentWidget(); + if (!(c instanceof Icon)) { + return null; + } + return ((Icon) c).getColor(); + } + + public String consumedLabel() { + Widget c = contentWidget(); + if (c instanceof Text) { + String d = ((Text) c).getData(); + return d == null ? "" : d; + } + if (c == null || c instanceof Icon || isCompositeContent()) { + return null; + } + try { + Log.p("Flutter runtime: " + widget().getClass().getSimpleName() + + " child " + c.getClass().getSimpleName() + + " is neither a Text nor an Icon and could not be resolved to one;" + + " the button renders no label"); + } catch (Throwable t) { + // headless: Log has no storage backend + } + // Deliberately NOT the widget's toString(). A Java class name is never a label + // anyone meant to show, and printing one puts "com.codename1.flutter…" in the middle + // of the app bar - which is exactly how this failure used to present. + return null; + } + + /** + * The material glyph consumed from an {@link Icon} content child, or 0. + */ + public char consumedIconChar() { + Widget c = contentWidget(); + if (c instanceof Icon && ((Icon) c).getIcon() != null) { + return ((Icon) c).getIcon().codePoint(); + } + // ...or the icon a .icon factory put beside the label. See + // ButtonBase.leadingIcon. + Widget lead = leadingIconWidget(); + if (lead instanceof Icon && ((Icon) lead).getIcon() != null) { + return ((Icon) lead).getIcon().codePoint(); + } + return 0; + } + + /** The icon set beside the label, looked through any wrapper. */ + private Widget leadingIconWidget() { + if (!(widget() instanceof ButtonBase)) { + return null; + } + return ((ButtonBase) widget()).getLeadingIcon(); + } + + private double iconSizeLp() { + Widget c = contentWidget(); + if (c instanceof Icon && ((Icon) c).getSize() != null) { + return ((Icon) c).getSize(); + } + if (widget() instanceof IconButton && ((IconButton) widget()).getIconSize() != null) { + return ((IconButton) widget()).getIconSize(); + } + return DEFAULT_ICON_SIZE_LP; + } + + private String uiid() { + Widget w = widget(); + if (w instanceof ElevatedButton) { + return "FlutterElevatedButton"; + } + if (w instanceof TextButton) { + return "FlutterTextButton"; + } + if (w instanceof OutlinedButton) { + return "FlutterOutlinedButton"; + } + return "FlutterIconButton"; + } + + // ------------------------------------------------------------------ + // Component + // ------------------------------------------------------------------ + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Button b = new Button(); + b.setUIID(uiid()); + // The listener reads the CURRENT widget config so onPressed updates + // never require listener rewiring. + b.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + dart.runtime.Funcs.VoidFunc0 f = onPressed(); + if (f != null) { + f.call(); + } + } + }); + apply(b); + return b; + } + + @Override + protected void updateComponent(Component c) { + apply((Button) c); + } + + private void apply(Button b) { + // style first: the material icon glyph derives its color from the + // button's foreground style + style(b); + String label = consumedLabel(); + char glyph = consumedIconChar(); + b.setText(label == null ? "" : label); + if (glyph != 0) { + try { + FontImage.setMaterialIcon(b, glyph, Dp.mm(iconSizeLp())); + } catch (Exception err) { + // missing icon font: layout still reserves the box + } + } else { + b.setIcon(null); + } + b.setEnabled(onPressed() != null); + // AFTER setEnabled, not inside style(): style() runs first, when the + // button is still enabled by default, so a disabled-ink pass in there + // returned early every time and the bottom row of the button demo + // stayed the same purple as the top row. + try { + applyDisabledInk(b, Theme.of(this).colorScheme()); + } catch (Exception noTheme) { + // an unthemed button keeps its base look + } + } + + private void style(Button b) { + try { + ColorScheme cs = Theme.of(this).colorScheme(); + Widget w = widget(); + com.codename1.ui.plaf.Style all = b.getAllStyles(); + // the derived theme style may use millimeter units; without + // pinning to pixels our paddings get reinterpreted as mm (18x!) + all.setPaddingUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + int hpad = (int) Math.round(Dp.px(CAPSULE_HPAD_LP)); + int vpad = (int) Math.round(Dp.px(CAPSULE_VPAD_LP)); + double shapeR = requestedRadiusLp(); + if (shapeR > 0) { + // A style that NAMES a shape wins over the role's capsule. + // Shrine's NEXT asks for a 7dp bevelled rectangle and came out + // a full pill, because ButtonStyle.shape was held opaquely and + // never read. + applyRequestedShape(b, all, cs, w, shapeR, vpad, hpad); + } else if (w instanceof ElevatedButton) { + // Material 3's elevated button is a PALE face with a coloured label: + // surfaceContainerLow behind, primary on it. This painted the primary + // colour as the face and onPrimary as the label, which is the FILLED + // button's scheme -- so every elevated button in the gallery came out a + // solid block of accent where the reference has a tinted card. The + // dialog demo's SHOW DIALOG is the clearest case: solid purple against + // the reference's lavender with purple lettering. + all.setPadding(vpad, vpad, hpad, hpad); + all.setFgColor(cs.primary().rgb()); + all.setBorder(RoundBorder.create() + .rectangle(true) + .color(cs.surfaceContainerLow().rgb()) + .shadowOpacity(40)); + clearBackground(all); + } else if (w instanceof OutlinedButton) { + all.setPadding(vpad, vpad, hpad, hpad); + all.setFgColor(cs.primary().rgb()); + all.setBorder(RoundBorder.create() + .rectangle(true) + .opacity(0) + .stroke(Dp.mm(0.3), true) + .strokeColor(cs.primary().rgb()) + .strokeOpacity(160)); + clearBackground(all); + } else if (w instanceof TextButton) { + all.setPadding(vpad, vpad, hpad / 2, hpad / 2); + all.setFgColor(cs.primary().rgb()); + all.setBorder(Border.createEmpty()); + clearBackground(all); + } else { + // IconButton (and other glyph triggers): bare glyph + int pad = (int) Math.round(Dp.px(8)); + all.setPadding(pad, pad, pad, pad); + // Flutter's order: the button's own colour, then the ambient + // IconTheme, then the default ink. The middle step was missing, + // so an icon button in a themed app bar came out onSurface — + // a black back arrow on a bar whose theme asks for white. + com.codename1.flutter.Color tint = w instanceof IconButton + ? ((IconButton) w).getColor() : null; + if (tint == null) { + tint = IconTheme.of(this).color(); + } + all.setFgColor(tint != null ? tint.rgb() : cs.onSurface().rgb()); + all.setBorder(Border.createEmpty()); + clearBackground(all); + } + // The consumed Text's OWN colour, last, because it is the most specific + // thing anyone said about this label. + // + // A button takes its child's string and drops the style that came with it, so + // a label that states a colour lost it and wore the button role's instead. + // Shrine's CANCEL says onSurface and came out in the pale pink its role gives + // it, against the reference's near-black. + com.codename1.flutter.Color own = consumedLabelColor(); + if (own == null) { + own = consumedIconColor(); + } + if (own != null) { + all.setFgColor(own.rgb()); + } + } catch (Exception err) { + // styling is best-effort; the base theme look remains + } + } + + /** + * Greys a disabled button's label. + * + *

{@code setEnabled(false)} was already being called, but the colours + * above go on through {@code getAllStyles()}, which reaches the DISABLED + * style too -- so a disabled button was painted in the same full-strength + * primary as an enabled one and the button demo's bottom row was + * indistinguishable from its top row. Material paints a disabled label as + * onSurface at 38%, and Codename One's foreground is opaque, so the tint is + * composited against the surface here.

+ */ + private static void applyDisabledInk(Button b, ColorScheme cs) { + if (b.isEnabled() || cs == null || cs.onSurface() == null) { + return; + } + com.codename1.flutter.Color ink = cs.onSurface(); + com.codename1.flutter.Color behind = cs.surface() != null + ? cs.surface() : new com.codename1.flutter.Color(0xFFFFFFFFL); + com.codename1.flutter.Color faded = com.codename1.flutter.Color.alphaBlend( + new com.codename1.flutter.Color( + (0x61L << 24) | (ink.rgb() & 0xFFFFFFL)), + new com.codename1.flutter.Color(0xFF000000L | (behind.rgb() & 0xFFFFFF))); + b.getDisabledStyle().setFgColor(faded.rgb()); + } + + /** + * Makes a style's background truly absent, in every state. + * + *

{@code setBgTransparency(0)} alone does not: it silences the background COLOUR + * and leaves any background IMAGE painting. Codename One themes routinely give a + * button a gradient image for its pressed and selected states, and + * {@code getAllStyles()} reaches all four -- so an icon button on a dark bar flashed a + * pale panel when touched or focused and kept it while that state held. Flutter's + * icon button has no background of its own in any state; its feedback is the ink.

+ */ + private static void clearBackground(com.codename1.ui.plaf.Style all) { + all.setBgTransparency(0); + all.setBgImage(null); + all.setBackgroundType(com.codename1.ui.plaf.Style.BACKGROUND_NONE); + } + + /** Flutter's {@code kMinInteractiveDimension}. */ + private static final double MIN_INTERACTIVE_LP = 48; + + private com.codename1.flutter.Element contentChild; + + @Override + protected void syncChildren() { + // Mounted only in composite mode. Passing null the rest of the time is what + // UNMOUNTS the subtree when a rebuild turns a composite child into a plain Text. + contentChild = updateChild(contentChild, + isCompositeContent() ? rawContentWidget() : null, 0); + } + + @Override + public void visitChildren(dart.runtime.Funcs.VoidFunc1 v) { + if (contentChild != null) { + v.call(contentChild); + } + } + + /** + * Makes the mounted content transparent to touch so the press lands on the button + * underneath it. + * + *

The host is one flat container and the content's components attach AFTER the + * button's, so they sit above it and CN1 hands them the press -- a Label, which does + * nothing with it. The button is then unreachable through its own face. Anything that + * is a button in its own right is left alone: a nested one is entitled to the press.

+ */ + private void passPointerThrough(com.codename1.flutter.Element e) { + if (e == null) { + return; + } + if (e instanceof ButtonRenderElement) { + return; + } + if (e instanceof RenderElement) { + Component c = ((RenderElement) e).component(); + if (c != null && !(c instanceof Button)) { + c.setIgnorePointerEvents(true); + } + } + e.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(com.codename1.flutter.Element child) { + passPointerThrough(child); + } + }); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Component c = component(); + if (c == null) { + return constraints.smallest(); + } + RenderElement composite = contentChild == null ? null : findRenderElement(contentChild); + if (composite != null) { + // The subtree measures itself and the button is exactly as big as it. The + // minimum tap targets below belong to a button that draws its OWN label at its + // own padding; a mounted child already carries whatever padding it was given, + // and forcing 48lp onto it moved the compose page's account row off its rule. + Size cs = composite.layout(constraints); + setChildOffset(composite, 0, 0); + passPointerThrough(contentChild); + return constraints.constrain(cs); + } + Dimension d = c.getPreferredSize(); + double w = d.getWidth(); + double h = d.getHeight(); + if (isIconButton()) { + // Flutter's IconButton carries BoxConstraints(minWidth: minHeight: + // kMinInteractiveDimension) -- 48 logical pixels, the minimum touch target. + // Without it an icon button was only as big as its glyph and its padding: 40 + // here, and every strip built out of them came up short. The 2D + // transformations demo's footer is a row of two, so its bar measured 56 + // where the reference measures 64, and the board centred in the space that + // left sat 16 device pixels low -- most of that route's difference was this. + w = Math.max(w, Dp.px(MIN_INTERACTIVE_LP)); + h = Math.max(h, Dp.px(MIN_INTERACTIVE_LP)); + } else { + // Material 3's button minimum is 64x40, not the 36 of the 2018 spec. + w = Math.max(w, Dp.px(64)); + h = Math.max(h, Dp.px(40)); + } + return constraints.constrain(new Size(w, h)); + } + + /** + * The corner radius the widget's {@code ButtonStyle.shape} asks for, in + * logical pixels, or 0 when it names none. + * + *

Both of Material's angular shapes are honoured the same way: Codename + * One draws one rounded rectangle, so a bevelled border renders with + * rounded corners of the same radius rather than mitred ones. Closer than a + * capsule, which is what it was.

+ */ + private double requestedRadiusLp() { + if (!(widget() instanceof ButtonBase)) { + return 0; + } + ButtonStyle st = ((ButtonBase) widget()).getStyle(); + Object shape = st == null ? null : st.getShape(); + Object radius = null; + if (shape instanceof com.codename1.flutter.RoundedRectangleBorder) { + radius = ((com.codename1.flutter.RoundedRectangleBorder) shape).getBorderRadius(); + } else if (shape instanceof com.codename1.flutter.BeveledRectangleBorder) { + radius = ((com.codename1.flutter.BeveledRectangleBorder) shape).getBorderRadius(); + } + if (!(radius instanceof com.codename1.flutter.BorderRadius)) { + return 0; + } + com.codename1.flutter.Radius tl = + ((com.codename1.flutter.BorderRadius) radius).topLeft(); + return tl == null ? 0 : tl.x(); + } + + /** Paints the button with the radius its style asked for, in its role's colours. */ + private void applyRequestedShape(Button b, com.codename1.ui.plaf.Style all, + ColorScheme cs, Widget w, double radiusLp, int vpad, int hpad) { + all.setPadding(vpad, vpad, hpad, hpad); + com.codename1.ui.plaf.RoundRectBorder border = + com.codename1.ui.plaf.RoundRectBorder.create() + .useCache(false) + .cornerRadius(Dp.mm(radiusLp)); + if (w instanceof OutlinedButton) { + all.setFgColor(cs.primary().rgb()); + border = border.stroke(Dp.mm(0.3), true).strokeColor(cs.primary().rgb()) + .strokeOpacity(160); + clearBackground(all); + } else if (w instanceof TextButton) { + all.setFgColor(cs.primary().rgb()); + clearBackground(all); + } else { + all.setFgColor(cs.primary().rgb()); + all.setBgColor(cs.surfaceContainerLow().rgb()); + all.setBgTransparency(255); + } + all.setBorder(border); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonStyle.java new file mode 100644 index 00000000000..672badd71d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ButtonStyle.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.TextStyle; + +/** + * The visual overrides a material button applies on top of its theme + * defaults. Only produced by the buttons' {@code styleFrom} factory (see + * {@link ElevatedButton#styleFrom}, {@link TextButton#styleFrom}, + * {@link OutlinedButton#styleFrom}) and consumed opaquely through the buttons' + * {@code style:} parameter. + * + *

The visual properties the render layer models — foreground/background + * color, padding, elevation — are captured as flat fields; the geometric + * overrides (side, shape, alignment, tap-target size, visual density) are held + * opaquely for API shape in this pass.

+ */ +public class ButtonStyle { + + private Color foregroundColor; + private Color backgroundColor; + private Color shadowColor; + private Double elevation; + private TextStyle textStyle; + private EdgeInsets padding; + private Object side; + private Object shape; + private Object alignment; + private Object tapTargetSize; + private Object visualDensity; + + /** + * Builds a ButtonStyle from the flat overrides. Parameter order matches the + * {@code styleFrom} Dart stub shared by the three button classes. + */ + public static ButtonStyle styleFrom(Color foregroundColor, Color backgroundColor, Color shadowColor, + Double elevation, TextStyle textStyle, EdgeInsets padding, + Object side, Object shape, Object alignment, Object tapTargetSize, + Object visualDensity) { + ButtonStyle s = new ButtonStyle(); + s.foregroundColor = foregroundColor; + s.backgroundColor = backgroundColor; + s.shadowColor = shadowColor; + s.elevation = elevation; + s.textStyle = textStyle; + s.padding = padding; + s.side = side; + s.shape = shape; + s.alignment = alignment; + s.tapTargetSize = tapTargetSize; + s.visualDensity = visualDensity; + return s; + } + + public Color getForegroundColor() { + return foregroundColor; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + public Color getShadowColor() { + return shadowColor; + } + + public Double getElevation() { + return elevation; + } + + public TextStyle getTextStyle() { + return textStyle; + } + + public EdgeInsets getPadding() { + return padding; + } + + /** The requested outline shape, or null when the role's default applies. */ + public Object getShape() { + return shape; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Card.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Card.java new file mode 100644 index 00000000000..3b633b32748 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Card.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.ShapeBorder; +import com.codename1.flutter.Widget; + +/** + * A material card: a rounded, subtly elevated surface around its child. + * Backed by a CN1 Container (UIID "FlutterCard") with a 12lp round-rect + * border; default margin 4lp on every edge. + */ +public class Card extends Widget { + + private Color color; + private Double elevation; + private EdgeInsets margin; + private Widget child; + private ShapeBorder shape; + private Clip clipBehavior; + + public void color(Color v) { + this.color = v; + } + + public void shape(ShapeBorder v) { + this.shape = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public ShapeBorder getShape() { + return shape; + } + + public Clip getClipBehavior() { + return clipBehavior; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void margin(EdgeInsets v) { + this.margin = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Color getColor() { + return color; + } + + public Double getElevation() { + return elevation; + } + + public EdgeInsets getMargin() { + return margin; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new CardRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardRenderElement.java new file mode 100644 index 00000000000..fd3dd3cc73c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardRenderElement.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.plaf.RoundRectBorder; + +/** + * Render element for {@link Card}: a Container (UIID "FlutterCard") styled + * programmatically with a 12lp-corner round-rect border and a subtle shadow + * scaled from the elevation (best effort on CN1's shadow model). Layout is + * padding-like: the child is inset by the margin (default 4lp) and the card + * face component covers the element bounds minus the margin band — so the + * child's components, attached after the face in tree order, paint on top of + * it. + */ +public class CardRenderElement extends SingleChildRenderElement { + + /** Flutter's default card margin in logical pixels. */ + public static final double DEFAULT_MARGIN_LP = 4; + /** Material 3 card corner radius in logical pixels. */ + public static final double CORNER_LP = 12; + + private EdgeInsets marginPx = EdgeInsets.all(0); + + public CardRenderElement(Card widget) { + super(widget); + } + + private Card card() { + return (Card) widget(); + } + + @Override + protected Widget childWidget() { + return card().getChild(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Container face = new Container(); + face.setUIID("FlutterCard"); + face.getAllStyles().setPadding(0, 0, 0, 0); + face.getAllStyles().setMargin(0, 0, 0, 0); + applyStyle(face); + return face; + } + + @Override + protected void updateComponent(Component c) { + applyStyle(c); + } + + /// The inputs the current border was built from, so a rebuild that changes + /// neither reuses it. + /// + /// RoundRectBorder caches its rendered shadow against a client property keyed + /// by the BORDER INSTANCE, so handing the component a new instance on every + /// rebuild misses that cache every time -- and a miss on a shadowed border is + /// a full offscreen render plus a gaussian blur, plus one more client + /// property that is never read again. Rebuilding the style is cheap; its + /// border is not. + private int styledBg = -1; + private double styledElevation = -1; + private com.codename1.ui.plaf.Border styledBorder; + + private void applyStyle(Component face) { + try { + int bg = card().getColor() != null + ? card().getColor().rgb() + : Theme.of(this).colorScheme().surface().rgb(); + double elevation = card().getElevation() != null ? card().getElevation() : 1; + if (styledBorder != null && styledBg == bg && styledElevation == elevation) { + face.getAllStyles().setBgColor(bg); + face.getAllStyles().setBgTransparency(255); + face.getAllStyles().setBorder(styledBorder); + return; + } + // useCache(true) is NOT a performance choice, it is a correctness one. + // + // With the cache off, RoundRectBorder renders through + // createTargetComponentImage, which translates the Graphics by the + // shadow's shape offset and returns without undoing it. That + // Graphics is the live one, so the offset leaks into everything + // painted after this card: the cards demo drifted 9 device pixels + // down and 4 across per card, accumulating down the page, while the + // LAYOUT was exactly the reference's -- the component tree reported + // the right boxes and the pixels were somewhere else. The cached + // path renders into an offscreen image of its own, so the same + // unbalanced translate is discarded with it. + RoundRectBorder border = RoundRectBorder.create() + .useCache(true) + .cornerRadius(Dp.mm(CORNER_LP)); + if (elevation > 0) { + border = border + .shadowOpacity(Math.min(255, (int) Math.round(20 + elevation * 15))) + .shadowSpread((float) Math.min(3, 0.25f + elevation * 0.25f)) + .shadowY(1); + } + face.getAllStyles().setBgColor(bg); + face.getAllStyles().setBgTransparency(255); + face.getAllStyles().setBorder(border); + styledBg = bg; + styledElevation = elevation; + styledBorder = border; + } catch (Exception err) { + // styling is best-effort; layout must survive regardless + } + } + + private EdgeInsets marginLp() { + EdgeInsets m = card().getMargin(); + return m == null ? EdgeInsets.all(DEFAULT_MARGIN_LP) : m; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + EdgeInsets lp = marginLp(); + marginPx = EdgeInsets.only(Dp.px(lp.left()), Dp.px(lp.top()), Dp.px(lp.right()), Dp.px(lp.bottom())); + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size(marginPx.horizontal(), marginPx.vertical())); + } + Size cs = child.layout(constraints.deflate(marginPx)); + setChildOffset(child, marginPx.left(), marginPx.top()); + return constraints.constrain(new Size( + cs.width() + marginPx.horizontal(), cs.height() + marginPx.vertical())); + } + + @Override + public void position(int x, int y) { + super.position(x, y); + Component face = component(); + if (face != null) { + // shrink the card face to exclude the margin band + face.setX(x + (int) Math.round(marginPx.left())); + face.setY(y + (int) Math.round(marginPx.top())); + face.setWidth(Math.max(0, (int) Math.round(size().width() - marginPx.horizontal()))); + face.setHeight(Math.max(0, (int) Math.round(size().height() - marginPx.vertical()))); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardTheme.java new file mode 100644 index 00000000000..6910654e3d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardTheme.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; + +/** + * Material {@code CardTheme}: write-once card styling. + */ +public class CardTheme { + + private Color color; + private Color shadowColor; + private Color surfaceTintColor; + private Double elevation; + private EdgeInsets margin; + private Object shape; + private Object clipBehavior; + + public void color(Color v) { + this.color = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void margin(EdgeInsets v) { + this.margin = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void clipBehavior(Object v) { + this.clipBehavior = v; + } + + public Color color() { + return color; + } + + public Double elevation() { + return elevation; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardThemeData.java new file mode 100644 index 00000000000..b4bea4de275 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CardThemeData.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; + +/** + * Material {@code CardThemeData}: the Material-3 rename of {@link CardTheme}; + * same write-once card styling shape. + */ +public class CardThemeData { + + private Color color; + private Color shadowColor; + private Color surfaceTintColor; + private Double elevation; + private EdgeInsets margin; + private Object shape; + private Object clipBehavior; + + public void color(Color v) { + this.color = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void margin(EdgeInsets v) { + this.margin = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void clipBehavior(Object v) { + this.clipBehavior = v; + } + + public Color color() { + return color; + } + + public Double elevation() { + return elevation; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Checkbox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Checkbox.java new file mode 100644 index 00000000000..1c60a1ea1b2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Checkbox.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material checkbox with CONTROLLED semantics: the widget's {@code value} + * is authoritative. A user toggle fires {@code onChanged(newValue)} and the + * component is snapped back to the configured value — the app's + * setState/rebuild is what actually moves the checkbox. Backed by a CN1 + * {@link com.codename1.ui.CheckBox} (UIID "FlutterCheckbox"). + */ +public class Checkbox extends Widget { + + private Boolean value; + private boolean tristate; + private Funcs.VoidFunc1 onChanged; + + public void value(Boolean v) { + this.value = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + /** + * Whether the checkbox has a third "indeterminate" state + * ({@code Checkbox.tristate}). + * + *

This used to be accepted and discarded, and a null value was collapsed + * to false -- so the selection-controls demo's indeterminate box rendered as + * an ordinary empty one and the dash the reference shows was nowhere in the + * app.

+ */ + public void tristate(boolean v) { + this.tristate = v; + } + + /** Whether the box is in its third, indeterminate state. */ + public boolean isIndeterminate() { + return tristate && value == null; + } + + /** True when the widget states no handler, i.e. the box is disabled. */ + public boolean isEnabled() { + return onChanged != null; + } + + /** + * The value a tap asks for, in Flutter's order: false to true, true to null + * when tristate (to false otherwise), and null back to false. Treating null + * as "go to checked" and true as "go to unchecked" meant a user could never + * reach the indeterminate state of a tristate box. + */ + public Boolean nextValue() { + if (value == null) { + return Boolean.FALSE; + } + if (!value.booleanValue()) { + return Boolean.TRUE; + } + return tristate ? null : Boolean.FALSE; + } + + public boolean getValue() { + return value != null && value.booleanValue(); + } + + public Funcs.VoidFunc1 getOnChanged() { + return onChanged; + } + + @Override + public Element createElement() { + return new CheckboxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxRenderElement.java new file mode 100644 index 00000000000..37933c7a519 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxRenderElement.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.CheckBox; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.geom.Dimension; + +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link Checkbox}: a CN1 CheckBox (UIID + * "FlutterCheckbox") with controlled semantics — the user's toggle fires + * {@code onChanged} and the component is immediately re-snapped to the + * widget's configured value; only a rebuild with a new value moves it. + * Material tap target: 48x48lp minimum. + */ +public class CheckboxRenderElement extends RenderElement { + + /** Material's checkbox glyph size in logical pixels. */ + private static final double GLYPH_LP = 18; + + /** Material minimum tap target in logical pixels. */ + public static final double TAP_TARGET_LP = 48; + + private boolean applying; + + public CheckboxRenderElement(Checkbox widget) { + super(widget); + } + + private Checkbox checkbox() { + return (Checkbox) widget(); + } + + /** + * The value the current widget configuration mandates. + */ + public boolean configuredValue() { + return checkbox().getValue(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + // A LABEL painting the Material glyph, not a CN1 CheckBox. + // + // Codename One's CheckBox takes its box from the look and feel's image + // pair, so it has exactly two states and no way to say which glyph to + // draw. Material has three -- checked, empty and indeterminate -- and + // the demo shows all three beside a disabled row of the same. Drawing + // the glyph directly is the only way to say all of that, and it also + // puts the tick in the theme's colour rather than the look and feel's. + com.codename1.ui.Label box = new com.codename1.ui.Label(""); + box.setUIID("FlutterCheckbox"); + box.getAllStyles().setPadding(0, 0, 0, 0); + box.getAllStyles().setMargin(0, 0, 0, 0); + box.getAllStyles().setBgTransparency(0); + box.setFocusable(true); + box.addPointerReleasedListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (applying || !checkbox().isEnabled()) { + return; + } + userToggled(checkbox().nextValue()); + } + }); + apply(box); + return box; + } + + @Override + protected void updateComponent(Component c) { + apply((com.codename1.ui.Label) c); + } + + private void apply(com.codename1.ui.Label box) { + applying = true; + try { + Checkbox w = checkbox(); + char glyph; + if (w.isIndeterminate()) { + glyph = com.codename1.ui.FontImage.MATERIAL_INDETERMINATE_CHECK_BOX; + } else if (configuredValue()) { + glyph = com.codename1.ui.FontImage.MATERIAL_CHECK_BOX; + } else { + glyph = com.codename1.ui.FontImage.MATERIAL_CHECK_BOX_OUTLINE_BLANK; + } + box.getAllStyles().setFgColor(inkRgb(w)); + box.setEnabled(w.isEnabled()); + try { + com.codename1.ui.FontImage.setMaterialIcon(box, glyph, Dp.mm(GLYPH_LP)); + } catch (Exception noIconFont) { + // missing icon font: the tap target is still laid out + } + } finally { + applying = false; + } + } + + /** Material's checkbox ink: the accent when on, an outline when off, faded when disabled. */ + private int inkRgb(Checkbox w) { + com.codename1.flutter.Color ink = null; + com.codename1.flutter.Color behind = null; + try { + com.codename1.flutter.material.ColorScheme cs = + com.codename1.flutter.material.Theme.of(this).colorScheme(); + if (cs != null) { + behind = cs.surface(); + boolean on = w.isIndeterminate() || configuredValue(); + ink = on ? cs.primary() : cs.onSurfaceVariant(); + if (!w.isEnabled() && cs.onSurface() != null) { + ink = com.codename1.flutter.Color.alphaBlend( + new com.codename1.flutter.Color( + (0x61L << 24) | (cs.onSurface().rgb() & 0xFFFFFFL)), + new com.codename1.flutter.Color(0xFF000000L + | ((behind == null ? 0xFFFFFF : behind.rgb()) & 0xFFFFFF))); + } + } + } catch (Throwable noTheme) { + // an unthemed checkbox still has to paint + } + return ink == null ? 0x6200EE : ink.rgb(); + } + + /** + * Controlled toggle entry point (public so headless tests can drive it): + * fires onChanged with the attempted value, then re-applies the widget's + * configured value to the component. + */ + public void userToggled(Boolean attemptedValue) { + Funcs.VoidFunc1 f = checkbox().getOnChanged(); + if (f != null) { + f.call(attemptedValue); + } + Component c = component(); + if (c != null) { + apply((com.codename1.ui.Label) c); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double min = Dp.px(TAP_TARGET_LP); + double w = min; + double h = min; + Component c = component(); + if (c != null) { + Dimension d = c.getPreferredSize(); + w = Math.max(w, d.getWidth()); + h = Math.max(h, d.getHeight()); + } + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxThemeData.java new file mode 100644 index 00000000000..e25f5bec3d8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckboxThemeData.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Material {@code CheckboxThemeData}: write-once checkbox styling. All values + * here are MaterialStateProperty / border / density objects owned by other + * runtime areas, so they are held as opaque {@code Object}s in this pass. + */ +public class CheckboxThemeData { + + private Object fillColor; + private Object checkColor; + private Object overlayColor; + private Object materialTapTargetSize; + private Object shape; + private Object side; + private Object visualDensity; + private Object mouseCursor; + private Object splashRadius; + + public void fillColor(Object v) { + this.fillColor = v; + } + + public void checkColor(Object v) { + this.checkColor = v; + } + + public void overlayColor(Object v) { + this.overlayColor = v; + } + + public void materialTapTargetSize(Object v) { + this.materialTapTargetSize = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void side(Object v) { + this.side = v; + } + + public void visualDensity(Object v) { + this.visualDensity = v; + } + + public void mouseCursor(Object v) { + this.mouseCursor = v; + } + + public void splashRadius(Object v) { + this.splashRadius = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckedPopupMenuItem.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckedPopupMenuItem.java new file mode 100644 index 00000000000..c459c747918 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CheckedPopupMenuItem.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * A {@link PopupMenuItem} that shows a check mark when {@code checked} — + * Flutter's {@code CheckedPopupMenuItem}. Inherits value/child/onTap + * handling from PopupMenuItem; the leading check-mark reveal is deferred, so + * this pass records the checked state for API shape. + * + * @param the value type carried by this menu item + */ +public class CheckedPopupMenuItem extends PopupMenuItem { + + private boolean checked; + + public void checked(boolean v) { + this.checked = v; + } + + public boolean isChecked() { + return checked; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Chip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Chip.java new file mode 100644 index 00000000000..f6e9512a48f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Chip.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A compact material chip carrying a label and optional avatar/delete icon — + * Flutter's {@code Chip}. This milestone renders the {@code avatar} and + * {@code label} in a horizontal {@link Row}; the rounded background, delete + * affordance and material styling are deferred. + */ +public class Chip extends StatelessWidget { + + private Widget avatar; + private Widget label; + private Widget deleteIcon; + private Color backgroundColor; + private Color deleteIconColor; + private TextStyle labelStyle; + private Funcs.VoidFunc0 onDeleted; + + public void avatar(Widget v) { + this.avatar = v; + } + + public void label(Widget v) { + this.label = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + } + + public void deleteIcon(Widget v) { + this.deleteIcon = v; + } + + public void onDeleted(Funcs.VoidFunc0 v) { + this.onDeleted = v; + } + + public void deleteIconColor(Color v) { + this.deleteIconColor = v; + } + + public void deleteButtonTooltipMessage(String v) { + } + + public void side(Object v) { + } + + public void shape(Object v) { + } + + public void clipBehavior(Clip v) { + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void padding(Object v) { + } + + public void visualDensity(Object v) { + } + + public void materialTapTargetSize(Object v) { + } + + public void elevation(double v) { + } + + public void shadowColor(Color v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (avatar != null) { + kids.add(avatar); + } + if (label != null) { + kids.add(label); + } + if (deleteIcon != null) { + kids.add(deleteIcon); + } + if (kids.size() == 0) { + return new SizedBox(); + } + Row row = new Row(); + row.mainAxisSize(MainAxisSize.min); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(kids); + + // A chip is a CAPSULE, not a row. Built as a bare row it was an avatar and some + // text loose on the page -- the compose page's recipient had no pill behind it at + // all, and neither did anything else that uses one. + com.codename1.flutter.BoxDecoration d = + new com.codename1.flutter.BoxDecoration(); + d.color(resolveBackground(context)); + d.borderRadius(com.codename1.flutter.BorderRadius.circular(HEIGHT_LP / 2)); + + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.decoration(d); + box.padding(labelPadding(context)); + // Height only: a chip is exactly 32 logical pixels tall and as wide as it needs. + box.constraints(new com.codename1.flutter.rendering.BoxConstraints( + 0, Double.POSITIVE_INFINITY, HEIGHT_LP, HEIGHT_LP)); + box.child(row); + return box; + } + + /** Material's chip height in logical pixels. */ + private static final double HEIGHT_LP = 32; + /** Material's default label padding, horizontal only. */ + private static final double LABEL_HPAD_LP = 8; + + private EdgeInsets labelPadding(BuildContext context) { + ChipThemeData t = chipTheme(context); + if (t != null && t.labelPadding() != null) { + return t.labelPadding(); + } + return EdgeInsets.symmetric(LABEL_HPAD_LP, 0); + } + + /// The chip's own colour, then the theme's, then Material's surface. + private Color resolveBackground(BuildContext context) { + if (backgroundColor != null) { + return backgroundColor; + } + ChipThemeData t = chipTheme(context); + if (t != null && t.backgroundColor() != null) { + return t.backgroundColor(); + } + try { + return Theme.of(context).colorScheme().surfaceVariant(); + } catch (Throwable err) { + return null; + } + } + + private ChipThemeData chipTheme(BuildContext context) { + try { + ThemeData t = Theme.of(context); + return t == null ? null : t.chipTheme(); + } catch (Throwable err) { + return null; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChipThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChipThemeData.java new file mode 100644 index 00000000000..75ce0ae1a98 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChipThemeData.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code ChipThemeData}: write-once chip styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class ChipThemeData { + + private Color backgroundColor; + private Color disabledColor; + private Color selectedColor; + private Color secondarySelectedColor; + private Color deleteIconColor; + private Color shadowColor; + private EdgeInsets padding; + private EdgeInsets labelPadding; + private Object shape; + private TextStyle labelStyle; + private TextStyle secondaryLabelStyle; + private Brightness brightness; + private Double elevation; + private Double pressElevation; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void disabledColor(Color v) { + this.disabledColor = v; + } + + public void selectedColor(Color v) { + this.selectedColor = v; + } + + public void secondarySelectedColor(Color v) { + this.secondarySelectedColor = v; + } + + public void deleteIconColor(Color v) { + this.deleteIconColor = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void labelPadding(EdgeInsets v) { + this.labelPadding = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void secondaryLabelStyle(TextStyle v) { + this.secondaryLabelStyle = v; + } + + public void brightness(Brightness v) { + this.brightness = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void pressElevation(double v) { + this.pressElevation = v; + } + + public Color backgroundColor() { + return backgroundColor; + } + + public EdgeInsets labelPadding() { + return labelPadding; + } + + public Color secondarySelectedColor() { + return secondarySelectedColor; + } + + public Brightness brightness() { + return brightness; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChoiceChip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChoiceChip.java new file mode 100644 index 00000000000..291d315888a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ChoiceChip.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A chip that lets the user choose one option from a set — Flutter's + * {@code ChoiceChip}. This milestone renders {@code avatar} + {@code label} in a + * {@link Row}; the selected-state styling and {@code onSelected} tap are + * deferred. + */ +public class ChoiceChip extends StatelessWidget { + + private Widget avatar; + private Widget label; + private boolean selected; + private Color backgroundColor; + private TextStyle labelStyle; + private Funcs.VoidFunc1 onSelected; + + public void avatar(Widget v) { + this.avatar = v; + } + + public void label(Widget v) { + this.label = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + } + + public void selected(boolean v) { + this.selected = v; + } + + public void onSelected(Funcs.VoidFunc1 v) { + this.onSelected = v; + } + + public void pressElevation(Object v) { + } + + public void disabledColor(Color v) { + } + + public void selectedColor(Color v) { + } + + public void tooltip(Object v) { + } + + public void side(Object v) { + } + + public void shape(Object v) { + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void padding(Object v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (avatar != null) { + kids.add(avatar); + } + if (label != null) { + kids.add(label); + } + if (kids.size() == 0) { + return new SizedBox(); + } + Row row = new Row(); + row.mainAxisSize(MainAxisSize.min); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(kids); + return row; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircleAvatar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircleAvatar.java new file mode 100644 index 00000000000..d797a4069a7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircleAvatar.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.ImageProvider; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +/** + * A circular avatar showing an image or a child (initials/icon) — Flutter's + * {@code CircleAvatar}. + * + *

It used to render the child and nothing else, so an avatar was a bare + * character where the design has a filled disc: every row of the lists demo + * showed a small black number instead of a purple circle with a white one. + * + *

The default colours approximate Flutter's, which resolve through + * {@code primaryColorLight} and {@code primaryTextTheme}; the colour scheme's + * primary pair is the closest thing this runtime models.

+ */ +public class CircleAvatar extends StatelessWidget { + + private Widget child; + private Color backgroundColor; + private Color foregroundColor; + private ImageProvider backgroundImage; + private ImageProvider foregroundImage; + private Double radius; + + public void child(Widget v) { + this.child = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void foregroundColor(Color v) { + this.foregroundColor = v; + } + + public void backgroundImage(ImageProvider v) { + this.backgroundImage = v; + } + + public void foregroundImage(ImageProvider v) { + this.foregroundImage = v; + } + + public void onBackgroundImageError(Object v) { + } + + public void radius(double v) { + this.radius = v; + } + + public void minRadius(double v) { + } + + public void maxRadius(double v) { + } + + /** Flutter's {@code _defaultRadius}. */ + private static final double DEFAULT_RADIUS = 20; + + @Override + public Widget build(BuildContext context) { + double r = radius != null ? radius : DEFAULT_RADIUS; + + Color bg = backgroundColor; + Color fg = foregroundColor; + if (bg == null || fg == null) { + try { + ColorScheme cs = Theme.of(context).colorScheme(); + if (bg == null) { + bg = cs.primary(); + } + if (fg == null) { + fg = cs.onPrimary(); + } + } catch (Throwable t) { + // no ambient theme; the disc still gets its shape below + } + } + + com.codename1.flutter.BoxDecoration decoration = + new com.codename1.flutter.BoxDecoration(); + decoration.shape(com.codename1.flutter.BoxShape.circle); + if (bg != null) { + decoration.color(bg); + } + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.width(r * 2); + box.height(r * 2); + box.decoration(decoration); + box.alignment(com.codename1.flutter.Alignment.center); + Widget content = imageChild() != null ? imageChild() : child; + if (content != null && fg != null) { + com.codename1.flutter.TextStyle style = new com.codename1.flutter.TextStyle(); + style.color(fg); + content = com.codename1.flutter.widgets.DefaultTextStyle.wrap(style, content); + content = IconTheme.tint(fg, content); + } + if (content != null) { + // The disc is a circular BACKGROUND; the picture drawn on top of it + // is a rectangle unless something clips it, which is why every + // avatar came out square over a round patch of colour. + com.codename1.flutter.widgets.ClipOval round = + new com.codename1.flutter.widgets.ClipOval(); + round.child(content); + box.child(round); + } + return box; + } + + /** The avatar's picture, when it has one, sized to fill the disc. */ + private Widget imageChild() { + ImageProvider provider = foregroundImage != null ? foregroundImage : backgroundImage; + if (provider == null) { + return null; + } + com.codename1.flutter.widgets.Image img = new com.codename1.flutter.widgets.Image(); + img.image(provider); + img.fit(com.codename1.flutter.BoxFit.cover); + return img; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularNotchedRectangle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularNotchedRectangle.java new file mode 100644 index 00000000000..ebb3e4f1ae1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularNotchedRectangle.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * A {@link NotchedShape} that cuts a circular notch with small flanking fillets + * — Flutter's {@code CircularNotchedRectangle}. Signature-only this pass. + */ +public class CircularNotchedRectangle extends NotchedShape { + + private boolean inverted; + + public void inverted(boolean v) { this.inverted = v; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicator.java new file mode 100644 index 00000000000..d6cf4db333d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicator.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +/** + * A circular material progress indicator — Flutter's {@code + * CircularProgressIndicator}. A determinate {@code value} or an indeterminate + * spin are accepted; this pass reserves a square box sized to the default + * indicator diameter, deferring the arc paint and spin animation. + */ +public class CircularProgressIndicator extends Widget { + + private Double value; + private Color color; + private Color backgroundColor; + private Double strokeWidth; + + public void value(double v) { + this.value = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void color(Color v) { + this.color = v; + } + + public void valueColor(Object v) { + } + + public void strokeWidth(double v) { + this.strokeWidth = v; + } + + public void semanticsLabel(String v) { + } + + public void semanticsValue(String v) { + } + + public Double getValue() { + return value; + } + + /** The stroke width, or null for the Material default. */ + public Double getStrokeWidth() { + return strokeWidth; + } + + /** The arc colour, or null to take the theme's primary. */ + public Color getColor() { + return color; + } + + /** The track colour behind the arc, or null when there is none. */ + public Color getBackgroundColor() { + return backgroundColor; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new CircularProgressIndicatorRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicatorRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicatorRenderElement.java new file mode 100644 index 00000000000..00128bf9ccb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CircularProgressIndicatorRenderElement.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; + +/** + * Leaf render box for {@link CircularProgressIndicator}: paints the arc. + * + *

The widget used to build an empty {@code SizedBox} -- it reserved the + * right amount of space and drew nothing at all, so the progress-indicator demo + * showed a blank page where the reference shows two spinning arcs.

+ * + *

A determinate indicator sweeps {@code value} of a full turn from twelve + * o'clock. An indeterminate one sweeps a fixed-length arc whose head advances + * with the clock, which is the part of Flutter's two-phase animation that reads + * as motion; the arc's length also breathes, and that is not reproduced here. + * Codename One measures arc angles anticlockwise from three o'clock, so both + * the start and the direction have to be converted.

+ */ +public class CircularProgressIndicatorRenderElement extends RenderElement { + + /** Material's default indicator diameter and stroke, in logical pixels. */ + private static final double DIAMETER_LP = 36; + private static final double STROKE_LP = 4; + /** The fraction of a full turn an indeterminate arc covers. */ + private static final double SWEEP_FRACTION = 0.75; + /** How long the indeterminate head takes to go once round, in milliseconds. */ + private static final long PERIOD_MS = 1400; + + public CircularProgressIndicatorRenderElement(CircularProgressIndicator widget) { + super(widget); + } + + private CircularProgressIndicator indicator() { + return (CircularProgressIndicator) widget(); + } + + private int strokePx() { + Double w = indicator().getStrokeWidth(); + return (int) Math.max(1, Math.round(Dp.px(w == null ? STROKE_LP : w.doubleValue()))); + } + + private int arcRgb() { + Color c = indicator().getColor(); + if (c != null) { + return c.rgb(); + } + try { + ThemeData t = Theme.of(this); + if (t != null && t.colorScheme() != null && t.colorScheme().primary() != null) { + return t.colorScheme().primary().rgb(); + } + } catch (Throwable noTheme) { + // an unthemed indicator still has to paint + } + return 0x6200EE; + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + return new Arc(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double d = Dp.px(DIAMETER_LP); + double w = constraints.hasBoundedWidth() && constraints.maxWidth() < d + ? constraints.maxWidth() : d; + double h = constraints.hasBoundedHeight() && constraints.maxHeight() < d + ? constraints.maxHeight() : d; + return constraints.constrain(new Size(w, h)); + } + + /** The component that does the drawing, and keeps the spin running. */ + private final class Arc extends Component { + + Arc() { + setUIID("Container"); + getAllStyles().setBgTransparency(0); + getAllStyles().setPadding(0, 0, 0, 0); + getAllStyles().setMargin(0, 0, 0, 0); + } + + @Override + protected com.codename1.ui.geom.Dimension calcPreferredSize() { + // A bare Component prefers nothing at all, and a parent that lays + // its children out by preferred size then gives this one a zero box + // -- which paints nothing, however correct the arc code is. + int d = (int) Math.round(Dp.px(DIAMETER_LP)); + return new com.codename1.ui.geom.Dimension(d, d); + } + + @Override + protected void initComponent() { + super.initComponent(); + com.codename1.ui.Form f = getComponentForm(); + // Null-checked: initComponent can run before the component has a + // form, and an exception thrown here leaves the component + // uninitialised -- which shows up not as a crash but as a box that + // is laid out correctly and never paints. + if (f != null && indicator().getValue() == null) { + f.registerAnimated(this); + } + } + + @Override + protected void deinitialize() { + com.codename1.ui.Form f = getComponentForm(); + if (f != null) { + f.deregisterAnimated(this); + } + super.deinitialize(); + } + + @Override + public boolean animate() { + // Indeterminate only; a determinate arc repaints when its value changes. + return indicator().getValue() == null; + } + + @Override + public void paint(Graphics g) { + int stroke = strokePx(); + int d = Math.min(getWidth(), getHeight()) - stroke; + if (d <= 0) { + return; + } + int x = getX() + (getWidth() - d) / 2; + int y = getY() + (getHeight() - d) / 2; + Double v = indicator().getValue(); + int startDeg; + int sweepDeg; + if (v != null) { + // Clockwise from twelve o'clock: Codename One's zero is three + // o'clock and its positive direction is anticlockwise, so the + // sweep is negated and the start is a quarter turn ahead. + startDeg = 90; + sweepDeg = -(int) Math.round(360 * Math.max(0, Math.min(1, v.doubleValue()))); + } else { + long phase = System.currentTimeMillis() % PERIOD_MS; + startDeg = 90 - (int) Math.round(360.0 * phase / PERIOD_MS); + sweepDeg = -(int) Math.round(360 * SWEEP_FRACTION); + } + g.setColor(arcRgb()); + g.setAntiAliased(true); + if (g.isShapeSupported()) { + // A STROKED shape: Codename One's Graphics has no stroke width, + // so a plain arc is a hairline whatever Material asks for. + // GeneralPath.arc takes RADIANS. + com.codename1.ui.geom.GeneralPath path = + new com.codename1.ui.geom.GeneralPath(); + path.arc(x, y, d, d, + Math.toRadians(startDeg), Math.toRadians(sweepDeg)); + g.drawShape(path, new com.codename1.ui.Stroke(stroke, + com.codename1.ui.Stroke.CAP_BUTT, + com.codename1.ui.Stroke.JOIN_MITER, 1f)); + } else { + // Without shapes, a ring of concentric hairlines. Cruder, but a + // visible indicator beats a correct one nobody can see. + for (int i = 0; i < stroke; i++) { + g.drawArc(x + i, y + i, d - 2 * i, d - 2 * i, startDeg, sweepDeg); + } + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CloseButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CloseButton.java new file mode 100644 index 00000000000..5f90fa7747e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/CloseButton.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Icons; +import com.codename1.flutter.widgets.Icon; + +import dart.runtime.Funcs; + +/** + * A material close button: an {@link IconButton} showing an "X" that, when + * pressed, pops the current route (Flutter's {@code CloseButton}). + */ +public class CloseButton extends IconButton { + + public CloseButton() { + icon(new Icon(Icons.close)); + } + + public CloseButton(com.codename1.flutter.Key key, Color color, Funcs.VoidFunc0 onPressed) { + this(); + if (color != null) { + color(color); + } + if (onPressed != null) { + onPressed(onPressed); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ColorScheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ColorScheme.java new file mode 100644 index 00000000000..d893f1fa33d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ColorScheme.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.Color; + +/** + * A Material 3 color scheme. Two ways to build one: + *
    + *
  • {@link #fromSeed(Color, Brightness)} derives the full role set from a + * seed color with an HSL approximation of the M3 tonal palettes;
  • + *
  • the write-once named constructor ({@code ColorScheme(primary: ..., + * brightness: ...)}) sets roles explicitly — unset roles fall back to a + * related role so callers that specify only a subset still read sensibly.
  • + *
+ * The HSL derivation (not the full HCT algorithm) matches the earlier M1 scope. + */ +public class ColorScheme { + + private Brightness brightness; + private Color primary; + private Color onPrimary; + private Color primaryContainer; + private Color onPrimaryContainer; + private Color inversePrimary; + private Color secondary; + private Color onSecondary; + private Color secondaryContainer; + private Color onSecondaryContainer; + private Color tertiary; + private Color onTertiary; + private Color tertiaryContainer; + private Color onTertiaryContainer; + private Color error; + private Color onError; + private Color errorContainer; + private Color onErrorContainer; + private Color surface; + private Color onSurface; + private Color surfaceVariant; + private Color onSurfaceVariant; + private Color background; + private Color onBackground; + private Color outline; + private Color outlineVariant; + private Color surfaceContainerLowest; + private Color surfaceContainerLow; + private Color surfaceContainer; + private Color surfaceContainerHigh; + private Color surfaceContainerHighest; + private Color shadow; + private Color scrim; + private Color inverseSurface; + private Color onInverseSurface; + + public ColorScheme() { + } + + public static ColorScheme fromSeed(Color seedColor) { + return fromSeed(seedColor, null); + } + + /** + * Canonical two-parameter form: a null brightness means light. + */ + public static ColorScheme fromSeed(Color seedColor, Brightness brightness) { + double[] hsl = toHsl(seedColor.argb()); + double h = hsl[0]; + double s = hsl[1]; + ColorScheme c = new ColorScheme(); + c.brightness = brightness; + if (brightness == Brightness.dark) { + c.primary = fromHsl(h, Math.min(1, s + 0.15), 0.80); + c.inversePrimary = fromHsl(h, s, 0.40); + c.onPrimary = fromHsl(h, s, 0.20); + c.surface = fromHsl(h, Math.min(0.25, s), 0.06); + c.onSurface = new Color(0xFFE6E1E5); + c.secondary = fromHsl(h, s * 0.35, 0.70); + } else { + c.primary = fromHsl(h, s, 0.40); + c.inversePrimary = fromHsl(h, Math.min(1, s + 0.15), 0.80); + c.onPrimary = new Color(0xFFFFFFFF); + c.surface = fromHsl(h, Math.min(0.35, s), 0.98); + c.onSurface = new Color(0xFF1C1B1F); + c.secondary = fromHsl(h, s * 0.35, 0.45); + } + return c; + } + + private static final Color DEFAULT_SEED = new Color(0xFF6750A4); + + /** + * {@code ColorScheme.light}/{@code .dark} — the seeded defaults with any explicitly + * named role applied over them. + * + *

These took no parameters at all, so a scheme written out role by role (which is + * how all four studies define their palettes) was built entirely from the default seed + * and every colour the app asked for was dropped. The roles are applied over the seeded + * base rather than replacing it, so naming one does not blank the rest.

+ */ + public static ColorScheme light(Color primary, Color onPrimary, Color primaryContainer, + Color onPrimaryContainer, Color secondary, Color onSecondary, + Color secondaryContainer, Color onSecondaryContainer, Color tertiary, + Color onTertiary, Color error, Color onError, Color errorContainer, + Color onErrorContainer, Color surface, Color onSurface, Color surfaceVariant, + Color onSurfaceVariant, Color background, Color onBackground, Color outline, + Color shadow, Color inverseSurface, Color onInverseSurface, Color inversePrimary, + Brightness brightness) { + return applyRoles(fromSeed(DEFAULT_SEED, Brightness.light), primary, onPrimary, + primaryContainer, onPrimaryContainer, secondary, onSecondary, + secondaryContainer, onSecondaryContainer, tertiary, onTertiary, error, onError, + errorContainer, onErrorContainer, surface, onSurface, surfaceVariant, + onSurfaceVariant, background, onBackground, outline, shadow, inverseSurface, + onInverseSurface, inversePrimary, brightness); + } + + public static ColorScheme dark(Color primary, Color onPrimary, Color primaryContainer, + Color onPrimaryContainer, Color secondary, Color onSecondary, + Color secondaryContainer, Color onSecondaryContainer, Color tertiary, + Color onTertiary, Color error, Color onError, Color errorContainer, + Color onErrorContainer, Color surface, Color onSurface, Color surfaceVariant, + Color onSurfaceVariant, Color background, Color onBackground, Color outline, + Color shadow, Color inverseSurface, Color onInverseSurface, Color inversePrimary, + Brightness brightness) { + return applyRoles(fromSeed(DEFAULT_SEED, Brightness.dark), primary, onPrimary, + primaryContainer, onPrimaryContainer, secondary, onSecondary, + secondaryContainer, onSecondaryContainer, tertiary, onTertiary, error, onError, + errorContainer, onErrorContainer, surface, onSurface, surfaceVariant, + onSurfaceVariant, background, onBackground, outline, shadow, inverseSurface, + onInverseSurface, inversePrimary, brightness); + } + + private static ColorScheme applyRoles(ColorScheme c, Color primary, Color onPrimary, + Color primaryContainer, Color onPrimaryContainer, Color secondary, + Color onSecondary, Color secondaryContainer, Color onSecondaryContainer, + Color tertiary, Color onTertiary, Color error, Color onError, + Color errorContainer, Color onErrorContainer, Color surface, Color onSurface, + Color surfaceVariant, Color onSurfaceVariant, Color background, + Color onBackground, Color outline, Color shadow, Color inverseSurface, + Color onInverseSurface, Color inversePrimary, Brightness brightness) { + if (primary != null) { c.primary(primary); } + if (onPrimary != null) { c.onPrimary(onPrimary); } + if (primaryContainer != null) { c.primaryContainer(primaryContainer); } + if (onPrimaryContainer != null) { c.onPrimaryContainer(onPrimaryContainer); } + if (secondary != null) { c.secondary(secondary); } + if (onSecondary != null) { c.onSecondary(onSecondary); } + if (secondaryContainer != null) { c.secondaryContainer(secondaryContainer); } + if (onSecondaryContainer != null) { c.onSecondaryContainer(onSecondaryContainer); } + if (tertiary != null) { c.tertiary(tertiary); } + if (onTertiary != null) { c.onTertiary(onTertiary); } + if (error != null) { c.error(error); } + if (onError != null) { c.onError(onError); } + if (errorContainer != null) { c.errorContainer(errorContainer); } + if (onErrorContainer != null) { c.onErrorContainer(onErrorContainer); } + if (surface != null) { c.surface(surface); } + if (onSurface != null) { c.onSurface(onSurface); } + if (surfaceVariant != null) { c.surfaceVariant(surfaceVariant); } + if (onSurfaceVariant != null) { c.onSurfaceVariant(onSurfaceVariant); } + if (background != null) { c.background(background); } + if (onBackground != null) { c.onBackground(onBackground); } + if (outline != null) { c.outline(outline); } + if (shadow != null) { c.shadow(shadow); } + if (inverseSurface != null) { c.inverseSurface(inverseSurface); } + if (onInverseSurface != null) { c.onInverseSurface(onInverseSurface); } + if (inversePrimary != null) { c.inversePrimary(inversePrimary); } + if (brightness != null) { c.brightness(brightness); } + return c; + } + + // ------------------------------------------------------------------ + // Named-parameter setters + // ------------------------------------------------------------------ + + public void brightness(Brightness v) { this.brightness = v; } + public void primary(Color v) { this.primary = v; } + public void onPrimary(Color v) { this.onPrimary = v; } + public void primaryContainer(Color v) { this.primaryContainer = v; } + public void onPrimaryContainer(Color v) { this.onPrimaryContainer = v; } + public void inversePrimary(Color v) { this.inversePrimary = v; } + public void secondary(Color v) { this.secondary = v; } + public void onSecondary(Color v) { this.onSecondary = v; } + public void secondaryContainer(Color v) { this.secondaryContainer = v; } + public void onSecondaryContainer(Color v) { this.onSecondaryContainer = v; } + public void tertiary(Color v) { this.tertiary = v; } + public void onTertiary(Color v) { this.onTertiary = v; } + public void tertiaryContainer(Color v) { this.tertiaryContainer = v; } + public void onTertiaryContainer(Color v) { this.onTertiaryContainer = v; } + public void error(Color v) { this.error = v; } + public void onError(Color v) { this.onError = v; } + public void errorContainer(Color v) { this.errorContainer = v; } + public void onErrorContainer(Color v) { this.onErrorContainer = v; } + public void surface(Color v) { this.surface = v; } + public void onSurface(Color v) { this.onSurface = v; } + public void surfaceVariant(Color v) { this.surfaceVariant = v; } + public void onSurfaceVariant(Color v) { this.onSurfaceVariant = v; } + public void background(Color v) { this.background = v; } + public void onBackground(Color v) { this.onBackground = v; } + public void outline(Color v) { this.outline = v; } + public void outlineVariant(Color v) { this.outlineVariant = v; } + public void surfaceContainerLowest(Color v) { this.surfaceContainerLowest = v; } + public void surfaceContainerLow(Color v) { this.surfaceContainerLow = v; } + public void surfaceContainer(Color v) { this.surfaceContainer = v; } + public void surfaceContainerHigh(Color v) { this.surfaceContainerHigh = v; } + public void surfaceContainerHighest(Color v) { this.surfaceContainerHighest = v; } + public void shadow(Color v) { this.shadow = v; } + public void scrim(Color v) { this.scrim = v; } + public void inverseSurface(Color v) { this.inverseSurface = v; } + public void onInverseSurface(Color v) { this.onInverseSurface = v; } + + // ------------------------------------------------------------------ + // Getters (with role fallbacks for the unset subset) + // ------------------------------------------------------------------ + + private static Color or(Color a, Color b) { + return a != null ? a : b; + } + + public Brightness brightness() { return brightness == null ? Brightness.light : brightness; } + public Color primary() { return primary; } + public Color onPrimary() { return onPrimary; } + public Color primaryContainer() { return or(primaryContainer, primary); } + public Color onPrimaryContainer() { return or(onPrimaryContainer, onPrimary); } + public Color inversePrimary() { return or(inversePrimary, primary); } + public Color secondary() { return or(secondary, primary); } + public Color onSecondary() { return or(onSecondary, onPrimary); } + public Color secondaryContainer() { return or(secondaryContainer, secondary()); } + public Color onSecondaryContainer() { return or(onSecondaryContainer, onSecondary()); } + public Color tertiary() { return or(tertiary, secondary()); } + public Color onTertiary() { return or(onTertiary, onSecondary()); } + public Color tertiaryContainer() { return or(tertiaryContainer, tertiary()); } + public Color onTertiaryContainer() { return or(onTertiaryContainer, onTertiary()); } + public Color error() { return or(error, new Color(0xFFB00020)); } + public Color onError() { return or(onError, new Color(0xFFFFFFFF)); } + public Color errorContainer() { return or(errorContainer, error()); } + public Color onErrorContainer() { return or(onErrorContainer, onError()); } + public Color surface() { return surface; } + public Color onSurface() { return onSurface; } + public Color surfaceVariant() { return or(surfaceVariant, surface); } + public Color onSurfaceVariant() { return or(onSurfaceVariant, onSurface); } + public Color background() { return or(background, surface); } + public Color onBackground() { return or(onBackground, onSurface); } + /// Material 3's tonal surface containers. Flutter falls each of them back to + /// {@link #surface()} when the scheme names none, and several component + /// defaults are written in terms of them -- an elevated button's face is + /// surfaceContainerLow, not primary. + public Color surfaceContainerLowest() { return or(surfaceContainerLowest, surface); } + public Color surfaceContainerLow() { return or(surfaceContainerLow, surface); } + public Color surfaceContainer() { return or(surfaceContainer, surface); } + public Color surfaceContainerHigh() { return or(surfaceContainerHigh, surface); } + public Color surfaceContainerHighest() { return or(surfaceContainerHighest, surface); } + + public Color outline() { return or(outline, new Color(0xFF79747E)); } + /// Falls back to {@link #onBackground()}, which is what Flutter's ColorScheme does -- + /// and it is load bearing rather than a detail, because an M3 Divider takes its colour + /// from this role. A scheme written out by hand names neither, so falling back to + /// `outline` painted the compose page's rules in a pale grey where the reference draws + /// them almost black. + public Color outlineVariant() { return or(outlineVariant, onBackground()); } + public Color shadow() { return or(shadow, new Color(0xFF000000)); } + public Color scrim() { return or(scrim, new Color(0xFF000000)); } + public Color inverseSurface() { return or(inverseSurface, onSurface); } + public Color onInverseSurface() { return or(onInverseSurface, surface); } + + /** + * Returns a copy with the supplied (non-null) roles overridden. Parameter + * order matches the Dart stub. + */ + public ColorScheme copyWith(Brightness brightness, Color primary, Color onPrimary, + Color primaryContainer, Color onPrimaryContainer, Color secondary, + Color onSecondary, Color secondaryContainer, Color tertiary, + Color error, Color onError, Color surface, Color onSurface, + Color surfaceVariant, Color onSurfaceVariant, Color background, + Color onBackground, Color outline, Color inversePrimary, + Color inverseSurface, Color shadow) { + ColorScheme c = new ColorScheme(); + c.brightness = brightness != null ? brightness : this.brightness; + c.primary = primary != null ? primary : this.primary; + c.onPrimary = onPrimary != null ? onPrimary : this.onPrimary; + c.primaryContainer = primaryContainer != null ? primaryContainer : this.primaryContainer; + c.onPrimaryContainer = onPrimaryContainer != null ? onPrimaryContainer : this.onPrimaryContainer; + c.secondary = secondary != null ? secondary : this.secondary; + c.onSecondary = onSecondary != null ? onSecondary : this.onSecondary; + c.secondaryContainer = secondaryContainer != null ? secondaryContainer : this.secondaryContainer; + c.tertiary = tertiary != null ? tertiary : this.tertiary; + c.error = error != null ? error : this.error; + c.onError = onError != null ? onError : this.onError; + c.surface = surface != null ? surface : this.surface; + c.onSurface = onSurface != null ? onSurface : this.onSurface; + c.surfaceVariant = surfaceVariant != null ? surfaceVariant : this.surfaceVariant; + c.onSurfaceVariant = onSurfaceVariant != null ? onSurfaceVariant : this.onSurfaceVariant; + c.background = background != null ? background : this.background; + c.onBackground = onBackground != null ? onBackground : this.onBackground; + c.outline = outline != null ? outline : this.outline; + c.inversePrimary = inversePrimary != null ? inversePrimary : this.inversePrimary; + c.inverseSurface = inverseSurface != null ? inverseSurface : this.inverseSurface; + c.shadow = shadow != null ? shadow : this.shadow; + // carry the rest unchanged + c.onSecondaryContainer = this.onSecondaryContainer; + c.onTertiary = this.onTertiary; + c.tertiaryContainer = this.tertiaryContainer; + c.onTertiaryContainer = this.onTertiaryContainer; + c.errorContainer = this.errorContainer; + c.onErrorContainer = this.onErrorContainer; + c.outlineVariant = this.outlineVariant; + c.scrim = this.scrim; + c.onInverseSurface = this.onInverseSurface; + return c; + } + + // ------------------------------------------------------------------ + // HSL helpers + // ------------------------------------------------------------------ + + static double[] toHsl(int argb) { + double r = ((argb >> 16) & 0xFF) / 255.0; + double g = ((argb >> 8) & 0xFF) / 255.0; + double b = (argb & 0xFF) / 255.0; + double max = Math.max(r, Math.max(g, b)); + double min = Math.min(r, Math.min(g, b)); + double l = (max + min) / 2; + double h; + double s; + if (max == min) { + h = 0; + s = 0; + } else { + double d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + if (max == r) { + h = ((g - b) / d + (g < b ? 6 : 0)) * 60; + } else if (max == g) { + h = ((b - r) / d + 2) * 60; + } else { + h = ((r - g) / d + 4) * 60; + } + } + return new double[]{h, s, l}; + } + + static Color fromHsl(double h, double s, double l) { + double c = (1 - Math.abs(2 * l - 1)) * s; + double hh = (h % 360) / 60; + double x = c * (1 - Math.abs(hh % 2 - 1)); + double r = 0; + double g = 0; + double b = 0; + if (hh < 1) { + r = c; + g = x; + } else if (hh < 2) { + r = x; + g = c; + } else if (hh < 3) { + g = c; + b = x; + } else if (hh < 4) { + g = x; + b = c; + } else if (hh < 5) { + r = x; + b = c; + } else { + r = c; + b = x; + } + double m = l - c / 2; + int ri = (int) Math.round((r + m) * 255); + int gi = (int) Math.round((g + m) * 255); + int bi = (int) Math.round((b + m) * 255); + ri = Math.max(0, Math.min(255, ri)); + gi = Math.max(0, Math.min(255, gi)); + bi = Math.max(0, Math.min(255, bi)); + return new Color(0xFF000000 | (ri << 16) | (gi << 8) | bi); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataCell.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataCell.java new file mode 100644 index 00000000000..bf635ddd69b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataCell.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * One cell in a {@link DataRow} — Flutter's {@code DataCell}. Wraps the cell's + * {@code child} widget. + */ +public class DataCell { + + private final Widget child; + private boolean placeholder; + private boolean showEditIcon; + private Funcs.VoidFunc0 onTap; + + public DataCell(Widget child) { + this.child = child; + } + + public void placeholder(boolean v) { + this.placeholder = v; + } + + public void showEditIcon(boolean v) { + this.showEditIcon = v; + } + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public void onLongPress(Funcs.VoidFunc0 v) { + } + + public void onTapDown(Object v) { + } + + public Widget getChild() { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataColumn.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataColumn.java new file mode 100644 index 00000000000..71c2536b4a5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataColumn.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A column description for a {@link DataTable} — Flutter's {@code DataColumn}. + */ +public class DataColumn { + + private Widget label; + private String tooltip; + private boolean numeric; + private Funcs.VoidFunc2 onSort; + + public void label(Widget v) { + this.label = v; + } + + public void tooltip(String v) { + this.tooltip = v; + } + + public Funcs.VoidFunc2 getOnSort() { + return onSort; + } + + public void numeric(boolean v) { + this.numeric = v; + } + + public void onSort(Funcs.VoidFunc2 v) { + this.onSort = v; + } + + public Widget getLabel() { + return label; + } + + public boolean isNumeric() { + return numeric; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataRow.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataRow.java new file mode 100644 index 00000000000..0f53f249d4f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataRow.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * One row of a {@link DataTable} — Flutter's {@code DataRow}. Holds the row's + * {@link DataCell}s. + */ +public class DataRow { + + private DartList cells; + private Boolean selected; + private Long index; + private Funcs.VoidFunc1 onSelectChanged; + + public DataRow() { + } + + /** Dart's {@code DataRow.byIndex} named constructor. */ + public static DataRow byIndex(long index, + Boolean selected, + Funcs.VoidFunc1 onSelectChanged, + Object onLongPress, + Object color, + DartList cells) { + DataRow r = new DataRow(); + r.index = index; + r.selected = selected; + r.onSelectChanged = onSelectChanged; + r.cells = cells; + return r; + } + + public void cells(DartList v) { + this.cells = v; + } + + public void selected(boolean v) { + this.selected = v; + } + + public void onSelectChanged(Funcs.VoidFunc1 v) { + this.onSelectChanged = v; + } + + public void onLongPress(Object v) { + } + + public void color(Object v) { + } + + public DartList getCells() { + return cells; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTable.java new file mode 100644 index 00000000000..122ee686900 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTable.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Row; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A material data table — Flutter's {@code DataTable}. This milestone renders a + * {@link Column} of {@link Row}s: a heading row of the {@code columns}' labels + * followed by one row per {@link DataRow}, each cell wrapped in an + * {@link Expanded} so the columns share the width. Sorting, selection checkboxes + * and the material grid styling are deferred. + */ +public class DataTable extends StatelessWidget { + + private DartList columns; + private DartList rows; + + public void columns(DartList v) { + this.columns = v; + } + + public void rows(DartList v) { + this.rows = v; + } + + public void sortColumnIndex(Long v) { + } + + public void sortAscending(Boolean v) { + } + + public void onSelectAll(Funcs.VoidFunc1 v) { + } + + public void dataRowHeight(Double v) { + } + + public void headingRowHeight(Double v) { + } + + public void horizontalMargin(Double v) { + } + + public void columnSpacing(Double v) { + } + + public void showCheckboxColumn(Boolean v) { + } + + public void decoration(Object v) { + } + + private static Row rowOf(DartList cells) { + DartList flexed = new DartList(); + for (int i = 0; i < cells.size(); i++) { + Expanded e = new Expanded(); + e.child(cells.get(i)); + flexed.add(e); + } + Row r = new Row(); + r.children(flexed); + return r; + } + + @Override + public Widget build(BuildContext context) { + DartList body = new DartList(); + if (columns != null) { + DartList labels = new DartList(); + for (int i = 0; i < columns.size(); i++) { + labels.add(columns.get(i).getLabel()); + } + body.add(rowOf(labels)); + } + if (rows != null) { + for (int i = 0; i < rows.size(); i++) { + DataRow dr = rows.get(i); + DartList cellWidgets = new DartList(); + DartList cells = dr.getCells(); + if (cells != null) { + for (int j = 0; j < cells.size(); j++) { + cellWidgets.add(cells.get(j).getChild()); + } + } + body.add(rowOf(cellWidgets)); + } + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(body); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTableSource.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTableSource.java new file mode 100644 index 00000000000..ea82047e14c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DataTableSource.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.foundation.ChangeNotifier; + +/** + * A source of {@link DataRow}s for a {@link PaginatedDataTable} — Flutter's + * {@code DataTableSource}. Application code subclasses this, overriding + * {@link #getRow}, {@link #rowCount}, {@link #isRowCountApproximate} and + * {@link #selectedRowCount}, and calls {@code notifyListeners()} (inherited from + * {@link ChangeNotifier}) when the data changes. + */ +public abstract class DataTableSource implements ChangeNotifier { + + /** Held here, not in the shared map, so this goes when the object does (see ChangeNotifier). */ + private final java.util.List changeNotifierListeners = + new java.util.ArrayList(); + + @Override + public java.util.List changeNotifierListeners$() { + return changeNotifierListeners; + } + + + public abstract DataRow getRow(long index); + + public abstract long rowCount(); + + public abstract boolean isRowCountApproximate(); + + public abstract long selectedRowCount(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DatePickerDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DatePickerDialog.java new file mode 100644 index 00000000000..e77aeecd8aa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DatePickerDialog.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.async.Future; +import dart.core.DateTime; + +/** + * A material date-picker dialog — Flutter's {@code DatePickerDialog}. This + * milestone renders a placeholder surface; the calendar grid and confirm/cancel + * flow land in a later pass. The configured date range is retained. + */ +public class DatePickerDialog extends StatelessWidget { + + private String restorationId; + private DateTime initialDate; + private DateTime firstDate; + private DateTime lastDate; + private DateTime currentDate; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void initialDate(DateTime v) { + this.initialDate = v; + } + + public void firstDate(DateTime v) { + this.firstDate = v; + } + + public void lastDate(DateTime v) { + this.lastDate = v; + } + + public void currentDate(DateTime v) { + this.currentDate = v; + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } + + /** Top-level {@code showDatePicker(...)} — shows the dialog and completes with the chosen date. */ + public static Future show(BuildContext context, DateTime initialDate, + DateTime firstDate, DateTime lastDate) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DateRangePickerDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DateRangePickerDialog.java new file mode 100644 index 00000000000..dd5407f493d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DateRangePickerDialog.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.async.Future; +import dart.core.DateTime; + +/** + * A material date-range-picker dialog — Flutter's {@code DateRangePickerDialog}. + * This milestone renders a placeholder surface; the range calendar and + * confirm/cancel flow land in a later pass. The configured bounds are retained. + */ +public class DateRangePickerDialog extends StatelessWidget { + + private String restorationId; + private DateTime firstDate; + private DateTime lastDate; + private DateTime currentDate; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void firstDate(DateTime v) { + this.firstDate = v; + } + + public void lastDate(DateTime v) { + this.lastDate = v; + } + + public void currentDate(DateTime v) { + this.currentDate = v; + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } + + /** Top-level {@code showDateRangePicker(...)} — shows the dialog and completes with the chosen range. */ + public static Future show(BuildContext context, DateTime firstDate, DateTime lastDate) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DefaultTabController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DefaultTabController.java new file mode 100644 index 00000000000..66649da3e02 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DefaultTabController.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.Duration; + +/** + * Creates a {@link TabController} and shares it with descendant {@link TabBar} / + * {@link TabBarView} widgets — Flutter's {@code DefaultTabController}. This + * milestone renders the subtree ({@code child}); the descendant tab widgets + * currently default to index 0 rather than resolving the inherited controller, + * so {@link #of} returns a fresh controller of the configured length. + */ +public class DefaultTabController extends StatelessWidget { + + private long length; + private long initialIndex; + private Widget child; + + public void length(long v) { + this.length = v; + } + + public void initialIndex(long v) { + this.initialIndex = v; + } + + public void animationDuration(Duration v) { + } + + public void child(Widget v) { + this.child = v; + } + + public static TabController of(BuildContext context) { + TabController c = new TabController(); + c.length(1); + return c; + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogTheme.java new file mode 100644 index 00000000000..931658c9576 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogTheme.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code DialogTheme}: write-once dialog styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class DialogTheme { + + private Color backgroundColor; + private Double elevation; + private Color shadowColor; + private Color surfaceTintColor; + private Object shape; + private Object alignment; + private TextStyle titleTextStyle; + private TextStyle contentTextStyle; + private Object iconColor; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public void titleTextStyle(TextStyle v) { + this.titleTextStyle = v; + } + + public void contentTextStyle(TextStyle v) { + this.contentTextStyle = v; + } + + public void iconColor(Object v) { + this.iconColor = v; + } + + public Color backgroundColor() { + return backgroundColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogThemeData.java new file mode 100644 index 00000000000..97936d0bee6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DialogThemeData.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code DialogThemeData}: the Material-3 rename of {@link DialogTheme}; + * same write-once dialog styling shape. + */ +public class DialogThemeData { + + private Color backgroundColor; + private Double elevation; + private Color shadowColor; + private Color surfaceTintColor; + private Object shape; + private Object alignment; + private TextStyle titleTextStyle; + private TextStyle contentTextStyle; + private Object iconColor; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public void titleTextStyle(TextStyle v) { + this.titleTextStyle = v; + } + + public void contentTextStyle(TextStyle v) { + this.contentTextStyle = v; + } + + public void iconColor(Object v) { + this.iconColor = v; + } + + public Color backgroundColor() { + return backgroundColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Dialogs.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Dialogs.java new file mode 100644 index 00000000000..95cc6ead196 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Dialogs.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.FlutterRootLayout; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.layouts.BorderLayout; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Host class for Dart's top-level {@code showDialog} function. The built + * widget tree (typically an {@link AlertDialog}) mounts as a Flutter subtree + * inside a CN1 {@link com.codename1.ui.Dialog} shown MODELESSLY + * ({@code showPacked(..., false)}) — transpiled code continues to run after + * the {@code showDialog} call, exactly like Dart's non-awaited Future. + * + *

A static dialog stack records open dialogs; + * {@code Navigator.pop(context)} consults it first, so a TextButton action + * that pops dismisses the dialog (Flutter's dialogs-are-routes behavior). + * Tap-outside dismissal is deliberately disabled in M3 to keep the stack + * authoritative.

+ * + *

Headless (no Display): the widget tree still mounts (builder runs, the + * stack is maintained) with no CN1 dialog — unit-testable bookkeeping.

+ */ +public final class Dialogs { + + private static final List dialogStack = new ArrayList(); + + private Dialogs() { + } + + /** + * Shows the dialog and returns a future that completes when it closes, with the + * value handed to {@code Navigator.pop} -- null when it closed any other way. It + * returned nothing, so {@code final ok = await showDialog(...)} could not be written + * and code after an awaited dialog ran while it was still open. + */ + public static dart.async.Future showDialog(BuildContext context, Funcs.Func1 builder) { + return present(context, builder, BorderLayout.CENTER, false); + } + + /** + * A sheet that rises from the bottom edge, full width -- the shape of a modal popup + * rather than of a dialog. + * + *

Presenting one as a centred, packed dialog is not a near miss. A sheet states a + * height and leaves its width to the presentation, so packing sized it to its + * content: the Cupertino picker demo's 216-high sheet came up as a tall narrow strip + * down the middle of the screen instead of a full-width wheel along the bottom.

+ * + *

Dismissible by touching outside it, as the reference is by default.

+ */ + public static dart.async.Future showModalPopup(BuildContext context, + Funcs.Func1 builder) { + return present(context, builder, BorderLayout.SOUTH, true, true); + } + + private static dart.async.Future present(BuildContext context, Funcs.Func1 builder, + String position, boolean dismissOnOutsideTouch) { + return present(context, builder, position, dismissOnOutsideTouch, false); + } + + private static dart.async.Future present(BuildContext context, Funcs.Func1 builder, + String position, boolean dismissOnOutsideTouch, boolean stretch) { + DialogWidget rootWidget = new DialogWidget(builder); + // The dialog mounts as a root of its own, so its builder's Theme.of, Provider.of + // and Localizations lookups found nothing above it. In Flutter a dialog is a route + // built under the navigator; it continues from the same element a pushed route + // does. + Element inheritFrom = com.codename1.flutter.navigation.Navigator.pushingElement(context); + final DialogEntry e = new DialogEntry(); + if (Display.isInitialized()) { + // Codename One disposes a dialog itself when it is touched outside its bounds, + // which is how a modal popup is dismissed. That path never reached this stack: + // the entry stayed, so the next Navigator.pop consumed it instead of popping + // the page, and the dismissed popup's element tree stayed mounted. Every way + // the dialog closes now goes through close(). + com.codename1.ui.Dialog d = new com.codename1.ui.Dialog(new BorderLayout()) { + @Override + public void dispose() { + super.dispose(); + close(e); + } + }; + d.setDisposeWhenPointerOutOfBounds(dismissOnOutsideTouch); + Container c = FlutterUI.wrap(rootWidget, inheritFrom); + d.add(BorderLayout.CENTER, c); + e.dialog = d; + e.root = ((FlutterRootLayout) c.getLayout()).host().rootElement(); + dialogStack.add(e); + // modeless: returns immediately, the calling code keeps running. + // + // STRETCHED for a sheet: packing sizes both axes to the content, and a sheet + // states only its height -- so a 216-high picker came up as a tall narrow + // strip instead of spanning the screen. + if (stretch) { + d.showStretched(position, false); + } else { + d.showPacked(position, false); + } + } else { + RenderHost host = new RenderHost(); + e.root = FlutterUI.mount(rootWidget, host, new BuildOwner(), inheritFrom); + dialogStack.add(e); + } + return e.result.future(); + } + + /** + * Dismisses the topmost open dialog. Returns false when none is open — + * the caller (Navigator.pop) then pops a route instead. + */ + public static boolean popTopDialog() { + return popTopDialog(null); + } + + /** {@link #popTopDialog()} completing the dialog's future with {@code result}. */ + public static boolean popTopDialog(Object result) { + if (dialogStack.isEmpty()) { + return false; + } + DialogEntry e = dialogStack.get(dialogStack.size() - 1); + e.popResult = result; + close(e); + if (e.dialog != null) { + e.dialog.dispose(); + } + return true; + } + + /** Takes a dialog off the stack and unmounts its subtree -- once, however it closed. */ + private static void close(DialogEntry e) { + if (e.closed) { + return; + } + e.closed = true; + dialogStack.remove(e); + if (e.root != null) { + FlutterUI.unmountTree(e.root); + } + e.result.complete(e.popResult); + } + + /** + * The number of dialogs currently open. + */ + public static int openDialogCount() { + return dialogStack.size(); + } + + /** + * Test / hot-restart hook: forgets all open dialogs without disposing. + */ + public static void reset() { + dialogStack.clear(); + } + + private static final class DialogEntry { + com.codename1.ui.Dialog dialog; + Element root; + boolean closed; + /** What Navigator.pop handed back; null when the dialog closed another way. */ + Object popResult; + final dart.async.Completer result = new dart.async.Completer(); + } + + /** + * Adapter mounting the dialog's WidgetBuilder as a subtree root; the + * builder runs during the first build with an in-tree BuildContext. + */ + static final class DialogWidget extends StatelessWidget { + + private final Funcs.Func1 builder; + + DialogWidget(Funcs.Func1 builder) { + this.builder = builder; + } + + @Override + public Widget build(BuildContext context) { + return builder == null ? null : builder.call(context); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Divider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Divider.java new file mode 100644 index 00000000000..39e8461424d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Divider.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * A thin horizontal rule with vertical breathing room. {@code height} is the + * total vertical extent the divider occupies (default 16lp); + * {@code thickness} is the painted line (default 1lp). Backed by a CN1 + * hairline strip component (UIID "FlutterDivider"). + */ +public class Divider extends Widget { + + private Double height; + private Double thickness; + private Color color; + private Double indent; + private Double endIndent; + + public void height(double v) { + this.height = v; + } + + public void indent(double v) { + this.indent = v; + } + + public void endIndent(double v) { + this.endIndent = v; + } + + public Double getIndent() { + return indent; + } + + public Double getEndIndent() { + return endIndent; + } + + public void thickness(double v) { + this.thickness = v; + } + + public void color(Color v) { + this.color = v; + } + + public Double getHeight() { + return height; + } + + public Double getThickness() { + return thickness; + } + + public Color getColor() { + return color; + } + + @Override + public Element createElement() { + return new DividerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerRenderElement.java new file mode 100644 index 00000000000..6f9c02a1f13 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerRenderElement.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Label; + +/** + * Leaf render box for {@link Divider}: the element occupies the full + * {@code height} extent while the owned strip component (UIID + * "FlutterDivider") is shrunk to the {@code thickness} line centered inside + * it, painted via its background color. + */ +public class DividerRenderElement extends RenderElement { + + /** Flutter's default divider extent in logical pixels. */ + public static final double DEFAULT_HEIGHT_LP = 16; + /** Default painted line thickness in logical pixels. */ + public static final double DEFAULT_THICKNESS_LP = 1; + /** Material light-theme divider color (black at ~12% on white). */ + private static final int DEFAULT_COLOR = 0xE0E0E0; + + public DividerRenderElement(Divider widget) { + super(widget); + } + + private Divider divider() { + return (Divider) widget(); + } + + /// The ambient divider theme, or null. + /// + /// Every dimension below reads widget, then theme, then Flutter's default -- the + /// precedence Divider.createBorderSide uses. Reading only the widget meant a study that + /// states its rules once in its ThemeData got none of them. + private DividerThemeData theme() { + try { + ThemeData t = Theme.of(this); + return t == null ? null : t.dividerTheme(); + } catch (Throwable err) { + return null; + } + } + + private double heightLp() { + if (divider().getHeight() != null) { + return divider().getHeight(); + } + DividerThemeData dt = theme(); + if (dt != null && dt.space() != null) { + return dt.space(); + } + return DEFAULT_HEIGHT_LP; + } + + private double thicknessLp() { + if (divider().getThickness() != null) { + return divider().getThickness(); + } + DividerThemeData dt = theme(); + if (dt != null && dt.thickness() != null) { + return dt.thickness(); + } + return DEFAULT_THICKNESS_LP; + } + + private double indentLp() { + if (divider().getIndent() != null) { + return divider().getIndent(); + } + DividerThemeData dt = theme(); + return dt != null && dt.indent() != null ? dt.indent() : 0; + } + + private double endIndentLp() { + if (divider().getEndIndent() != null) { + return divider().getEndIndent(); + } + DividerThemeData dt = theme(); + return dt != null && dt.endIndent() != null ? dt.endIndent() : 0; + } + + /// Widget colour, then the divider theme's, then the scheme's outlineVariant -- and + /// black when there is no theme at all, which is the colour a BorderSide defaults to. + private int colorRgb() { + if (divider().getColor() != null) { + return divider().getColor().rgb(); + } + DividerThemeData dt = theme(); + if (dt != null && dt.color() != null) { + return dt.color().rgb(); + } + try { + ColorScheme cs = Theme.of(this).colorScheme(); + if (cs != null && cs.outlineVariant() != null) { + return cs.outlineVariant().rgb(); + } + } catch (Throwable err) { + // no ambient theme: fall through + } + return DEFAULT_COLOR; + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Label strip = new Label("", "FlutterDivider"); + stylesToSet(strip).setPadding(0, 0, 0, 0); + stylesToSet(strip).setMargin(0, 0, 0, 0); + applyStyle(strip); + return strip; + } + + @Override + protected void updateComponent(Component c) { + applyStyle(c); + } + + private void applyStyle(Component strip) { + stylesToSet(strip).setBgColor(colorRgb()); + stylesToSet(strip).setBgTransparency(255); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : 0; + return constraints.constrain(new Size(w, Dp.px(heightLp()))); + } + + @Override + public void position(int x, int y) { + super.position(x, y); + Component strip = component(); + if (strip != null) { + int t = Math.max(1, (int) Math.round(Dp.px(thicknessLp()))); + t = (int) Math.min(t, Math.round(size().height())); + strip.setY(y + (int) Math.round((size().height() - t) / 2)); + strip.setHeight(t); + // The rule is inset from both ends; the BOX is not. Flutter puts the indent on + // the line's own margin, so a divider still occupies the full width it was + // given and only the painted part is short. + int start = (int) Math.round(Dp.px(indentLp())); + int end = (int) Math.round(Dp.px(endIndentLp())); + int w = (int) Math.round(size().width()) - start - end; + if (w > 0) { + strip.setX(x + start); + strip.setWidth(w); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerThemeData.java new file mode 100644 index 00000000000..41e63645d01 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DividerThemeData.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +/** + * Material {@code DividerThemeData}: write-once divider styling. + */ +public class DividerThemeData { + + private Double thickness; + private Color color; + private Double space; + private Double indent; + private Double endIndent; + + public void thickness(double v) { + this.thickness = v; + } + + public void color(Color v) { + this.color = v; + } + + public void space(double v) { + this.space = v; + } + + public void indent(double v) { + this.indent = v; + } + + public void endIndent(double v) { + this.endIndent = v; + } + + public Double thickness() { + return thickness; + } + + public Color color() { + return color; + } + + public Double space() { + return space; + } + + public Double indent() { + return indent; + } + + public Double endIndent() { + return endIndent; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Drawer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Drawer.java new file mode 100644 index 00000000000..e617a26635b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Drawer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * A material navigation drawer panel. As a root Scaffold's {@code drawer} + * it renders into the CN1 Toolbar side menu; embedded Scaffolds ignore it + * with a log warning (see {@link ScaffoldRenderElement}). Standard Material + * width: 304lp. + */ +public class Drawer extends Widget { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new DrawerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DrawerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DrawerRenderElement.java new file mode 100644 index 00000000000..1ca7bb691ce --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/DrawerRenderElement.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +/** + * Render element for {@link Drawer}: sizes to the standard Material drawer + * width (304lp) when unconstrained (the side-menu preferred-size dry pass) + * and fills whatever the side menu hands it otherwise; the child subtree + * fills the panel. + */ +public class DrawerRenderElement extends SingleChildRenderElement { + + /** Standard Material drawer width in logical pixels. */ + public static final double WIDTH_LP = 304; + + public DrawerRenderElement(Drawer widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((Drawer) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double width = constraints.hasBoundedWidth() + ? constraints.maxWidth() + : Dp.px(WIDTH_LP); + double height = constraints.hasBoundedHeight() ? constraints.maxHeight() : 0; + RenderElement child = renderChild(); + if (child != null) { + Size cs = child.layout(new BoxConstraints( + width, width, 0, + constraints.hasBoundedHeight() ? height : Double.POSITIVE_INFINITY)); + setChildOffset(child, 0, 0); + height = Math.max(height, cs.height()); + } + return constraints.constrain(new Size(width, height)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ElevatedButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ElevatedButton.java new file mode 100644 index 00000000000..2964ce964f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ElevatedButton.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The material filled button: a primary-colored capsule with the on-primary + * foreground, backed by a CN1 Button (UIID "FlutterElevatedButton"). + */ +public class ElevatedButton extends ButtonBase { + + /** + * Builds a {@link ButtonStyle} to hand to an ElevatedButton's {@code + * style:} parameter. Parameter order matches the Dart stub. + */ + public static ButtonStyle styleFrom(com.codename1.flutter.Color foregroundColor, + com.codename1.flutter.Color backgroundColor, com.codename1.flutter.Color shadowColor, + Double elevation, com.codename1.flutter.TextStyle textStyle, + com.codename1.flutter.EdgeInsets padding, Object side, Object shape, Object alignment, + Object tapTargetSize, Object visualDensity) { + return ButtonStyle.styleFrom(foregroundColor, backgroundColor, shadowColor, elevation, textStyle, + padding, side, shape, alignment, tapTargetSize, visualDensity); + } + + /** + * {@code ElevatedButton.icon}: a button whose content is an icon followed + * by a label. This milestone consumes the label as the button content (the + * leading icon is used when no label is supplied); a later pass composes + * both into a Row. + */ + public static ElevatedButton icon(com.codename1.flutter.Key key, + dart.runtime.Funcs.VoidFunc0 onPressed, ButtonStyle style, + com.codename1.flutter.Widget icon, com.codename1.flutter.Widget label) { + ElevatedButton b = new ElevatedButton(); + b.key(key); + b.onPressed(onPressed); + b.style(style); + b.child(label != null ? label : icon); + if (label != null) { + b.leadingIcon(icon); + } + return b; + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanel.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanel.java new file mode 100644 index 00000000000..6bd1cd12010 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanel.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * One panel in an {@link ExpansionPanelList} — Flutter's {@code ExpansionPanel}. + * A configuration object with a {@code headerBuilder(context, isExpanded)} and a + * {@code body}. + */ +public class ExpansionPanel { + + private Funcs.Func2 headerBuilder; + private Widget body; + private boolean isExpanded; + + public void headerBuilder(Funcs.Func2 v) { + this.headerBuilder = v; + } + + public void body(Widget v) { + this.body = v; + } + + public void isExpanded(boolean v) { + this.isExpanded = v; + } + + public void canTapOnHeader(boolean v) { + } + + public void backgroundColor(Color v) { + } + + public Funcs.Func2 getHeaderBuilder() { + return headerBuilder; + } + + public Widget getBody() { + return body; + } + + public boolean isExpanded() { + return isExpanded; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanelList.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanelList.java new file mode 100644 index 00000000000..c1c20d139a4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionPanelList.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A material list of expandable {@link ExpansionPanel}s — Flutter's + * {@code ExpansionPanelList}. This milestone renders each panel's header + * followed by its body; header taps that fire {@code expansionCallback} and the + * expand/collapse animation are deferred (bodies render expanded). + */ +public class ExpansionPanelList extends StatelessWidget { + + private DartList children; + private Funcs.VoidFunc2 expansionCallback; + + public void children(DartList v) { + this.children = v; + } + + public void expansionCallback(Funcs.VoidFunc2 v) { + this.expansionCallback = v; + } + + public void animationDuration(Object v) { + } + + public void expandedHeaderPadding(Object v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (children != null) { + for (int i = 0; i < children.size(); i++) { + ExpansionPanel p = children.get(i); + if (p.getHeaderBuilder() != null) { + Widget header = p.getHeaderBuilder().call(context, p.isExpanded()); + if (header != null) { + kids.add(header); + } + } + if (p.getBody() != null) { + kids.add(p.getBody()); + } + } + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionTile.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionTile.java new file mode 100644 index 00000000000..355b355a671 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ExpansionTile.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A single-line {@link ListTile} that expands to reveal children — Flutter's + * {@code ExpansionTile}. This milestone renders the {@code title} followed by + * the {@code children}; the expand/collapse toggle that fires + * {@code onExpansionChanged} is deferred (children render expanded). + */ +public class ExpansionTile extends StatelessWidget { + + private Widget title; + private Widget subtitle; + private Widget leading; + private Widget trailing; + private DartList children; + private boolean initiallyExpanded; + private Funcs.VoidFunc1 onExpansionChanged; + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void leading(Widget v) { + this.leading = v; + } + + public void trailing(Widget v) { + this.trailing = v; + } + + public void children(DartList v) { + this.children = v; + } + + public void initiallyExpanded(boolean v) { + this.initiallyExpanded = v; + } + + public void onExpansionChanged(Funcs.VoidFunc1 v) { + this.onExpansionChanged = v; + } + + public void childrenPadding(Object v) { + } + + public void backgroundColor(Color v) { + } + + public void collapsedBackgroundColor(Color v) { + } + + public void textColor(Color v) { + } + + public void iconColor(Color v) { + } + + public void tilePadding(Object v) { + } + + public void expandedAlignment(Object v) { + } + + public void expandedCrossAxisAlignment(Object v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + ListTile header = new ListTile(); + if (leading != null) { + header.leading(leading); + } + if (title != null) { + header.title(title); + } + if (subtitle != null) { + header.subtitle(subtitle); + } + if (trailing != null) { + header.trailing(trailing); + } + kids.add(header); + if (children != null) { + for (int i = 0; i < children.size(); i++) { + kids.add(children.get(i)); + } + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FabRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FabRenderElement.java new file mode 100644 index 00000000000..80f91c52bbf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FabRenderElement.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.FontImage; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.geom.Dimension; + +/** + * Leaf render box for the material {@link FloatingActionButton}, owning a + * real CN1 {@code com.codename1.components.FloatingActionButton} created via + * {@code createFAB(char)} and positioned absolutely by the parent Scaffold + * (no bindFabToContainer — the flat Flutter layout places it directly). + * + *

The Icon child is consumed as configuration rather than mounted as a + * child element; {@code tooltip} is stored but not rendered in M1 (CN1 has + * no hover tooltips on touch platforms).

+ */ +public class FabRenderElement extends RenderElement { + + public FabRenderElement(FloatingActionButton widget) { + super(widget); + } + + private FloatingActionButton fab() { + return (FloatingActionButton) widget(); + } + + private char iconChar() { + char c = iconOf(fab().isExtended() ? fab().getIcon() : fab().getChild()); + return c == 0 ? FontImage.MATERIAL_ADD : c; + } + + /** The glyph inside {@code w}, descending through the wrappers a theme adds. */ + private static char iconOf(com.codename1.flutter.Widget w) { + for (int depth = 0; w != null && depth < 6; depth++) { + if (w instanceof Icon) { + Icon ic = (Icon) w; + return ic.getIcon() == null ? 0 : ic.getIcon().codePoint(); + } + if (w instanceof com.codename1.flutter.widgets.HasIcon) { + com.codename1.flutter.IconData d = + ((com.codename1.flutter.widgets.HasIcon) w).iconData(); + return d == null ? 0 : d.codePoint(); + } + if (!(w instanceof com.codename1.flutter.widgets.HasChild)) { + return 0; + } + w = ((com.codename1.flutter.widgets.HasChild) w).getChild(); + } + return 0; + } + + /** The label of an extended FAB, or null when it has none. */ + private String labelText() { + if (!fab().isExtended()) { + return null; + } + com.codename1.flutter.Widget w = fab().getChild(); + for (int depth = 0; w != null && depth < 6; depth++) { + if (w instanceof com.codename1.flutter.widgets.Text) { + return ((com.codename1.flutter.widgets.Text) w).getData(); + } + if (!(w instanceof com.codename1.flutter.widgets.HasChild)) { + return null; + } + w = ((com.codename1.flutter.widgets.HasChild) w).getChild(); + } + return null; + } + + @Override + protected Component createComponent() { + // A plain Button wearing the FAB's UIID, in BOTH forms. + // + // An extended fab is a labelled pill, and Codename One's + // FloatingActionButton cannot be one: its setText stores the string for + // the text-badge popup and only forwards it to the Button when the + // instance IS a badge, so the label never rendered. Every study's "Back" + // button is one of these, and each drew a bare round plus sign. + // + // The regular form cannot use it either, for a subtler reason: that + // class re-installs its own circular border from styleChanged() every + // time the background colour is set, so the Material 3 shape put on it + // here was replaced the moment the colour followed, and the button + // painted 96 device pixels of surface inside the 168 pixel box this + // element had laid out for it. + com.codename1.ui.Button b = new com.codename1.ui.Button(); + b.setUIID("FloatingActionButton"); + // The listener reads the CURRENT widget config so onPressed updates + // never require listener rewiring. + b.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + dart.runtime.Funcs.VoidFunc0 f = fab().getOnPressed(); + if (f != null) { + f.call(); + } + } + }); + applyStyle(b); + // The glyph is rasterised in the style's CURRENT foreground, so it has to + // come after the style is applied. Setting it first burned the theme's + // default ink into the image and the button then wore a dark plus sign + // on a purple surface where the reference has a white one. + setGlyph(b); + return b; + } + + @Override + protected void updateComponent(Component c) { + applyStyle(c); + setGlyph(c); + } + + /** + * Removes the drop shadow a Codename One round border draws for itself. + * + *

The button's elevation belongs to the Material surface under it, which is what + * Flutter shades and what a notched bar already accounts for. A border that also + * draws one produces a second, harder ring that does not match anything -- most + * obviously on the reply study's FAB, where it sits in the bar's notch and the ring + * reads as a smudge around the cut-out rather than as lift.

+ * + *

Both round borders are handled, and only the shadow is touched: the theme still + * owns the shape and the stroke.

+ */ + private static void stripBorderShadow(com.codename1.ui.plaf.Style all) { + com.codename1.ui.plaf.Border b = all.getBorder(); + if (b instanceof com.codename1.ui.plaf.RoundBorder) { + all.setBorder(((com.codename1.ui.plaf.RoundBorder) b).shadowOpacity(0)); + } else if (b instanceof com.codename1.ui.plaf.RoundRectBorder) { + all.setBorder(((com.codename1.ui.plaf.RoundRectBorder) b).shadowOpacity(0)); + } + } + + /// The material glyph, in whatever foreground the style now carries. + private void setGlyph(Component c) { + if (c instanceof com.codename1.ui.Button) { + FontImage.setMaterialIcon((com.codename1.ui.Button) c, iconChar(), + com.codename1.components.FloatingActionButton.getIconDefaultSize()); + } + } + + /** + * The extended (pill) form, plus the FAB's own colours. + * + *

A round FAB grows a circular border; an extended one is a capsule with + * its label beside the glyph. Codename One's RoundBorder defaults to circle + * growth, so a pill needs {@code rectangle(true)} — without it the label + * either vanished or ballooned the button into a disc.

+ */ + /** Material 3's FAB corner radius. */ + private static final double FAB_CORNER_LP = 16; + + private void applyStyle(Component c) { + String label = labelText(); + if (c instanceof com.codename1.ui.Button) { + ((com.codename1.ui.Button) c).setText(label == null ? "" : label); + } + com.codename1.ui.plaf.Style all = c.getAllStyles(); + // No margin. A Codename One button carries one from its theme, and it + // insets the surface INSIDE the box this element lays out, so a FAB + // given Material's 56 logical pixels painted 96 device pixels of colour + // in a 168 pixel box. Flutter's FAB has no margin of its own; the + // Scaffold positions it. + all.setMarginUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + all.setMargin(0, 0, 0, 0); + if (label != null) { + all.setPaddingUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + int h = (int) Math.round(com.codename1.flutter.rendering.Dp.px(16)); + int v = (int) Math.round(com.codename1.flutter.rendering.Dp.px(12)); + all.setPadding(v, v, h, h); + com.codename1.ui.plaf.Border b = all.getBorder(); + if (b instanceof com.codename1.ui.plaf.RoundBorder) { + all.setBorder(((com.codename1.ui.plaf.RoundBorder) b).rectangle(true)); + } + } + if (label == null && !(all.getBorder() instanceof com.codename1.ui.plaf.RoundRectBorder)) { + // A Material 3 FAB is a ROUNDED SQUARE -- a 16 logical pixel corner + // radius -- not the disc Material 2 used. Codename One's own + // FloatingActionButton takes its shape from its theme entry, so a + // theme carrying no entry for the UIID leaves the button an ordinary + // rectangle and a background colour fills a hard-cornered block. + all.setBorder(com.codename1.ui.plaf.RoundRectBorder.create() + .cornerRadius((float) (com.codename1.flutter.rendering.Dp.px(FAB_CORNER_LP) + / com.codename1.ui.Display.getInstance().convertToPixels(1f))) + .strokeOpacity(0) + .shadowOpacity(0)); + } + stripBorderShadow(all); + com.codename1.flutter.Color bg = fab().getBackgroundColor(); + com.codename1.flutter.Color fgDefault = null; + if (bg == null) { + // Material 3's default FAB colours are primaryContainer over + // onPrimaryContainer. Resolving neither left the button wearing + // whatever the Codename One theme happened to carry, which is how + // the bottom-app-bar demo drew a pale lavender button with a purple + // glyph where the reference is purple with a white one -- the two + // roles exactly inverted. + try { + ColorScheme scheme = Theme.of(this).colorScheme(); + if (scheme != null) { + bg = scheme.primaryContainer(); + fgDefault = scheme.onPrimaryContainer(); + } + } catch (Throwable ignore) { + // no ambient theme + } + } + if (bg != null) { + com.codename1.ui.plaf.Border b = all.getBorder(); + if (b instanceof com.codename1.ui.plaf.RoundBorder) { + all.setBorder(((com.codename1.ui.plaf.RoundBorder) b).color(bg.rgb())); + } else { + ThemeDataAdapter.paintColor(all, bg); + } + } + com.codename1.flutter.Color fg = fab().getForegroundColor(); + if (fg == null) { + fg = fgDefault; + } + if (fg != null) { + all.setFgColor(fg.rgb()); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Component c = component(); + if (c == null) { + return constraints.smallest(); + } + return constraints.constrain( + materialSize(fab().isExtended(), c.getPreferredSize().getWidth())); + } + + /** + * The size Material gives a floating action button, in device pixels. + * + *

A regular one is a FIXED square. Taking the component's preferred size + * instead made it as small as its glyph plus whatever padding the theme in + * force happened to carry -- 83 device pixels against the reference's 168, + * less than half. An extended one is a capsule: the height is fixed too and + * only the width follows the label, and never below the minimum.

+ */ + static Size materialSize(boolean extended, double preferredWidth) { + if (!extended) { + double side = com.codename1.flutter.rendering.Dp.px(FAB_SIZE_LP); + return new Size(side, side); + } + return new Size( + Math.max(com.codename1.flutter.rendering.Dp.px(EXTENDED_MIN_WIDTH_LP), + preferredWidth), + com.codename1.flutter.rendering.Dp.px(EXTENDED_HEIGHT_LP)); + } + + /** Material's regular FAB is this many logical pixels on a side. */ + public static final double FAB_SIZE_LP = 56; + /** The extended form's fixed height -- 56 in Material 3, not M2's 48. */ + public static final double EXTENDED_HEIGHT_LP = 56; + /** The extended form's minimum width. */ + public static final double EXTENDED_MIN_WIDTH_LP = 80; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FilterChip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FilterChip.java new file mode 100644 index 00000000000..a2c1a18be9d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FilterChip.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A chip that lets the user toggle one of a set of filters — Flutter's + * {@code FilterChip}. This milestone renders {@code avatar} + {@code label} in a + * {@link Row}; the selected-state styling and {@code onSelected} tap are + * deferred. + */ +public class FilterChip extends StatelessWidget { + + private Widget avatar; + private Widget label; + private boolean selected; + private Color backgroundColor; + private TextStyle labelStyle; + private Funcs.VoidFunc1 onSelected; + + public void avatar(Widget v) { + this.avatar = v; + } + + public void label(Widget v) { + this.label = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + } + + public void selected(boolean v) { + this.selected = v; + } + + public void onSelected(Funcs.VoidFunc1 v) { + this.onSelected = v; + } + + public void pressElevation(Object v) { + } + + public void disabledColor(Color v) { + } + + public void selectedColor(Color v) { + } + + public void tooltip(Object v) { + } + + public void side(Object v) { + } + + public void shape(Object v) { + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void padding(Object v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (avatar != null) { + kids.add(avatar); + } + if (label != null) { + kids.add(label); + } + if (kids.size() == 0) { + return new SizedBox(); + } + Row row = new Row(); + row.mainAxisSize(MainAxisSize.min); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(kids); + return row; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButton.java new file mode 100644 index 00000000000..13845507531 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButton.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material floating action button, backed by the real CN1 + * {@code com.codename1.components.FloatingActionButton}. M1 consumes an + * {@link com.codename1.flutter.widgets.Icon Icon} child as configuration + * (its glyph becomes the FAB icon); other child widgets are not mounted. + */ +public class FloatingActionButton extends Widget { + + private Funcs.VoidFunc0 onPressed; + private String tooltip; + private Widget child; + private Object heroTag; + private com.codename1.flutter.Color backgroundColor; + private com.codename1.flutter.Color foregroundColor; + private Double elevation; + private Widget icon; + private boolean isExtended; + + public void heroTag(Object v) { + this.heroTag = v; + } + + public void backgroundColor(com.codename1.flutter.Color v) { + this.backgroundColor = v; + } + + public void foregroundColor(com.codename1.flutter.Color v) { + this.foregroundColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void tooltip(String v) { + this.tooltip = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Funcs.VoidFunc0 getOnPressed() { + return onPressed; + } + + public String getTooltip() { + return tooltip; + } + + public Widget getChild() { + return child; + } + + /** The leading glyph of an extended FAB, or null. */ + public Widget getIcon() { + return icon; + } + + /** Whether this is the pill-shaped {@code FloatingActionButton.extended} form. */ + public boolean isExtended() { + return isExtended; + } + + public com.codename1.flutter.Color getBackgroundColor() { + return backgroundColor; + } + + public com.codename1.flutter.Color getForegroundColor() { + return foregroundColor; + } + + /** + * {@code FloatingActionButton.extended}: a pill-shaped FAB carrying a label + * and, usually, a leading glyph. + * + *

The icon and the background colour used to be dropped and the result + * rendered as an ordinary round FAB with the default plus sign — which is + * what every gallery study showed instead of its "Back to gallery" pill.

+ */ + public static FloatingActionButton extended(Key key, Funcs.VoidFunc0 onPressed, Widget label, + Widget icon, String tooltip, Object heroTag, Color backgroundColor) { + FloatingActionButton f = new FloatingActionButton(); + f.key(key); + f.onPressed(onPressed); + f.tooltip(tooltip); + f.child(label); + f.icon = icon; + f.isExtended = true; + f.heroTag(heroTag); + if (backgroundColor != null) { + f.backgroundColor(backgroundColor); + } + return f; + } + + @Override + public Element createElement() { + return new FabRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonLocation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonLocation.java new file mode 100644 index 00000000000..06eb6f207f7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonLocation.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Where a {@code FloatingActionButton} is placed within a {@code Scaffold} — + * Flutter's {@code FloatingActionButtonLocation}. + * + *

Flutter exposes these as static const instances of a class; the app only + * ever names a constant and the Scaffold slot receives it untyped, so an enum + * carrying the matching constant names is sufficient for this pass.

+ */ +public enum FloatingActionButtonLocation { + startTop, miniStartTop, centerTop, miniCenterTop, endTop, miniEndTop, + startFloat, miniStartFloat, centerFloat, miniCenterFloat, endFloat, miniEndFloat, + startDocked, miniStartDocked, centerDocked, miniCenterDocked, endDocked, miniEndDocked +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonThemeData.java new file mode 100644 index 00000000000..13e43051e93 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingActionButtonThemeData.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code FloatingActionButtonThemeData}: write-once FAB styling. + * Named Dart constructor parameters map to setter methods; unset values stay + * null. + */ +public class FloatingActionButtonThemeData { + + private Color foregroundColor; + private Color backgroundColor; + private Color focusColor; + private Color hoverColor; + private Color splashColor; + private Double elevation; + private Double focusElevation; + private Double hoverElevation; + private Double disabledElevation; + private Double highlightElevation; + private Object shape; + private Boolean enableFeedback; + private Double iconSize; + private Object sizeConstraints; + private TextStyle extendedTextStyle; + + public void foregroundColor(Color v) { + this.foregroundColor = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void focusColor(Color v) { + this.focusColor = v; + } + + public void hoverColor(Color v) { + this.hoverColor = v; + } + + public void splashColor(Color v) { + this.splashColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void focusElevation(double v) { + this.focusElevation = v; + } + + public void hoverElevation(double v) { + this.hoverElevation = v; + } + + public void disabledElevation(double v) { + this.disabledElevation = v; + } + + public void highlightElevation(double v) { + this.highlightElevation = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void enableFeedback(boolean v) { + this.enableFeedback = v; + } + + public void iconSize(double v) { + this.iconSize = v; + } + + public void sizeConstraints(Object v) { + this.sizeConstraints = v; + } + + public void extendedTextStyle(TextStyle v) { + this.extendedTextStyle = v; + } + + public Color backgroundColor() { + return backgroundColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingLabelBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingLabelBehavior.java new file mode 100644 index 00000000000..2c28310ff6c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FloatingLabelBehavior.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * How a {@link TextField}'s floating label behaves — Flutter's + * {@code FloatingLabelBehavior}. {@link #auto} floats the label on focus/input, + * {@link #always} keeps it floated, {@link #never} keeps it inline as a hint. + */ +public enum FloatingLabelBehavior { + never, auto, always +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FlutterShapeBorderPainter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FlutterShapeBorderPainter.java new file mode 100644 index 00000000000..27556d488cb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/FlutterShapeBorderPainter.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Rect; +import com.codename1.flutter.ShapeBorder; +import com.codename1.flutter.TextDirection; +import com.codename1.flutter.rendering.GraphicsCanvas; +import com.codename1.ui.Component; +import com.codename1.ui.Graphics; +import com.codename1.ui.plaf.Border; + +/** + * A Codename One border that asks a Flutter {@link ShapeBorder} to draw itself. + * + *

Flutter borders are arbitrary outlines: Shrine's input fields chamfer their + * corners, and approximating that with a rounded rectangle is what made them + * read as square. Codename One draws arbitrary shapes perfectly well -- a + * chamfer is a polygon -- and {@link GraphicsCanvas} already turns a Flutter + * Path into one. This is the adapter that lets the border's OWN code run, so + * any shape an application defines paints exactly as written rather than being + * matched to the nearest thing the runtime knows how to draw.

+ */ +public final class FlutterShapeBorderPainter extends Border { + + private final ShapeBorder border; + + public FlutterShapeBorderPainter(ShapeBorder border) { + this.border = border; + } + + @Override + public void paintBorderBackground(Graphics g, Component c) { + // The fill, if any, is the component's own style background; this + // border only draws the outline. + } + + @Override + public void paint(Graphics g, Component c) { + if (border == null) { + return; + } + boolean priorAa = g.isAntiAliased(); + g.setAntiAliased(true); + try { + // Inset by half the stroke so the outline sits INSIDE the box, the + // way Flutter centres a border side on the edge of its rect. + // The Graphics carries the offset and the canvas carries only the + // scale. GraphicsCanvas applies its origin BEFORE its scale, so an + // origin in device pixels lands at a third of where it belongs at + // 3x -- the outline was being drawn, correctly shaped, near the top + // left of the screen instead of around the field. + double dpr = com.codename1.flutter.rendering.Dp.scale(); + // An AFFINE transform, not g.translate: translate() shifts the + // integer draw origin, while the shape this border draws goes + // through the transform pipeline. Mixing the two put the outline a + // component's offset away from the box it belongs to on any port + // that honours the matrix. The matrix below is scale dpr with the + // component's origin in the translation column -- the same shape as + // the AffineScale sample in scripts/hellocodenameone. + com.codename1.ui.Transform prior = null; + boolean affine = g.isTransformSupported(); + if (affine) { + prior = g.getTransform(); + com.codename1.ui.geom.AffineTransform at = new com.codename1.ui.geom.AffineTransform( + (float) dpr, 0f, + 0f, (float) dpr, + (float) c.getX(), (float) c.getY()); + g.setTransform(at.toTransform()); + } + try { + // The canvas adds no scale of its own now: the matrix carries it. + GraphicsCanvas canvas = new GraphicsCanvas(g, 0, 0, affine ? 1 : dpr); + double w = c.getWidth() / dpr; + double h = c.getHeight() / dpr; + if (!affine) { + // No transform support: fall back to drawing at absolute + // coordinates, which is what the canvas does unaided. + canvas = new GraphicsCanvas(g, (int) (c.getX() * dpr), + (int) (c.getY() * dpr), dpr); + } + border.paint(canvas, Rect.fromLTRB(0, 0, w, h), null, 0, 0, TextDirection.ltr); + } finally { + if (affine) { + g.setTransform(prior); + } + } + } catch (Throwable cannotPaint) { + com.codename1.flutter.FlutterErrorReport.record(cannotPaint); + } finally { + g.setAntiAliased(priorAa); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconButton.java new file mode 100644 index 00000000000..7d1b82a8150 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconButton.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * The material icon button: a bare tappable icon, backed by a CN1 Button + * (UIID "FlutterIconButton"). The {@code icon} widget is consumed as + * configuration (an {@link com.codename1.flutter.widgets.Icon Icon}'s glyph + * becomes the material icon). + */ +public class IconButton extends Widget { + + private Funcs.VoidFunc0 onPressed; + private Widget icon; + private Double iconSize; + private Color color; + private String tooltip; + private EdgeInsets padding; + private Color hoverColor; + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void tooltip(String v) { + this.tooltip = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void hoverColor(Color v) { + this.hoverColor = v; + } + + public void splashRadius(double v) { + } + + public void alignment(Object v) { + } + + public void visualDensity(Object v) { + } + + public String getTooltip() { + return tooltip; + } + + public void icon(Widget v) { + this.icon = v; + } + + public void iconSize(double v) { + this.iconSize = v; + } + + public void color(Color v) { + this.color = v; + } + + public Funcs.VoidFunc0 getOnPressed() { + return onPressed; + } + + public Widget getIcon() { + return icon; + } + + public Double getIconSize() { + return iconSize; + } + + public Color getColor() { + return color; + } + + @Override + public Element createElement() { + return new ButtonRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconTheme.java new file mode 100644 index 00000000000..902bf08b60b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconTheme.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.InheritedWidget; + +/** + * Establishes an ambient {@link IconThemeData} for its subtree — Flutter's + * {@code IconTheme}. Descendant {@code Icon}s read {@code IconTheme.of(context)} + * for their default size and colour. + * + *

This is how an app bar tints its glyphs. The bar sets one icon theme and + * every icon below it picks the colour up; nothing hands each icon a colour + * individually. While {@link #of(BuildContext)} answered a fresh default, that + * whole mechanism was inert: the gallery's white-on-purple bars rendered black + * glyphs, because a Codename One style does not inherit a foreground colour + * from a parent container the way the ambient theme is expected to.

+ */ +public class IconTheme extends InheritedWidget { + + private IconThemeData data; + + public void data(IconThemeData v) { + this.data = v; + } + + public IconThemeData getData() { + return data; + } + + /** + * Dart's {@code IconTheme.of(context)}: the nearest ambient icon theme, + * falling back to the material theme's, then to an empty one. + * + *

Merged down the chain, as Flutter does: an {@code IconTheme.merge} + * that only sets a colour must not erase the size an outer theme set.

+ */ + public static IconThemeData of(BuildContext context) { + IconThemeData resolved = null; + if (context != null) { + IconTheme t = context.maybeDependOnInheritedWidgetOfExactType(IconTheme.class); + if (t != null) { + resolved = t.data; + } + } + // Only walk the tree a second time for the material theme when the + // nearer icon theme actually leaves something to inherit. Every Icon in + // the app calls this on every update, and each lookup is a walk to the + // root — asking for the theme unconditionally doubled that for no gain. + if (resolved != null && resolved.color() != null && resolved.size() != null) { + return resolved; + } + IconThemeData themed = null; + try { + themed = Theme.of(context).iconTheme(); + } catch (Throwable t) { + // no ambient material theme + } + if (resolved == null) { + return themed != null ? themed : new IconThemeData(); + } + if (themed == null) { + return resolved; + } + return merged(themed, resolved); + } + + /** {@code over} wins field by field; anything it leaves null falls through to {@code under}. */ + private static IconThemeData merged(IconThemeData under, IconThemeData over) { + IconThemeData out = new IconThemeData(); + out.color(over.color() != null ? over.color() : under.color()); + Double size = over.size() != null ? over.size() : under.size(); + if (size != null) { + out.size(size.doubleValue()); + } + Double opacity = over.opacity() != null ? over.opacity() : under.opacity(); + if (opacity != null) { + out.opacity(opacity.doubleValue()); + } + return out; + } + + /** Dart's {@code IconTheme.merge(...)} named constructor. */ + public static IconTheme merge(Key key, IconThemeData data, Widget child) { + IconTheme t = new IconTheme(); + t.key(key); + t.data(data); + t.child(child); + return t; + } + + /** Convenience for the runtime's own wrapping: an icon theme of one colour. */ + public static IconTheme tint(com.codename1.flutter.Color color, Widget child) { + IconThemeData d = new IconThemeData(); + d.color(color); + IconTheme t = new IconTheme(); + t.data(d); + t.child(child); + return t; + } + + @Override + public boolean updateShouldNotify(InheritedWidget oldWidget) { + if (!(oldWidget instanceof IconTheme)) { + return true; + } + IconThemeData was = ((IconTheme) oldWidget).data; + if (was == data) { + return false; + } + if (was == null || data == null) { + return true; + } + return !sameColor(was.color(), data.color()) || !sameSize(was.size(), data.size()); + } + + private static boolean sameColor(com.codename1.flutter.Color a, com.codename1.flutter.Color b) { + return a == b || (a != null && b != null && a.value() == b.value()); + } + + private static boolean sameSize(Double a, Double b) { + return a == b || (a != null && b != null && a.doubleValue() == b.doubleValue()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconThemeData.java new file mode 100644 index 00000000000..c4b44c1d77c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/IconThemeData.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +/** + * Material {@code IconThemeData}: a write-once icon styling bundle. Named Dart + * constructor parameters map to setter methods; unset values stay null and + * inherit. {@link #copyWith} merges non-null overrides onto a copy. + */ +public class IconThemeData { + + private Color color; + private Double size; + private Double opacity; + private Double fill; + private Double weight; + private Double grade; + private Double opticalSize; + private Object shadows; + private Boolean applyTextScaling; + + public void color(Color v) { + this.color = v; + } + + public void size(double v) { + this.size = v; + } + + public void opacity(double v) { + this.opacity = v; + } + + public void fill(double v) { + this.fill = v; + } + + public void weight(double v) { + this.weight = v; + } + + public void grade(double v) { + this.grade = v; + } + + public void opticalSize(double v) { + this.opticalSize = v; + } + + public void shadows(Object v) { + this.shadows = v; + } + + public void applyTextScaling(boolean v) { + this.applyTextScaling = v; + } + + public Color color() { + return color; + } + + public Double size() { + return size; + } + + public Double opacity() { + return opacity; + } + + public IconThemeData copyWith(Color color, Double size, Double opacity, Double fill, + Double weight, Double grade, Double opticalSize, Object shadows, + Boolean applyTextScaling) { + IconThemeData c = new IconThemeData(); + c.color = color != null ? color : this.color; + c.size = size != null ? size : this.size; + c.opacity = opacity != null ? opacity : this.opacity; + c.fill = fill != null ? fill : this.fill; + c.weight = weight != null ? weight : this.weight; + c.grade = grade != null ? grade : this.grade; + c.opticalSize = opticalSize != null ? opticalSize : this.opticalSize; + c.shadows = shadows != null ? shadows : this.shadows; + c.applyTextScaling = applyTextScaling != null ? applyTextScaling : this.applyTextScaling; + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Ink.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Ink.java new file mode 100644 index 00000000000..7570bcc5582 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Ink.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BoxFit; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Decoration; +import com.codename1.flutter.EdgeInsetsGeometry; +import com.codename1.flutter.ImageProvider; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Paints a decoration (or image) as part of the Material so ink splashes render + * above it — Flutter's {@code Ink} (and its {@code Ink.image} named constructor). + * + *

The decoration is painted by delegating to a {@link com.codename1.flutter.widgets.Container}, + * which already knows how to paint a colour, a box decoration and a background image. It + * used to render only the child, so an Ink used for a card's tinted or pictured background + * came out blank. + */ +public class Ink extends StatelessWidget { + + private EdgeInsetsGeometry padding; + private Color color; + private Decoration decoration; + private double width; + private double height; + private Widget child; + private ImageProvider image; + private BoxFit fit; + + public void padding(EdgeInsetsGeometry v) { this.padding = v; } + public void color(Color v) { this.color = v; } + public void decoration(Decoration v) { this.decoration = v; } + public void width(double v) { this.width = v; } + public void height(double v) { this.height = v; } + public void child(Widget v) { this.child = v; } + + /** Dart's {@code Ink.image(...)} named constructor. */ + public static Ink image(com.codename1.flutter.Key key, ImageProvider image, BoxFit fit, Widget child, + Double width, Double height, EdgeInsetsGeometry padding, Object colorFilter, Object alignment, + Object repeat, Object centerSlice, Object onImageError) { + Ink ink = new Ink(); + ink.image = image; + ink.fit = fit; + ink.child = child; + if (width != null) ink.width = width; + if (height != null) ink.height = height; + ink.padding = padding; + return ink; + } + + @Override + public Widget build(BuildContext context) { + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + if (color != null) { + box.color(color); + } + if (decoration != null) { + box.decoration(decoration); + } else if (image != null) { + com.codename1.flutter.DecorationImage backdrop = new com.codename1.flutter.DecorationImage(); + backdrop.image(image); + if (fit != null) { + backdrop.fit(fit); + } + com.codename1.flutter.BoxDecoration d = new com.codename1.flutter.BoxDecoration(); + d.image(backdrop); + box.decoration(d); + } + if (padding != null) { + box.padding(padding); + } + if (width > 0) { + box.width(width); + } + if (height > 0) { + box.height(height); + } + box.child(child); + return box; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkResponse.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkResponse.java new file mode 100644 index 00000000000..2a30954fa5d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkResponse.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BorderRadius; +import com.codename1.flutter.Color; +import com.codename1.flutter.ShapeBorder; +import com.codename1.flutter.widgets.GestureDetector; + +/** + * The material tap-target with ink feedback — Flutter's {@code InkResponse} + * (the superclass of {@link InkWell}). M2 renders it exactly like a + * {@link GestureDetector} (transparent overlay, no ripple); the ink splash and + * highlight are retained as configuration for a later milestone. + */ +public class InkResponse extends GestureDetector { + + private Color splashColor; + private Color highlightColor; + private Color focusColor; + private Color hoverColor; + private ShapeBorder customBorder; + private BorderRadius borderRadius; + private Double radius; + private Boolean containedInkWell; + + public void splashColor(Color v) { + this.splashColor = v; + } + + public void highlightColor(Color v) { + this.highlightColor = v; + } + + public void focusColor(Color v) { + this.focusColor = v; + } + + public void hoverColor(Color v) { + this.hoverColor = v; + } + + public void customBorder(ShapeBorder v) { + this.customBorder = v; + } + + public void borderRadius(BorderRadius v) { + this.borderRadius = v; + } + + public void radius(double v) { + this.radius = v; + } + + public void containedInkWell(boolean v) { + this.containedInkWell = v; + } + + public Color getSplashColor() { + return splashColor; + } + + public Color getHighlightColor() { + return highlightColor; + } + + public BorderRadius getBorderRadius() { + return borderRadius; + } + + /** The explicit splash radius in logical pixels, or null to size it to the box. */ + public Double getRadius() { + return radius; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkWell.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkWell.java new file mode 100644 index 00000000000..f25245f1e93 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InkWell.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The material rectangular tap-target — Flutter's {@code InkWell}, a + * {@link InkResponse} specialised to a rectangular highlight with contained + * ink. The splash and press highlight are painted by + * {@code GestureOverlayRenderElement}, which is the component that owns the + * tap area; this class only carries the configuration. + */ +public class InkWell extends InkResponse { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputChip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputChip.java new file mode 100644 index 00000000000..9432c316794 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputChip.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A chip representing a complex piece of information (a contact, tag, ...) that + * can be selected, pressed or deleted — Flutter's {@code InputChip}. This + * milestone renders {@code avatar} + {@code label} in a {@link Row}; the + * selection/press/delete interactions are deferred. + */ +public class InputChip extends StatelessWidget { + + private Widget avatar; + private Widget label; + private Widget deleteIcon; + private boolean selected; + private Color backgroundColor; + private Color deleteIconColor; + private TextStyle labelStyle; + private Funcs.VoidFunc1 onSelected; + private Funcs.VoidFunc0 onPressed; + private Funcs.VoidFunc0 onDeleted; + + public void avatar(Widget v) { + this.avatar = v; + } + + public void label(Widget v) { + this.label = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + } + + public void selected(boolean v) { + this.selected = v; + } + + public void isEnabled(boolean v) { + } + + public void onSelected(Funcs.VoidFunc1 v) { + this.onSelected = v; + } + + public void deleteIcon(Widget v) { + this.deleteIcon = v; + } + + public void onDeleted(Funcs.VoidFunc0 v) { + this.onDeleted = v; + } + + public void deleteIconColor(Color v) { + this.deleteIconColor = v; + } + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void pressElevation(Object v) { + } + + public void disabledColor(Color v) { + } + + public void selectedColor(Color v) { + } + + public void tooltip(Object v) { + } + + public void side(Object v) { + } + + public void shape(Object v) { + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void padding(Object v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (avatar != null) { + kids.add(avatar); + } + if (label != null) { + kids.add(label); + } + if (deleteIcon != null) { + kids.add(deleteIcon); + } + if (kids.size() == 0) { + return new SizedBox(); + } + Row row = new Row(); + row.mainAxisSize(MainAxisSize.min); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(kids); + return row; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecoration.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecoration.java new file mode 100644 index 00000000000..e29adc46449 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecoration.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Decoration configuration for a {@link TextField}. M3 renders both + * {@code labelText} and {@code hintText} through the CN1 hint mechanism + * (labelText wins when both are set) — a floating label is a later + * milestone. + */ +public class InputDecoration { + + private String labelText; + private String hintText; + private String helperText; + private String errorText; + private String prefixText; + private String suffixText; + private com.codename1.flutter.Widget icon; + private com.codename1.flutter.Widget prefixIcon; + private com.codename1.flutter.Widget suffixIcon; + private Boolean filled; + private com.codename1.flutter.Color fillColor; + private com.codename1.flutter.InputBorder border; + private com.codename1.flutter.TextStyle labelStyle; + private com.codename1.flutter.TextStyle hintStyle; + private com.codename1.flutter.EdgeInsetsGeometry contentPadding; + private FloatingLabelBehavior floatingLabelBehavior; + + public void labelText(String v) { + this.labelText = v; + } + + public void hintText(String v) { + this.hintText = v; + } + + public void helperText(String v) { + this.helperText = v; + } + + public void errorText(String v) { + this.errorText = v; + } + + public void prefixText(String v) { + this.prefixText = v; + } + + public void suffixText(String v) { + this.suffixText = v; + } + + public void icon(com.codename1.flutter.Widget v) { + this.icon = v; + } + + public void prefixIcon(com.codename1.flutter.Widget v) { + this.prefixIcon = v; + } + + public void suffixIcon(com.codename1.flutter.Widget v) { + this.suffixIcon = v; + } + + public void filled(boolean v) { + this.filled = v; + } + + public void fillColor(com.codename1.flutter.Color v) { + this.fillColor = v; + } + + public void border(com.codename1.flutter.InputBorder v) { + this.border = v; + } + + public void labelStyle(com.codename1.flutter.TextStyle v) { + this.labelStyle = v; + } + + public void hintStyle(com.codename1.flutter.TextStyle v) { + this.hintStyle = v; + } + + public void contentPadding(com.codename1.flutter.EdgeInsetsGeometry v) { + this.contentPadding = v; + } + + public void floatingLabelBehavior(FloatingLabelBehavior v) { + this.floatingLabelBehavior = v; + } + + public String getLabelText() { + return labelText; + } + + /** Whether the field paints a solid fill behind its content. */ + public boolean isFilled() { + return filled != null && filled.booleanValue(); + } + + /** The fill colour, or null to take the theme's. */ + public com.codename1.flutter.Color getFillColor() { + return fillColor; + } + + /** The glyph shown before the content, or null. */ + /** + * {@code icon} -- the glyph OUTSIDE the decorated box, to its left. + * + *

Distinct from {@code prefixIcon}, which sits inside the fill and the + * border. The text-field demo uses this one for the person, phone and + * envelope beside its fields.

+ */ + public com.codename1.flutter.Widget getIcon() { + return icon; + } + + /** {@code hintStyle} -- the type the placeholder is set in. */ + public com.codename1.flutter.TextStyle getHintStyle() { + return hintStyle; + } + + /** The hint drawn under the field, or null. */ + public String getHelperText() { + return helperText; + } + + public com.codename1.flutter.Widget getSuffixIcon() { + return suffixIcon; + } + + public com.codename1.flutter.Widget getPrefixIcon() { + return prefixIcon; + } + + /** The requested border, or null for the theme's. */ + public com.codename1.flutter.InputBorder getBorder() { + return border; + } + + /** The requested content padding, or null. */ + public com.codename1.flutter.EdgeInsetsGeometry getContentPadding() { + return contentPadding; + } + + public String getHintText() { + return hintText; + } + + /** + * {@code InputDecoration.collapsed}: a minimal decoration with no label, + * border or padding — only a hint. Styling parameters are accepted for API + * shape and ignored at this milestone. + */ + public static InputDecoration collapsed(String hintText, Object hintStyle, Object border, + Boolean filled, com.codename1.flutter.Color fillColor) { + InputDecoration d = new InputDecoration(); + d.hintText(hintText); + // No border and no padding are what COLLAPSED means -- they are the whole point + // of the factory, not defaults it happens to inherit. Keeping only the hint left + // the result indistinguishable from a plain InputDecoration, so every collapsed + // field still drew the full outlined box: Reply's compose screen boxed its + // subject and its message body, where the reference has neither. + d.border(com.codename1.flutter.InputBorder.none); + d.contentPadding(com.codename1.flutter.EdgeInsets.all(0)); + if (border instanceof com.codename1.flutter.InputBorder) { + d.border((com.codename1.flutter.InputBorder) border); + } + if (hintStyle instanceof com.codename1.flutter.TextStyle) { + d.hintStyle((com.codename1.flutter.TextStyle) hintStyle); + } + if (filled != null) { + d.filled(filled.booleanValue()); + } + if (fillColor != null) { + d.fillColor(fillColor); + } + return d; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecorationThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecorationThemeData.java new file mode 100644 index 00000000000..8e01379dd6a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/InputDecorationThemeData.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsetsGeometry; +import com.codename1.flutter.TextStyle; + +/** + * Theming values applied to descendant InputDecorators — Flutter's + * {@code InputDecorationThemeData} (the Material-3 value-type spelling of the + * older {@code InputDecorationTheme}). Signature-only: the gallery only sets a + * handful of fields and never reads them back, so borders are held opaquely. + */ +public class InputDecorationThemeData { + + private TextStyle labelStyle; + private TextStyle floatingLabelStyle; + private TextStyle helperStyle; + private TextStyle hintStyle; + private TextStyle errorStyle; + private TextStyle prefixStyle; + private TextStyle suffixStyle; + private TextStyle counterStyle; + private boolean filled; + private Color fillColor; + private Color focusColor; + private Color hoverColor; + private EdgeInsetsGeometry contentPadding; + private boolean isDense; + private boolean isCollapsed; + private Object border; + private Object enabledBorder; + private Object focusedBorder; + private Object errorBorder; + private Object focusedErrorBorder; + private Object disabledBorder; + private Object floatingLabelBehavior; + private double gapPadding; + private boolean alignLabelWithHint; + private Object constraints; + + /** Whether descendant fields paint a fill by default. */ + public boolean isFilled() { return filled; } + + /** The default fill colour for descendant fields. */ + public Color getFillColor() { return fillColor; } + + /** The default content padding for descendant fields. */ + public Object getBorder() { + return border; + } + + public Object getEnabledBorder() { + return enabledBorder; + } + + public EdgeInsetsGeometry getContentPadding() { return contentPadding; } + + /** The default type for descendant fields' labels. */ + public TextStyle getLabelStyle() { return labelStyle; } + + public void labelStyle(TextStyle v) { this.labelStyle = v; } + public void floatingLabelStyle(TextStyle v) { this.floatingLabelStyle = v; } + public void helperStyle(TextStyle v) { this.helperStyle = v; } + public void hintStyle(TextStyle v) { this.hintStyle = v; } + public void errorStyle(TextStyle v) { this.errorStyle = v; } + public void prefixStyle(TextStyle v) { this.prefixStyle = v; } + public void suffixStyle(TextStyle v) { this.suffixStyle = v; } + public void counterStyle(TextStyle v) { this.counterStyle = v; } + public void filled(boolean v) { this.filled = v; } + public void fillColor(Color v) { this.fillColor = v; } + public void focusColor(Color v) { this.focusColor = v; } + public void hoverColor(Color v) { this.hoverColor = v; } + public void contentPadding(EdgeInsetsGeometry v) { this.contentPadding = v; } + public void isDense(boolean v) { this.isDense = v; } + public void isCollapsed(boolean v) { this.isCollapsed = v; } + public void border(Object v) { this.border = v; } + public void enabledBorder(Object v) { this.enabledBorder = v; } + public void focusedBorder(Object v) { this.focusedBorder = v; } + public void errorBorder(Object v) { this.errorBorder = v; } + public void focusedErrorBorder(Object v) { this.focusedErrorBorder = v; } + public void disabledBorder(Object v) { this.disabledBorder = v; } + public void floatingLabelBehavior(Object v) { this.floatingLabelBehavior = v; } + public void gapPadding(double v) { this.gapPadding = v; } + public void alignLabelWithHint(boolean v) { this.alignLabelWithHint = v; } + public void constraints(Object v) { this.constraints = v; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java new file mode 100644 index 00000000000..93d2c2e8cb2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LicensePage.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * The Material page listing the open-source licenses of the app's packages — + * Flutter's {@code LicensePage}. + * + *

It rendered nothing at all, so "View licenses" led to a blank screen. There is no + * package license registry to enumerate here — that is a Dart-tooling artifact — so the page + * shows the application's own identity and legalese, which is the part an app actually + * supplies. + */ +public class LicensePage extends StatelessWidget { + + private String applicationName; + private String applicationVersion; + private Widget applicationIcon; + private String applicationLegalese; + + public void applicationName(String v) { this.applicationName = v; } + public void applicationVersion(String v) { this.applicationVersion = v; } + public void applicationIcon(Widget v) { this.applicationIcon = v; } + public void applicationLegalese(String v) { this.applicationLegalese = v; } + + /** Dart's top-level {@code showLicensePage(...)}: pushes a license page. */ + public static void show(BuildContext context, String applicationName, String applicationVersion, + Widget applicationIcon, String applicationLegalese, Boolean useRootNavigator) { + final LicensePage page = new LicensePage(); + page.applicationName(applicationName); + page.applicationVersion(applicationVersion); + page.applicationIcon(applicationIcon); + page.applicationLegalese(applicationLegalese); + com.codename1.flutter.navigation.MaterialPageRoute route = + new com.codename1.flutter.navigation.MaterialPageRoute(); + route.builder(new dart.runtime.Funcs.Func1() { + @Override + public Widget call(BuildContext routeContext) { + return page; + } + }); + com.codename1.flutter.navigation.Navigator.push(context, route); + } + + @Override + public Widget build(BuildContext context) { + dart.core.DartList rows = new dart.core.DartList(); + if (applicationIcon != null) { + rows.add(applicationIcon); + } + if (applicationName != null) { + rows.add(new com.codename1.flutter.widgets.Text(applicationName)); + } + if (applicationVersion != null) { + rows.add(new com.codename1.flutter.widgets.Text(applicationVersion)); + } + if (applicationLegalese != null) { + rows.add(new com.codename1.flutter.widgets.Text(applicationLegalese)); + } + com.codename1.flutter.widgets.Column body = new com.codename1.flutter.widgets.Column(); + body.children(rows); + + com.codename1.flutter.widgets.Padding padded = + new com.codename1.flutter.widgets.Padding(); + padded.padding(com.codename1.flutter.EdgeInsets.all(24)); + padded.child(body); + + AppBar bar = new AppBar(); + bar.title(new com.codename1.flutter.widgets.Text("Licenses")); + Scaffold page = new Scaffold(); + page.appBar(bar); + page.body(padded); + return page; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LinearProgressIndicator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LinearProgressIndicator.java new file mode 100644 index 00000000000..7c9e9349aa0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LinearProgressIndicator.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.ColoredBox; +import com.codename1.flutter.widgets.SizedBox; + +/** + * A horizontal material progress bar — Flutter's {@code LinearProgressIndicator}. + * This milestone renders a thin 4lp bar filled with the indicator {@code color} + * (a determinate {@code value} is accepted but the fill fraction and the + * indeterminate sweep animation are deferred). + */ +public class LinearProgressIndicator extends StatelessWidget { + + private Double value; + private Color color; + private Color backgroundColor; + private Double minHeight; + + public void value(double v) { + this.value = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void color(Color v) { + this.color = v; + } + + public void valueColor(Object v) { + } + + public void minHeight(double v) { + this.minHeight = v; + } + + public void semanticsLabel(String v) { + } + + public void semanticsValue(String v) { + } + + public void borderRadius(Object v) { + } + + @Override + public Widget build(BuildContext context) { + SizedBox box = new SizedBox(); + box.height(minHeight != null ? minHeight : 4.0); + Color fill = color != null ? color : backgroundColor; + if (fill != null) { + ColoredBox cb = new ColoredBox(); + cb.color(fill); + box.child(cb); + } + return box; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTile.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTile.java new file mode 100644 index 00000000000..d8ce14095ed --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTile.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material list row: leading | (title above subtitle) | trailing, with a + * 56lp minimum height, 16lp horizontal padding and an onTap callback. + * Backed by a CN1 Container (UIID "FlutterListTile") plus a transparent tap + * overlay. + */ +public class ListTile extends Widget { + + private Widget leading; + private Widget title; + private Widget subtitle; + private Widget trailing; + private Funcs.VoidFunc0 onTap; + private boolean selected; + + public void selected(boolean v) { + this.selected = v; + } + + public void contentPadding(com.codename1.flutter.EdgeInsetsGeometry v) { + } + + public void mouseCursor(Object v) { + } + + public void dense(boolean v) { + } + + public boolean getSelected() { + return selected; + } + + public void leading(Widget v) { + this.leading = v; + } + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void trailing(Widget v) { + this.trailing = v; + } + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public Widget getLeading() { + return leading; + } + + public Widget getTitle() { + return title; + } + + public Widget getSubtitle() { + return subtitle; + } + + public Widget getTrailing() { + return trailing; + } + + public Funcs.VoidFunc0 getOnTap() { + return onTap; + } + + @Override + public Element createElement() { + return new ListTileRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileControlAffinity.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileControlAffinity.java new file mode 100644 index 00000000000..9a314c8d029 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileControlAffinity.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Which edge a {@code CheckboxListTile} / {@code RadioListTile} / + * {@code SwitchListTile} puts its control on — Flutter's + * {@code ListTileControlAffinity}. + * + *

The default, {@code platform}, is not "whatever looks reasonable": Flutter + * resolves it to the LEADING edge for a checkbox or a radio and the TRAILING + * edge for a switch, on every platform. Every settings list in the gallery is + * laid out that way, and putting the control on the wrong edge mirrors the + * whole row.

+ */ +public enum ListTileControlAffinity { + leading, trailing, platform; + + /** + * Whether a control with this affinity trails. + * + * @param affinity the value the app supplied, possibly null or an + * unrecognised object (the transpiler hands enums + * through as values, but a stub may pass anything) + * @param platformTrails what {@code platform} means for this control — + * true for a switch, false for a checkbox or radio + */ + public static boolean isTrailing(Object affinity, boolean platformTrails) { + if (affinity == trailing) { + return true; + } + if (affinity == leading) { + return false; + } + return platformTrails; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java new file mode 100644 index 00000000000..9a25e45ab2f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ListTileRenderElement.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; + +import dart.runtime.Funcs; + +/** + * Composite render element for {@link ListTile}. Owns a background Container + * (UIID "FlutterListTile", attached before the children so it paints behind + * them) and mounts leading / title / subtitle / trailing subtrees plus a + * synthesized transparent tap overlay LAST (so it sits on top for pointer + * dispatch, like GestureDetector's overlay). + * + *

Material geometry: 16lp horizontal padding, 16lp gaps between the + * leading / text block / trailing sections, title stacked above subtitle, + * leading and trailing vertically centered, 56lp minimum height (8lp + * vertical padding when the content is taller).

+ */ +public class ListTileRenderElement extends RenderElement { + + /** Material list-tile minimum height in logical pixels, for a tile with no subtitle. */ + public static final double MIN_HEIGHT_LP = 56; + + /// Material's height for a tile that has a subtitle. + /// + /// A list tile's height comes from how many LINES it has, not from how big the + /// things inside it are, and in particular the leading widget never drives it -- it + /// is centred in the tile and is allowed to overflow. Letting it drive the height + /// made every tile as tall as its own avatar: Crane's destination list carries a + /// 60lp thumbnail, which with the vertical padding came to 76lp where Material's + /// two-line tile is 72lp, so each row ran 4lp long and the rows below it drifted + /// further out of register with every one that was added -- 43 device pixels by the + /// fourth row. + public static final double TWO_LINE_HEIGHT_LP = 72; + /** Horizontal padding in logical pixels. */ + public static final double HPAD_LP = 16; + /** Gap between sections in logical pixels. */ + public static final double GAP_LP = 16; + /** Vertical padding applied when the content overflows 56lp. */ + public static final double VPAD_LP = 8; + + private Element leadingChild; + private Element titleChild; + private Element subtitleChild; + private Element trailingChild; + private Element overlayChild; + + public ListTileRenderElement(ListTile widget) { + super(widget); + } + + private ListTile tile() { + return (ListTile) widget(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Container c = new Container(); + c.setUIID("FlutterListTile"); + c.getAllStyles().setPadding(0, 0, 0, 0); + c.getAllStyles().setMargin(0, 0, 0, 0); + return c; + } + + @Override + protected void syncChildren() { + leadingChild = updateChild(leadingChild, tile().getLeading(), 0); + titleChild = updateChild(titleChild, tile().getTitle(), 1); + subtitleChild = updateChild(subtitleChild, tile().getSubtitle(), 2); + trailingChild = updateChild(trailingChild, tile().getTrailing(), 3); + overlayChild = updateChild(overlayChild, new TileOverlay(), 4); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (leadingChild != null) { + visitor.call(leadingChild); + } + if (titleChild != null) { + visitor.call(titleChild); + } + if (subtitleChild != null) { + visitor.call(subtitleChild); + } + if (trailingChild != null) { + visitor.call(trailingChild); + } + if (overlayChild != null) { + visitor.call(overlayChild); + } + } + + void fireTap() { + Funcs.VoidFunc0 f = tile().getOnTap(); + if (f != null) { + f.call(); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double hpad = Dp.px(HPAD_LP); + double gap = Dp.px(GAP_LP); + double vpad = Dp.px(VPAD_LP); + + RenderElement leading = RenderElement.findRenderElement(leadingChild); + RenderElement title = RenderElement.findRenderElement(titleChild); + RenderElement subtitle = RenderElement.findRenderElement(subtitleChild); + RenderElement trailing = RenderElement.findRenderElement(trailingChild); + RenderElement overlay = RenderElement.findRenderElement(overlayChild); + + BoxConstraints loose = new BoxConstraints( + 0, constraints.hasBoundedWidth() ? constraints.maxWidth() : Double.POSITIVE_INFINITY, + 0, Double.POSITIVE_INFINITY); + + Size leadingSize = leading != null ? leading.layout(loose) : Size.ZERO; + Size trailingSize = trailing != null ? trailing.layout(loose) : Size.ZERO; + + double sideWidth = hpad * 2 + + (leading != null ? leadingSize.width() + gap : 0) + + (trailing != null ? trailingSize.width() + gap : 0); + + BoxConstraints textConstraints; + if (constraints.hasBoundedWidth()) { + double avail = Math.max(0, constraints.maxWidth() - sideWidth); + textConstraints = new BoxConstraints(0, avail, 0, Double.POSITIVE_INFINITY); + } else { + textConstraints = loose; + } + Size titleSize = title != null ? title.layout(textConstraints) : Size.ZERO; + Size subtitleSize = subtitle != null ? subtitle.layout(textConstraints) : Size.ZERO; + + double textW = Math.max(titleSize.width(), subtitleSize.width()); + double textH = titleSize.height() + subtitleSize.height(); + + double width = constraints.hasBoundedWidth() + ? constraints.maxWidth() + : sideWidth + textW; + // The TEXT may push a tile past its nominal height; the leading and trailing may + // not. Material fixes the height by line count and centres the other two inside + // it, overflowing them if it must. + double nominal = Dp.px(subtitle != null ? TWO_LINE_HEIGHT_LP : MIN_HEIGHT_LP); + double height = constraints.constrainHeight( + Math.max(nominal, textH + vpad * 2)); + + if (leading != null) { + setChildOffset(leading, hpad, (height - leadingSize.height()) / 2); + } + double textX = hpad + (leading != null ? leadingSize.width() + gap : 0); + double textY = (height - textH) / 2; + if (title != null) { + setChildOffset(title, textX, textY); + } + if (subtitle != null) { + setChildOffset(subtitle, textX, textY + titleSize.height()); + } + if (trailing != null) { + setChildOffset(trailing, width - hpad - trailingSize.width(), + (height - trailingSize.height()) / 2); + } + if (overlay != null) { + overlay.layout(BoxConstraints.tight(width, height)); + setChildOffset(overlay, 0, 0); + } + return constraints.constrain(new Size(width, height)); + } + + // ------------------------------------------------------------------ + // Tap overlay (synthesized, mounts after the visible children) + // ------------------------------------------------------------------ + + static class TileOverlay extends Widget { + @Override + public Element createElement() { + return new TileOverlayElement(this); + } + } + + static class TileOverlayElement extends RenderElement { + + TileOverlayElement(TileOverlay widget) { + super(widget); + } + + private ListTileRenderElement tileElement() { + Element p = parent(); + return p instanceof ListTileRenderElement ? (ListTileRenderElement) p : null; + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + return new OverlayComponent(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + // the parent hands us tight constraints matching the tile bounds + return constraints.smallest(); + } + + class OverlayComponent extends Component { + + OverlayComponent() { + setUIID("FlutterGesture"); + setGrabsPointerEvents(true); + setFocusable(false); + getAllStyles().setBgTransparency(0); + getAllStyles().setPadding(0, 0, 0, 0); + getAllStyles().setMargin(0, 0, 0, 0); + } + + @Override + public void paint(Graphics g) { + // paints nothing — pure hit area + } + + @Override + public void pointerReleased(int x, int y) { + boolean wasDrag = isDragActivated(); + super.pointerReleased(x, y); + if (!wasDrag && contains(x, y)) { + ListTileRenderElement t = tileElement(); + if (t != null) { + t.fireTap(); + } + } + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LocalizationsScope.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LocalizationsScope.java new file mode 100644 index 00000000000..e96c4119b1b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/LocalizationsScope.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.InheritedValueProvider; +import com.codename1.flutter.provider.SingleChildWidget; + +import java.util.List; + +/** + * Publishes the localized-resource objects loaded from a MaterialApp's + * {@code localizationsDelegates} to its subtree, keyed by runtime type. This is + * how {@code GalleryLocalizations.of(context)} (which resolves through + * {@link com.codename1.flutter.widgets.Localizations#of}) finds its instance: + * {@code providedValueFor} returns the first loaded object assignable to the + * requested type. + */ +public class LocalizationsScope extends SingleChildWidget implements InheritedValueProvider { + + private List resources; + private final dart.runtime.Funcs.Func0> supplier; + + public LocalizationsScope(List resources) { + this.resources = resources; + this.supplier = null; + } + + /** + * A scope whose resources load on first lookup. Deferring matters: loading + * during the app's build reads the delegate list at the earliest possible + * moment, which on a lazily-initialised backend can be before the class + * holding it has run its static initialiser. + */ + public LocalizationsScope(dart.runtime.Funcs.Func0> supplier) { + this.supplier = supplier; + } + + private List resources() { + if (resources == null && supplier != null) { + resources = supplier.call(); + if (resources != null && resources.isEmpty()) { + try { + com.codename1.io.Log.p("Flutter runtime: no localizations resolved for this app; " + + "every Foo.of(context) below will be null"); + } catch (Throwable ignore) { + // headless: Log has no storage backend + } + } + } + return resources; + } + + @Override + public Object providedValueFor(Class type) { + List rs = resources(); + if (rs != null && type != null) { + for (Object r : rs) { + if (com.codename1.flutter.Element.isInstanceOf(type, r)) { + return r; + } + } + } + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Material.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Material.java new file mode 100644 index 00000000000..704e7c7d972 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Material.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; + +/** + * A piece of material — Flutter's {@code Material}. Provides a surface color + * (and, in Flutter, elevation shadow and ink effects) behind its child. This + * milestone paints the {@code color} surface and sizes to the child; elevation + * shadow, shape and ink are retained but not yet rendered. + */ +public class Material extends Widget { + + private Object type; + private double elevation; + private Color color; + private Color shadowColor; + private Color surfaceTintColor; + private TextStyle textStyle; + private Object borderRadius; + private Object shape; + private boolean borderOnForeground = true; + private Clip clipBehavior = Clip.none; + private Widget child; + + public void type(Object v) { + this.type = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void color(Color v) { + this.color = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void textStyle(TextStyle v) { + this.textStyle = v; + } + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void borderOnForeground(boolean v) { + this.borderOnForeground = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + /** The duration of ink/elevation animations — Flutter's {@code animationDuration}. */ + public void animationDuration(dart.core.Duration v) { + } + + public void child(Widget v) { + this.child = v; + } + + /// The shape given to this Material — a RoundedRectangleBorder carries the radius. + /// Whether this surface clips its subtree to its shape. + public Clip getClipBehavior() { + return clipBehavior; + } + + public Object getShape() { + return shape; + } + + /// The borderRadius given directly (Material accepts either form). + public Object getBorderRadius() { + return borderRadius; + } + + public Color getColor() { + return color; + } + + public double getElevation() { + return elevation; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new MaterialRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialApp.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialApp.java new file mode 100644 index 00000000000..6100d4d6b99 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialApp.java @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Locale; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.ThemeMode; +import com.codename1.flutter.Widget; +import com.codename1.flutter.navigation.MaterialPageRoute; +import com.codename1.flutter.navigation.Route; +import com.codename1.flutter.navigation.RouteSettings; +import com.codename1.flutter.services.SystemUiOverlayStyle; +import com.codename1.flutter.widgets.ScrollBehavior; +import com.codename1.ui.Display; + +import dart.core.DartIterable; +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * The material application shell. Renders its {@code home} as its only + * child, provides the theme that {@link Theme#of} resolves by walking up the + * element tree, and (M4) selects the EFFECTIVE theme from + * {@code theme}/{@code darkTheme} per {@code themeMode} — installing it into + * the CN1 UIManager via {@link ThemeDataAdapter} when the app mounts and + * whenever the effective theme changes across rebuilds (see + * {@link MaterialAppElement}). + */ +public class MaterialApp extends StatelessWidget + implements com.codename1.flutter.navigation.Navigator.RouteTableHost { + + /** This app's own route table — see {@link #build}. */ + private com.codename1.flutter.navigation.Navigator.RouteTable routeTable; + + @Override + public void routeTable(com.codename1.flutter.navigation.Navigator.RouteTable table) { + this.routeTable = table; + } + + @Override + public com.codename1.flutter.navigation.Navigator.RouteTable routeTable() { + return routeTable; + } + + private String title; + private ThemeData theme; + private ThemeData darkTheme; + private ThemeMode themeMode; + private Widget home; + private Object routes; + private String initialRoute; + private String restorationScopeId; + private boolean debugShowCheckedModeBanner = true; + private boolean resizeToAvoidBottomInset = true; + private Object localizationsDelegates; + private Object supportedLocales; + private Locale locale; + private SystemUiOverlayStyle systemOverlayStyle; + private Funcs.Func1 onGenerateRoute; + private Funcs.Func1 onUnknownRoute; + private ScrollBehavior scrollBehavior; + private Funcs.Func2, DartIterable, Locale> localeListResolutionCallback; + + public void title(String v) { + this.title = v; + } + + /** The key for the app's root Navigator — Flutter's {@code navigatorKey}. */ + public void navigatorKey(Object v) { + } + + public void theme(ThemeData v) { + this.theme = v; + } + + public void darkTheme(ThemeData v) { + this.darkTheme = v; + } + + public void themeMode(ThemeMode v) { + this.themeMode = v; + } + + public void home(Widget v) { + this.home = v; + } + + /** + * The app's named-route table — Flutter's {@code MaterialApp.routes} (a + * {@code Map}). Held untyped; the Navigator resolves + * a pushed route name against it. + */ + public void routes(Object v) { + this.routes = v; + } + + /** The name of the first route shown — Flutter's {@code initialRoute}. */ + public void initialRoute(String v) { + this.initialRoute = v; + } + + /** + * The identifier under which this app's state is saved and restored — + * Flutter's {@code restorationScopeId}. + */ + public void restorationScopeId(String v) { + this.restorationScopeId = v; + } + + /** Whether the debug "DEBUG" banner shows — Flutter's flag of the same name. */ + public void debugShowCheckedModeBanner(boolean v) { + this.debugShowCheckedModeBanner = v; + } + + /** + * Whether the body resizes when the on-screen keyboard appears — Flutter's + * {@code resizeToAvoidBottomInset} (mirrored on MaterialApp for apps that + * set it app-wide). + */ + public void resizeToAvoidBottomInset(boolean v) { + this.resizeToAvoidBottomInset = v; + } + + /** + * The app's localizations delegates — Flutter's {@code localizationsDelegates} + * (an {@code Iterable}). Held untyped. + */ + public void localizationsDelegates(Object v) { + this.localizationsDelegates = v; + } + + /** + * The locales this app declares support for — Flutter's + * {@code supportedLocales} (an {@code Iterable}). Held untyped. + */ + public void supportedLocales(Object v) { + this.supportedLocales = v; + } + + /** Forces a specific locale, overriding the device locale — Flutter's {@code locale}. */ + public void locale(Locale v) { + this.locale = v; + } + + /** + * The overlay style (status/navigation bar) applied app-wide — Flutter's + * {@code SystemUiOverlayStyle}. + */ + public void systemOverlayStyle(SystemUiOverlayStyle v) { + this.systemOverlayStyle = v; + } + + /** + * A callback that builds a route for a name not found in {@link #routes} — + * Flutter's {@code onGenerateRoute} ({@code RouteFactory}). + */ + public void onGenerateRoute(Funcs.Func1 v) { + this.onGenerateRoute = v; + } + + /** + * The last-resort route factory for a name neither {@link #routes} nor + * {@link #onGenerateRoute} could build — Flutter's {@code onUnknownRoute}. + */ + public void onUnknownRoute(Funcs.Func1 v) { + this.onUnknownRoute = v; + } + + /** The app-wide scroll behavior — Flutter's {@code scrollBehavior}. */ + public void scrollBehavior(ScrollBehavior v) { + this.scrollBehavior = v; + } + + /** + * Resolves the device's preferred locale list against the supported locales + * — Flutter's {@code localeListResolutionCallback}. + */ + public void localeListResolutionCallback(Funcs.Func2, DartIterable, Locale> v) { + this.localeListResolutionCallback = v; + } + + public Object getRoutes() { + return routes; + } + + public String getInitialRoute() { + return initialRoute; + } + + public String getRestorationScopeId() { + return restorationScopeId; + } + + public boolean isDebugShowCheckedModeBanner() { + return debugShowCheckedModeBanner; + } + + public boolean isResizeToAvoidBottomInset() { + return resizeToAvoidBottomInset; + } + + public Object getLocalizationsDelegates() { + return localizationsDelegates; + } + + public Object getSupportedLocales() { + return supportedLocales; + } + + public Locale getLocale() { + return locale; + } + + public SystemUiOverlayStyle getSystemOverlayStyle() { + return systemOverlayStyle; + } + + public String getTitle() { + return title; + } + + public ThemeData getTheme() { + return theme; + } + + public ThemeData getDarkTheme() { + return darkTheme; + } + + public ThemeMode getThemeMode() { + return themeMode; + } + + public Widget getHome() { + return home; + } + + /** + * The theme this app is actually showing right now: {@code darkTheme} + * when dark is in effect (per {@link #wantsDark}) and one was provided, + * else {@code theme} (matching Flutter's fallback to {@code theme} when + * {@code darkTheme} is absent). With neither set, a default ThemeData is + * returned whose brightness follows the dark request. + */ + public ThemeData effectiveTheme() { + boolean dark = wantsDark(themeMode, platformDark()); + ThemeData t = (dark && darkTheme != null) ? darkTheme : theme; + if (t == null) { + t = new ThemeData(); + if (dark) { + t.brightness(Brightness.dark); + } + } + return t; + } + + /** + * The themeMode decision table (pure — headless-testable): {@code dark} + * and {@code light} are absolute; {@code system} (or null, its default) + * follows the platform flag, treating null/unknown as light. + */ + public static boolean wantsDark(ThemeMode mode, Boolean platformDark) { + if (mode == ThemeMode.dark) { + return true; + } + if (mode == ThemeMode.light) { + return false; + } + return Boolean.TRUE.equals(platformDark); + } + + /** + * The platform dark-mode flag from the CN1 Display, or null when no + * Display exists (headless) or the port can't report it. + */ + public static Boolean platformDark() { + try { + if (Display.isInitialized()) { + return Display.getInstance().isDarkMode(); + } + } catch (Throwable ignore) { + // headless or unsupported port + } + return null; + } + + @Override + public Widget build(BuildContext context) { + // Publish the app's route table so Navigator.pushNamed(...) from anywhere + // below can resolve a name the same way this build does. The table belongs + // to THIS app: a study is a MaterialApp of its own, and one global table + // meant opening a study permanently replaced the gallery's. + boolean rootApp = com.codename1.flutter.navigation.Navigator.installRouteTable( + context, routes, onGenerateRoute, onUnknownRoute); + + Widget content = home; + // A routing-based app (no home widget) renders its initial route — Flutter + // calls onGenerateRoute with the initialRoute (default "/") and mounts the + // resulting route's page. new_gallery relies on this entirely. + // + // Resolved ONCE per element, not per build. An app-wide model above MaterialApp + // rebuilds it whenever a setting changes, and re-running the route builder each + // time hands back a brand new page, throwing away what the user was looking at. + // Flutter does not re-resolve either: its route stack is Navigator state, not + // something recomputed from the widget on every build. + if (content == null) { + MaterialAppElement self = context instanceof MaterialAppElement + ? (MaterialAppElement) context : null; + content = self == null ? null : self.routeContent(); + if (content == null) { + Route route = com.codename1.flutter.navigation.Navigator.resolveRoute( + context, initialRoute != null ? initialRoute : "/", null); + if (route instanceof MaterialPageRoute) { + Funcs.Func1 b = + ((MaterialPageRoute) route).getBuilder(); + if (b != null) { + content = b.call(context); + } + } + if (self != null) { + self.routeContent(content); + } + } + } + // Below the localizations scope, exactly where Flutter puts the app's Navigator. + // A push that arrives from outside the widget tree - a deep link, a notification + // tap, a test harness - inherits from here, so it sees the same Theme, + // MediaQuery, Localizations and providers a push from a widget would. + // The app's theme is INSTALLED as a widget, exactly as Flutter installs + // one below WidgetsApp. Without it `Theme.of` found no Theme ancestor, + // fell through to `findAncestorWidgetOfExactType(MaterialApp)` -- a walk + // to the root -- and reported a missing ancestor on the way. Every themed + // widget calls Theme.of on every build, so the intended hash lookup was + // never actually taken, and the diagnostic budget for genuinely missing + // providers was spent on this one false alarm. + return wrapWithTheme(wrapWithLocalizations( + new com.codename1.flutter.navigation.Navigator.RootScope(content, rootApp))); + } + + /** + * Publishes {@link #effectiveTheme()} to the subtree as a real Theme + * widget, together with the two ambient defaults Flutter installs + * alongside it: the icon theme and the default text style. + * + *

Neither was present, so {@code IconTheme.of} and + * {@code DefaultTextStyle.of} found nothing above them anywhere in the app + * and each fell back to a walk plus a synthesised default -- on every Icon + * and every Text, on every build. Installing them is both the faithful + * shape and the one that makes those lookups a hash hit.

+ */ + private Widget wrapWithTheme(Widget content) { + if (content == null) { + return null; + } + ThemeData data = effectiveTheme(); + + com.codename1.flutter.widgets.DefaultTextStyle text = + new com.codename1.flutter.widgets.DefaultTextStyle(); + com.codename1.flutter.TextStyle body = null; + try { + body = data.textTheme() == null ? null : data.textTheme().bodyMedium(); + } catch (Throwable ignore) { + body = null; + } + if (body != null) { + text.style(body); + } + text.child(content); + + IconTheme icons = new IconTheme(); + icons.data(data.iconTheme()); + icons.child(text); + + Theme t = new Theme(); + t.data(data); + t.child(icons); + // No MediaQuery is installed here, deliberately. Flutter's + // MediaQuery.fromView is fed by a view whose metrics are already known; + // ours would have to snapshot the Display during the app's FIRST build, + // which happens before the Form is showing and therefore before the + // safe-area insets exist. Every descendant then inherited a zero top + // inset and the whole app rode 44dp too high. MediaQuery.of resolves + // against the Display instead, and MediaQueryData caches that. + return t; + } + + /** + * Publishes the localized resources loaded from {@link #localizationsDelegates} + * so {@code Foo.of(context)} lookups below resolve. Flutter installs a + * Localizations widget above the app content; this mirrors that with a single + * {@link LocalizationsScope} carrying every delegate's synchronously-loaded value. + */ + private Widget wrapWithLocalizations(Widget content) { + if (content == null) { + return null; + } + // The scope is installed UNCONDITIONALLY and loads its resources on + // first lookup rather than here. Loading eagerly during build reads + // the app's delegate list at the earliest possible moment — before, + // on a lazily-initialised backend, the class holding it has + // necessarily run its static initialiser — and an empty result then + // meant no scope at all, so every `Foo.of(context)!` below died with + // no clue why. Deferring makes the lookup ask when the answer is + // knowable. + LocalizationsScope scope = new LocalizationsScope(new Funcs.Func0>() { + @Override + public java.util.List call() { + return loadLocalizations(); + } + }); + scope.child(content); + return scope; + } + + private java.util.List loadLocalizations() { + java.util.List out = new java.util.ArrayList(); + if (localizationsDelegates instanceof Iterable) { + Locale loc = effectiveLocale(); + logResolvedLocale(loc); + for (Object d : (Iterable) localizationsDelegates) { + if (d instanceof com.codename1.flutter.l10n.LocalizationsDelegate) { + try { + dart.async.Future f = + ((com.codename1.flutter.l10n.LocalizationsDelegate) d).load(loc); + Object v = f != null ? f.getNow() : null; + if (v != null) { + out.add(v); + logLoaded(d, v); + } else { + logLoaded(d, null); + } + } catch (Throwable err) { + // A delegate that fails contributes nothing, and the app + // then dies on `Foo.of(context)!` far away — so say which + // one failed rather than swallowing it. + try { + com.codename1.io.Log.p("Flutter runtime: localizations delegate " + + d.getClass().getName() + " failed for locale " + + loc + ": " + err); + } catch (Throwable ignore) { + // headless: Log has no storage backend + } + } + } + } + } + return out; + } + + /** + * Records which locale the app's localizations were resolved for. A + * delegate that has no table for that locale returns nothing, and the app + * then dies on {@code Foo.of(context)!} — so the locale actually used is + * the first thing worth knowing. + */ + private static boolean loggedLocale; + + /** What each delegate actually produced — the list is what every lookup searches. */ + /** + * Localisation tracing, GATED. These ran on every app's startup path, and a + * Log.p is not free -- on a device it is file IO, and six of them landed + * inside the first frame. They answer a question ("which delegate produced + * the resources this app is using?") worth keeping, just not worth paying + * for when nobody asked it. + */ + private static void logLoaded(Object delegate, Object value) { + if (!com.codename1.flutter.Trace.on()) { + return; + } + try { + com.codename1.io.Log.p("Flutter runtime: delegate " + + delegate.getClass().getName() + " -> " + + (value == null ? "null" : value.getClass().getName())); + } catch (Throwable ignore) { + // headless: Log has no storage backend + } + } + + private void logResolvedLocale(Locale loc) { + if (loggedLocale || !com.codename1.flutter.Trace.on()) { + return; + } + loggedLocale = true; + try { + com.codename1.io.Log.p("Flutter runtime: resolving localizations for locale " + + (loc == null ? "null" : loc.languageCode() + "_" + loc.countryCode())); + } catch (Throwable ignore) { + // headless: Log has no storage backend + } + } + + /** + * The locale the app runs in — Flutter's resolution order: an explicit {@code locale} + * wins, otherwise the app's {@code localeListResolutionCallback} is asked to choose + * from the device's locales, otherwise the first supported locale. + * + *

Asking the callback is not optional politeness. It is where an app learns what the + * device asked for: the gallery's records the device locale in a global that its own + * {@code GalleryOptions.locale} falls back to, and everything derived from that — the + * text direction, most obviously — is null until the callback has run. Skipping it left + * {@code resolvedTextDirection()} returning null, and the settings icon's tap handler + * asserts that value is non-null, so opening settings threw.

+ */ + private Locale effectiveLocale() { + if (locale != null) { + return locale; + } + Locale chosen = askResolutionCallback(); + if (chosen != null) { + return chosen; + } + if (supportedLocales instanceof Iterable) { + for (Object l : (Iterable) supportedLocales) { + if (l instanceof Locale) { + return (Locale) l; + } + } + } + return new Locale("en", null); + } + + /// Runs the app's locale-resolution callback once, over the device's locales. + private Locale askResolutionCallback() { + if (localeListResolutionCallback == null || resolutionAsked) { + return resolvedByCallback; + } + resolutionAsked = true; + try { + DartList device = deviceLocales(); + DartIterable supported = supportedLocaleList(); + resolvedByCallback = localeListResolutionCallback.call(device, supported); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + return resolvedByCallback; + } + + private boolean resolutionAsked; + private Locale resolvedByCallback; + + /// What the platform reports, as Flutter's ordered preference list. + private DartList deviceLocales() { + DartList out = new DartList(); + try { + String lang = com.codename1.l10n.L10NManager.getInstance().getLanguage(); + if (lang != null && lang.length() > 0) { + out.add(new Locale(lang, null)); + } + } catch (Throwable ignore) { + // headless + } + if (out.isEmpty()) { + out.add(new Locale("en", null)); + } + return out; + } + + private DartIterable supportedLocaleList() { + DartList out = new DartList(); + if (supportedLocales instanceof Iterable) { + for (Object l : (Iterable) supportedLocales) { + if (l instanceof Locale) { + out.add((Locale) l); + } + } + } + return DartIterable.wrap(out); + } + + @Override + public Element createElement() { + return new MaterialAppElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialAppElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialAppElement.java new file mode 100644 index 00000000000..8b55a3b8b31 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialAppElement.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.StatelessElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.ui.Form; + +import java.util.Map; + +/** + * Element for {@link MaterialApp}: installs the app's EFFECTIVE theme (per + * themeMode/darkTheme) into the CN1 UIManager before the subtree + * mounts, so every component created below picks the themed Flutter* styles + * up; on every widget update the effective theme is recomputed and, when it + * changed (a themeMode/theme/darkTheme switch across rebuilds), the overlay + * is re-installed, the Form's styles are refreshed and the whole element + * subtree re-applies its programmatic styling + * ({@link Element#themeChanged()}). + */ +public class MaterialAppElement extends StatelessElement { + + /** The prop table last installed, for change detection. */ + private Map installedProps; + + /** + * The widget the initial route resolved to, kept for the life of this element. + * + *

{@code MaterialApp.build} resolves {@code initialRoute} through + * {@code onGenerateRoute}. Doing that on EVERY build means any rebuild of the app - + * and an app-wide model sitting above MaterialApp causes one on every settings change - + * re-runs the route builder and hands back a brand new page, discarding whatever the + * user was looking at. Flutter does not re-resolve, because the route stack is + * Navigator STATE rather than something recomputed from the widget.

+ * + *

Resolved once and reused, so a rebuild updates the existing page in place. Routes + * pushed later are their own Forms and are unaffected.

+ */ + private Widget routeContent; + + public Widget routeContent() { + return routeContent; + } + + public void routeContent(Widget v) { + this.routeContent = v; + } + + public MaterialAppElement(MaterialApp widget) { + super(widget); + } + + private MaterialApp app() { + return (MaterialApp) widget(); + } + + @Override + public void mount(Element parent, int slot) { + // Install before super.mount: the children inflate (and create their + // CN1 components) during the first build inside super.mount. + RenderHost h = parent != null ? parent.host() : host(); + installEffectiveTheme(app().effectiveTheme(), h); + super.mount(parent, slot); + } + + // No size-changed listener here, deliberately. One was added to keep a + // root MediaQuery snapshot honest across a window resize; that snapshot + // turned out to be wrong for a different reason and was removed, leaving a + // listener that rebuilt the ENTIRE application every time the Form + // reported a size -- which a desktop window does once, just after it is + // shown. Measured on the Mac build that was the whole first screen built + // twice: 2099 elements and 751 components where the app has 1062 and 376. + // MediaQuery.of resolves against the Display at the moment it is asked, so + // there is nothing here that a resize can invalidate. + + @Override + public void update(Widget newWidget) { + ThemeData eff = ((MaterialApp) newWidget).effectiveTheme(); + boolean changed = !ThemeDataAdapter.themeProps(eff).equals(installedProps); + if (changed) { + installEffectiveTheme(eff, host()); + } + super.update(newWidget); + if (changed) { + // Reused widget instances skip Element.update, so force every + // render element to re-apply its (theme-derived) programmatic + // styling and re-measure. + themeChanged(); + if (host() != null) { + host().revalidate(); + } + } + } + + private void installEffectiveTheme(ThemeData eff, RenderHost h) { + installedProps = ThemeDataAdapter.themeProps(eff); + ThemeDataAdapter.install(eff); + Form f = h == null ? null : h.form(); + if (f != null) { + // Re-derive the existing components' UIID styles from the new + // overlay, then style the Form itself per-instance. + try { + f.refreshTheme(); + } catch (Throwable ignore) { + // headless + } + ThemeDataAdapter.applyToForm(f, eff); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialBanner.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialBanner.java new file mode 100644 index 00000000000..41fe69ea72b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialBanner.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Row; + +import dart.core.DartList; + +/** + * A material banner: a prominent message with optional leading icon and action + * buttons — Flutter's {@code MaterialBanner}. This milestone renders a + * {@link Row} of the {@code leading} widget and {@code content}, with the + * {@code actions} laid out in a trailing {@link Row} below. + */ +public class MaterialBanner extends StatelessWidget { + + private Widget content; + private Widget leading; + private DartList actions; + private TextStyle contentTextStyle; + private Color backgroundColor; + + public void content(Widget v) { + this.content = v; + } + + public void contentTextStyle(TextStyle v) { + this.contentTextStyle = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public void elevation(double v) { + } + + public void leading(Widget v) { + this.leading = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void surfaceTintColor(Color v) { + } + + public void shadowColor(Color v) { + } + + public void dividerColor(Color v) { + } + + public void padding(Object v) { + } + + public void leadingPadding(Object v) { + } + + public void forceActionsBelow(boolean v) { + } + + public void overflowAlignment(Object v) { + } + + public void animation(Object v) { + } + + public void onVisible(Object v) { + } + + @Override + public Widget build(BuildContext context) { + DartList top = new DartList(); + if (leading != null) { + top.add(leading); + // Material's gap between the leading icon and the message. Without + // it the icon and the text were touching at the top-left corner. + com.codename1.flutter.widgets.SizedBox gap = + new com.codename1.flutter.widgets.SizedBox(); + gap.width(LEADING_GAP_LP); + top.add(gap); + } + if (content != null) { + Expanded e = new Expanded(); + e.child(content); + top.add(e); + } + Row topRow = new Row(); + topRow.crossAxisAlignment(CrossAxisAlignment.center); + topRow.children(top); + + DartList rows = new DartList(); + rows.add(topRow); + if (actions != null && actions.size() > 0) { + Row actionRow = new Row(); + // Actions sit at the END of the banner, not the start. Laid out from + // the left they read as two more links under the message rather than + // as the banner's buttons. + actionRow.mainAxisAlignment(com.codename1.flutter.MainAxisAlignment.end); + actionRow.children(actions); + rows.add(actionRow); + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(rows); + + // The banner is INSET, and it closes with a rule. Built bare it was + // packed hard into the top-left corner with nothing separating it from + // the list underneath. + com.codename1.flutter.widgets.Padding pad = + new com.codename1.flutter.widgets.Padding(); + pad.padding(com.codename1.flutter.EdgeInsets.fromLTRB( + PAD_LP, PAD_LP, PAD_LP, 0)); + pad.child(col); + + DartList stackRows = new DartList(); + stackRows.add(pad); + stackRows.add(new Divider()); + Column outer = new Column(); + outer.crossAxisAlignment(CrossAxisAlignment.stretch); + outer.mainAxisSize(MainAxisSize.min); + outer.children(stackRows); + return outer; + } + + /** Material's banner inset, in logical pixels. */ + private static final double PAD_LP = 16; + /** The gap between the leading icon and the message. */ + private static final double LEADING_GAP_LP = 16; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialConstants.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialConstants.java new file mode 100644 index 00000000000..21c5b876d2c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialConstants.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import dart.core.Duration; + +/** + * Top-level {@code const} values of Flutter's {@code package:flutter/material.dart} + * that new_gallery references directly, mirrored as Java statics. The transpiler + * routes the bare identifiers ({@code kToolbarHeight}, ...) to these fields. + */ +public final class MaterialConstants { + + private MaterialConstants() { + } + + /** Flutter's {@code kToolbarHeight}: the default AppBar height (logical px). */ + public static final double kToolbarHeight = 56.0; + + /** Flutter's {@code kFloatingActionButtonMargin}: default FAB margin (logical px). */ + public static final double kFloatingActionButtonMargin = 16.0; + + /** Flutter's {@code kThemeAnimationDuration}: theme cross-fade duration. */ + public static final Duration kThemeAnimationDuration = Duration.of(0, 0, 0, 0, 200, 0); + + /** Flutter's {@code kBottomNavigationBarHeight}: the default bottom nav bar height (logical px). */ + public static final double kBottomNavigationBarHeight = 56.0; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialRenderElement.java new file mode 100644 index 00000000000..dff943f7887 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialRenderElement.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; + +/** + * Render element for {@link Material}: a CN1 Container (UIID "FlutterMaterial") + * filled with the surface color, covering the element bounds behind the child. + * Sizes to the child, or fills the bounded incoming axes when childless. The + * child's components attach after the face, so they paint on top. + */ +public class MaterialRenderElement extends com.codename1.flutter.widgets.EffectRenderElement { + + public MaterialRenderElement(Material widget) { + super(widget); + } + + private Material material() { + return (Material) widget(); + } + + @Override + protected Widget effectChild() { + return material().getChild(); + } + + + + /** + * A rounded Material paints its own surface (see {@link #paintSurface}), so all this + * has to do is stop Codename One painting a second, square one underneath. + * + *

It used to hand the job to a {@code RoundRectBorder}. That draws the fill as the + * component BACKGROUND, and a background is painted before {@code paint()} runs - so it + * landed outside the rounded clip this element installs and squared the corners off + * again from behind. Rounding is not a style the surface happens to have; it is the + * same shape the subtree is clipped to, and both come from one path here.

+ */ + private void applyStyle(Component face) { + try { + boolean rounded = maxCornerRadiusLp() > 0; + if (material().getColor() != null) { + com.codename1.flutter.material.ThemeDataAdapter.paintColor( + face.getAllStyles(), material().getColor()); + } + if (rounded) { + face.getAllStyles().setBorder(com.codename1.ui.plaf.Border.createEmpty()); + face.getAllStyles().setBgTransparency(0); + } + } catch (Exception err) { + // best-effort + } + } + + /** + * Fills the surface, and casts its elevation shadow, in the shape the subtree is about + * to be clipped to — Flutter's {@code Material(color:, shape:, elevation:)}. + * + *

The shadow is drawn OUTSIDE the surface, as Flutter's is: a few progressively + * wider, fainter rings under the card. Codename One's own shadow reserves its spread + * INSIDE the component box and shifts the surface to make room, which is a different + * thing altogether and moved the card clear of its own bounds.

+ */ + private void paintSurface(com.codename1.ui.Graphics g, int[] q, double elevation) { + com.codename1.flutter.Color c = material().getColor(); + if (c == null || c.alpha() == 0) { + return; + } + int rgb = (int) (c.value() & 0xFFFFFFL); + boolean oldAA = g.isAntiAliased(); + int oldColor = g.getColor(); + int oldAlpha = g.getAlpha(); + g.setAntiAliased(true); + try { + // Material's elevation shadow: roughly a blur of twice the elevation, dropped by + // half of it. fillShapeShadow does the fill and the blur in one accelerated draw + // with no retained bitmap, which is what makes it affordable on a card that + // repaints every frame of a scroll. + if (elevation > 0 && g.isShapeShadowSupported()) { + g.fillShapeShadow(clipShape(q[0], q[1], q[2], q[3], q[4], q[5], q[6], q[7]), + rgb, c.alpha(), 0x000000, 0.28f, + (int) Math.round(com.codename1.flutter.rendering.Dp.px(elevation * 2)), + 0, (int) Math.round( + com.codename1.flutter.rendering.Dp.px(elevation / 2.0))); + return; + } + if (elevation > 0) { + paintShadowRings(g, q, elevation); + } + g.setColor(rgb); + g.setAlpha(c.alpha()); + // AFTER the rings: they are built through the same reused path, so taking this + // shape earlier would hand the fill whatever the last ring left behind. + g.fillShape(clipShape(q[0], q[1], q[2], q[3], q[4], q[5], q[6], q[7])); + } finally { + g.setAntiAliased(oldAA); + g.setAlpha(oldAlpha); + g.setColor(oldColor); + } + } + + /// How many rounded rects approximate the blur where the port has no real one. + /// Each is a full fill, per card per frame, so this is deliberately small: four reads + /// as a soft edge, and more is not visible at these opacities. + private static final int SHADOW_RINGS = 4; + + /// Alpha of each shadow ring, innermost first, out of 255. + /// + /// The rings are FILLED shapes drawn outside-in, so each one darkens everything + /// inside it as well: with a single alpha for all four, the band against the surface + /// is hit four times and ends up far darker than Material's shadow, and the falloff + /// outward is linear where Material's is a blur that drops off fast. + /// + /// Calibrated against the reference, as darkening of white beside an elevation-6 + /// circle -- the Reply study's compose button, which is the largest single elevated + /// surface in the gallery: + /// + /// ```text + /// band reference one alpha for all these alphas + /// 1 0.071 0.148 0.071 + /// 2 0.035 0.077 0.035 + /// 3 0.020 0.039 0.020 + /// 4 0.008 0.039 0.008 + /// ``` + /// + /// The three opacities Material composes a shadow from do not change with elevation + /// -- only the blur and the offset do, and the ring extents already scale with it -- + /// so these hold across elevations rather than fitting the one that was measured. + private static final int[] RING_ALPHA = {10, 4, 3, 2}; + + /** + * The elevation shadow where {@code fillShapeShadow} is unavailable (the iOS port among + * them): a few progressively larger, fainter rounded rects under the card, drawn + * outside-in so their alpha accumulates towards the surface. + * + *

Deliberately NOT cached to a bitmap. Caching a per-card shadow image is what made + * these same cards a RAM and jank problem on Android, and the surface has to be redrawn + * every frame of a scroll anyway.

+ */ + private void paintShadowRings(com.codename1.ui.Graphics g, int[] q, double elevation) { + int spread = Math.max(1, (int) Math.round( + com.codename1.flutter.rendering.Dp.px(elevation))); + int drop = Math.max(1, (int) Math.round( + com.codename1.flutter.rendering.Dp.px(elevation / 2.0))); + g.setColor(0x000000); + for (int i = SHADOW_RINGS; i >= 1; i--) { + int e = Math.max(1, spread * i / SHADOW_RINGS); + g.setAlpha(RING_ALPHA[i - 1]); + g.fillShape(clipShape(q[0] - e, q[1] - e + drop, q[2] + e * 2, q[3] + e * 2, + grown(q[4], e), grown(q[5], e), grown(q[6], e), grown(q[7], e))); + } + } + + /// The matching corner on a shadow ring {@code by} pixels outside a corner of radius + /// {@code r}. A squared corner stays squared - rounding it would put a curve back on a + /// corner the clip deliberately cut off at the edge of a viewport. + private static int grown(int r, int by) { + return r <= 0 ? 0 : r + by; + } + + @Override + protected void paintWithEffect(com.codename1.ui.Graphics g, + com.codename1.ui.Container pane, Subtree paintChildren) { + styleOnce(pane); + double[] lp = cornerRadiiLp(); + int rtl = (int) Math.round(com.codename1.flutter.rendering.Dp.px(lp[0])); + int rtr = (int) Math.round(com.codename1.flutter.rendering.Dp.px(lp[1])); + int rbr = (int) Math.round(com.codename1.flutter.rendering.Dp.px(lp[2])); + int rbl = (int) Math.round(com.codename1.flutter.rendering.Dp.px(lp[3])); + if (Math.max(Math.max(rtl, rtr), Math.max(rbr, rbl)) <= 0) { + // Square surface: nothing to paint here that the component's own background + // does not already do (applyStyle leaves it in place in this case). + paintChildren.paint(g); + return; + } + boolean clips = !noShapeClip() && g.isShapeClipSupported() + && material().getClipBehavior() != com.codename1.flutter.Clip.none; + if (!noShapeClip() && !g.isShapeClipSupported()) { + com.codename1.flutter.FlutterErrorReport.unimplemented("Material", + "this port cannot clip to a shape, so the subtree paints square-cornered"); + } + int x = pane.getX(); + int y = pane.getY(); + int w = pane.getWidth(); + int h = pane.getHeight(); + // setClip(Shape) REPLACES the clip rather than intersecting it, and the study card + // lives in a horizontally scrolling carousel that is already clipping us - so + // replacing outright would let a half-scrolled card paint outside its viewport. The + // shape we install must therefore be the rounded rect INTERSECTED with the incoming + // clip. + // + // That intersection is computed directly rather than with GeneralPath.intersection(). + // The general polygon clipper allocates heavily, and a half-scrolled card overflows + // its viewport on EVERY frame of a drag, so it ran per card per frame: a `sample` of + // a wedged app caught the EDT parked in usleep inside cn1BibopMaybeGc, called from + // allocArray under ShapeUtil.intersection, called from here. Paint was driving the + // collector, which is what the stalls and the half-drawn frames were. It was also + // wrong - the clipper does not survive a path entirely inside the rectangle, which + // is why this used to need a special case to stop it cutting the icons out of every + // category row. + // + // Intersecting a rounded rect with an axis-aligned rect needs no clipper: the result + // is the intersected BOUNDS, rounded at exactly those corners the clip left intact. + // A corner the clip cut through is off-screen anyway, so squaring it is invisible. + int cx = g.getClipX(); + int cy = g.getClipY(); + int cw = g.getClipWidth(); + int ch = g.getClipHeight(); + if (clipGeom == null) { + clipGeom = new int[8]; + } + int cap = Math.min(w, h) / 2; + if (!clipGeometry(clipGeom, x, y, w, h, Math.min(rtl, cap), Math.min(rtr, cap), + Math.min(rbr, cap), Math.min(rbl, cap), cx, cy, cw, ch)) { + // Entirely clipped out: painting the subtree could only produce invisible pixels. + return; + } + int[] q = clipGeom; + if ("true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.debugClip", "false"))) { + // Reported through the error channel so it comes back over bench_errors, which + // dedupes: a device needs no new tooling to answer "what geometry did this card + // actually compute", and guessing at that has been expensive. + com.codename1.flutter.FlutterErrorReport.unimplemented("MaterialClip", + "box=" + x + "," + y + "," + w + "," + h + + " clip=" + cx + "," + cy + "," + cw + "," + ch + + " r=" + rtl + "," + rtr + "," + rbr + "," + rbl + " out=" + q[0] + "," + q[1] + "," + q[2] + "," + q[3] + + " corners=" + q[4] + "," + q[5] + "," + q[6] + "," + q[7] + + " shapeClip=" + g.isShapeClipSupported() + + " shadow=" + g.isShapeShadowSupported()); + } + // Surface first, then the subtree on top of it, both in the same shape. The surface + // is painted here rather than as the component's background because a background is + // painted before paint() runs, i.e. outside the clip below - which is exactly how + // the corners used to end up square from behind. + paintSurface(g, q, material().getElevation()); + if (!clips) { + paintChildren.paint(g); + return; + } + // push/pop, not save-the-four-ints-and-restore: the four ints are a RECTANGLE, + // so restoring that way degrades whatever shaped clip an ancestor had + // established to its bounding box. A Material inside a ClipRRect therefore left + // the outer shape no longer holding, and the subtree looked mis-layered rather + // than merely unclipped. This is the idiom the shaped-clipping tests in + // scripts/hellocodenameone use. + g.pushClip(); + try { + g.setClip(clipShape(q[0], q[1], q[2], q[3], q[4], q[5], q[6], q[7])); + paintChildren.paint(g); + } finally { + g.popClip(); + } + } + + /// Scratch for {@link #clipGeometry}, owned per element so the paint path stays + /// allocation-free. + private int[] clipGeom; + + /** + * The rounded rectangle {@code (x,y,w,h)} radius {@code r}, intersected with the clip + * {@code (cx,cy,cw,ch)}, as bounds plus a radius per corner. + * + *

Writes {@code x, y, w, h, tlRadius, trRadius, brRadius, blRadius} into {@code out} + * and returns whether anything is visible at all.

+ * + *

Package-private and static so the geometry can be asserted directly: it decides + * what the user sees at the edge of every scrolling viewport, and it is not something + * a layout test would catch.

+ */ + static boolean clipGeometry(int[] out, int x, int y, int w, int h, int r, + int cx, int cy, int cw, int ch) { + return clipGeometry(out, x, y, w, h, r, r, r, r, cx, cy, cw, ch); + } + + /** + * As above, with a radius per corner -- Flutter rounds corners independently, and a + * surface that rounds one of them is not a surface with a single radius. + */ + static boolean clipGeometry(int[] out, int x, int y, int w, int h, + int rtl, int rtr, int rbr, int rbl, int cx, int cy, int cw, int ch) { + int ix = Math.max(x, cx); + int iy = Math.max(y, cy); + int ix2 = Math.min(x + w, cx + cw); + int iy2 = Math.min(y + h, cy + ch); + if (ix2 <= ix || iy2 <= iy) { + return false; + } + // Keep the arcs inside the visible box. Only reachable once the clip has already cut + // an edge (r is <= half the full box), i.e. a card reduced to a sliver at the screen + // edge, where a slightly tighter corner cannot be seen. + int cap = Math.min(ix2 - ix, iy2 - iy) / 2; + + // An edge the clip did not move is an edge whose two corners are still the card's own. + boolean l = x >= ix; + boolean t = y >= iy; + boolean rt = x + w <= ix2; + boolean b = y + h <= iy2; + out[0] = ix; + out[1] = iy; + out[2] = ix2 - ix; + out[3] = iy2 - iy; + out[4] = l && t ? capped(rtl, cap) : 0; + out[5] = rt && t ? capped(rtr, cap) : 0; + out[6] = rt && b ? capped(rbr, cap) : 0; + out[7] = l && b ? capped(rbl, cap) : 0; + return true; + } + + private static int capped(int r, int cap) { + return Math.max(0, Math.min(r, cap)); + } + + /// A/B switch for the rounded clip, flipped at runtime with + /// {@code Display.setProperty("cn1.flutter.noShapeClip", "true")}. + /// + /// Exists because the clip is a per-card, per-frame native call whose cost is only + /// measurable on a device, and turning it off is the one experiment that separates + /// "the clip is expensive" from "something else is". Read per paint deliberately: an + /// A/B you have to rebuild for is an A/B you run once and mis-attribute. + private static boolean noShapeClip() { + return "true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.noShapeClip", "false")); + } + + /// The clip path, rebuilt in place rather than reallocated. + /// + /// This is rebuilt on most frames of a drag (the geometry really is changing), so the + /// point is not to skip the work but to keep it out of the allocator: a scrolling + /// carousel paints this per card per frame, and paint that allocates is paint that can + /// be interrupted by a collection - see the note in {@link #paintWithEffect}. Reusing + /// the instance is safe because every port copies the shape into its own + /// representation on setClip rather than holding this one. + private com.codename1.ui.geom.GeneralPath clipPath; + private int clipKeyA; + private int clipKeyB; + + /// A rectangle with an independent radius per corner, in the coordinate space a + /// component paints in - parent-relative, because the Graphics has already accumulated + /// its ancestors' translation. Radii run clockwise from the top left. + com.codename1.ui.geom.Shape clipShape(int x, int y, int w, int h, + int tl, int tr, int br, int bl) { + // Cheap identity for "same shape as last time": the carousel settles between drags + // and every static card then re-installs a clip the port can recognise as unchanged. + int a = (x * 31 + y) * 31 * 31 + w * 31 + h; + int b = ((tl * 31 + tr) * 31 + br) * 31 + bl; + if (clipPath != null && a == clipKeyA && b == clipKeyB) { + return clipPath; + } + clipKeyA = a; + clipKeyB = b; + if (clipPath == null) { + clipPath = new com.codename1.ui.geom.GeneralPath(); + } else { + clipPath.reset(); + } + com.codename1.ui.geom.GeneralPath p = clipPath; + p.moveTo(x + tl, y); + p.lineTo(x + w - tr, y); + arc(p, x + w - tr, y + tr, tr, -90); + p.lineTo(x + w, y + h - br); + arc(p, x + w - br, y + h - br, br, 0); + p.lineTo(x + bl, y + h); + arc(p, x + bl, y + h - bl, bl, 90); + p.lineTo(x, y + tl); + arc(p, x + tl, y + tl, tl, 180); + p.closePath(); + return p; + } + + /** + * Appends one 90° corner as short line segments, sweeping clockwise from + * {@code startDeg} about ({@code cx},{@code cy}). + * + *

Line segments rather than a {@code quadTo} because a clip has to survive being + * handed to a GPU, and the ports test for a POLYGON to decide how: Codename One's iOS + * backend renders a polygon clip through a stencil, and anything it cannot reduce to + * one falls back to the shape's BOUNDING BOX - which is a square-cornered card. The + * curve buys nothing here anyway: at a 10dp radius these segments are under two pixels + * each, and the same path also fills the surface, so shape and fill cannot disagree.

+ */ + private static void arc(com.codename1.ui.geom.GeneralPath p, int cx, int cy, int r, + int startDeg) { + if (r <= 0) { + return; + } + int segs = Math.max(3, Math.min(10, r / 3)); + for (int i = 1; i <= segs; i++) { + double a = Math.toRadians(startDeg + 90.0 * i / segs); + p.lineTo((float) (cx + r * Math.cos(a)), (float) (cy + r * Math.sin(a))); + } + } + + /// Applies the surface style when it first paints or after its configuration + /// changes. Re-deriving a RoundRectBorder on every frame would allocate per paint. + private String styleSignature; + + private void styleOnce(com.codename1.ui.Container pane) { + double[] r = cornerRadiiLp(); + String sig = r[0] + "," + r[1] + "," + r[2] + "," + r[3] + + "|" + material().getElevation() + "|" + + (material().getColor() == null ? "-" : material().getColor().value()); + if (sig.equals(styleSignature)) { + return; + } + styleSignature = sig; + applyStyle(pane); + } + + + /// The corner radius in logical pixels from the shape or an explicit borderRadius. + /** + * The radius this surface clips its subtree to, in pixels; 0 when it does not clip. + * + *

Descendants need this because a clip is not reliable on every port — see + * {@code ImageRenderElement.enclosingCornerRadius}, where an image that fills the + * surface rounds its own bitmap instead of trusting one.

+ */ + public int clipRadiusPx() { + if (material().getClipBehavior() == com.codename1.flutter.Clip.none) { + return 0; + } + // One radius, so this can only answer for a surface whose corners agree. A + // surface that rounds some corners and not others has no uniform radius, and + // answering with one would round corners the surface leaves square; the caller + // falls back to the clip, which carries all four. + double[] r = cornerRadiiLp(); + if (r[0] != r[1] || r[1] != r[2] || r[2] != r[3]) { + return 0; + } + return (int) Math.round(com.codename1.flutter.rendering.Dp.px(r[0])); + } + + /// Scratch for {@link #cornerRadiiLp}, owned per element so the paint path stays + /// allocation-free. + private double[] radiiLp; + + /// The four corner radii in logical pixels, in the order {@code tl, tr, br, bl}. + /// + /// Flutter's radii are per corner, and reading only the top-left one squared every + /// surface that rounds some corners and not others. The gallery's settings button is + /// exactly that -- {@code BorderRadiusDirectional.only(bottomStart: 10)} -- so it + /// drew as a plain white block where the reference has a rounded bottom-left corner. + /// + /// A directional radius is resolved left-to-right, as the rest of the runtime + /// resolves {@code AlignmentDirectional} and {@code EdgeInsetsDirectional}. + private double[] cornerRadiiLp() { + if (radiiLp == null) { + radiiLp = new double[4]; + } + double[] out = radiiLp; + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + if (material().getShape() instanceof com.codename1.flutter.CircleBorder) { + // A CircleBorder is the circle inscribed in the box, which as a + // rounded rectangle is a corner radius of half the shorter side. + // The shape was unrecognised and fell through to a radius of zero, + // so a surface wearing one drew square: Reply's compose button is + // an OpenContainer with closedShape: CircleBorder(), and it + // rendered as an orange block sitting on the bottom bar. + com.codename1.flutter.rendering.Size box = size(); + if (box == null || box.width() <= 0 || box.height() <= 0) { + return out; + } + double scale = com.codename1.flutter.rendering.Dp.scale(); + double shorterPx = Math.min(box.width(), box.height()); + double circle = scale > 0 ? shorterPx / 2 / scale : 0; + out[0] = circle; + out[1] = circle; + out[2] = circle; + out[3] = circle; + return out; + } + Object r = material().getShape() instanceof com.codename1.flutter.RoundedRectangleBorder + ? ((com.codename1.flutter.RoundedRectangleBorder) material().getShape()).getBorderRadius() + : material().getBorderRadius(); + if (r instanceof com.codename1.flutter.BorderRadius) { + com.codename1.flutter.BorderRadius b = (com.codename1.flutter.BorderRadius) r; + out[0] = radiusX(b.topLeft()); + out[1] = radiusX(b.topRight()); + out[2] = radiusX(b.bottomRight()); + out[3] = radiusX(b.bottomLeft()); + } else if (r instanceof com.codename1.flutter.BorderRadiusDirectional) { + com.codename1.flutter.BorderRadiusDirectional b = + (com.codename1.flutter.BorderRadiusDirectional) r; + out[0] = radiusX(b.topStart()); + out[1] = radiusX(b.topEnd()); + out[2] = radiusX(b.bottomEnd()); + out[3] = radiusX(b.bottomStart()); + } + return out; + } + + private static double radiusX(com.codename1.flutter.Radius r) { + return r == null ? 0 : r.x(); + } + + /// The largest of the four corner radii, for the decisions that only need to know + /// whether this surface is rounded at all. + private double maxCornerRadiusLp() { + double[] r = cornerRadiiLp(); + return Math.max(Math.max(r[0], r[1]), Math.max(r[2], r[3])); + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialScrollBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialScrollBehavior.java new file mode 100644 index 00000000000..2557a4308d3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialScrollBehavior.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.widgets.ScrollBehavior; + +/** + * The Material default scroll behaviour — Flutter's {@code + * MaterialScrollBehavior}. new_gallery's shrine app installs + * {@code const MaterialScrollBehavior().copyWith(scrollbars: false)} on its + * MaterialApp. + */ +public class MaterialScrollBehavior extends ScrollBehavior { + + public MaterialScrollBehavior() { + } + + @Override + protected ScrollBehavior newInstance() { + return new MaterialScrollBehavior(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialState.java new file mode 100644 index 00000000000..8b34b6181ea --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialState.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The interactive states a material component can be in, mirroring Flutter's + * {@code MaterialState}. Passed to a {@link MaterialStateProperty} resolver as + * a set so a theme can vary a value (a color, elevation, ...) per state. + */ +public enum MaterialState { + hovered, focused, pressed, dragged, selected, scrolledUnder, disabled, error +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialStateProperty.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialStateProperty.java new file mode 100644 index 00000000000..3e1cc317d37 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialStateProperty.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +import dart.core.DartSet; +import dart.runtime.Funcs; + +/** + * A value that may depend on a component's interactive {@link MaterialState} + * set, mirroring Flutter's {@code MaterialStateProperty}. Built either from a + * single constant ({@link #all}) or from a resolver callback + * ({@link #resolveWith}); {@link #resolve} evaluates it for a given state set. + * + *

The resolver's return type is generic in Flutter ({@code T}); this pass + * models the color-valued case the gallery uses (checkbox/radio/switch + * fill/thumb/track colors).

+ */ +public class MaterialStateProperty { + + private final Funcs.Func1, Color> resolver; + private final Object constant; + private final boolean isConstant; + + private MaterialStateProperty(Funcs.Func1, Color> resolver, + Object constant, boolean isConstant) { + this.resolver = resolver; + this.constant = constant; + this.isConstant = isConstant; + } + + /** A property that is {@code value} in every state. */ + public static MaterialStateProperty all(Object value) { + return new MaterialStateProperty(null, value, true); + } + + /** A property computed from the active state set on each read. */ + public static MaterialStateProperty resolveWith(Funcs.Func1, Color> resolver) { + return new MaterialStateProperty(resolver, null, false); + } + + /** Evaluates this property for {@code states}. */ + public Object resolve(DartSet states) { + if (isConstant) { + return constant; + } + return resolver == null ? null : resolver.call(states); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialType.java new file mode 100644 index 00000000000..a7ea10ba204 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/MaterialType.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The kinds of material surface a {@link Material} can paint — Flutter's + * {@code MaterialType}. Selects the default shape/clip behavior of the surface + * (the feature-discovery overlay uses {@link #transparency}). + */ +public enum MaterialType { + canvas, card, circle, button, transparency +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRail.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRail.java new file mode 100644 index 00000000000..b93b0db82d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRail.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.animation.AlwaysStoppedAnimation; + +import dart.core.DartList; + +/** + * A vertical Material navigation rail — Flutter's {@code NavigationRail}, the + * desktop/tablet counterpart of a BottomNavigationBar. Signature-only this + * pass: destinations and styling are captured; {@link #extendedAnimation} hands + * back a settled 1.0 animation so descendants that drive off the extend state + * render in their extended layout. + */ +public class NavigationRail extends StatelessWidget { + + private Color backgroundColor; + private boolean extended; + private Widget leading; + private Widget trailing; + private DartList destinations; + private long selectedIndex; + private Object onDestinationSelected; + private double elevation; + private double groupAlignment; + private NavigationRailLabelType labelType; + private TextStyle unselectedLabelTextStyle; + private TextStyle selectedLabelTextStyle; + private IconThemeData unselectedIconTheme; + private IconThemeData selectedIconTheme; + private double minWidth; + private double minExtendedWidth; + /// Material 3 draws the selection indicator by default, so this starts true + /// -- reading it as false left the selected destination with no pill behind + /// its icon at all. + private boolean useIndicator = true; + private Color indicatorColor; + private Object indicatorShape; + + public void backgroundColor(Color v) { this.backgroundColor = v; } + public void extended(boolean v) { this.extended = v; } + public void leading(Widget v) { this.leading = v; } + public void trailing(Widget v) { this.trailing = v; } + public void destinations(DartList v) { this.destinations = v; } + public void selectedIndex(long v) { this.selectedIndex = v; } + public void onDestinationSelected(dart.runtime.Funcs.VoidFunc1 v) { this.onDestinationSelected = v; } + public void elevation(double v) { this.elevation = v; } + public void groupAlignment(double v) { this.groupAlignment = v; } + public void labelType(NavigationRailLabelType v) { this.labelType = v; } + public void unselectedLabelTextStyle(TextStyle v) { this.unselectedLabelTextStyle = v; } + public void selectedLabelTextStyle(TextStyle v) { this.selectedLabelTextStyle = v; } + public void unselectedIconTheme(IconThemeData v) { this.unselectedIconTheme = v; } + public void selectedIconTheme(IconThemeData v) { this.selectedIconTheme = v; } + public void minWidth(double v) { this.minWidth = v; } + public void minExtendedWidth(double v) { this.minExtendedWidth = v; } + public void useIndicator(boolean v) { this.useIndicator = v; } + public void indicatorColor(Color v) { this.indicatorColor = v; } + public void indicatorShape(Object v) { this.indicatorShape = v; } + + /** + * Dart's {@code NavigationRail.extendedAnimation(context)}: the 0..1 + * animation of the rail's extended state. Deferred rendering supplies a + * settled (1.0) animation. + */ + public static Animation extendedAnimation(BuildContext context) { + return new AlwaysStoppedAnimation(1.0); + } + + /** Flutter's default {@code minWidth}. */ + private static final double DEFAULT_WIDTH_LP = 72; + /** Flutter's default {@code minExtendedWidth}. */ + private static final double DEFAULT_EXTENDED_WIDTH_LP = 256; + /** The gap above and below {@code leading} -- Flutter's _verticalSpacer. */ + private static final double VERTICAL_SPACER_LP = 8; + /** The Material 3 selection indicator behind the icon. */ + private static final double INDICATOR_WIDTH_LP = 56; + private static final double INDICATOR_HEIGHT_LP = 32; + /** Vertical padding around each destination. */ + private static final double DESTINATION_PADDING_LP = 12; + /** The gap between an icon and the label under it. */ + private static final double LABEL_GAP_LP = 4; + + /** + * The rail: a fixed-width column of the leading widget, the destinations and + * the trailing widget. + * + *

This used to return {@code leading} and nothing else, so a rail whose + * destinations ARE its content rendered as a single floating button -- + * the navigation-rail demo drew its create button in the middle of an + * otherwise empty page, with no rail behind it and no destinations at + * all.

+ */ + @Override + public Widget build(BuildContext context) { + double width = extended + ? (minExtendedWidth > 0 ? minExtendedWidth : DEFAULT_EXTENDED_WIDTH_LP) + : (minWidth > 0 ? minWidth : DEFAULT_WIDTH_LP); + DartList kids = new DartList(); + if (leading != null) { + kids.add(gap(VERTICAL_SPACER_LP)); + kids.add(leading); + kids.add(gap(VERTICAL_SPACER_LP)); + } + if (destinations != null) { + for (int i = 0; i < destinations.size(); i++) { + NavigationRailDestination d = destinations.get(i); + if (d != null) { + kids.add(destinationTile(context, d, i == selectedIndex, width)); + } + } + } + if (trailing != null) { + kids.add(trailing); + } + com.codename1.flutter.widgets.Column column = new com.codename1.flutter.widgets.Column(); + column.mainAxisSize(com.codename1.flutter.MainAxisSize.max); + column.mainAxisAlignment(com.codename1.flutter.MainAxisAlignment.start); + column.crossAxisAlignment(com.codename1.flutter.CrossAxisAlignment.center); + column.children(kids); + + com.codename1.flutter.widgets.Container rail = + new com.codename1.flutter.widgets.Container(); + rail.width(width); + // A rail is a SURFACE, and it is what separates it from the page beside + // it. Flutter falls back to colorScheme.surface when the rail names no + // colour, and without that the demo's rail was invisible: white + // destinations on a white page. + Color surface = backgroundColor != null ? backgroundColor : surfaceTint(context); + if (surface != null) { + rail.color(surface); + } + rail.child(column); + return rail; + } + + /// One destination: its icon, under the Material 3 indicator when selected, + /// with the label beneath it when the label type asks for one. + private Widget destinationTile(BuildContext context, NavigationRailDestination d, + boolean selected, double width) { + Widget icon = selected && d.getSelectedIcon() != null + ? d.getSelectedIcon() : d.getIcon(); + Widget top = icon; + if (selected && useIndicator && icon != null) { + com.codename1.flutter.BoxDecoration pill = new com.codename1.flutter.BoxDecoration(); + pill.color(indicatorColor != null ? indicatorColor : indicatorTint(context)); + pill.borderRadius(com.codename1.flutter.BorderRadius.circular(INDICATOR_HEIGHT_LP / 2)); + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.width(INDICATOR_WIDTH_LP); + box.height(INDICATOR_HEIGHT_LP); + box.decoration(pill); + box.alignment(com.codename1.flutter.Alignment.center); + box.child(icon); + top = box; + } + DartList parts = new DartList(); + if (top != null) { + parts.add(top); + } + boolean showLabel = labelType == NavigationRailLabelType.all + || (labelType == NavigationRailLabelType.selected && selected); + if (showLabel && d.getLabel() != null) { + parts.add(gap(LABEL_GAP_LP)); + parts.add(d.getLabel()); + } + com.codename1.flutter.widgets.Column tile = new com.codename1.flutter.widgets.Column(); + tile.mainAxisSize(com.codename1.flutter.MainAxisSize.min); + tile.mainAxisAlignment(com.codename1.flutter.MainAxisAlignment.center); + tile.crossAxisAlignment(com.codename1.flutter.CrossAxisAlignment.center); + tile.children(parts); + + com.codename1.flutter.widgets.Container slot = + new com.codename1.flutter.widgets.Container(); + slot.width(width); + slot.padding(com.codename1.flutter.EdgeInsets.symmetric(0, DESTINATION_PADDING_LP)); + slot.alignment(com.codename1.flutter.Alignment.center); + slot.child(tile); + return slot; + } + + /// The rail's own surface colour. + private static Color surfaceTint(BuildContext context) { + try { + ColorScheme scheme = Theme.of(context).colorScheme(); + return scheme == null ? null : scheme.surface(); + } catch (Throwable noTheme) { + return null; + } + } + + /// Material 3 tints the indicator with secondaryContainer. + private static Color indicatorTint(BuildContext context) { + try { + ColorScheme scheme = Theme.of(context).colorScheme(); + return scheme == null ? null : scheme.secondaryContainer(); + } catch (Throwable noTheme) { + return null; + } + } + + private static Widget gap(double heightLp) { + com.codename1.flutter.widgets.SizedBox b = new com.codename1.flutter.widgets.SizedBox(); + b.height(heightLp); + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailDestination.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailDestination.java new file mode 100644 index 00000000000..bd569445370 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailDestination.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.EdgeInsetsGeometry; +import com.codename1.flutter.Widget; + +/** + * A single selectable entry in a {@link NavigationRail} — Flutter's + * {@code NavigationRailDestination}. Signature-only: the icon/label widgets are + * captured for the rail to lay out; disabled/tooltip are recorded but unused. + */ +public class NavigationRailDestination { + + private Widget icon; + private Widget selectedIcon; + private Widget label; + private EdgeInsetsGeometry padding; + private boolean disabled; + private String indicatorColorTooltip; + + public void icon(Widget v) { this.icon = v; } + public void selectedIcon(Widget v) { this.selectedIcon = v; } + public void label(Widget v) { this.label = v; } + public void padding(EdgeInsetsGeometry v) { this.padding = v; } + public void disabled(boolean v) { this.disabled = v; } + public void indicatorColorTooltip(String v) { this.indicatorColorTooltip = v; } + + public Widget getIcon() { return icon; } + public Widget getSelectedIcon() { return selectedIcon; } + public Widget getLabel() { return label; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailLabelType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailLabelType.java new file mode 100644 index 00000000000..34ce4068393 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailLabelType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * How a {@link NavigationRail} labels its destinations — Flutter's + * {@code NavigationRailLabelType}: never, only the selected one, or all. + */ +public enum NavigationRailLabelType { + none, selected, all +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailThemeData.java new file mode 100644 index 00000000000..70eedbd5323 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NavigationRailThemeData.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Theming values for descendant {@link NavigationRail}s — Flutter's + * {@code NavigationRailThemeData}. Reached both as a constructed theme value and + * via {@code Theme.of(context).navigationRailTheme}; the reply study reads + * {@link #unselectedLabelTextStyle()} for its folder-section colors. + */ +public class NavigationRailThemeData { + + private Color backgroundColor; + private double elevation; + private TextStyle unselectedLabelTextStyle; + private TextStyle selectedLabelTextStyle; + private IconThemeData unselectedIconTheme; + private IconThemeData selectedIconTheme; + private double groupAlignment; + private NavigationRailLabelType labelType; + private boolean useIndicator; + private Color indicatorColor; + private Object indicatorShape; + private double minWidth; + private double minExtendedWidth; + + public void backgroundColor(Color v) { this.backgroundColor = v; } + public void elevation(double v) { this.elevation = v; } + public void unselectedLabelTextStyle(TextStyle v) { this.unselectedLabelTextStyle = v; } + public void selectedLabelTextStyle(TextStyle v) { this.selectedLabelTextStyle = v; } + public void unselectedIconTheme(IconThemeData v) { this.unselectedIconTheme = v; } + public void selectedIconTheme(IconThemeData v) { this.selectedIconTheme = v; } + public void groupAlignment(double v) { this.groupAlignment = v; } + public void labelType(NavigationRailLabelType v) { this.labelType = v; } + public void useIndicator(boolean v) { this.useIndicator = v; } + public void indicatorColor(Color v) { this.indicatorColor = v; } + public void indicatorShape(Object v) { this.indicatorShape = v; } + public void minWidth(double v) { this.minWidth = v; } + public void minExtendedWidth(double v) { this.minExtendedWidth = v; } + + public Color backgroundColor() { return backgroundColor; } + public double elevation() { return elevation; } + public TextStyle unselectedLabelTextStyle() { return unselectedLabelTextStyle; } + public TextStyle selectedLabelTextStyle() { return selectedLabelTextStyle; } + public IconThemeData unselectedIconTheme() { return unselectedIconTheme; } + public IconThemeData selectedIconTheme() { return selectedIconTheme; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedShape.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedShape.java new file mode 100644 index 00000000000..134487b1d8f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedShape.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The strategy that carves a notch out of a shape for a docked FAB — Flutter's + * {@code NotchedShape} interface. + */ +public abstract class NotchedShape { + + /** + * The outline of {@code host} with a notch carved for {@code guest}. + * + *

Declared here so a transpiled strategy -- the mail study's + * {@code WaterfallNotchedRectangle} is one -- can be CALLED. This was an empty marker + * class, so the shape a BottomAppBar was given could be stored and never asked for + * anything, and the bar drew as a plain rectangle with the docked button sitting on + * an edge it should have been cut into.

+ * + *

Returns null when the strategy cannot produce a path, which is the answer for + * anything that has not overridden it; the caller then draws the host unchanged.

+ */ + public com.codename1.flutter.Path getOuterPath(com.codename1.flutter.Rect host, + com.codename1.flutter.Rect guest) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurface.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurface.java new file mode 100644 index 00000000000..e6e6d6eadc1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurface.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.HasChild; + +/** + * Fills its box with a {@link NotchedShape}'s outline and paints its child on top. + * + *

Not a Flutter widget: it is the piece a {@code BottomAppBar} needs and Flutter gets + * from its own render object. A Container fills its box, and the whole point of a notched + * shape is that the box is not what should be filled -- the docked button sits in a + * cut-out of the bar's top edge, and behind that cut-out the page shows through.

+ */ +public class NotchedSurface extends Widget implements HasChild { + + private NotchedShape shape; + private Color color; + private double notchMargin; + private Widget child; + + public void shape(NotchedShape v) { + this.shape = v; + } + + public void color(Color v) { + this.color = v; + } + + public void notchMargin(double v) { + this.notchMargin = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + public NotchedShape getShape() { + return shape; + } + + public Color getColor() { + return color; + } + + public double getNotchMargin() { + return notchMargin; + } + + @Override + public Element createElement() { + return new NotchedSurfaceRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurfaceRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurfaceRenderElement.java new file mode 100644 index 00000000000..255a95bf60e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/NotchedSurfaceRenderElement.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Rect; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.widgets.EffectRenderElement; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Paints {@link NotchedSurface}: the bar's outline with a notch cut for the docked + * button, then the subtree on top. + * + *

Filled with {@code fillShape}, which every port honours -- unlike a shape CLIP, + * which does not confine an image or a gradient. The geometry is read at PAINT time + * rather than at build time: the bar is built before the scaffold lays its button out, + * so at build time there is nothing to read.

+ */ +public class NotchedSurfaceRenderElement extends EffectRenderElement { + + public NotchedSurfaceRenderElement(Widget widget) { + super(widget); + } + + private NotchedSurface surface() { + return (NotchedSurface) widget(); + } + + @Override + protected Widget effectChild() { + return surface().getChild(); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + com.codename1.flutter.Color fill = surface().getColor(); + NotchedShape notch = surface().getShape(); + int w = pane.getWidth(); + int h = pane.getHeight(); + if (fill == null || notch == null || w <= 0 || h <= 0) { + paintChildren.paint(g); + return; + } + double dpr = Dp.scale(); + if (dpr <= 0) { + dpr = 1; + } + Rect host = Rect.fromLTWH(0, 0, w / dpr, h / dpr); + Rect guest = guestRect(new Size(w / dpr, h / dpr), logical( + ScaffoldRenderElement.fabSizeOf(this), dpr), + ScaffoldRenderElement.fabLocationOf(this), surface().getNotchMargin()); + com.codename1.flutter.Path outline = guest == null ? null + : notch.getOuterPath(host, guest); + int color = g.getColor(); + int alpha = g.getAlpha(); + boolean aa = g.isAntiAliased(); + try { + g.setAntiAliased(true); + g.setColor(fill.rgb()); + g.setAlpha(fill.alpha()); + if (outline == null || !g.isShapeSupported()) { + g.fillRect(pane.getX(), pane.getY(), w, h); + } else { + g.fillShape(toGeneralPath(outline, pane.getX(), pane.getY(), dpr)); + } + } finally { + g.setAntiAliased(aa); + g.setColor(color); + g.setAlpha(alpha); + } + paintChildren.paint(g); + } + + /// A Flutter path in Codename One geometry, offset to this pane and scaled to device + /// pixels. + /// + /// Built here rather than through GraphicsCanvas: a canvas draw from this position + /// produced nothing at all, while {@code fillShape} -- which is what a Material + /// surface uses from the same kind of element -- paints. Only the verbs a notched + /// outline uses are handled; anything else closes the subpath so a partial outline + /// never leaks into the fill. + private static com.codename1.ui.geom.GeneralPath toGeneralPath( + com.codename1.flutter.Path path, int ox, int oy, double dpr) { + com.codename1.ui.geom.GeneralPath out = new com.codename1.ui.geom.GeneralPath(); + double cx = 0; + double cy = 0; + for (com.codename1.flutter.Path.Segment seg : path.segments()) { + double[] v = seg.coords; + if ("moveTo".equals(seg.verb)) { + out.moveTo(mx(v[0], ox, dpr), mx(v[1], oy, dpr)); + cx = v[0]; + cy = v[1]; + } else if ("lineTo".equals(seg.verb)) { + out.lineTo(mx(v[0], ox, dpr), mx(v[1], oy, dpr)); + cx = v[0]; + cy = v[1]; + } else if ("quadraticBezierTo".equals(seg.verb) || "conicTo".equals(seg.verb)) { + out.quadTo(mx(v[0], ox, dpr), mx(v[1], oy, dpr), + mx(v[2], ox, dpr), mx(v[3], oy, dpr)); + cx = v[2]; + cy = v[3]; + } else if ("cubicTo".equals(seg.verb)) { + out.curveTo(mx(v[0], ox, dpr), mx(v[1], oy, dpr), + mx(v[2], ox, dpr), mx(v[3], oy, dpr), + mx(v[4], ox, dpr), mx(v[5], oy, dpr)); + cx = v[4]; + cy = v[5]; + } else if ("arcToPoint".equals(seg.verb)) { + // The notch's floor. A chord here is what made the dimple curve down, cut + // straight across and come back up -- a bump between two curves. + double[] arc = com.codename1.flutter.rendering.GraphicsCanvas.arcToPoint( + cx, cy, v[0], v[1], v[2], v[5] != 0, v[6] != 0, + com.codename1.flutter.rendering.GraphicsCanvas.ARC_SEGMENTS); + if (arc == null) { + out.lineTo(mx(v[0], ox, dpr), mx(v[1], oy, dpr)); + } else { + for (int i = 0; i < arc.length; i += 2) { + out.lineTo(mx(arc[i], ox, dpr), mx(arc[i + 1], oy, dpr)); + } + } + cx = v[0]; + cy = v[1]; + } else if ("close".equals(seg.verb)) { + out.closePath(); + } + } + return out; + } + + private static float mx(double lp, int origin, double dpr) { + return (float) (origin + lp * dpr); + } + + private static Size logical(Size px, double dpr) { + return px == null ? null : new Size(px.width() / dpr, px.height() / dpr); + } + + /** + * The docked button's box in the BAR's own coordinates, or null when nothing is + * docked. A docked button straddles the bar's TOP edge, which is what puts the notch + * there rather than inside the bar. + */ + static Rect guestRect(Size bar, Size fab, FloatingActionButtonLocation where, + double notchMargin) { + if (bar == null || fab == null || fab.width() <= 0 || fab.height() <= 0) { + return null; + } + double w = fab.width() + notchMargin * 2; + double h = fab.height() + notchMargin * 2; + double cx; + if (where == FloatingActionButtonLocation.endDocked + || where == FloatingActionButtonLocation.endFloat) { + cx = bar.width() - 16 - fab.width() / 2; + } else if (where == FloatingActionButtonLocation.startDocked + || where == FloatingActionButtonLocation.startFloat) { + cx = 16 + fab.width() / 2; + } else { + cx = bar.width() / 2; + } + return Rect.fromLTWH(cx - w / 2, -h / 2, w, h); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/OutlinedButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/OutlinedButton.java new file mode 100644 index 00000000000..2a4d01ce1d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/OutlinedButton.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The material outlined button: a 1lp-outline capsule with primary-colored + * text and a transparent fill, backed by a CN1 Button + * (UIID "FlutterOutlinedButton"). + */ +public class OutlinedButton extends ButtonBase { + + /** + * Builds a {@link ButtonStyle} to hand to an OutlinedButton's {@code + * style:} parameter. Parameter order matches the Dart stub. + */ + public static ButtonStyle styleFrom(com.codename1.flutter.Color foregroundColor, + com.codename1.flutter.Color backgroundColor, com.codename1.flutter.Color shadowColor, + Double elevation, com.codename1.flutter.TextStyle textStyle, + com.codename1.flutter.EdgeInsets padding, Object side, Object shape, Object alignment, + Object tapTargetSize, Object visualDensity) { + return ButtonStyle.styleFrom(foregroundColor, backgroundColor, shadowColor, elevation, textStyle, + padding, side, shape, alignment, tapTargetSize, visualDensity); + } + + /** + * {@code OutlinedButton.icon}: a button whose content is an icon followed + * by a label. This milestone consumes the label as the button content (the + * leading icon is used when no label is supplied); a later pass composes + * both into a Row. + */ + public static OutlinedButton icon(com.codename1.flutter.Key key, + dart.runtime.Funcs.VoidFunc0 onPressed, ButtonStyle style, + com.codename1.flutter.Widget icon, com.codename1.flutter.Widget label) { + OutlinedButton b = new OutlinedButton(); + b.key(key); + b.onPressed(onPressed); + b.style(style); + b.child(label != null ? label : icon); + if (label != null) { + b.leadingIcon(icon); + } + return b; + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsBuilder.java new file mode 100644 index 00000000000..efc16453618 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsBuilder.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * A route transition for one platform — Flutter's {@code PageTransitionsBuilder}. + * + *

The base type only; concrete builders are supplied by the app (Rally hands + * {@link PageTransitionsTheme} one per platform). Route transitions are driven by Codename + * One's own machinery, so these are recorded rather than run — but the type has to exist + * for a theme that names it in a map to compile.

+ */ +public class PageTransitionsBuilder { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsTheme.java new file mode 100644 index 00000000000..caac62f1e0b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PageTransitionsTheme.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Per-platform page transition builders — Flutter's {@code PageTransitionsTheme}. + * + *

Recorded, not yet driven: route transitions here come from Codename One's own + * transition machinery. Rally configures one, so the type has to exist for its theme to + * transpile at all.

+ */ +public class PageTransitionsTheme { + + private Object builders; + + public void builders(Object v) { + this.builders = v; + } + + public Object getBuilders() { + return builders; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PaginatedDataTable.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PaginatedDataTable.java new file mode 100644 index 00000000000..b081a749e26 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PaginatedDataTable.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Row; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A {@link DataTable} that pages through a {@link DataTableSource} — Flutter's + * {@code PaginatedDataTable}. This milestone renders the optional {@code header}, + * a heading row of the {@code columns}' labels, and the current page of rows + * ({@code rowsPerPage} rows from {@code initialFirstRowIndex}) pulled from the + * source. Paging controls, sorting and selection are deferred. + */ +public class PaginatedDataTable extends StatelessWidget { + + /** The default value for {@code rowsPerPage} — Flutter's {@code defaultRowsPerPage}. */ + public static final long defaultRowsPerPage = 10; + + private Widget header; + private DartList columns; + private long rowsPerPage = 10; + private long initialFirstRowIndex; + private DataTableSource source; + private Long sortColumnIndex; + private Boolean sortAscending; + private Funcs.VoidFunc1 onPageChanged; + + public void header(Widget v) { + this.header = v; + } + + public void actions(DartList v) { + } + + public void columns(DartList v) { + this.columns = v; + } + + public void sortColumnIndex(Long v) { + this.sortColumnIndex = v; + } + + public void sortAscending(Boolean v) { + this.sortAscending = v; + } + + public void onSelectAll(Funcs.VoidFunc1 v) { + } + + public void dataRowHeight(Double v) { + } + + public void headingRowHeight(Double v) { + } + + public void horizontalMargin(Double v) { + } + + public void columnSpacing(Double v) { + } + + public void showCheckboxColumn(Boolean v) { + } + + public void showFirstLastButtons(Boolean v) { + } + + public void initialFirstRowIndex(Long v) { + this.initialFirstRowIndex = v; + } + + public void onPageChanged(Funcs.VoidFunc1 v) { + this.onPageChanged = v; + } + + public void rowsPerPage(Long v) { + this.rowsPerPage = v; + } + + public void availableRowsPerPage(DartList v) { + } + + public void onRowsPerPageChanged(Funcs.VoidFunc1 v) { + } + + public void source(DataTableSource v) { + this.source = v; + } + + public void checkboxHorizontalMargin(Object v) { + } + + public void controller(Object v) { + } + + public void primary(Boolean v) { + } + + /** Material's row metrics, in logical pixels. */ + private static final double HEADING_HEIGHT_LP = 56; + private static final double ROW_HEIGHT_LP = 48; + private static final double CELL_SPACING_LP = 12; + /** What a numeric column needs before its values start wrapping. */ + private static final double MIN_NUMERIC_COLUMN_LP = 64; + /** What the label column needs before the names start wrapping. */ + private static final double MIN_LABEL_COLUMN_LP = 160; + + /** + * One table row. + * + *

Column widths follow Flutter's: the LABEL column absorbs the slack while the + * numeric ones take only what they need. Giving every column an equal share — which is + * what this did — squeezed the dessert names into a two-character ribbon while the + * percentage columns sat in acres of space.

+ */ + private Row rowOf(DartList cells, double height) { + DartList flexed = new DartList(); + for (int i = 0; i < cells.size(); i++) { + boolean numeric = isNumericColumn(i); + com.codename1.flutter.widgets.Container cell = + new com.codename1.flutter.widgets.Container(); + cell.padding(com.codename1.flutter.EdgeInsets.symmetric(0, CELL_SPACING_LP / 2)); + cell.alignment(numeric + ? com.codename1.flutter.Alignment.centerRight + : com.codename1.flutter.Alignment.centerLeft); + cell.child(cells.get(i)); + Expanded e = new Expanded(); + // The first column is the label column and gets the room; Flutter sizes the + // numeric ones to content, and a 3:1 split is the same shape without needing + // intrinsic widths. + e.flex(i == 0 ? 4 : 1); + e.child(cell); + flexed.add(e); + } + Row r = new Row(); + r.crossAxisAlignment(CrossAxisAlignment.center); + r.children(flexed); + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + // A MINIMUM height, not a fixed one. Material's row is 48lp, but a label that wraps + // to two lines needs more, and pinning the height cut those rows through the middle + // of the second line - the dessert names came out sliced in half. + com.codename1.flutter.rendering.BoxConstraints rowBox = + new com.codename1.flutter.rendering.BoxConstraints( + 0, Double.POSITIVE_INFINITY, height, Double.POSITIVE_INFINITY); + box.constraints(rowBox); + box.padding(com.codename1.flutter.EdgeInsets.symmetric(CELL_SPACING_LP / 2, + CELL_SPACING_LP)); + box.child(r); + return rowOfBox(box); + } + + /** Wraps the sized row so the Column sees a Row-shaped child. */ + private static Row rowOfBox(Widget box) { + DartList one = new DartList(); + Expanded e = new Expanded(); + e.child(box); + one.add(e); + Row r = new Row(); + r.children(one); + return r; + } + + private boolean isNumericColumn(int index) { + return columns != null && index < columns.size() && columns.get(index).isNumeric(); + } + + /** + * The heading label, with the sort arrow on the column currently sorted and a tap that + * re-sorts — the part that made the header decorative rather than usable. + */ + private Widget headingCell(final int index) { + final DataColumn column = columns.get(index); + Widget label = column.getLabel(); + if (sortColumnIndex != null && sortColumnIndex.longValue() == index) { + DartList parts = new DartList(); + parts.add(label); + com.codename1.flutter.widgets.Icon arrow = new com.codename1.flutter.widgets.Icon( + isSortAscending() + ? com.codename1.flutter.Icons.keyboard_arrow_up + : com.codename1.flutter.Icons.keyboard_arrow_down); + arrow.size(16); + parts.add(arrow); + Row withArrow = new Row(); + withArrow.mainAxisSize(MainAxisSize.min); + withArrow.children(parts); + label = withArrow; + } + if (column.getOnSort() == null) { + return label; + } + InkWell tap = new InkWell(); + tap.child(label); + tap.onTap(new Funcs.VoidFunc0() { + @Override + public void call() { + boolean sameColumn = sortColumnIndex != null + && sortColumnIndex.longValue() == index; + // Tapping the sorted column reverses it; a new column starts ascending. + boolean ascending = sameColumn ? !isSortAscending() : true; + column.getOnSort().call(Long.valueOf(index), Boolean.valueOf(ascending)); + } + }); + return tap; + } + + private boolean isSortAscending() { + return sortAscending == null || sortAscending.booleanValue(); + } + + /** The footer: which rows are showing, and the way to the next page. */ + private Widget footer(long total) { + final long first = initialFirstRowIndex; + final long last = Math.min(first + rowsPerPage, total); + DartList parts = new DartList(); + + com.codename1.flutter.widgets.Text range = new com.codename1.flutter.widgets.Text( + (total == 0 ? 0 : first + 1) + "-" + last + " of " + total); + parts.add(range); + parts.add(pageButton(com.codename1.flutter.Icons.chevron_left, + first > 0, Math.max(0, first - rowsPerPage))); + parts.add(pageButton(com.codename1.flutter.Icons.chevron_right, + last < total, first + rowsPerPage)); + + Row row = new Row(); + row.mainAxisAlignment(com.codename1.flutter.MainAxisAlignment.end); + row.crossAxisAlignment(CrossAxisAlignment.center); + row.children(parts); + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.height(HEADING_HEIGHT_LP); + box.padding(com.codename1.flutter.EdgeInsets.symmetric(0, CELL_SPACING_LP)); + box.child(row); + return box; + } + + private Widget pageButton(com.codename1.flutter.IconData glyph, boolean enabled, + final long targetFirstRow) { + IconButton b = new IconButton(); + b.icon(new com.codename1.flutter.widgets.Icon(glyph)); + if (enabled && onPageChanged != null) { + b.onPressed(new Funcs.VoidFunc0() { + @Override + public void call() { + onPageChanged.call(Long.valueOf(targetFirstRow)); + } + }); + } + return b; + } + + @Override + public Widget build(BuildContext context) { + DartList body = new DartList(); + if (header != null) { + com.codename1.flutter.widgets.Container headerBox = + new com.codename1.flutter.widgets.Container(); + headerBox.height(64); + headerBox.padding(com.codename1.flutter.EdgeInsets.symmetric(0, CELL_SPACING_LP * 2)); + headerBox.alignment(com.codename1.flutter.Alignment.centerLeft); + headerBox.child(header); + body.add(headerBox); + } + if (columns != null) { + DartList labels = new DartList(); + for (int i = 0; i < columns.size(); i++) { + labels.add(headingCell(i)); + } + body.add(rowOf(labels, HEADING_HEIGHT_LP)); + } + long total = 0; + if (source != null) { + total = source.rowCount(); + long end = initialFirstRowIndex + rowsPerPage; + for (long i = initialFirstRowIndex; i < end && i < total; i++) { + DataRow dr = source.getRow(i); + if (dr == null || dr.getCells() == null) { + continue; + } + DartList cells = dr.getCells(); + DartList cellWidgets = new DartList(); + for (int j = 0; j < cells.size(); j++) { + cellWidgets.add(cells.get(j).getChild()); + } + body.add(rowOf(cellWidgets, ROW_HEIGHT_LP)); + } + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(body); + + // Wide tables SCROLL rather than squeeze. Eight columns do not fit a phone, and + // dividing the width between them regardless turned "16.0" into "16." over "0" and + // stacked every row two lines high. Flutter scrolls its table for the same reason. + com.codename1.flutter.widgets.Container wide = + new com.codename1.flutter.widgets.Container(); + wide.width(minimumWidthLp()); + wide.child(col); + com.codename1.flutter.widgets.SingleChildScrollView across = + new com.codename1.flutter.widgets.SingleChildScrollView(); + across.scrollDirection(com.codename1.flutter.Axis.horizontal); + across.child(wide); + + // The header and the pager belong to the table, not to the scrolled area, so they + // stay put while the columns move under them - as they do in Flutter. + DartList outer = new DartList(); + outer.add(across); + outer.add(footer(total)); + Column framed = new Column(); + framed.crossAxisAlignment(CrossAxisAlignment.stretch); + framed.mainAxisSize(MainAxisSize.min); + framed.children(outer); + return framed; + } + + /** The width the columns need before anything has to wrap. */ + private double minimumWidthLp() { + int count = columns == null ? 0 : columns.size(); + if (count == 0) { + return MIN_LABEL_COLUMN_LP; + } + return MIN_LABEL_COLUMN_LP + (count - 1) * MIN_NUMERIC_COLUMN_LP + + count * CELL_SPACING_LP; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PersistentBottomSheetController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PersistentBottomSheetController.java new file mode 100644 index 00000000000..02e3c93329f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PersistentBottomSheetController.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import dart.async.Future; + +/** + * The handle returned by {@link ScaffoldState#showBottomSheet} + * ({@code PersistentBottomSheetController} in Flutter). Its {@code closed} + * future completes with the sheet's result when the sheet is dismissed; at this + * milestone the sheet is not mounted, so the future is already complete. + */ +public class PersistentBottomSheetController { + + /** + * A future that resolves when the sheet is dismissed. Not persisted here, so + * it resolves immediately with a null result. + */ + public Future closed() { + return Future.value(null); + } + + public void close() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButton.java new file mode 100644 index 00000000000..a9fba9767cb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButton.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Shows a menu of {@link PopupMenuItem}s when pressed — Flutter's + * {@code PopupMenuButton}. This milestone renders the trigger ({@code child} + * or {@code icon}); building/presenting the menu and firing {@code onSelected} + * is deferred. + * + *

Non-generic on the Java side (the Dart stub is generic): {@code onSelected} + * is a generic method so an explicitly-typed selection callback + * ({@code (String v) => ...}) infers its parameter type at the call site, + * which a raw generic class would erase away.

+ */ +public class PopupMenuButton extends Widget { + + private Funcs.Func1 itemBuilder; + private Object initialValue; + private Funcs.VoidFunc1 onSelected; + private Funcs.VoidFunc0 onCanceled; + private String tooltip; + private double elevation = 8; + private Object padding; + private Widget icon; + private double iconSize; + private Object offset; + private boolean enabled = true; + private Object shape; + private Color color; + private Object position; + private Widget child; + + public void itemBuilder(Funcs.Func1 v) { + this.itemBuilder = v; + } + + public void initialValue(Object v) { + this.initialValue = v; + } + + public void onSelected(Funcs.VoidFunc1 v) { + this.onSelected = v; + } + + public void onCanceled(Funcs.VoidFunc0 v) { + this.onCanceled = v; + } + + public void tooltip(String v) { + this.tooltip = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void padding(Object v) { + this.padding = v; + } + + public void icon(Widget v) { + this.icon = v; + } + + public void iconSize(double v) { + this.iconSize = v; + } + + public void offset(Object v) { + this.offset = v; + } + + public void enabled(boolean v) { + this.enabled = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void color(Color v) { + this.color = v; + } + + public void position(Object v) { + this.position = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + public Widget getIcon() { + return icon; + } + + public Funcs.Func1 getItemBuilder() { + return itemBuilder; + } + + public Funcs.VoidFunc1 getOnSelected() { + return onSelected; + } + + public Funcs.VoidFunc0 getOnCanceled() { + return onCanceled; + } + + public boolean isEnabled() { + return enabled; + } + + /** + * The widget the button shows: the explicit {@code child} or {@code icon}, else the + * overflow glyph Flutter falls back to. Without that default a menu button written the + * usual way — neither child nor icon, as the gallery's app bar demo writes it — laid out + * as a zero-sized nothing, so the menu was not merely inert but invisible. + */ + public Widget effectiveTrigger() { + return effectiveTrigger(null); + } + + /** The same, resolving the adaptive icon against {@code context}'s theme. */ + public Widget effectiveTrigger(com.codename1.flutter.BuildContext context) { + if (child != null) { + return child; + } + if (icon != null) { + return icon; + } + // Flutter's default here is Icons.adaptive.more, which is HORIZONTAL + // on iOS and macOS and vertical everywhere else. Hard-coding more_vert + // put upright dots on an iOS screen where the reference shows an + // ellipsis, on the app bar and on every popup menu in the app. + Object p = null; + try { + com.codename1.flutter.material.ThemeData t = + context == null ? null : Theme.of(context); + p = t == null ? null : t.platform(); + } catch (Throwable noTheme) { + // an unthemed menu still needs an icon + } + boolean apple = p == com.codename1.flutter.TargetPlatform.iOS + || p == com.codename1.flutter.TargetPlatform.macOS; + return new com.codename1.flutter.widgets.Icon(apple + ? com.codename1.flutter.Icons.more_horiz + : com.codename1.flutter.Icons.more_vert); + } + + @Override + public Element createElement() { + return new PopupMenuButtonRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButtonRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButtonRenderElement.java new file mode 100644 index 00000000000..e28b0a95ed7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuButtonRenderElement.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Render element for {@link PopupMenuButton}: draws the trigger as a real button and OPENS + * THE MENU when it is pressed. + * + *

It is a {@link ButtonRenderElement} rather than a tappable wrapper around the glyph, + * and that is not incidental. An InkWell's gesture pane is an ordinary sibling in the flat + * component list, so a scroll view's own pane — added later, sitting above — swallowed the + * press before it arrived: the menu button drew correctly and did nothing, which is + * indistinguishable from a missing handler. A CN1 Button receives its own events, exactly as + * the neighbouring IconButtons in the same app bar already did.

+ * + *

With neither {@code child} nor {@code icon} the trigger is Flutter's overflow glyph, so + * a menu button written the ordinary way is visible at all.

+ */ +public class PopupMenuButtonRenderElement extends ButtonRenderElement { + + private final Funcs.VoidFunc0 open = new Funcs.VoidFunc0() { + @Override + public void call() { + // Read through widget() so a rebuilt configuration is honoured rather than the + // one that happened to be current when this element was created. + PopupMenus.show(PopupMenuButtonRenderElement.this, button(), component()); + } + }; + + public PopupMenuButtonRenderElement(PopupMenuButton widget) { + super(widget); + } + + private PopupMenuButton button() { + return (PopupMenuButton) widget(); + } + + @Override + protected Widget rawContentWidget() { + return button().effectiveTrigger(this); + } + + @Override + protected Funcs.VoidFunc0 onPressed() { + // A null handler would also DISABLE the button, which is what we want when the Dart + // says the menu is disabled. + return button().isEnabled() ? open : null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuDivider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuDivider.java new file mode 100644 index 00000000000..6cec39c0e53 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuDivider.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.HasChild; +import com.codename1.flutter.widgets.PassThroughRenderElement; + +/** + * A horizontal divider between entries in a {@link PopupMenuButton}'s menu — + * Flutter's {@code PopupMenuDivider}. Rendered as a {@link Divider}. + */ +public class PopupMenuDivider extends PopupMenuEntry implements HasChild { + + private double height = 16; + private final Divider divider = new Divider(); + + public void height(double v) { + this.height = v; + } + + @Override + public Widget getChild() { + return divider; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuEntry.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuEntry.java new file mode 100644 index 00000000000..1a07cb81d13 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuEntry.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +/** + * Base class for entries in a popup menu — Flutter's {@code PopupMenuEntry}. + * The concrete entry is {@link PopupMenuItem}. + */ +public abstract class PopupMenuEntry extends Widget { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuItem.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuItem.java new file mode 100644 index 00000000000..02e92040923 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenuItem.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.HasChild; +import com.codename1.flutter.widgets.PassThroughRenderElement; + +import dart.runtime.Funcs; + +/** + * An item in a {@link PopupMenuButton}'s menu — Flutter's + * {@code PopupMenuItem}. Carries the selection {@code value} and a child + * widget. Rendered as its child when materialized (the menu presentation is + * deferred). See {@link PassThroughRenderElement}. + */ +public class PopupMenuItem extends PopupMenuEntry implements HasChild { + + private Object value; + private boolean enabled = true; + private double height = 48; + private Object padding; + private Object textStyle; + private Object mouseCursor; + private Funcs.VoidFunc0 onTap; + private Widget child; + + public void value(Object v) { + this.value = v; + } + + public void enabled(boolean v) { + this.enabled = v; + } + + public void height(double v) { + this.height = v; + } + + public void padding(Object v) { + this.padding = v; + } + + public void textStyle(Object v) { + this.textStyle = v; + } + + public void mouseCursor(Object v) { + this.mouseCursor = v; + } + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getValue() { + return value; + } + + public Funcs.VoidFunc0 getOnTap() { + return onTap; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenus.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenus.java new file mode 100644 index 00000000000..9d3a9220fdf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/PopupMenus.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.FlutterRootLayout; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.widgets.Column; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.layouts.BorderLayout; + +import dart.core.DartList; +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Presents a {@link PopupMenuButton}'s menu and reports the selection — the part of + * {@code showMenu} the button needs. + * + *

The entries come from the button's {@code itemBuilder}, which is a Dart callback + * returning a list of {@link PopupMenuEntry}. Each selectable entry is wrapped in an + * {@link InkWell} that dismisses the menu and fires {@code onSelected} with that entry's + * value, so a menu behaves the way the Dart says it does rather than merely appearing.

+ * + *

The menu itself is a CN1 dialog holding a Flutter subtree, the same arrangement + * {@link Dialogs} uses for {@code showDialog} — modeless, so transpiled code continues + * after the call exactly as it does in Dart.

+ */ +public final class PopupMenus { + + private static com.codename1.ui.Dialog openMenu; + private static Element openRoot; + /** The page behind the menu, and the tint it had before we cleared it. */ + private static com.codename1.ui.Form tinted; + private static int savedTint; + + private PopupMenus() { + } + + /** + * Builds and shows {@code button}'s menu. Does nothing when the button is disabled or + * has no itemBuilder. + */ + public static void show(BuildContext context, PopupMenuButton button) { + show(context, button, null); + } + + /** + * Shows the menu anchored to {@code anchor} — the button's own component. + * + *

Anchoring matters beyond neatness: a plain dialog is centred and dims the screen + * behind it, which is a modal gesture. Flutter's menu is a small surface beside the + * control that opened it, and CN1's popup dialog is that same shape.

+ */ + public static void show(BuildContext context, PopupMenuButton button, + com.codename1.ui.Component anchor) { + if (button == null || !button.isEnabled() || button.getItemBuilder() == null) { + return; + } + List> entries = entriesOf(context, button); + if (entries.isEmpty()) { + return; + } + MenuWidget menu = new MenuWidget(button, entries); + if (!Display.isInitialized()) { + // headless: mount the tree so the bookkeeping is testable, with no dialog + openRoot = FlutterUI.mount(menu, new RenderHost(), new BuildOwner()); + return; + } + dismiss(); + com.codename1.ui.Dialog d = new com.codename1.ui.Dialog(new BorderLayout()); + d.setDisposeWhenPointerOutOfBounds(true); + Container c = FlutterUI.wrap(menu); + d.add(BorderLayout.CENTER, c); + openMenu = d; + openRoot = ((FlutterRootLayout) c.getLayout()).host().rootElement(); + if (anchor != null) { + // Beside the button — Flutter's menu, and CN1's popup dialog. The popup's own + // chrome is dropped: CN1 draws a speech-bubble arrow and dims the screen behind + // it, and a Material menu does neither. The Material surface underneath supplies + // the rounded card. + stripPopupChrome(d); + d.showPopupDialog(anchor); + } else { + // Modeless, like showDialog: the caller keeps running, as the Dart expects. + d.showPacked(BorderLayout.NORTH, false); + } + } + + /** + * Removes the arrow border and the dimming a CN1 popup dialog brings with it, so what + * shows is the Material surface and nothing else. + */ + private static void stripPopupChrome(com.codename1.ui.Dialog d) { + try { + // The UIID is deliberately left alone: showPopupDialog derives the popup's + // surface from it, and renaming it away takes the card with the arrow. + d.setBlurBackgroundRadius(-1); + d.getContentPane().getAllStyles().setPadding(0, 0, 0, 0); + d.getContentPane().getAllStyles().setMargin(0, 0, 0, 0); + // No arrow: CN1 draws a speech bubble pointing at the anchor, and a Material + // menu is a plain rounded card. The arrow rides on the border, so replacing the + // border with a rounded one removes the point and keeps the surface. + d.getDialogStyle().setBorder(com.codename1.ui.plaf.RoundRectBorder.create() + .cornerRadius(1f) + .shadowOpacity(30)); + // And no scrim: a dialog TINTS the page behind it, which reads as modal, while + // a menu is a light touch. The tint lives on the page rather than on us, so it + // is cleared there and put back when the menu closes. + tinted = com.codename1.ui.Display.getInstance().getCurrent(); + if (tinted != null) { + savedTint = tinted.getTintColor(); + tinted.setTintColor(0); + } + } catch (Throwable t) { + // chrome is cosmetic; a themed popup still works + } + } + + /** Puts back the tint the page had before the menu covered it. */ + private static void restoreTint() { + if (tinted != null) { + try { + tinted.setTintColor(savedTint); + } catch (Throwable t) { + // best effort: a wrong tint is better than a failed dismiss + } + tinted = null; + } + } + + /** Closes the open menu, if any. */ + public static void dismiss() { + restoreTint(); + if (openRoot != null) { + FlutterUI.unmountTree(openRoot); + openRoot = null; + } + if (openMenu != null) { + openMenu.dispose(); + openMenu = null; + } + } + + /** Whether a menu is currently open — test and hot-restart hook. */ + public static boolean isOpen() { + return openRoot != null; + } + + /** Runs the itemBuilder and collects whatever entries it produced. */ + @SuppressWarnings("unchecked") + private static List> entriesOf(BuildContext context, + PopupMenuButton button) { + List> out = new ArrayList>(); + Object built; + try { + built = button.getItemBuilder().call(context); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.unimplemented("PopupMenuButton", + "itemBuilder failed: " + t); + return out; + } + if (built instanceof Iterable) { + for (Object o : (Iterable) built) { + if (o instanceof PopupMenuEntry) { + out.add((PopupMenuEntry) o); + } + } + } + return out; + } + + /** + * The menu surface: a Material card holding one row per entry. + */ + static final class MenuWidget extends StatelessWidget { + + private final PopupMenuButton button; + private final List> entries; + + MenuWidget(PopupMenuButton button, List> entries) { + this.button = button; + this.entries = entries; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Widget build(BuildContext context) { + DartList rows = new DartList(); + for (int i = 0; i < entries.size(); i++) { + final PopupMenuEntry entry = entries.get(i); + if (entry instanceof PopupMenuDivider) { + rows.add(new PopupMenuDivider()); + continue; + } + if (!(entry instanceof PopupMenuItem)) { + continue; + } + final PopupMenuItem item = (PopupMenuItem) entry; + // Material menu item metrics: 48lp tall, 16lp either side, start-aligned. + com.codename1.flutter.widgets.Container box = + new com.codename1.flutter.widgets.Container(); + box.padding(com.codename1.flutter.EdgeInsets.symmetric(0, 16)); + box.height(48); + box.alignment(com.codename1.flutter.AlignmentDirectional.centerStart); + box.child(item.getChild()); + InkWell row = new InkWell(); + row.child(box); + if (item.isEnabled()) { + row.onTap(new Funcs.VoidFunc0() { + @Override + public void call() { + select(item); + } + }); + } + rows.add(row); + } + Column col = new Column(); + col.children(rows); + col.mainAxisSize(com.codename1.flutter.MainAxisSize.min); + Material surface = new Material(); + surface.child(col); + return surface; + } + + /** Dismiss first, then report — the order Flutter's menu route uses. */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void select(PopupMenuItem item) { + dismiss(); + Funcs.VoidFunc0 tap = item.getOnTap(); + if (tap != null) { + tap.call(); + } + Funcs.VoidFunc1 onSelected = ((PopupMenuButton) button).getOnSelected(); + if (onSelected != null && item.getValue() != null) { + onSelected.call(item.getValue()); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Radio.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Radio.java new file mode 100644 index 00000000000..1a558ad1cbb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Radio.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material radio button. Grouping is by VALUE EQUALITY, not by a CN1 + * ButtonGroup: this radio renders selected exactly when {@code value} equals + * {@code groupValue} (Dart {@code ==} semantics). Selecting it fires + * {@code onChanged(value)}; the app's rebuild with a new groupValue is what + * moves the selection (controlled semantics). Backed by a CN1 + * {@link com.codename1.ui.RadioButton} (UIID "FlutterRadio"). + */ +public class Radio extends Widget { + + private T value; + private T groupValue; + private Funcs.VoidFunc1 onChanged; + + public void value(T v) { + this.value = v; + } + + public void groupValue(T v) { + this.groupValue = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public T getValue() { + return value; + } + + public T getGroupValue() { + return groupValue; + } + + public Funcs.VoidFunc1 getOnChanged() { + return onChanged; + } + + @Override + public Element createElement() { + return new RadioRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioListTile.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioListTile.java new file mode 100644 index 00000000000..43cdb006c0c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioListTile.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A {@link ListTile} whose trailing (or leading) control is a {@link Radio} — + * Flutter's {@code RadioListTile}. Tapping anywhere on the row selects the + * radio, firing {@code onChanged(value)}; grouping is by value equality against + * {@code groupValue} (see {@link Radio}). Composed as a ListTile hosting the + * radio. + */ +public class RadioListTile extends StatelessWidget { + + private Object value; + private Object groupValue; + private Funcs.VoidFunc1 onChanged; + private Widget title; + private Widget subtitle; + private Widget secondary; + + public void value(Object v) { + this.value = v; + } + + public void groupValue(Object v) { + this.groupValue = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void secondary(Widget v) { + this.secondary = v; + } + + public void isThreeLine(boolean v) { + } + + public void selected(boolean v) { + } + + public void dense(boolean v) { + } + + private Object controlAffinity; + + public void controlAffinity(Object v) { + this.controlAffinity = v; + } + + public void activeColor(Object v) { + } + + public void contentPadding(Object v) { + } + + @Override + public Widget build(BuildContext context) { + Radio radio = new Radio(); + radio.value(value); + radio.groupValue(groupValue); + radio.onChanged(onChanged); + + ListTile tile = new ListTile(); + if (title != null) { + tile.title(title); + } + if (subtitle != null) { + tile.subtitle(subtitle); + } + // Flutter puts a radio on the LEADING edge by default — + // ListTileControlAffinity.platform resolves to leading for radios and + // checkboxes (only a switch trails). Putting it on the trailing edge + // mirrored every settings list in the app. + if (ListTileControlAffinity.isTrailing(controlAffinity, false)) { + if (secondary != null) { + tile.leading(secondary); + } + tile.trailing(radio); + } else { + tile.leading(radio); + if (secondary != null) { + tile.trailing(secondary); + } + } + return tile; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioRenderElement.java new file mode 100644 index 00000000000..3b903653dbf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioRenderElement.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.RadioButton; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.geom.Dimension; + +import dart.runtime.DartRuntime; +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link Radio}: a CN1 RadioButton (UIID "FlutterRadio") + * deliberately NOT placed in a CN1 ButtonGroup — the selected state is + * derived from Dart-equality of {@code value} vs {@code groupValue}, and the + * component is snapped back to that derived state after every user press + * (controlled semantics). Material tap target: 48x48lp minimum. + */ +public class RadioRenderElement extends RenderElement { + + /** Material minimum tap target in logical pixels. */ + public static final double TAP_TARGET_LP = 48; + + private boolean applying; + + public RadioRenderElement(Radio widget) { + super(widget); + } + + private Radio radio() { + return (Radio) widget(); + } + + /** + * Whether the current configuration renders this radio selected: + * Dart equality of value vs groupValue. + */ + public boolean selected() { + return DartRuntime.eq(radio().getValue(), radio().getGroupValue()); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + RadioButton rb = new RadioButton(); + rb.setUIID("FlutterRadio"); + rb.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (applying) { + return; + } + userSelected(); + } + }); + apply(rb); + return rb; + } + + @Override + protected void updateComponent(Component c) { + apply((RadioButton) c); + } + + private void apply(RadioButton rb) { + applying = true; + try { + rb.setSelected(selected()); + // A null onChanged is Flutter's disabled radio, as the checkbox, switch and + // slider renderers already mirror; the radio stayed enabled and focusable. + rb.setEnabled(radio().getOnChanged() != null); + } finally { + applying = false; + } + } + + /** + * Controlled select entry point (public so headless tests can drive it): + * fires onChanged with this radio's value, then re-applies the state + * derived from the CURRENT groupValue. + */ + public void userSelected() { + Funcs.VoidFunc1 f = radio().getOnChanged(); + if (f != null) { + f.call(radio().getValue()); + } + Component c = component(); + if (c != null) { + apply((RadioButton) c); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double min = Dp.px(TAP_TARGET_LP); + double w = min; + double h = min; + Component c = component(); + if (c != null) { + Dimension d = c.getPreferredSize(); + w = Math.max(w, d.getWidth()); + h = Math.max(h, d.getHeight()); + } + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioThemeData.java new file mode 100644 index 00000000000..b9982d60085 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RadioThemeData.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Material {@code RadioThemeData}: write-once radio-button styling. The values + * are {@link MaterialStateProperty}/density/cursor objects owned by other + * runtime areas, so they are held opaquely as {@code Object} in this pass. + * Named Dart constructor parameters map to setter methods. + */ +public class RadioThemeData { + + private Object fillColor; + private Object overlayColor; + private Object splashRadius; + private Object materialTapTargetSize; + private Object visualDensity; + private Object mouseCursor; + + public void fillColor(Object v) { + this.fillColor = v; + } + + public void overlayColor(Object v) { + this.overlayColor = v; + } + + public void splashRadius(Object v) { + this.splashRadius = v; + } + + public void materialTapTargetSize(Object v) { + this.materialTapTargetSize = v; + } + + public void visualDensity(Object v) { + this.visualDensity = v; + } + + public void mouseCursor(Object v) { + this.mouseCursor = v; + } + + public Object getFillColor() { + return fillColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeLabels.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeLabels.java new file mode 100644 index 00000000000..aceec5d0a53 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeLabels.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Text labels shown above the two thumbs of a {@link RangeSlider} — Flutter's + * {@code RangeLabels}. + */ +public class RangeLabels { + + private final String start; + private final String end; + + public RangeLabels(String start, String end) { + this.start = start; + this.end = end; + } + + public String start() { + return start; + } + + public String end() { + return end; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeSlider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeSlider.java new file mode 100644 index 00000000000..de6a6859a73 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeSlider.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A slider selecting a range between two thumbs — Flutter's {@code RangeSlider}. + * This milestone renders a single {@link Slider} spanning the current range + * (its thumb at {@code values.start}); the second thumb and range-drag gestures + * are deferred. {@code onChanged} carries a {@link RangeValues}. + */ +public class RangeSlider extends StatelessWidget { + + private RangeValues values; + private Double min; + private Double max; + private Long divisions; + private RangeLabels labels; + private Color activeColor; + private Color inactiveColor; + private Funcs.VoidFunc1 onChanged; + private Funcs.VoidFunc1 onChangeStart; + private Funcs.VoidFunc1 onChangeEnd; + + public void values(RangeValues v) { + this.values = v; + } + + public void min(double v) { + this.min = v; + } + + public void max(double v) { + this.max = v; + } + + public void divisions(long v) { + this.divisions = v; + } + + public void labels(RangeLabels v) { + this.labels = v; + } + + public void activeColor(Color v) { + this.activeColor = v; + } + + public void inactiveColor(Color v) { + this.inactiveColor = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void onChangeStart(Funcs.VoidFunc1 v) { + this.onChangeStart = v; + } + + public void onChangeEnd(Funcs.VoidFunc1 v) { + this.onChangeEnd = v; + } + + public void semanticFormatterCallback(Object v) { + } + + @Override + public Widget build(BuildContext context) { + Slider s = new Slider(); + s.min(min == null ? 0.0 : min); + s.max(max == null ? 1.0 : max); + s.value(values != null ? values.start() : (min == null ? 0.0 : min)); + if (divisions != null) { + s.divisions(divisions); + } + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeValues.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeValues.java new file mode 100644 index 00000000000..4ec869505c6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RangeValues.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * An immutable pair of {@code start}/{@code end} values for a + * {@link RangeSlider} — Flutter's {@code RangeValues}. + */ +public class RangeValues { + + private final double start; + private final double end; + + public RangeValues(double start, double end) { + this.start = start; + this.end = end; + } + + public double start() { + return start; + } + + public double end() { + return end; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RawMaterialButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RawMaterialButton.java new file mode 100644 index 00000000000..08de2d961cc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RawMaterialButton.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +/** + * The lowest-level material button — Flutter's {@code RawMaterialButton}. + * Shares {@link ButtonBase}'s press/child plumbing and adds a raw + * {@code fillColor}. Used by color-picker swatches and other custom buttons + * that want the material tap semantics without the higher-level button styles. + */ +public class RawMaterialButton extends ButtonBase { + + private Color fillColor; + private Double elevation; + private com.codename1.flutter.EdgeInsets padding; + private Object shape; + + public void fillColor(Color v) { + this.fillColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void padding(com.codename1.flutter.EdgeInsets v) { + this.padding = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public Color getFillColor() { + return fillColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RefreshIndicator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RefreshIndicator.java new file mode 100644 index 00000000000..370323d46ec --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/RefreshIndicator.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +import dart.runtime.Funcs; + +/** + * Wraps a scrollable to add pull-to-refresh — Flutter's + * {@code RefreshIndicator}. This milestone renders the {@code child}; the + * overscroll gesture that triggers {@code onRefresh} (a {@code Future}-returning + * callback) is deferred. + */ +public class RefreshIndicator extends StatelessWidget { + + private Widget child; + private Funcs.Func0 onRefresh; + + public void child(Widget v) { + this.child = v; + } + + public void displacement(double v) { + } + + public void onRefresh(Funcs.Func0 v) { + this.onRefresh = v; + } + + public void color(Color v) { + } + + public void backgroundColor(Color v) { + } + + public void strokeWidth(double v) { + } + + public void notificationPredicate(Object v) { + } + + public void semanticsLabel(String v) { + } + + public void semanticsValue(String v) { + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Scaffold.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Scaffold.java new file mode 100644 index 00000000000..d39a7dfc9b8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Scaffold.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * The basic material page layout: an optional app bar, a body, an optional + * floating action button overlaid bottom-right, an optional navigation + * drawer (root Scaffolds only — rendered into the Toolbar side menu) and an + * optional bottom navigation bar. + */ +public class Scaffold extends Widget { + + private Widget appBar; + private Widget body; + private Widget floatingActionButton; + private FloatingActionButtonLocation floatingActionButtonLocation; + private Widget drawer; + private Widget bottomNavigationBar; + private Color backgroundColor; + private Boolean resizeToAvoidBottomInset; + private DartList persistentFooterButtons; + private Widget endDrawer; + private Widget bottomSheet; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void resizeToAvoidBottomInset(boolean v) { + this.resizeToAvoidBottomInset = v; + } + + public void persistentFooterButtons(DartList v) { + this.persistentFooterButtons = v; + } + + /** {@code Scaffold.persistentFooterButtons} — controls pinned above the bottom edge. */ + public DartList getPersistentFooterButtons() { + return persistentFooterButtons; + } + + public void endDrawer(Widget v) { + this.endDrawer = v; + } + + public void bottomSheet(Widget v) { + this.bottomSheet = v; + } + + public void floatingActionButtonLocation(FloatingActionButtonLocation v) { + this.floatingActionButtonLocation = v; + } + + /** Where the FAB sits; null means Flutter's default, {@code endFloat}. */ + public FloatingActionButtonLocation getFloatingActionButtonLocation() { + return floatingActionButtonLocation; + } + + private boolean extendBody; + + /** Whether the body extends behind the bottom navigation bar — Flutter's {@code extendBody}. */ + public void extendBody(boolean v) { + this.extendBody = v; + } + + /** + * {@code Scaffold.extendBody}. + * + *

This was an EMPTY SETTER: the flag was discarded, so the body always stopped + * above the bottom bar. A bar with a notch then had nothing to reveal through it -- + * the mail study cuts a notch for its docked button and the gap came out opaque, + * showing the scaffold's own background where the list should run underneath.

+ */ + public boolean getExtendBody() { + return extendBody; + } + + /** Whether the body extends behind the app bar — Flutter's {@code extendBodyBehindAppBar}. */ + public void extendBodyBehindAppBar(boolean v) { + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + public void appBar(Widget v) { + this.appBar = v; + } + + public void body(Widget v) { + this.body = v; + } + + public void floatingActionButton(Widget v) { + this.floatingActionButton = v; + } + + public void drawer(Widget v) { + this.drawer = v; + } + + public void bottomNavigationBar(Widget v) { + this.bottomNavigationBar = v; + } + + public Widget getAppBar() { + return appBar; + } + + public Widget getBody() { + return body; + } + + public Widget getFloatingActionButton() { + return floatingActionButton; + } + + public Widget getDrawer() { + return drawer; + } + + public Widget getBottomNavigationBar() { + return bottomNavigationBar; + } + + private static final ScaffoldState STATE = new ScaffoldState() { + }; + + /** + * The nearest scaffold's mutable state ({@code Scaffold.of(context)}), + * used to show snack bars and bottom sheets and to open the drawers. + */ + public static ScaffoldState of(BuildContext context) { + return STATE; + } + + @Override + public Element createElement() { + return new ScaffoldRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessenger.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessenger.java new file mode 100644 index 00000000000..951455b9cb6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessenger.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Access point for showing SnackBars — Flutter's {@code ScaffoldMessenger}. + * As a widget it simply renders its {@code child} (it scopes messenger state + * to its subtree); M3 keeps one messenger state per app process (a single + * static state is equivalent for one running app), exposed via the static + * {@link #of(BuildContext)}. + */ +public class ScaffoldMessenger extends StatelessWidget { + + private static final ScaffoldMessengerState state = new ScaffoldMessengerState(); + + private Widget child; + + public ScaffoldMessenger() { + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + return child; + } + + public static ScaffoldMessengerState of(BuildContext context) { + return state; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessengerState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessengerState.java new file mode 100644 index 00000000000..343ee9ef98b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldMessengerState.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.components.ToastBar; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Text; +import com.codename1.io.Log; +import com.codename1.ui.Display; + +/** + * Shows {@link SnackBar}s through CN1's {@link ToastBar}. The SnackBar's + * content widget is consumed as a message string: a {@code Text} child + * supplies its data; any other widget falls back to {@code toString()} with + * a log warning (mirroring button-label consumption). Headless (no Display) + * the message is only recorded, which keeps the consumption logic testable. + */ +public class ScaffoldMessengerState { + + private String lastMessage; + private long lastDurationMillis; + + ScaffoldMessengerState() { + } + + /** + * {@code hideCurrentSnackBar}: dismisses the visible SnackBar. This runtime + * shows SnackBars through the auto-expiring {@link ToastBar}, so there is no + * retained handle to hide; the call clears the recorded message. + */ + public void hideCurrentSnackBar(Object reason) { + lastMessage = null; + } + + public void showSnackBar(SnackBar snackBar) { + if (snackBar == null) { + return; + } + String msg = consumeMessage(snackBar.getContent()); + long ms = snackBar.durationMillis(); + lastMessage = msg; + lastDurationMillis = ms; + if (!Display.isInitialized()) { + return; + } + ToastBar.Status status = ToastBar.getInstance().createStatus(); + status.setMessage(msg); + status.setExpires((int) ms); + status.show(); + } + + private static String consumeMessage(Widget content) { + if (content == null) { + return ""; + } + if (content instanceof Text) { + String d = ((Text) content).getData(); + return d == null ? "" : d; + } + try { + Log.p("Flutter runtime: SnackBar content " + content.getClass().getSimpleName() + + " is not a Text; using its toString() as the message"); + } catch (Throwable t) { + // headless: Log has no storage backend + } + return String.valueOf(content); + } + + /** + * The message most recently passed to {@link #showSnackBar} (test hook). + */ + public String lastMessage() { + return lastMessage; + } + + /** + * The duration (ms) most recently passed to {@link #showSnackBar} + * (test hook). + */ + public long lastDurationMillis() { + return lastDurationMillis; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldRenderElement.java new file mode 100644 index 00000000000..7c9aa0306b8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldRenderElement.java @@ -0,0 +1,752 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.animation.AnimationController; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.FlutterRootLayout; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Toolbar; +import com.codename1.ui.layouts.BorderLayout; + +import dart.runtime.Funcs; + +/** + * Render element for {@link Scaffold}. Two modes: + * + *
    + *
  • Root mode (topmost render element of a {@code FlutterUI.runApp} + * tree, i.e. no render ancestor and the host has a Form): the appBar + * subtree is routed into a dedicated {@link RenderHost} whose container + * becomes the Form Toolbar's title component (its own FlutterRootLayout + * lays the title out); the drawer subtree is routed into the Toolbar's + * side menu; the bottomNavigationBar subtree is routed into the Form's + * BorderLayout SOUTH region; the body fills the whole Flutter canvas; + * the FAB is positioned inside the canvas bottom-right with a 16lp + * margin.
  • + *
  • Embedded mode (inside {@code FlutterUI.wrap} or below other + * render elements): the appBar renders as an in-canvas strip at the + * top, the bottomNavigationBar as an in-canvas strip at the bottom, + * the body fills the rest, the FAB overlays bottom-right above the + * bottom strip. The drawer is IGNORED in embedded mode (with a log + * warning) — a side menu needs the Form Toolbar.
  • + *
+ * + *

Drawer and bottomNavigationBar hosts are decided at mount time: adding + * them to a root Scaffold in a later rebuild logs a warning instead of + * re-plumbing the Form (M3 limitation).

+ */ +public class ScaffoldRenderElement extends RenderElement { + + private static final double FAB_MARGIN_LP = 16; + + private Element appBarChild; + private Element bodyChild; + private Element fabChild; + private Element drawerChild; + private Element bottomNavChild; + private Element footerChild; + + private boolean rootMode; + private RenderHost toolbarHost; + private RenderHost drawerHost; + private RenderHost southHost; + private boolean warnedDrawer; + private boolean warnedLateNav; + + public ScaffoldRenderElement(Scaffold widget) { + super(widget); + } + + private Scaffold scaffold() { + return (Scaffold) widget(); + } + + /** + * The Scaffold's own face: an opaque fill covering its bounds, which the + * children then paint over (they attach after it in tree order). + * + *

Flutter's Scaffold is a Material — it is opaque, not a + * transparent frame. That matters whenever two Scaffolds are stacked, as + * in a backdrop: without the fill, the page underneath shows through every + * gap between the front page's children.

+ */ + @Override + protected Component createComponent() { + if (!com.codename1.ui.Display.isInitialized()) { + return null; + } + Container face = new Container(); + face.setUIID("FlutterScaffold"); + face.getAllStyles().setPadding(0, 0, 0, 0); + face.getAllStyles().setMargin(0, 0, 0, 0); + applyBackground(face); + return face; + } + + @Override + protected void updateComponent(Component c) { + applyBackground(c); + } + + @Override + public void themeChanged() { + super.themeChanged(); + if (component() != null) { + applyBackground(component()); + } + } + + private void applyBackground(Component face) { + com.codename1.flutter.Color bg = effectiveBackground(); + if (bg != null) { + ThemeDataAdapter.paintColor(face.getAllStyles(), bg); + } + } + + /** + * {@code Scaffold.backgroundColor} when given, else the theme's + * {@code scaffoldBackgroundColor}, else {@code colorScheme.background} — + * Flutter's own resolution order. + */ + private com.codename1.flutter.Color effectiveBackground() { + if (scaffold().getBackgroundColor() != null) { + return scaffold().getBackgroundColor(); + } + try { + ThemeData theme = Theme.of(this); + if (theme.scaffoldBackgroundColor() != null) { + return theme.scaffoldBackgroundColor(); + } + return theme.colorScheme().background(); + } catch (Throwable t) { + return null; + } + } + + @Override + public void mount(Element parent, int slot) { + // Decide the mode before children mount (they inherit hosts from it). + rootMode = false; + Element a = parent; + boolean hasRenderAncestor = false; + while (a != null) { + if (a instanceof RenderElement) { + hasRenderAncestor = true; + break; + } + a = a.parent(); + } + RenderHost mountHost = parent != null ? parent.host() : host(); + if (!hasRenderAncestor && mountHost != null && mountHost.form() != null) { + rootMode = true; + if (scaffold().getAppBar() != null) { + mountHost.formToolbarBound(true); + } + prepareToolbarHost(mountHost.form()); + prepareDrawerHost(mountHost.form()); + prepareBottomHost(mountHost.form()); + } + super.mount(parent, slot); + } + + private void prepareToolbarHost(Form form) { + if (scaffold().getAppBar() == null) { + return; + } + Toolbar tb = ensureToolbar(form); + toolbarHost = new RenderHost(); + toolbarHost.toolbarTitleHost(true); + toolbarHost.toolbar(tb); + Container titleCnt = new Container(new FlutterRootLayout(toolbarHost)); + toolbarHost.container(titleCnt); + toolbarHost.rootSupplier(new Funcs.Func0() { + @Override + public Element call() { + return appBarChild; + } + }); + tb.setTitleComponent(titleCnt); + } + + /** + * Routes the drawer subtree into the Form Toolbar's side menu via a + * dedicated host container (its own FlutterRootLayout sizes the panel). + */ + private void prepareDrawerHost(Form form) { + if (scaffold().getDrawer() == null) { + return; + } + Toolbar tb = ensureToolbar(form); + drawerHost = new RenderHost(); + Container drawerCnt = new Container(new FlutterRootLayout(drawerHost)); + drawerHost.container(drawerCnt); + drawerHost.rootSupplier(new Funcs.Func0() { + @Override + public Element call() { + return drawerChild; + } + }); + tb.addComponentToSideMenu(drawerCnt); + } + + /** + * Routes the bottomNavigationBar subtree into the Form's SOUTH region + * via a dedicated host container (the Form lays it out; its preferred + * height comes from the bar's 80lp layout). + */ + private void prepareBottomHost(Form form) { + if (scaffold().getBottomNavigationBar() == null) { + return; + } + southHost = new RenderHost(); + Container southCnt = new Container(new FlutterRootLayout(southHost)); + southHost.container(southCnt); + southHost.rootSupplier(new Funcs.Func0() { + @Override + public Element call() { + return bottomNavChild; + } + }); + form.add(BorderLayout.SOUTH, southCnt); + } + + private static Toolbar ensureToolbar(Form form) { + Toolbar tb = form.getToolbar(); + if (tb == null) { + tb = new Toolbar(); + form.setToolbar(tb); + } + // FlutterUI hides the Toolbar of a Form nothing claimed. A Scaffold asking for + // it here is claiming it, which can happen after that decision was taken if a + // rebuild introduces an AppBar where there was none. + tb.setHidden(false); + tb.setVisible(true); + return tb; + } + + @Override + protected RenderHost hostForChild(int slot) { + if (slot == 0 && rootMode && toolbarHost != null) { + return toolbarHost; + } + if (slot == 3 && rootMode && drawerHost != null) { + return drawerHost; + } + if (slot == 4 && rootMode && southHost != null) { + return southHost; + } + return host(); + } + + @Override + protected void syncChildren() { + appBarChild = updateChild(appBarChild, scaffold().getAppBar(), 0); + bodyChild = updateChild(bodyChild, bodyWidget(), 1); + footerChild = updateChild(footerChild, footerWidget(), 5); + syncDrawer(); + syncBottomNav(); + // LAST, because components attach in mount order and that is this host's + // paint order. Flutter's _ScaffoldSlot puts the FAB after the persistent + // footer and the bottom navigation bar, so it floats over both; mounted + // before them it is painted under them, and a DOCKED fab -- which + // straddles the bar's top edge by design -- loses its whole bottom half. + fabChild = updateChild(fabChild, fabWidget(), 2); + } + + /// Scales the floating action button in and out, instead of it appearing and + /// vanishing between one frame and the next. + /// + /// Flutter's Scaffold runs every change of this slot through + /// {@code FloatingActionButtonAnimator.scaling} over + /// {@code kFloatingActionButtonSegue}. Dropping the widget outright is a visibly + /// different thing: Reply hides the button while its mailbox drawer opens, and the + /// reference still shows it 50ms in and has scaled it away by 100ms, where ours was + /// simply gone on the first frame of the gesture -- the button did not leave, it was + /// never there. + private static final int FAB_SEGUE_MS = 200; + + private AnimationController fabScale; + private com.codename1.flutter.Widget lastFab; + + private com.codename1.flutter.Widget fabWidget() { + com.codename1.flutter.Widget now = scaffold().getFloatingActionButton(); + if (fabScale == null) { + if (now == null) { + return null; + } + fabScale = new AnimationController(); + fabScale.duration(dart.core.Duration.of(0, 0, 0, 0, FAB_SEGUE_MS, 0)); + // Present from the start: a scaffold that opens WITH a button did not + // animate one in, and scaling the first frame up would be an entrance + // nobody asked for. + fabScale.value(1.0); + fabScale.addListener(new dart.runtime.Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + } + }); + } + if (now != null) { + lastFab = now; + if (fabScale.value().doubleValue() < 1.0) { + fabScale.forward(null); + } + } else if (lastFab != null) { + // Going: keep building the button that is leaving until it has finished + // leaving. Nothing else is holding it, so dropping it here is what made the + // exit instant. + if (fabScale.value().doubleValue() > 0.0) { + fabScale.reverse(null); + now = lastFab; + } else { + lastFab = null; + } + } + if (now == null) { + return null; + } + com.codename1.flutter.animation.ScaleTransition scaled = + new com.codename1.flutter.animation.ScaleTransition(); + scaled.scale(fabScale); + scaled.child(now); + return scaled; + } + + /** + * {@code Scaffold.persistentFooterButtons} as a row pinned above the bottom + * of the scaffold — Flutter aligns them to the end over a divider. + * + *

They were captured and never rendered, so the 2D-transformations demo + * lost its reset and edit controls along with the strip they sit on.

+ */ + private com.codename1.flutter.Widget footerWidget() { + dart.core.DartList buttons = + scaffold().getPersistentFooterButtons(); + if (buttons == null || buttons.isEmpty()) { + return null; + } + com.codename1.flutter.widgets.Row row = new com.codename1.flutter.widgets.Row(); + row.children(buttons); + row.mainAxisAlignment(com.codename1.flutter.MainAxisAlignment.end); + row.mainAxisSize(com.codename1.flutter.MainAxisSize.max); + com.codename1.flutter.widgets.Padding pad = new com.codename1.flutter.widgets.Padding(); + pad.padding(com.codename1.flutter.EdgeInsets.symmetric(8, 8)); + pad.child(row); + return pad; + } + + /** + * The body, with the top safe-area inset already spent when this scaffold + * has an app bar. + * + *

Flutter's Scaffold does the same. The app bar is what clears the notch, + * so anything below it — including a nested Scaffold with an app bar of its + * own — must not clear it a second time. The gallery nests exactly that way: + * a demo page's Scaffold sits in the body of the page's own Scaffold, and + * without this its bar would be pushed down by a notch that has already been + * accounted for.

+ */ + /** + * The body, under the ambient theme's body text style. + * + *

A Scaffold IS a Material in Flutter, and a Material is what establishes the + * default text style for what it contains. That matters under a NESTED theme: a + * {@code Theme} is an inherited widget and wraps nothing, so on its own it changes + * what {@code Theme.of} answers while the ambient text style stays as whoever built + * it last left it -- the application's.

+ * + *

Without this a page that installs a theme of its own took its sizes from that + * theme and everything else from the application's, because a Text merges its own + * style OVER the ambient one and overrides only the fields it sets. Measured on the + * typography demo, whose 96sp display role states no height and no family: it + * inherited the application's Montserrat at a line height of 1.43 -- a body role's + * height on a display role -- and its two wrapped lines sat 141 logical pixels apart + * against the reference's 115.

+ */ + private com.codename1.flutter.Widget underThemeTextStyle( + com.codename1.flutter.Widget body) { + com.codename1.flutter.TextStyle style = null; + try { + ThemeData theme = Theme.of(this); + style = theme == null || theme.textTheme() == null + ? null : theme.textTheme().bodyMedium(); + } catch (Throwable ignore) { + // A Scaffold outside any theme still has to render its body. + style = null; + } + return style == null ? body + : com.codename1.flutter.widgets.DefaultTextStyle.wrap(style, body); + } + + private com.codename1.flutter.Widget bodyWidget() { + com.codename1.flutter.Widget body = scaffold().getBody(); + if (body == null) { + return body; + } + body = underThemeTextStyle(body); + // Flutter's own rule for the body slot: the top padding goes when there + // is an app bar to stand in for it, and the BOTTOM padding goes when + // there is a bottom bar or a footer standing in for that. What is left + // reaches the body, and a scroll view inside it applies it along its own + // axis -- which is how a full-screen list keeps clear of the display + // cutout. Leaving the bottom padding in place under a bottom bar counted + // it twice and lengthened every such list by 34 logical pixels. + boolean removeTop = scaffold().getAppBar() != null; + boolean removeBottom = scaffold().getBottomNavigationBar() != null + || scaffold().getPersistentFooterButtons() != null; + if (!removeTop && !removeBottom) { + return body; + } + return com.codename1.flutter.MediaQuery.removePadding(this, Boolean.FALSE, + removeTop ? Boolean.TRUE : Boolean.FALSE, Boolean.FALSE, + removeBottom ? Boolean.TRUE : Boolean.FALSE, body); + } + + private void syncDrawer() { + if (rootMode && drawerHost != null) { + drawerChild = updateChild(drawerChild, scaffold().getDrawer(), 3); + return; + } + if (scaffold().getDrawer() != null && !warnedDrawer) { + warnedDrawer = true; + warn(rootMode + ? "Scaffold.drawer added after mount is ignored (M3 limitation)" + : "Scaffold.drawer is ignored on embedded Scaffolds (needs the Form Toolbar)"); + } + } + + private void syncBottomNav() { + if (rootMode && southHost == null) { + // decided at mount; a later-added bar can't be re-plumbed into + // the Form (M3 limitation) + if (scaffold().getBottomNavigationBar() != null && !warnedLateNav) { + warnedLateNav = true; + warn("Scaffold.bottomNavigationBar added after mount is ignored (M3 limitation)"); + } + return; + } + bottomNavChild = updateChild(bottomNavChild, scaffold().getBottomNavigationBar(), 4); + } + + private static void warn(String msg) { + try { + com.codename1.io.Log.p("Flutter runtime: " + msg); + } catch (Throwable t) { + // headless: Log has no storage backend + } + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (appBarChild != null) { + visitor.call(appBarChild); + } + if (bodyChild != null) { + visitor.call(bodyChild); + } + if (drawerChild != null) { + visitor.call(drawerChild); + } + if (bottomNavChild != null) { + visitor.call(bottomNavChild); + } + if (footerChild != null) { + visitor.call(footerChild); + } + // Visited last for the same reason it is mounted last: this order is + // the host's paint order, and the FAB floats over the bottom strip. + if (fabChild != null) { + visitor.call(fabChild); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double width = constraints.hasBoundedWidth() ? constraints.maxWidth() : 0; + double height = constraints.hasBoundedHeight() ? constraints.maxHeight() : 0; + + // App bar: only laid out here in embedded (strip) mode; in root mode + // it lives in the Toolbar's title host and the Form lays it out. + double appBarHeight = 0; + RenderElement appBarRender = renderOf(appBarChild); + if (appBarRender != null && !rootMode) { + Size as = appBarRender.layout(new BoxConstraints( + constraints.hasBoundedWidth() ? width : 0, + constraints.hasBoundedWidth() ? width : Double.POSITIVE_INFINITY, + 0, Double.POSITIVE_INFINITY)); + appBarHeight = as.height(); + setChildOffset(appBarRender, 0, 0); + width = Math.max(width, as.width()); + } + + // Bottom navigation strip: embedded mode only; in root mode it lives + // in the Form's SOUTH host. + double navHeight = 0; + // Computed before the body is laid out, because the body has to stop + // short of it too. + double bottomBand = coversTheDisplay(constraints.hasBoundedHeight() + ? new Size(0, constraints.maxHeight()) : null) + ? bottomSafeAreaPx() : 0; + RenderElement navRender = renderOf(bottomNavChild); + if (navRender != null && !rootMode) { + Size ns = navRender.layout(new BoxConstraints( + constraints.hasBoundedWidth() ? width : 0, + constraints.hasBoundedWidth() ? width : Double.POSITIVE_INFINITY, + 0, Double.POSITIVE_INFINITY)); + navHeight = ns.height(); + width = Math.max(width, ns.width()); + } + // The bar carries the display's inset itself (BottomAppBar.withBottomInset), so + // adding it here as well both double-counts it and outlives the bar: a bar that + // animates away left the band behind as a stripe of its own colour. The band is + // only ours to add when there is no bar to carry it. + if (navHeight > 0) { + bottomBand = 0; + } + + // Persistent footer buttons sit above the bottom strip. + double footerHeight = 0; + RenderElement footerRender = renderOf(footerChild); + if (footerRender != null) { + Size fs = footerRender.layout(new BoxConstraints( + constraints.hasBoundedWidth() ? width : 0, + constraints.hasBoundedWidth() ? width : Double.POSITIVE_INFINITY, + 0, Double.POSITIVE_INFINITY)); + footerHeight = fs.height(); + width = Math.max(width, fs.width()); + } + + // Body fills the remaining area. + RenderElement bodyRender = renderOf(bodyChild); + if (bodyRender != null) { + // extendBody: the body runs BEHIND the bottom bar instead of stopping above + // it, which is what lets a notch in that bar reveal the page underneath. The + // flag used to be discarded, so the mail study's notch came out opaque. + double bodyBottom = scaffold().getExtendBody() + ? 0 : navHeight + footerHeight + bottomBand; + BoxConstraints bodyConstraints; + if (constraints.hasBoundedWidth() && constraints.hasBoundedHeight()) { + bodyConstraints = BoxConstraints.tight(width, + Math.max(0, height - appBarHeight - bodyBottom)); + } else { + bodyConstraints = constraints.loosen().deflate( + com.codename1.flutter.EdgeInsets.only(0, appBarHeight, 0, bodyBottom)); + } + Size bs = bodyRender.layout(bodyConstraints); + setChildOffset(bodyRender, 0, appBarHeight); + width = Math.max(width, bs.width()); + height = Math.max(height, + appBarHeight + bs.height() + navHeight + footerHeight); + } + + Size self = constraints.constrain(new Size(width, height)); + + // The bottom strip sits above the display's own bottom padding, not + // flush with the screen. Flutter leaves that band to the scaffold's + // background: the reply study's bar is Material's 80 logical pixels with + // 34 of dark beneath it, which together read as one 114-tall bar. Laying + // the bar flush instead pushed the whole body 34 lower and left a white + // strip where the reference is dark -- the single largest wrong band on + // that route, 280 device pixels tall and 97% wrong. + // + // Only when this scaffold IS the display, which is the one thing that + // separates a full-screen study from a demo shown inside a card. The + // size says it, and by here the size is known. + if (navRender != null && !rootMode) { + // The strip OWNS the band: it is laid out that much taller and stays + // flush with the bottom edge, with its content held at the top. That + // is what the reference draws -- the reply study's bar reads as one + // 114 logical pixel block of colour whose Inbox row sits in the top + // 56 of it, not as an 80 tall bar floating above a gap. + navRender.layout(BoxConstraints.tight(self.width(), navHeight)); + setChildOffset(navRender, 0, Math.max(0, self.height() - navHeight)); + } + if (footerRender != null) { + footerRender.layout(BoxConstraints.tight(self.width(), footerHeight)); + setChildOffset(footerRender, 0, + Math.max(0, self.height() - navHeight - bottomBand - footerHeight)); + } + + RenderElement fabRender = renderOf(fabChild); + if (fabRender != null) { + Size fs = fabRender.layout(BoxConstraints.loose(self.width(), self.height())); + // Published for the bottom bar, which has to cut a notch for it. Flutter hands + // the same thing over as ScaffoldGeometry.floatingActionButtonArea. Read at + // PAINT time, not build time: the bar is built before this layout runs, so at + // build time there is nothing to read. + fabSize = fs; + setChildOffset(fabRender, + fabX(scaffold().getFloatingActionButtonLocation(), self.width(), fs.width()), + fabY(scaffold().getFloatingActionButtonLocation(), self.height(), + fs.height(), contentInset(navHeight + bottomBand, + bottomSafeAreaPx()))); + } + return self; + } + + /// The size the docked floating action button was last laid out at, in DEVICE + /// pixels, or null. + private Size fabSize; + + /// The nearest enclosing Scaffold's last floating-action-button size, or null. The + /// bottom bar needs it to carve its notch, and the bar is not a child of the button + /// -- they are two slots of the same Scaffold -- so it has to ask. + public static Size fabSizeOf(com.codename1.flutter.Element from) { + com.codename1.flutter.Element e = from; + while (e != null) { + if (e instanceof ScaffoldRenderElement) { + return ((ScaffoldRenderElement) e).fabSize; + } + e = e.parent(); + } + return null; + } + + /// Where the nearest enclosing Scaffold docks its floating action button. + public static FloatingActionButtonLocation fabLocationOf( + com.codename1.flutter.Element from) { + com.codename1.flutter.Element e = from; + while (e != null) { + if (e instanceof ScaffoldRenderElement) { + return ((ScaffoldRenderElement) e).scaffold().getFloatingActionButtonLocation(); + } + e = e.parent(); + } + return null; + } + + /** + * How far the content stops short of the bottom of the scaffold. + * + *

The bottom strip when there is one, and the display's own bottom + * padding when there is not. NOT both: a bottom bar is what holds the + * content off the edge, so adding the inset on top double-counts it. That + * lifted the reply study's docked button clear of its bar and into the mail + * list, where it disappeared behind a card.

+ */ + /** @see #contentInsetPx(double) */ + static double contentInset(double navHeight, double bottomSafeArea) { + return navHeight > 0 ? navHeight : bottomSafeArea; + } + + /// Whether this scaffold IS the display, which is what decides whether the + /// display's bottom padding is a band the scaffold owns below its bar. + private boolean coversTheDisplay(Size self) { + try { + if (self == null || self.height() <= 0) { + return false; + } + Size screen = com.codename1.flutter.MediaQuery.sizeOf(this); + return screen != null && Math.abs(Dp.px(screen.height()) - self.height()) < 2; + } catch (Throwable noMediaQuery) { + return false; + } + } + + /** + * The bottom safe-area inset in device pixels. + * + *

Flutter measures a floating action button from the bottom of the + * CONTENT, which excludes the display's own bottom padding -- the home + * indicator on this device. Measuring from the bottom of the scaffold + * instead put the starter study's button 102 device pixels lower than the + * reference's, sitting over the indicator rather than above it.

+ */ + private double bottomSafeAreaPx() { + try { + com.codename1.flutter.EdgeInsets p = com.codename1.flutter.MediaQuery.paddingOf(this); + return p == null ? 0 : Dp.px(p.bottom()); + } catch (Throwable noMediaQuery) { + return 0; + } + } + + /** + * Where the FAB sits horizontally, from its + * {@code FloatingActionButtonLocation} -- start, center or end, with + * Flutter's 16 logical pixel margin at either edge. + */ + static double fabX(FloatingActionButtonLocation where, double scaffoldWidth, double fabWidth) { + double margin = Dp.px(FAB_MARGIN_LP); + if (where != null && where.name().indexOf("enter") >= 0) { + return Math.max(0, (scaffoldWidth - fabWidth) / 2); + } + if (where != null && where.name().startsWith("start")) { + return margin; + } + if (where != null && where.name().startsWith("miniStart")) { + return margin; + } + // Flutter's default is endFloat. + return Math.max(0, scaffoldWidth - fabWidth - margin); + } + + /** + * Where the FAB sits vertically. + * + *

A FLOATING fab clears the bottom strip by Flutter's margin. A DOCKED + * one straddles the strip's top edge -- its centre sits exactly on it, + * which is what lets a notched BottomAppBar cut a hole for it. Reply's + * compose button is centreDocked, and with the location discarded it drew + * as an ordinary bottom-right float, in the corner, over the bar.

+ */ + static double fabY(FloatingActionButtonLocation where, double scaffoldHeight, + double fabHeight, double navHeight) { + double margin = Dp.px(FAB_MARGIN_LP); + String name = where == null ? "endFloat" : where.name(); + double contentBottom = scaffoldHeight - navHeight; + if (name.indexOf("Top") >= 0) { + return margin; + } + if (name.indexOf("Docked") >= 0) { + // Never below the screen, which is Flutter's own clamp. + return Math.max(0, Math.min(contentBottom - fabHeight / 2, + scaffoldHeight - fabHeight - margin)); + } + return Math.max(0, contentBottom - fabHeight - margin); + } + + /** + * The render element for one of our child slots, or null; children + * routed to another host (root-mode appBar/drawer/bottom bar) are + * excluded from this host's layout by the base class's host filter. + */ + private RenderElement renderOf(Element child) { + RenderElement r = findRenderElement(child); + if (r != null && r.host() != host()) { + return rootMode ? null : r; + } + return r; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldState.java new file mode 100644 index 00000000000..92fdc1a6ff7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ScaffoldState.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * The mutable state of a {@link Scaffold} ({@code ScaffoldState} in Flutter), + * reached via {@code Scaffold.of(context)}. Only the surface exercised by the + * gallery is modelled: showing a bottom sheet (returning a controller whose + * {@code closed} future the caller awaits), showing snack bars, and opening the + * drawers. Rendering of these is a later milestone; the methods keep the right + * shape so callers transpile and compile. + */ +public class ScaffoldState { + + /** + * Shows a persistent bottom sheet built by {@code builder}, returning a + * controller. The sheet is not mounted at this milestone; the controller's + * {@code closed} future completes immediately. + */ + public PersistentBottomSheetController showBottomSheet(Funcs.Func1 builder, + Double elevation, Color backgroundColor, Object shape, Clip clipBehavior, + Object constraints, Boolean enableDrag) { + return new PersistentBottomSheetController(); + } + + public void showSnackBar(SnackBar snackBar) { + } + + public void openDrawer() { + } + + public void openEndDrawer() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ShowValueIndicator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ShowValueIndicator.java new file mode 100644 index 00000000000..3dec187582c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ShowValueIndicator.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** When a slider's value-indicator bubble is shown — Flutter's {@code ShowValueIndicator}. */ +public enum ShowValueIndicator { + onlyForDiscrete, + onlyForContinuous, + always, + never +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialog.java new file mode 100644 index 00000000000..67c56b7760e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialog.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; + +/** + * A Material dialog presenting an optional {@code title} above a vertical list + * of option {@code children} (typically {@link SimpleDialogOption}s) — Flutter's + * {@code SimpleDialog}. This pass composes the title and options into a + * {@link Column}; dialog chrome (shape, elevation, inset padding) is recorded + * for API shape and rendered by the enclosing dialog host. + */ +public class SimpleDialog extends StatelessWidget { + + private Widget title; + private EdgeInsets titlePadding; + private TextStyle titleTextStyle; + private DartList children; + private EdgeInsets contentPadding; + private Color backgroundColor; + private Double elevation; + private Color shadowColor; + private Color surfaceTintColor; + private String semanticLabel; + private EdgeInsets insetPadding; + private Clip clipBehavior = Clip.none; + private Object shape; + private Object alignment; + + public void title(Widget v) { + this.title = v; + } + + public void titlePadding(EdgeInsets v) { + this.titlePadding = v; + } + + public void titleTextStyle(TextStyle v) { + this.titleTextStyle = v; + } + + public void children(DartList v) { + this.children = v; + } + + public void contentPadding(EdgeInsets v) { + this.contentPadding = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void shadowColor(Color v) { + this.shadowColor = v; + } + + public void surfaceTintColor(Color v) { + this.surfaceTintColor = v; + } + + public void semanticLabel(String v) { + this.semanticLabel = v; + } + + public void insetPadding(EdgeInsets v) { + this.insetPadding = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v == null ? Clip.none : v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public Widget getTitle() { + return title; + } + + public DartList getChildren() { + return children; + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (title != null) { + kids.add(title); + } + if (children != null) { + for (int i = 0; i < children.size(); i++) { + kids.add(children.get(i)); + } + } + Column col = new Column(); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialogOption.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialogOption.java new file mode 100644 index 00000000000..b8bcf24c6b0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SimpleDialogOption.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A single tappable option inside a {@link SimpleDialog} — Flutter's + * {@code SimpleDialogOption}. Tapping fires {@code onPressed} (conventionally to pop the + * dialog with a value). + * + *

The tap used to go nowhere, which made a SimpleDialog a list you could read and not + * answer. Padding follows Material's option metrics (16lp horizontal, 8lp vertical). + */ +public class SimpleDialogOption extends StatelessWidget { + + private Object onPressed; + private EdgeInsets padding; + private Widget child; + + public void onPressed(dart.runtime.Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getOnPressed() { + return onPressed; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + com.codename1.flutter.widgets.Padding pad = new com.codename1.flutter.widgets.Padding(); + pad.padding(padding != null ? padding : EdgeInsets.symmetric(8, 16)); + pad.child(child); + if (!(onPressed instanceof dart.runtime.Funcs.VoidFunc0)) { + return pad; + } + InkWell tap = new InkWell(); + tap.child(pad); + tap.onTap((dart.runtime.Funcs.VoidFunc0) onPressed); + return tap; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Slider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Slider.java new file mode 100644 index 00000000000..cc64a048a04 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Slider.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material slider over a double range with CONTROLLED semantics: drags + * fire {@code onChanged(newValue)} and the thumb snaps back to the widget's + * configured {@code value} until a rebuild moves it. Backed by a CN1 + * {@link com.codename1.ui.Slider} (UIID "FlutterSlider") whose int progress + * model the double range is scaled onto ({@code divisions} steps when given, + * a fine-grained default otherwise). + */ +public class Slider extends Widget { + + private double value; + private Double min; + private Double max; + private Long divisions; + private Funcs.VoidFunc1 onChanged; + private String label; + private Funcs.Func1 semanticFormatterCallback; + private com.codename1.flutter.Color activeColor; + private com.codename1.flutter.Color inactiveColor; + private Funcs.VoidFunc1 onChangeStart; + private Funcs.VoidFunc1 onChangeEnd; + + public void label(String v) { + this.label = v; + } + + public void semanticFormatterCallback(Funcs.Func1 v) { + this.semanticFormatterCallback = v; + } + + public void activeColor(com.codename1.flutter.Color v) { + this.activeColor = v; + } + + public void inactiveColor(com.codename1.flutter.Color v) { + this.inactiveColor = v; + } + + public void onChangeStart(Funcs.VoidFunc1 v) { + this.onChangeStart = v; + } + + public void onChangeEnd(Funcs.VoidFunc1 v) { + this.onChangeEnd = v; + } + + public void value(double v) { + this.value = v; + } + + public void min(double v) { + this.min = v; + } + + public void max(double v) { + this.max = v; + } + + public void divisions(long v) { + this.divisions = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public double getValue() { + return value; + } + + /** Flutter default: 0.0. */ + public double getMin() { + return min == null ? 0.0 : min; + } + + /** Flutter default: 1.0. */ + public double getMax() { + return max == null ? 1.0 : max; + } + + public Long getDivisions() { + return divisions; + } + + public Funcs.VoidFunc1 getOnChanged() { + return onChanged; + } + + public Funcs.VoidFunc1 getOnChangeStart() { + return onChangeStart; + } + + public Funcs.VoidFunc1 getOnChangeEnd() { + return onChangeEnd; + } + + @Override + public Element createElement() { + return new SliderRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderRenderElement.java new file mode 100644 index 00000000000..a2115d0f86a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderRenderElement.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.events.DataChangedListener; +import com.codename1.ui.geom.Dimension; + +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link Slider}: an editable CN1 + * {@link com.codename1.ui.Slider} (UIID "FlutterSlider"). The double range + * [min, max] is scaled onto CN1's int progress 0..steps where steps = + * divisions (when given) or {@link #DEFAULT_STEPS} for a continuous feel. + * Controlled: drags fire onChanged with the scaled double, then the progress + * snaps back to the configured value. Fills the available width, 44lp + * minimum height (Material tap-friendly track area). + */ +public class SliderRenderElement extends RenderElement { + + /** Steps used for a "continuous" slider (no divisions). */ + public static final long DEFAULT_STEPS = 1000; + /** Material slider interaction height in logical pixels. */ + public static final double MIN_HEIGHT_LP = 44; + /** Intrinsic width when the incoming width is unbounded. */ + public static final double DEFAULT_WIDTH_LP = 160; + + private boolean applying; + + public SliderRenderElement(Slider widget) { + super(widget); + } + + private Slider slider() { + return (Slider) widget(); + } + + /** + * The number of int steps the double range is scaled onto. + */ + public long steps() { + Long d = slider().getDivisions(); + return (d != null && d > 0) ? d : DEFAULT_STEPS; + } + + // ------------------------------------------------------------------ + // double <-> int scaling (pure, headless-testable) + // ------------------------------------------------------------------ + + /** + * Maps a double value in [min, max] to an int progress in [0, steps]. + */ + public static int progressFor(double value, double min, double max, long steps) { + if (max <= min || steps <= 0) { + return 0; + } + double clamped = Math.max(min, Math.min(max, value)); + return (int) Math.round((clamped - min) / (max - min) * steps); + } + + /** + * Maps an int progress in [0, steps] back to the double range. + */ + public static double valueFor(int progress, double min, double max, long steps) { + if (steps <= 0) { + return min; + } + int clamped = Math.max(0, Math.min((int) steps, progress)); + return min + (max - min) * clamped / steps; + } + + /** + * The int progress the current configuration maps to. + */ + public int configuredProgress() { + Slider w = slider(); + return progressFor(w.getValue(), w.getMin(), w.getMax(), steps()); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + com.codename1.ui.Slider s = new com.codename1.ui.Slider(); + s.setUIID("FlutterSlider"); + s.setEditable(true); + s.addDataChangedListener(new DataChangedListener() { + @Override + public void dataChanged(int type, int index) { + if (applying) { + return; + } + userDragged(index); + } + }); + // The interaction's bounds. onChangeStart and onChangeEnd were stored and never + // called, so code that pauses work as a drag begins, or commits the value on + // release, never ran. + s.addPointerPressedListener(new com.codename1.ui.events.ActionListener() { + @Override + public void actionPerformed(com.codename1.ui.events.ActionEvent evt) { + userDragStarted(); + } + }); + s.addPointerReleasedListener(new com.codename1.ui.events.ActionListener() { + @Override + public void actionPerformed(com.codename1.ui.events.ActionEvent evt) { + userDragEnded(); + } + }); + apply(s); + return s; + } + + @Override + protected void updateComponent(Component c) { + apply((com.codename1.ui.Slider) c); + } + + private void apply(com.codename1.ui.Slider s) { + applying = true; + try { + s.setMinValue(0); + s.setMaxValue((int) steps()); + s.setProgress(configuredProgress()); + // A Slider with no onChanged is DISABLED, and Material greys every + // part of it. Never set, our disabled sliders kept the accent and + // the demo's second slider was indistinguishable from its first. + s.setEnabled(slider().getOnChanged() != null); + applyColours(s); + } finally { + applying = false; + } + } + + /** + * Paints the track and thumb from the theme, directly on the component. + * + *

Not through theme properties: Codename One's Slider caches its + * {@code *Full} styles inside {@code setUIID}, which runs when the component + * is created -- before a Flutter ThemeData has been turned into properties. + * Set that way the colours were simply missed, and the demo's sliders came + * up with a grey active track and a navy thumb against the reference's + * accent purple.

+ */ + private void applyColours(com.codename1.ui.Slider s) { + try { + ColorScheme cs = Theme.of(this).colorScheme(); + if (cs == null || cs.primary() == null) { + return; + } + int accent = cs.primary().rgb(); + int track = cs.surfaceVariant() != null + ? cs.surfaceVariant().rgb() : cs.surface().rgb(); + // Inactive track and thumb. + s.getSliderEmptyUnselectedStyle().setBgColor(track); + s.getSliderEmptyUnselectedStyle().setBgTransparency(255); + s.getSliderEmptyUnselectedStyle().setFgColor(accent); + s.getSliderEmptySelectedStyle().setBgColor(track); + s.getSliderEmptySelectedStyle().setBgTransparency(255); + s.getSliderEmptySelectedStyle().setFgColor(accent); + // Active track. + s.getSliderFullSelectedStyle().setBgColor(accent); + s.getSliderFullSelectedStyle().setBgTransparency(255); + s.getSliderFullUnselectedStyle().setBgColor(accent); + s.getSliderFullUnselectedStyle().setBgTransparency(255); + } catch (Throwable noTheme) { + // an unthemed slider keeps the base look + } + } + + /** + * Controlled drag entry point (public so headless tests can drive it): + * fires onChanged with the progress scaled back to the double range, + * then re-applies the configured value. + */ + public void userDragged(int progress) { + Slider w = slider(); + Funcs.VoidFunc1 f = w.getOnChanged(); + if (f != null) { + double v = valueFor(progress, w.getMin(), w.getMax(), steps()); + lastDragValue = Double.valueOf(v); + f.call(v); + } + Component c = component(); + if (c != null) { + apply((com.codename1.ui.Slider) c); + } + } + + /** The last value handed to onChanged during the current interaction, or null. */ + private Double lastDragValue; + private boolean interacting; + + /** + * An interaction began: onChangeStart with the value the slider shows now, once. + * Only for an enabled slider (one with onChanged), as in Flutter. + */ + public void userDragStarted() { + Slider w = slider(); + if (interacting || w.getOnChanged() == null) { + return; + } + interacting = true; + lastDragValue = null; + Funcs.VoidFunc1 start = w.getOnChangeStart(); + if (start != null) { + start.call(w.getValue()); + } + } + + /** The interaction ended: onChangeEnd with the last value onChanged was given, once. */ + public void userDragEnded() { + if (!interacting) { + return; + } + interacting = false; + Slider w = slider(); + Funcs.VoidFunc1 end = w.getOnChangeEnd(); + if (end != null) { + end.call(lastDragValue != null ? lastDragValue.doubleValue() : w.getValue()); + } + lastDragValue = null; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Component c = component(); + double prefW = Dp.px(DEFAULT_WIDTH_LP); + double prefH = Dp.px(MIN_HEIGHT_LP); + if (c != null) { + Dimension d = c.getPreferredSize(); + prefW = Math.max(prefW, d.getWidth()); + prefH = Math.max(prefH, d.getHeight()); + } + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : prefW; + return constraints.constrain(new Size(w, prefH)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderTheme.java new file mode 100644 index 00000000000..fcff2ce9107 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderTheme.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.InheritedWidget; + +/** + * Establishes an ambient {@link SliderThemeData} for its subtree — Flutter's + * {@code SliderTheme}. Descendant {@code Slider}/{@code RangeSlider} widgets read + * {@code SliderTheme.of(context)} for their visual configuration. + * + *

It is a real {@link InheritedWidget}: {@code of} used to return a fresh default no + * matter what the tree said, so any slider styling in the app was silently discarded.

+ */ +public class SliderTheme extends InheritedWidget { + + private SliderThemeData data; + + public void data(SliderThemeData v) { + this.data = v; + } + + public SliderThemeData getData() { + return data; + } + + /** Dart's {@code SliderTheme.of(context)}: the nearest enclosing slider theme. */ + public static SliderThemeData of(BuildContext context) { + SliderTheme t = context == null ? null + : context.dependOnInheritedWidgetOfExactType(SliderTheme.class); + if (t != null && t.data != null) { + return t.data; + } + return new SliderThemeData(); + } + + @Override + public boolean updateShouldNotify(InheritedWidget oldWidget) { + return !(oldWidget instanceof SliderTheme) || ((SliderTheme) oldWidget).data != data; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderThemeData.java new file mode 100644 index 00000000000..7cacd6f0a20 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SliderThemeData.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * The visual configuration of a {@link Slider} / {@link RangeSlider} — + * Flutter's {@code SliderThemeData}. new_gallery's sliders demo derives a custom + * theme via {@code theme.sliderTheme.copyWith(...)} and reads back + * {@link #thumbColor()} / {@link #disabledThumbColor()} / + * {@link #valueIndicatorColor()}. + * + *

Shape parameters (thumb/track/tick-mark/value-indicator shapes) are held + * opaquely as {@code Object}; their {@code SliderComponentShape} / + * {@code RangeSliderThumbShape} base types are owned by the widget-extension + * category. Named constructor parameters map to same-named setters; + * {@link #copyWith} takes every parameter positionally in declaration order.

+ */ +public class SliderThemeData { + + private Double trackHeight; + private Color activeTrackColor; + private Color inactiveTrackColor; + private Color disabledActiveTrackColor; + private Color disabledInactiveTrackColor; + private Color activeTickMarkColor; + private Color inactiveTickMarkColor; + private Color disabledActiveTickMarkColor; + private Color disabledInactiveTickMarkColor; + private Color thumbColor; + private Color disabledThumbColor; + private Color overlayColor; + private Color valueIndicatorColor; + private Object overlayShape; + private Object tickMarkShape; + private Object thumbShape; + private Object trackShape; + private Object valueIndicatorShape; + private Object rangeThumbShape; + private Object rangeTrackShape; + private Object rangeTickMarkShape; + private Object rangeValueIndicatorShape; + private ShowValueIndicator showValueIndicator; + private TextStyle valueIndicatorTextStyle; + + public SliderThemeData() { + } + + // ------------------------------------------------------------------ + // Named-parameter setters + // ------------------------------------------------------------------ + + public void trackHeight(double v) { this.trackHeight = v; } + public void activeTrackColor(Color v) { this.activeTrackColor = v; } + public void inactiveTrackColor(Color v) { this.inactiveTrackColor = v; } + public void disabledActiveTrackColor(Color v) { this.disabledActiveTrackColor = v; } + public void disabledInactiveTrackColor(Color v) { this.disabledInactiveTrackColor = v; } + public void activeTickMarkColor(Color v) { this.activeTickMarkColor = v; } + public void inactiveTickMarkColor(Color v) { this.inactiveTickMarkColor = v; } + public void disabledActiveTickMarkColor(Color v) { this.disabledActiveTickMarkColor = v; } + public void disabledInactiveTickMarkColor(Color v) { this.disabledInactiveTickMarkColor = v; } + public void thumbColor(Color v) { this.thumbColor = v; } + public void disabledThumbColor(Color v) { this.disabledThumbColor = v; } + public void overlayColor(Color v) { this.overlayColor = v; } + public void valueIndicatorColor(Color v) { this.valueIndicatorColor = v; } + public void overlayShape(Object v) { this.overlayShape = v; } + public void tickMarkShape(Object v) { this.tickMarkShape = v; } + public void thumbShape(Object v) { this.thumbShape = v; } + public void trackShape(Object v) { this.trackShape = v; } + public void valueIndicatorShape(Object v) { this.valueIndicatorShape = v; } + public void rangeThumbShape(Object v) { this.rangeThumbShape = v; } + public void rangeTrackShape(Object v) { this.rangeTrackShape = v; } + public void rangeTickMarkShape(Object v) { this.rangeTickMarkShape = v; } + public void rangeValueIndicatorShape(Object v) { this.rangeValueIndicatorShape = v; } + public void showValueIndicator(ShowValueIndicator v) { this.showValueIndicator = v; } + public void valueIndicatorTextStyle(TextStyle v) { this.valueIndicatorTextStyle = v; } + + // ------------------------------------------------------------------ + // Getters + // ------------------------------------------------------------------ + + public Double trackHeight() { return trackHeight; } + public Color activeTrackColor() { return activeTrackColor; } + public Color inactiveTrackColor() { return inactiveTrackColor; } + public Color activeTickMarkColor() { return activeTickMarkColor; } + public Color inactiveTickMarkColor() { return inactiveTickMarkColor; } + public Color thumbColor() { return thumbColor; } + public Color disabledThumbColor() { return disabledThumbColor; } + public Color overlayColor() { return overlayColor; } + public Color valueIndicatorColor() { return valueIndicatorColor; } + public ShowValueIndicator showValueIndicator() { return showValueIndicator; } + public TextStyle valueIndicatorTextStyle() { return valueIndicatorTextStyle; } + + /** + * Returns a copy with the supplied fields overridden (null keeps current). + * Parameters are positional in Dart declaration order. + */ + public SliderThemeData copyWith( + Double trackHeight, Color activeTrackColor, Color inactiveTrackColor, + Color disabledActiveTrackColor, Color disabledInactiveTrackColor, + Color activeTickMarkColor, Color inactiveTickMarkColor, + Color disabledActiveTickMarkColor, Color disabledInactiveTickMarkColor, + Color thumbColor, Color disabledThumbColor, Color overlayColor, + Color valueIndicatorColor, + Object overlayShape, Object tickMarkShape, Object thumbShape, + Object trackShape, Object valueIndicatorShape, Object rangeThumbShape, + Object rangeTrackShape, Object rangeTickMarkShape, Object rangeValueIndicatorShape, + ShowValueIndicator showValueIndicator, TextStyle valueIndicatorTextStyle) { + SliderThemeData c = new SliderThemeData(); + c.trackHeight = trackHeight != null ? trackHeight : this.trackHeight; + c.activeTrackColor = activeTrackColor != null ? activeTrackColor : this.activeTrackColor; + c.inactiveTrackColor = inactiveTrackColor != null ? inactiveTrackColor : this.inactiveTrackColor; + c.disabledActiveTrackColor = disabledActiveTrackColor != null ? disabledActiveTrackColor : this.disabledActiveTrackColor; + c.disabledInactiveTrackColor = disabledInactiveTrackColor != null ? disabledInactiveTrackColor : this.disabledInactiveTrackColor; + c.activeTickMarkColor = activeTickMarkColor != null ? activeTickMarkColor : this.activeTickMarkColor; + c.inactiveTickMarkColor = inactiveTickMarkColor != null ? inactiveTickMarkColor : this.inactiveTickMarkColor; + c.disabledActiveTickMarkColor = disabledActiveTickMarkColor != null ? disabledActiveTickMarkColor : this.disabledActiveTickMarkColor; + c.disabledInactiveTickMarkColor = disabledInactiveTickMarkColor != null ? disabledInactiveTickMarkColor : this.disabledInactiveTickMarkColor; + c.thumbColor = thumbColor != null ? thumbColor : this.thumbColor; + c.disabledThumbColor = disabledThumbColor != null ? disabledThumbColor : this.disabledThumbColor; + c.overlayColor = overlayColor != null ? overlayColor : this.overlayColor; + c.valueIndicatorColor = valueIndicatorColor != null ? valueIndicatorColor : this.valueIndicatorColor; + c.overlayShape = overlayShape != null ? overlayShape : this.overlayShape; + c.tickMarkShape = tickMarkShape != null ? tickMarkShape : this.tickMarkShape; + c.thumbShape = thumbShape != null ? thumbShape : this.thumbShape; + c.trackShape = trackShape != null ? trackShape : this.trackShape; + c.valueIndicatorShape = valueIndicatorShape != null ? valueIndicatorShape : this.valueIndicatorShape; + c.rangeThumbShape = rangeThumbShape != null ? rangeThumbShape : this.rangeThumbShape; + c.rangeTrackShape = rangeTrackShape != null ? rangeTrackShape : this.rangeTrackShape; + c.rangeTickMarkShape = rangeTickMarkShape != null ? rangeTickMarkShape : this.rangeTickMarkShape; + c.rangeValueIndicatorShape = rangeValueIndicatorShape != null ? rangeValueIndicatorShape : this.rangeValueIndicatorShape; + c.showValueIndicator = showValueIndicator != null ? showValueIndicator : this.showValueIndicator; + c.valueIndicatorTextStyle = valueIndicatorTextStyle != null ? valueIndicatorTextStyle : this.valueIndicatorTextStyle; + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBar.java new file mode 100644 index 00000000000..1b25f061122 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBar.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.Duration; +import dart.core.UnsupportedError; + +/** + * A brief message shown at the bottom of the screen via + * {@link ScaffoldMessengerState#showSnackBar}. The content widget is + * CONSUMED as configuration (a {@code Text} child becomes the message + * string, like button labels) — a SnackBar is never mounted as an element, + * so {@link #createElement()} is unsupported. + */ +public class SnackBar extends Widget { + + /** Flutter's default SnackBar duration: 4 seconds. */ + public static final long DEFAULT_DURATION_MS = 4000; + + private Widget content; + private Duration duration; + private SnackBarAction action; + private SnackBarBehavior behavior; + private com.codename1.flutter.Color backgroundColor; + + public void action(SnackBarAction v) { + this.action = v; + } + + public void behavior(SnackBarBehavior v) { + this.behavior = v; + } + + public void backgroundColor(com.codename1.flutter.Color v) { + this.backgroundColor = v; + } + + public SnackBarAction getAction() { + return action; + } + + public void content(Widget v) { + this.content = v; + } + + public void duration(Duration v) { + this.duration = v; + } + + public Widget getContent() { + return content; + } + + public Duration getDuration() { + return duration; + } + + /** + * The effective display time in milliseconds. + */ + public long durationMillis() { + return duration == null ? DEFAULT_DURATION_MS : duration.inMilliseconds(); + } + + @Override + public Element createElement() { + throw new UnsupportedError("SnackBar is consumed by ScaffoldMessengerState.showSnackBar, not mounted"); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarAction.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarAction.java new file mode 100644 index 00000000000..66162f76c5c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarAction.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +import dart.runtime.Funcs; + +/** + * An action button shown alongside a {@link SnackBar}'s content — Flutter's + * {@code SnackBarAction}. Configuration only (label + press callback); the + * {@link SnackBar} consumes it when shown. + */ +public class SnackBarAction { + + private String label; + private Funcs.VoidFunc0 onPressed; + private Color textColor; + private Color disabledTextColor; + + public void label(String v) { + this.label = v; + } + + public void onPressed(Funcs.VoidFunc0 v) { + this.onPressed = v; + } + + public void textColor(Color v) { + this.textColor = v; + } + + public void disabledTextColor(Color v) { + this.disabledTextColor = v; + } + + public String getLabel() { + return label; + } + + public String label() { + return label; + } + + public Funcs.VoidFunc0 getOnPressed() { + return onPressed; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarBehavior.java new file mode 100644 index 00000000000..248509e797b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarBehavior.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Whether a snack bar is docked to the bottom edge ({@link #fixed}) or floats + * above it ({@link #floating}), mirroring Flutter's {@code SnackBarBehavior}. + */ +public enum SnackBarBehavior { + fixed, floating +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarThemeData.java new file mode 100644 index 00000000000..26170e28645 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SnackBarThemeData.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code SnackBarThemeData}: write-once snack-bar styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class SnackBarThemeData { + + private Color backgroundColor; + private Color actionTextColor; + private Color disabledActionTextColor; + private TextStyle contentTextStyle; + private Double elevation; + private Object shape; + private SnackBarBehavior behavior; + private Double width; + private Object insetPadding; + private Boolean showCloseIcon; + private Color closeIconColor; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void actionTextColor(Color v) { + this.actionTextColor = v; + } + + public void disabledActionTextColor(Color v) { + this.disabledActionTextColor = v; + } + + public void contentTextStyle(TextStyle v) { + this.contentTextStyle = v; + } + + public void elevation(double v) { + this.elevation = v; + } + + public void shape(Object v) { + this.shape = v; + } + + public void behavior(SnackBarBehavior v) { + this.behavior = v; + } + + public void width(double v) { + this.width = v; + } + + public void insetPadding(Object v) { + this.insetPadding = v; + } + + public void showCloseIcon(boolean v) { + this.showCloseIcon = v; + } + + public void closeIconColor(Color v) { + this.closeIconColor = v; + } + + public Color backgroundColor() { + return backgroundColor; + } + + public SnackBarBehavior behavior() { + return behavior; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StandardComponentType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StandardComponentType.java new file mode 100644 index 00000000000..c8b35b0a064 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StandardComponentType.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Key; +import com.codename1.flutter.ValueKey; + +/** + * One of the standard, individually-keyed components a scaffold builds (the + * back / close / drawer / more buttons) — Flutter's {@code StandardComponentType}. + * Each value exposes a stable {@link #key()} the app can target for tests or + * theming. + */ +public final class StandardComponentType { + + public static final StandardComponentType backButton = + new StandardComponentType("backButton"); + public static final StandardComponentType closeButton = + new StandardComponentType("closeButton"); + public static final StandardComponentType drawerButton = + new StandardComponentType("drawerButton"); + public static final StandardComponentType moreButton = + new StandardComponentType("moreButton"); + + private final Key key; + + private StandardComponentType(String name) { + this.key = new ValueKey("StandardComponentType." + name); + } + + /** The stable key identifying this component in the widget tree. */ + public Key key() { + return key; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Step.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Step.java new file mode 100644 index 00000000000..9bcb3a073d2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Step.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; + +/** + * One step of a {@link Stepper} — Flutter's {@code Step}. A configuration + * object holding the step's {@code title}, optional {@code subtitle} and + * {@code content}. + */ +public class Step { + + private Widget title; + private Widget subtitle; + private Widget content; + private Object state; + private boolean isActive; + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void content(Widget v) { + this.content = v; + } + + public void state(Object v) { + this.state = v; + } + + public void isActive(boolean v) { + this.isActive = v; + } + + public void stepStyle(Object v) { + } + + public Widget getTitle() { + return title; + } + + public Widget getSubtitle() { + return subtitle; + } + + public Widget getContent() { + return content; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepState.java new file mode 100644 index 00000000000..f7bff39bedc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepState.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The state of a {@link Step} in a {@link Stepper} — Flutter's + * {@code StepState}. + */ +public enum StepState { + indexed, editing, complete, disabled, error +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Stepper.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Stepper.java new file mode 100644 index 00000000000..784ea25ec12 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Stepper.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A vertical (or horizontal) sequence of {@link Step}s — Flutter's + * {@code Stepper}. This milestone renders every step's title followed by its + * content in a {@link Column}; the collapse-to-current-step behavior and the + * continue/cancel controls are deferred. + */ +public class Stepper extends StatelessWidget { + + private DartList steps; + private StepperType type; + private long currentStep; + private Funcs.VoidFunc1 onStepTapped; + private Funcs.VoidFunc0 onStepContinue; + private Funcs.VoidFunc0 onStepCancel; + + public void steps(DartList v) { + this.steps = v; + } + + public void physics(Object v) { + } + + public void type(StepperType v) { + this.type = v; + } + + public void currentStep(long v) { + this.currentStep = v; + } + + public void onStepTapped(Funcs.VoidFunc1 v) { + this.onStepTapped = v; + } + + public void onStepContinue(Funcs.VoidFunc0 v) { + this.onStepContinue = v; + } + + public void onStepCancel(Funcs.VoidFunc0 v) { + this.onStepCancel = v; + } + + public void controlsBuilder(Object v) { + } + + public void elevation(double v) { + } + + public void margin(Object v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (steps != null) { + for (int i = 0; i < steps.size(); i++) { + Step s = steps.get(i); + if (s.getTitle() != null) { + kids.add(s.getTitle()); + } + if (s.getSubtitle() != null) { + kids.add(s.getSubtitle()); + } + if (s.getContent() != null) { + kids.add(s.getContent()); + } + } + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepperType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepperType.java new file mode 100644 index 00000000000..fdac6ba4291 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/StepperType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Whether a {@link Stepper} lays its steps out vertically or horizontally — + * Flutter's {@code StepperType}. + */ +public enum StepperType { + vertical, horizontal +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Switch.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Switch.java new file mode 100644 index 00000000000..ffa98ce693b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Switch.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A material switch with CONTROLLED semantics (see {@link Checkbox}): a user + * toggle fires {@code onChanged(newValue)} and the component snaps back to + * the widget's configured value until a rebuild moves it. Backed by a CN1 + * {@link com.codename1.components.Switch} (UIID "FlutterSwitch"). + */ +public class Switch extends Widget { + + private boolean value; + private Funcs.VoidFunc1 onChanged; + + public void value(boolean v) { + this.value = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + /** The color of the track/thumb when the switch is on — Flutter's {@code activeColor}. */ + public void activeColor(com.codename1.flutter.Color v) { + } + + public boolean getValue() { + return value; + } + + public Funcs.VoidFunc1 getOnChanged() { + return onChanged; + } + + @Override + public Element createElement() { + return new SwitchRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchListTile.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchListTile.java new file mode 100644 index 00000000000..dec7efcec4a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchListTile.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A {@link ListTile} whose trailing (or leading) control is a {@link Switch} — + * Flutter's {@code SwitchListTile}. Tapping the row toggles the switch, firing + * {@code onChanged(newValue)} with CONTROLLED semantics (see {@link Switch}). + * Composed as a ListTile hosting the switch. + */ +public class SwitchListTile extends StatelessWidget { + + private boolean value; + private Funcs.VoidFunc1 onChanged; + private Widget title; + private Widget subtitle; + private Widget secondary; + + public void value(boolean v) { + this.value = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void secondary(Widget v) { + this.secondary = v; + } + + public void isThreeLine(boolean v) { + } + + public void selected(boolean v) { + } + + public void dense(boolean v) { + } + + private Object controlAffinity; + + public void controlAffinity(Object v) { + this.controlAffinity = v; + } + + public void activeColor(Object v) { + } + + public void contentPadding(Object v) { + } + + @Override + public Widget build(BuildContext context) { + Switch sw = new Switch(); + sw.value(value); + sw.onChanged(onChanged); + + ListTile tile = new ListTile(); + if (title != null) { + tile.title(title); + } + if (subtitle != null) { + tile.subtitle(subtitle); + } + // A switch trails by default; see ListTileControlAffinity. + if (ListTileControlAffinity.isTrailing(controlAffinity, true)) { + if (secondary != null) { + tile.leading(secondary); + } + tile.trailing(sw); + } else { + tile.leading(sw); + if (secondary != null) { + tile.trailing(secondary); + } + } + return tile; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchRenderElement.java new file mode 100644 index 00000000000..7a19555fcdb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchRenderElement.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.geom.Dimension; + +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link Switch}: a CN1 + * {@link com.codename1.components.Switch} constructed with the + * "FlutterSwitch" UIID, controlled like {@link CheckboxRenderElement}. The + * action listener (user interaction only — programmatic setValue fires + * change, not action events) reports the flip and snaps back. + * Intrinsic size: the M3 switch track 52x32lp minimum. + */ +public class SwitchRenderElement extends RenderElement { + + /** M3 switch track width in logical pixels. */ + public static final double TRACK_WIDTH_LP = 52; + /** M3 switch track height in logical pixels. */ + public static final double TRACK_HEIGHT_LP = 32; + + private boolean applying; + + public SwitchRenderElement(Switch widget) { + super(widget); + } + + private Switch switchWidget() { + return (Switch) widget(); + } + + /** + * The value the current widget configuration mandates. + */ + public boolean configuredValue() { + return switchWidget().getValue(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + com.codename1.components.Switch sw = new com.codename1.components.Switch("FlutterSwitch"); + sw.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (applying) { + return; + } + userToggled(((com.codename1.components.Switch) component()).isValue()); + } + }); + apply(sw); + return sw; + } + + @Override + protected void updateComponent(Component c) { + apply((com.codename1.components.Switch) c); + } + + private void apply(com.codename1.components.Switch sw) { + applying = true; + try { + sw.setValue(configuredValue()); + // A null onChanged is Flutter's disabled switch; the checkbox and button + // renderers already mirror that, and the switch kept enabled styling, + // focus and pointer behaviour. + sw.setEnabled(switchWidget().getOnChanged() != null); + } finally { + applying = false; + } + } + + /** + * Controlled toggle entry point (public so headless tests can drive it): + * fires onChanged with the attempted value, then re-applies the widget's + * configured value. + */ + public void userToggled(boolean attemptedValue) { + Funcs.VoidFunc1 f = switchWidget().getOnChanged(); + if (f != null) { + f.call(attemptedValue); + } + Component c = component(); + if (c != null) { + apply((com.codename1.components.Switch) c); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double w = Dp.px(TRACK_WIDTH_LP); + double h = Dp.px(TRACK_HEIGHT_LP); + Component c = component(); + if (c != null) { + Dimension d = c.getPreferredSize(); + w = Math.max(w, d.getWidth()); + h = Math.max(h, d.getHeight()); + } + return constraints.constrain(new Size(w, h)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchThemeData.java new file mode 100644 index 00000000000..98fe31df82a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/SwitchThemeData.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Material {@code SwitchThemeData}: write-once switch styling. The values are + * {@link MaterialStateProperty}/density/cursor objects owned by other runtime + * areas, so they are held opaquely as {@code Object} in this pass. Named Dart + * constructor parameters map to setter methods. + */ +public class SwitchThemeData { + + private Object thumbColor; + private Object trackColor; + private Object trackOutlineColor; + private Object overlayColor; + private Object splashRadius; + private Object materialTapTargetSize; + private Object thumbIcon; + private Object mouseCursor; + + public void thumbColor(Object v) { + this.thumbColor = v; + } + + public void trackColor(Object v) { + this.trackColor = v; + } + + public void trackOutlineColor(Object v) { + this.trackOutlineColor = v; + } + + public void overlayColor(Object v) { + this.overlayColor = v; + } + + public void splashRadius(Object v) { + this.splashRadius = v; + } + + public void materialTapTargetSize(Object v) { + this.materialTapTargetSize = v; + } + + public void thumbIcon(Object v) { + this.thumbIcon = v; + } + + public void mouseCursor(Object v) { + this.mouseCursor = v; + } + + public Object getThumbColor() { + return thumbColor; + } + + public Object getTrackColor() { + return trackColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tab.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tab.java new file mode 100644 index 00000000000..a788c0d1730 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tab.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.SizedBox; +import com.codename1.flutter.widgets.Text; + +import dart.core.DartList; + +/** + * A single tab label for a {@link TabBar} — Flutter's {@code Tab}. Composes its + * {@code text} (and/or {@code icon}) as the visible content; an explicit + * {@code child} overrides both. + */ +public class Tab extends StatelessWidget { + + private String text; + private Widget icon; + private Object iconMargin; + private Double height; + private Widget child; + + public void text(String v) { + this.text = v; + } + + public void icon(Widget v) { + this.icon = v; + } + + public void iconMargin(Object v) { + this.iconMargin = v; + } + + public void height(double v) { + this.height = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + if (child != null) { + return child; + } + Widget label = text != null ? new Text(text) : null; + if (icon != null && label != null) { + DartList kids = new DartList(); + kids.add(icon); + kids.add(label); + Column col = new Column(); + col.children(kids); + return col; + } + if (icon != null) { + return icon; + } + if (label != null) { + return label; + } + return new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBar.java new file mode 100644 index 00000000000..b3cc741e657 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBar.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.MainAxisAlignment; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.EdgeInsetsGeometry; +import com.codename1.flutter.widgets.DefaultTextStyle; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SingleChildScrollView; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A horizontal row of tabs — Flutter's {@code TabBar}. Renders the {@code tabs} + * as an evenly-spaced (or, when {@code isScrollable}, horizontally scrollable) + * {@link Row}. Selection tinting, the sliding indicator and gesture-driven tab + * switching via the {@link TabController} are deferred; the labels render. + */ +public class TabBar extends StatelessWidget { + + private DartList tabs; + private TabController controller; + private boolean isScrollable; + private Color labelColor; + private Color unselectedLabelColor; + private TextStyle labelStyle; + private EdgeInsetsGeometry labelPadding; + private TextStyle unselectedLabelStyle; + private Funcs.VoidFunc1 onTap; + + public void tabs(DartList v) { + this.tabs = v; + } + + public void controller(TabController v) { + this.controller = v; + } + + public void isScrollable(boolean v) { + this.isScrollable = v; + } + + public void indicatorColor(Color v) { + } + + public void indicatorWeight(double v) { + } + + public void indicatorPadding(Object v) { + } + + public void indicator(Object v) { + } + + public void indicatorSize(Object v) { + } + + public void labelColor(Color v) { + this.labelColor = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void labelPadding(Object v) { + this.labelPadding = v instanceof EdgeInsetsGeometry ? (EdgeInsetsGeometry) v : null; + } + + public void unselectedLabelColor(Color v) { + this.unselectedLabelColor = v; + } + + public void unselectedLabelStyle(TextStyle v) { + this.unselectedLabelStyle = v; + } + + public void padding(Object v) { + } + + public void dragStartBehavior(Object v) { + } + + public void mouseCursor(Object v) { + } + + public void enableFeedback(boolean v) { + } + + public void physics(Object v) { + } + + public void onTap(Funcs.VoidFunc1 v) { + this.onTap = v; + } + + /** Flutter's {@code _kTabHeight}: the height of a text-only tab bar. */ + public static final double TAB_HEIGHT_LP = 46; + + @Override + public Widget build(BuildContext context) { + Row row = new Row(); + // A scrollable bar packs its tabs from the start and lets the row + // overflow; a fixed one shares the width between them. + row.mainAxisAlignment(isScrollable + ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween); + row.children(styledTabs()); + Widget content = row; + if (isScrollable) { + SingleChildScrollView sv = new SingleChildScrollView(); + sv.scrollDirection(com.codename1.flutter.Axis.horizontal); + sv.child(row); + content = sv; + } + // A tab bar has a FIXED height — Flutter declares it as a + // PreferredSizeWidget for exactly this reason. Without it a scrollable + // bar is greedy: offered the whole app bar it took all of it, leaving + // the toolbar row zero pixels tall, so the colors demo lost its title + // and stacked its tab strip over the top of the bar. + com.codename1.flutter.widgets.SizedBox box = + new com.codename1.flutter.widgets.SizedBox(); + box.height(TAB_HEIGHT_LP); + box.child(content); + return box; + } + + /** Flutter's {@code kTabLabelPadding}, used when the bar names none. */ + private static final double DEFAULT_LABEL_H_PADDING_LP = 16; + + /** + * The tabs, each padded and coloured the way Flutter's TabBar does it. + * + *

Flutter pads every tab with {@code labelPadding} and styles it through + * the surrounding text style: {@code labelStyle}/{@code labelColor} for the + * selected tab and {@code unselectedLabelStyle}/{@code unselectedLabelColor} + * for the rest. Dropping the padding ran the labels together -- Crane's + * three tabs rendered as one word, "FLYSLEEPEAT" -- and dropping the colour + * painted them in the default ink on a bar whose whole point was that they + * are white.

+ */ + private DartList styledTabs() { + DartList out = new DartList(); + if (tabs == null) { + return out; + } + long selected = controller == null ? 0 : controller.index(); + EdgeInsetsGeometry pad = labelPadding != null ? labelPadding + : EdgeInsets.symmetric(DEFAULT_LABEL_H_PADDING_LP, 0); + for (int i = 0; i < tabs.size(); i++) { + Widget tab = tabs.get(i); + if (tab == null) { + continue; + } + boolean isSelected = i == selected; + TextStyle base = isSelected ? labelStyle + : (unselectedLabelStyle != null ? unselectedLabelStyle : labelStyle); + Color ink = isSelected ? labelColor + : (unselectedLabelColor != null ? unselectedLabelColor : labelColor); + Widget styled = tab; + if (base != null || ink != null) { + TextStyle style = new TextStyle(); + if (base != null) { + style = style.merge(base); + } + if (ink != null) { + style.color(ink); + } + styled = DefaultTextStyle.wrap(style, tab); + } + Padding p = new Padding(); + p.padding(pad); + p.child(styled); + out.add(p); + } + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarTheme.java new file mode 100644 index 00000000000..5b8e1c560a7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarTheme.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code TabBarTheme}: write-once tab-bar styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class TabBarTheme { + + private Color indicatorColor; + private Color labelColor; + private Color unselectedLabelColor; + private TextStyle labelStyle; + private TextStyle unselectedLabelStyle; + private Object indicator; + private Object indicatorSize; + private Object labelPadding; + private Object overlayColor; + private Object dividerColor; + + public void indicatorColor(Color v) { + this.indicatorColor = v; + } + + public void labelColor(Color v) { + this.labelColor = v; + } + + public void unselectedLabelColor(Color v) { + this.unselectedLabelColor = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void unselectedLabelStyle(TextStyle v) { + this.unselectedLabelStyle = v; + } + + public void indicator(Object v) { + this.indicator = v; + } + + public void indicatorSize(Object v) { + this.indicatorSize = v; + } + + public void labelPadding(Object v) { + this.labelPadding = v; + } + + public void overlayColor(Object v) { + this.overlayColor = v; + } + + public void dividerColor(Object v) { + this.dividerColor = v; + } + + public Color labelColor() { + return labelColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarThemeData.java new file mode 100644 index 00000000000..418f726024b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarThemeData.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material {@code TabBarThemeData}: the Material-3 rename of {@link TabBarTheme}; + * same write-once tab-bar styling shape. + */ +public class TabBarThemeData { + + private Color indicatorColor; + private Color labelColor; + private Color unselectedLabelColor; + private TextStyle labelStyle; + private TextStyle unselectedLabelStyle; + private Object indicator; + private Object indicatorSize; + private Object labelPadding; + private Object overlayColor; + private Object dividerColor; + + public void indicatorColor(Color v) { + this.indicatorColor = v; + } + + public void labelColor(Color v) { + this.labelColor = v; + } + + public void unselectedLabelColor(Color v) { + this.unselectedLabelColor = v; + } + + public void labelStyle(TextStyle v) { + this.labelStyle = v; + } + + public void unselectedLabelStyle(TextStyle v) { + this.unselectedLabelStyle = v; + } + + public void indicator(Object v) { + this.indicator = v; + } + + public void indicatorSize(Object v) { + this.indicatorSize = v; + } + + public void labelPadding(Object v) { + this.labelPadding = v; + } + + public void overlayColor(Object v) { + this.overlayColor = v; + } + + public void dividerColor(Object v) { + this.dividerColor = v; + } + + public Color labelColor() { + return labelColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarView.java new file mode 100644 index 00000000000..93d5aa759b5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabBarView.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; + +/** + * The page view paired with a {@link TabBar} — Flutter's {@code TabBarView}. + * Shows the child at the {@link TabController}'s current index (index 0 when no + * controller is attached). The horizontal swipe transition between pages is + * deferred; the selected page renders. + */ +public class TabBarView extends StatelessWidget { + + private DartList children; + private TabController controller; + + public void children(DartList v) { + this.children = v; + } + + public void controller(TabController v) { + this.controller = v; + } + + public void physics(Object v) { + } + + public void dragStartBehavior(Object v) { + } + + public void viewportFraction(double v) { + } + + public void clipBehavior(Clip v) { + } + + @Override + public Widget build(BuildContext context) { + if (children == null || children.size() == 0) { + return new SizedBox(); + } + int idx = controller != null ? (int) controller.index() : 0; + if (idx < 0 || idx >= children.size()) { + idx = 0; + } + return children.get(idx); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabController.java new file mode 100644 index 00000000000..54fa0f7d11e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TabController.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.animation.Animation; +import com.codename1.flutter.animation.AnimationController; +import com.codename1.flutter.animation.TickerProvider; +import com.codename1.flutter.foundation.ChangeNotifier; + +import dart.core.Duration; + +/** + * Coordinates tab selection between a {@link TabBar} and a {@link TabBarView} — + * Flutter's {@code TabController}. Holds the selected {@code index} over a fixed + * {@code length}, notifies listeners on change (it is a {@link ChangeNotifier}), + * and exposes an {@link Animation} whose value tracks the selected index + * (0..length-1) so index-driven animations resolve. Tab-change gestures and the + * cross-fade flight are not yet wired; {@link #animateTo} sets the index + * immediately. + */ +public class TabController implements ChangeNotifier { + + /** Held here, not in the shared map, so this goes when the object does (see ChangeNotifier). */ + private final java.util.List changeNotifierListeners = + new java.util.ArrayList(); + + @Override + public java.util.List changeNotifierListeners$() { + return changeNotifierListeners; + } + + + private long length; + private long index; + private long previousIndex; + private final AnimationController controller = new AnimationController(); + + public TabController() { + controller.lowerBound(0.0); + controller.upperBound(Double.MAX_VALUE); + } + + // Named-parameter setters. + + public void length(long v) { + this.length = v; + controller.upperBound(v <= 1 ? 1.0 : (double) (v - 1)); + } + + public void initialIndex(long v) { + this.index = v; + this.previousIndex = v; + controller.value((double) v); + } + + public void animationDuration(Duration v) { + if (v != null) { + controller.duration(v); + } + } + + public void vsync(TickerProvider v) { + // self-driven; provider unused + } + + // Dart getters / setters. + + public long index() { + return index; + } + + public void index(long v) { + if (v == index) { + return; + } + previousIndex = index; + index = v; + controller.value((double) v); + notifyListeners(); + } + + public long length() { + return length; + } + + public long previousIndex() { + return previousIndex; + } + + public boolean indexIsChanging() { + return false; + } + + public double offset() { + return 0.0; + } + + public Animation animation() { + return controller; + } + + public void animateTo(long value, Duration duration, com.codename1.flutter.animation.Curve curve) { + index(value); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextButton.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextButton.java new file mode 100644 index 00000000000..3905bfa88d8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextButton.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The material text button: borderless primary-colored text, backed by a + * CN1 Button (UIID "FlutterTextButton"). + */ +public class TextButton extends ButtonBase { + + /** + * Builds a {@link ButtonStyle} to hand to a TextButton's {@code style:} + * parameter. Parameter order matches the Dart stub. + */ + public static ButtonStyle styleFrom(com.codename1.flutter.Color foregroundColor, + com.codename1.flutter.Color backgroundColor, com.codename1.flutter.Color shadowColor, + Double elevation, com.codename1.flutter.TextStyle textStyle, + com.codename1.flutter.EdgeInsets padding, Object side, Object shape, Object alignment, + Object tapTargetSize, Object visualDensity) { + return ButtonStyle.styleFrom(foregroundColor, backgroundColor, shadowColor, elevation, textStyle, + padding, side, shape, alignment, tapTargetSize, visualDensity); + } + + /** + * {@code TextButton.icon}: a button whose content is an icon followed by a + * label. This milestone consumes the label as the button content (the + * leading icon is used when no label is supplied); a later pass composes + * both into a Row. + */ + public static TextButton icon(com.codename1.flutter.Key key, + dart.runtime.Funcs.VoidFunc0 onPressed, ButtonStyle style, + com.codename1.flutter.Widget icon, com.codename1.flutter.Widget label) { + TextButton b = new TextButton(); + b.key(key); + b.onPressed(onPressed); + b.style(style); + b.child(label != null ? label : icon); + if (label != null) { + b.leadingIcon(icon); + } + return b; + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextEditingController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextEditingController.java new file mode 100644 index 00000000000..2fff286ff9b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextEditingController.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * A controller for an editable text field. The controller and the CN1 text + * component are kept in two-way sync by {@link TextFieldRenderElement}: + * user edits flow into {@link #text()} (and notify listeners), while + * {@link #setText(String)}/{@link #clear()} push into the mounted component. + * + *

Transpiler surface: the Dart constructor's named {@code text:} parameter + * becomes the {@link #text(String)} setter, the Dart {@code text} getter + * becomes {@link #text()}, and Dart {@code controller.text = v} assignments + * are emitted as the explicit {@link #setText(String)} method.

+ */ +public class TextEditingController { + + private String value = ""; + private final List listeners = new ArrayList(); + /** + * Every mounted field this controller drives. It was one reference, replaced by each + * bind, so with a controller shared between fields a programmatic setText reached + * only the last one, and when that one unmounted the controller forgot the others. + */ + private final List bound = new ArrayList(); + + /** The most recently bound field that is still mounted, or null. */ + private TextFieldRenderElement liveField() { + for (int i = bound.size() - 1; i >= 0; i--) { + if (bound.get(i).isMounted()) { + return bound.get(i); + } + } + return null; + } + + public TextEditingController() { + } + + /** + * Named parameter setter for the Dart {@code text:} constructor parameter + * (initial value, no listener notification). + */ + public void text(String v) { + this.value = v == null ? "" : v; + } + + /** + * The current text. When a mounted TextField is bound this reads the + * component's live text. + */ + public String text() { + TextFieldRenderElement live = liveField(); + if (live != null) { + String s = live.componentText(); + if (s != null) { + value = s; + } + } + return value; + } + + /** + * The value this controller holds, WITHOUT consulting the component. + * + *

{@link #text()} prefers the bound component's live text, which is + * right once the field is showing the value and wrong before it ever + * received one: reading it during the first apply overwrote the initial + * text with the empty component's, so a controller built as + * {@code TextEditingController(text: '25')} left the field blank and threw + * the 25 away. The sliders demo's editable value is exactly that.

+ */ + String rawValue() { + return value; + } + + /** + * Imperative setter (Dart {@code controller.text = v}): updates the bound + * component when mounted and notifies listeners. + */ + public void setText(String v) { + this.value = v == null ? "" : v; + pushToFields(null); + notifyListeners(); + } + + public void clear() { + setText(""); + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.remove(listener); + } + } + + /** + * Dart's {@code ChangeNotifier.dispose}: drops all listeners and unbinds + * from any mounted component. Idempotent. + */ + public void dispose() { + listeners.clear(); + bound.clear(); + } + + /** Shows the current value in every mounted field except {@code source}. */ + private void pushToFields(TextFieldRenderElement source) { + for (TextFieldRenderElement e : new ArrayList(bound)) { + if (e != source && e.isMounted()) { + e.applyControllerText(this.value); + } + } + } + + // ------------------------------------------------------------------ + // Framework plumbing (package private) + // ------------------------------------------------------------------ + + void bind(TextFieldRenderElement e) { + if (e != null && !bound.contains(e)) { + bound.add(e); + } + } + + void unbind(TextFieldRenderElement e) { + bound.remove(e); + } + + /** + * A user edit arrived from the component: absorb it (no push-back) and + * notify listeners. + */ + void valueFromComponent(String s) { + valueFromComponent(s, null); + } + + /** A user edit in {@code source}: absorb it and show it in the other bound fields. */ + void valueFromComponent(String s, TextFieldRenderElement source) { + this.value = s == null ? "" : s; + pushToFields(source); + notifyListeners(); + } + + private void notifyListeners() { + com.codename1.flutter.foundation.Listeners.notify(listeners); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextField.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextField.java new file mode 100644 index 00000000000..84ee095bdbe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextField.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.FocusNode; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.services.TextInputAction; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A material single-line text input, backed by a CN1 + * {@code com.codename1.ui.TextField} (UIID "FlutterTextField"). Supports a + * {@link TextEditingController} (two-way sync), {@link InputDecoration} + * label/hint (rendered as the CN1 hint in M3), obscured (password) input, + * enabled/disabled state and onChanged/onSubmitted callbacks. + */ +public class TextField extends Widget { + + private TextEditingController controller; + private InputDecoration decoration; + private Boolean obscureText; + private Boolean enabled; + private Funcs.VoidFunc1 onChanged; + private Funcs.VoidFunc1 onSubmitted; + private TextStyle style; + private Color cursorColor; + private TextInputAction textInputAction; + private String restorationId; + private Funcs.VoidFunc0 onTap; + private Long maxLines = 1L; + private Long minLines; + private Long maxLength; + private DartList autofillHints; + private Object keyboardType; + private Object textCapitalization; + private FocusNode focusNode; + + public void textAlign(TextAlign v) { + } + + public void style(TextStyle v) { + this.style = v; + } + + public void cursorColor(Color v) { + this.cursorColor = v; + } + + public void textInputAction(TextInputAction v) { + this.textInputAction = v; + } + + public void restorationId(String v) { + this.restorationId = v; + } + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public Funcs.VoidFunc0 getOnTap() { + return onTap; + } + + public void maxLines(long v) { + this.maxLines = v; + } + + public void minLines(long v) { + this.minLines = v; + } + + /** How many lines the editor may grow to; 1 for a single-line field. */ + public Long getMaxLines() { + return maxLines; + } + + /** How many lines the editor shows before it has any content. */ + public Long getMinLines() { + return minLines; + } + + /** + * The character limit, which is also what makes Material draw a counter. + * + *

Accepted and discarded before, so the demo's phone-number field showed + * neither the limit nor the {@code 0/14} the reference counts out under + * it.

+ */ + public void maxLength(long v) { + this.maxLength = v; + } + + public Long getMaxLength() { + return maxLength; + } + + public void autofillHints(DartList v) { + this.autofillHints = v; + } + + public void keyboardType(Object v) { + this.keyboardType = v; + } + + public void textCapitalization(Object v) { + this.textCapitalization = v; + } + + public void focusNode(FocusNode v) { + this.focusNode = v; + } + + public FocusNode getFocusNode() { + return focusNode; + } + + public TextStyle getStyle() { + return style; + } + + public void controller(TextEditingController v) { + this.controller = v; + } + + public void decoration(InputDecoration v) { + this.decoration = v; + } + + public void obscureText(boolean v) { + this.obscureText = v; + } + + public void enabled(boolean v) { + this.enabled = v; + } + + public void onChanged(Funcs.VoidFunc1 v) { + this.onChanged = v; + } + + public void onSubmitted(Funcs.VoidFunc1 v) { + this.onSubmitted = v; + } + + public TextEditingController getController() { + return controller; + } + + public InputDecoration getDecoration() { + return decoration; + } + + public boolean isObscureText() { + return obscureText != null && obscureText; + } + + public boolean isEnabled() { + return enabled == null || enabled; + } + + public Funcs.VoidFunc1 getOnChanged() { + return onChanged; + } + + public Funcs.VoidFunc1 getOnSubmitted() { + return onSubmitted; + } + + @Override + public Element createElement() { + return new TextFieldRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFieldRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFieldRenderElement.java new file mode 100644 index 00000000000..31a4df226b7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFieldRenderElement.java @@ -0,0 +1,1085 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.TextArea; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.DataChangedListener; +import com.codename1.ui.geom.Dimension; + +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link TextField}: owns a CN1 + * {@link com.codename1.ui.TextField} (UIID "FlutterTextField"). + * + *

Controller sync is two-way: user edits (DataChangedListener) flow into + * the bound {@link TextEditingController} and fire {@code onChanged}; + * {@code controller.setText/clear} push back into the component via + * {@link #applyControllerText}. The {@code applying} guard stops the + * programmatic push from re-entering the data-changed path.

+ * + *

Material geometry: fills the available width, minimum height 48lp. The + * decoration's labelText renders as the CN1 hint in M3 (hintText is the + * fallback); a floating label is a later milestone.

+ */ +public class TextFieldRenderElement extends RenderElement { + + /** Material minimum text-field height in logical pixels. */ + public static final double MIN_HEIGHT_LP = 48; + /** Intrinsic width when the incoming width is unbounded. */ + public static final double DEFAULT_WIDTH_LP = 200; + + private boolean applying; + private TextEditingController boundController; + /// The editor itself. When the decoration carries a prefix icon this is a + /// CHILD of the component this element owns, so every read and write has to + /// go through here rather than through {@link #component()}. + private com.codename1.ui.TextField field; + /// The icon-plus-editor container, when the decoration has a prefix icon. + /// The decoration's surface belongs to this, not to the editor inside it. + private com.codename1.ui.Container decoratedRow; + + public TextFieldRenderElement(TextField widget) { + super(widget); + } + + private TextField textField() { + return (TextField) widget(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + com.codename1.ui.TextField tf = new com.codename1.ui.TextField(); + tf.setUIID("FlutterTextField"); + tf.addDataChangedListener(new DataChangedListener() { + @Override + public void dataChanged(int type, int index) { + if (applying) { + return; + } + userEdited(componentText()); + } + }); + // A Flutter text field does NOT take focus on arrival, and a focused + // Codename One field blinks a caret. Every page with a field opened + // showing a caret the reference does not draw -- the text-field demo, + // Shrine's login, the sliders demo's editable value. Codename One gives + // the form's initial focus to the first focusable component, so the + // field hands it back once, after the form is up. A later tap focuses it + // normally. + tf.addPointerPressedListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + touched = true; + // onTap was stored and never called, so a tap-driven field -- a + // read-only date field that opens a picker -- focused and did nothing. + Funcs.VoidFunc0 tap = textField().getOnTap(); + if (tap != null) { + try { + tap.call(); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + } + }); + tf.setDoneListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + Funcs.VoidFunc1 f = textField().getOnSubmitted(); + if (f != null) { + f.call(componentText()); + } + } + }); + tf.addFocusListener(new com.codename1.ui.events.FocusListener() { + @Override + public void focusGained(Component cmp) { + // The focus Codename One hands a field on arrival is not a Flutter + // focus (releaseUnrequestedFocus hands it back), so it is not reported. + if (boundFocus != null && (focusReleased || touched)) { + boundFocus.hostFocusChanged(true); + } + } + + @Override + public void focusLost(Component cmp) { + if (boundFocus != null) { + boundFocus.hostFocusChanged(false); + } + } + }); + field = tf; + Component out = decorated(tf); + apply(tf); + return out; + } + + /** + * The editor, or the editor beside its prefix icon. + * + *

{@code InputDecoration.prefixIcon} was stored and never read, so + * Crane's search form -- four rows whose whole affordance is the glyph that + * says what the row is for -- rendered as four bare capsules. Codename One's + * text field has no icon slot, so the icon and the editor share a container + * and the decoration's surface moves onto it: in Flutter the fill and the + * border enclose the icon too.

+ */ + private Component decorated(com.codename1.ui.TextField tf) { + com.codename1.flutter.widgets.Icon inside = iconAt(true); + com.codename1.flutter.widgets.Icon outside = iconAt(false); + com.codename1.flutter.widgets.Icon suffix = suffixIcon(); + com.codename1.flutter.widgets.Icon chosen = inside != null ? inside : outside; + if ((chosen == null || chosen.getIcon() == null) && suffix == null) { + return withHelper(tf, tf); + } + com.codename1.ui.Container row = + new com.codename1.ui.Container(new com.codename1.ui.layouts.BorderLayout()); + if (chosen != null && chosen.getIcon() != null) { + row.add(com.codename1.ui.layouts.BorderLayout.WEST, glyphLabel(chosen)); + } + if (suffix != null) { + // suffixIcon: the trailing glyph INSIDE the decoration -- the demo's + // password field is a visibility toggle and had none at all. + com.codename1.ui.Label glyph = glyphLabel(suffix); + // An IconButton suffix keeps its button: a tap on the glyph runs the + // CURRENT widget's onPressed, looked up at tap time so a rebuilt + // callback is never stale. Drawing only the glyph left the password + // visibility toggle showing and doing nothing. + glyph.addPointerReleasedListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + IconButton b = suffixButton(); + if (b != null && b.getOnPressed() != null) { + evt.consume(); + try { + b.getOnPressed().call(); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + } + }); + row.add(com.codename1.ui.layouts.BorderLayout.EAST, glyph); + } + row.add(com.codename1.ui.layouts.BorderLayout.CENTER, tf); + if (inside != null) { + // prefixIcon sits INSIDE the decoration, so the fill and the border + // move onto the row and enclose the glyph too. A second surface + // behind the editor would draw a filled block inside the filled one. + row.setUIID("FlutterTextField"); + decoratedRow = row; + tf.setUIID("Container"); + tf.getAllStyles().setBgTransparency(0); + tf.getAllStyles().setBorder(com.codename1.ui.plaf.Border.createEmpty()); + } else { + // `icon` sits OUTSIDE it: the field keeps its own surface and the + // glyph stands clear of it, with Material's gap between them. + row.setUIID("Container"); + row.getAllStyles().setBgTransparency(0); + } + return withHelper(tf, row); + } + + /// The decoration's inside ({@code prefixIcon}) or outside ({@code icon}) + /// glyph, when it is an Icon -- the only form this can draw. + private com.codename1.flutter.widgets.Icon iconAt(boolean inside) { + InputDecoration d = textField().getDecoration(); + Widget w = d == null ? null : (inside ? d.getPrefixIcon() : d.getIcon()); + return w instanceof com.codename1.flutter.widgets.Icon + ? (com.codename1.flutter.widgets.Icon) w : null; + } + + /// One material glyph as a label, in the icon's colour or the ambient + /// IconTheme's. + private com.codename1.ui.Label glyphLabel(com.codename1.flutter.widgets.Icon icon) { + com.codename1.ui.Label label = new com.codename1.ui.Label("", "Container"); + com.codename1.ui.plaf.Style glyphStyle = + new com.codename1.ui.plaf.Style(label.getUnselectedStyle()); + com.codename1.flutter.Color tint = iconColor(icon); + if (tint != null) { + glyphStyle.setFgColor(tint.rgb()); + label.getAllStyles().setFgColor(tint.rgb()); + } + glyphStyle.setBgTransparency(0); + label.getAllStyles().setMarginUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + label.getAllStyles().setMargin(0, 0, 0, (int) Math.round(Dp.px(ICON_GAP_LP))); + try { + // Dp.mm, because FontImage sizes glyphs in MILLIMETRES. Handing it + // logical pixels asked for a 24mm glyph and drew an icon taller than + // the row it sits in. + label.setIcon(com.codename1.ui.FontImage.createMaterial( + icon.getIcon().codePoint(), glyphStyle, Dp.mm(PREFIX_ICON_LP))); + } catch (Exception headlessOrNoFont) { + // the row still reserves the space + } + return label; + } + + /** Material's gap between an outside icon and the field. */ + private static final double ICON_GAP_LP = 16; + + /** Material's prefix icon size. */ + private static final double PREFIX_ICON_LP = 24; + + /// The icon's own colour, or the ambient IconTheme's -- the same chain + /// {@code IconRenderElement} follows. Without the fallback the glyph is + /// painted in the default ink, which on Crane's purple rows is black. + private com.codename1.flutter.Color iconColor(com.codename1.flutter.widgets.Icon icon) { + if (icon != null && icon.getColor() != null) { + return icon.getColor(); + } + try { + IconThemeData themed = IconTheme.of(this); + return themed == null ? null : themed.color(); + } catch (Throwable noTheme) { + return null; + } + } + + @Override + protected void updateComponent(Component c) { + if (field != null) { + apply(field); + } + } + + @Override + public void unmount() { + super.unmount(); + bindFocus(null); + if (boundController != null) { + boundController.unbind(this); + boundController = null; + } + } + + private void apply(com.codename1.ui.TextField tf) { + applying = true; + try { + TextField w = textField(); + tf.setConstraint(w.isObscureText() ? TextArea.PASSWORD : TextArea.ANY); + // maxLength is a limit, not only a counter: Flutter stops the edit. + // Zero is a real limit (no characters at all); only null and Flutter's + // negative noMaxLength mean "unlimited". + if (w.getMaxLength() != null && w.getMaxLength().longValue() >= 0) { + // The native cap counts UTF-16 units, and maxLength counts characters: a + // single emoji can be eleven units. It is therefore only a generous upper + // bound; userEdited enforces the real limit on character boundaries. + tf.setMaxSize((int) Math.min(Integer.MAX_VALUE, w.getMaxLength().longValue() * 16)); + } + // maxLines was stored and never read, so the demo's "Life story" + // field -- which asks for three lines -- came up one line tall and + // every line the user typed scrolled the one before it out of view. + // Codename One's TextField IS a TextArea, so the rows are simply + // its own; it just has to be told it is not single-line. + long rows = w.getMaxLines() == null ? 1 : w.getMaxLines().longValue(); + if (w.getMinLines() != null && w.getMinLines().longValue() > rows) { + rows = w.getMinLines().longValue(); + } + if (rows > 1) { + tf.setSingleLineTextArea(false); + tf.setRows((int) rows); + } else { + tf.setSingleLineTextArea(true); + } + tf.setEditable(w.isEnabled()); + tf.setEnabled(w.isEnabled()); + InputDecoration d = w.getDecoration(); + if (d != null) { + String hint = d.getLabelText() != null ? d.getLabelText() : d.getHintText(); + tf.setHint(hint == null ? "" : hint); + applyTextStyle(tf, d); + applyDecoration(decoratedRow != null ? (Component) decoratedRow : (Component) tf, d); + } + releaseUnrequestedFocus(tf); + bindFocus(w.getFocusNode()); + if (counterLabel != null) { + counterLabel.setText(counterText()); + } + rebindController(); + if (boundController != null) { + // rawValue, not text(): text() reads the bound component first, + // which during this very call is still empty -- and reading it + // REPLACES the controller's initial value with that emptiness. + String want = boundController.rawValue(); + if (!eq(tf.getText(), want)) { + tf.setText(want); + } + } + } finally { + applying = false; + } + } + + /** + * The type the field's own text is set in. + * + *

{@code TextField.style} was stored and never read, so a field rendered + * at whatever size Codename One's default font happens to be.

+ * + *

Deliberately NOT applied to the hint. Flutter builds the hint from + * hintStyle over the theme's hintColor, not from the input's colour -- and + * a field whose input colour is white, which is every row of Crane's search + * form, would otherwise show a white placeholder on a light fill.

+ */ + private void applyTextStyle(com.codename1.ui.TextField tf, InputDecoration d) { + applyOne(tf.getAllStyles(), inputStyle(), backdropRgb()); + if (tf.getHintLabel() != null) { + applyOne(tf.getHintLabel().getAllStyles(), hintStyle(d), backdropRgb()); + } + } + + /** + * The type the input itself is set in: the theme's {@code bodyLarge}, with the + * field's own style over it. + * + *

Only the field's own style was applied, so a field that states none -- which is + * most of them -- fell back to whatever Codename One's default font happens to be + * and ignored the theme entirely. The Rally study is where that shows: it sets + * {@code bodyLarge} to a 40-point serif, and its login fields came up in small sans + * where the reference reads them in large serif.

+ * + *

{@code bodyLarge} rather than {@code titleMedium}: the latter is the Material 2 + * answer, which this used. Material 3 resolves a text field's style through + * {@code _m3InputStyle}, and that is {@code textTheme.bodyLarge}.

+ */ + private com.codename1.flutter.TextStyle inputStyle() { + com.codename1.flutter.TextStyle style = new com.codename1.flutter.TextStyle(); + try { + ThemeData theme = Theme.of(this); + if (theme != null && theme.textTheme() != null) { + style = style.merge(theme.textTheme().bodyLarge()); + } + } catch (Throwable noTheme) { + // a field outside any theme still has to render + } + return textField().getStyle() == null ? style + : style.merge(textField().getStyle()); + } + + /** + * The type the placeholder is set in. + * + *

Flutter's InputDecorator builds it as {@code titleMedium} merged with + * the field's own style, RECOLOURED with the theme's hintColor, then merged + * with an explicit hintStyle. The recolour is the part that is easy to lose: + * a placeholder that keeps the INPUT's colour is white on every row of + * Crane's search form, where the input colour is white.

+ */ + private com.codename1.flutter.TextStyle hintStyle(InputDecoration d) { + // A LABEL is not a placeholder, and they part company on colour. + // + // One string reaches the component either way -- Codename One shows one piece of + // text in an empty field -- so this asked the theme for a hint colour whatever it + // was showing. Flutter only does that for a hintText; a labelText resting in the + // field takes onSurfaceVariant, which is a good deal darker. Shrine's login states + // labelText and came out in pale grey where the reference reads it in the study's + // brown: 52 dark pixels against the reference's 3297. + boolean isLabel = d != null && d.getLabelText() != null; + com.codename1.flutter.TextStyle style = new com.codename1.flutter.TextStyle(); + com.codename1.flutter.Color tint = null; + try { + ThemeData theme = Theme.of(this); + style = style.merge(inputStyle()); + if (isLabel && theme.colorScheme() != null + && theme.colorScheme().onSurfaceVariant() != null) { + tint = theme.colorScheme().onSurfaceVariant(); + } else { + tint = theme.hintColor() != null ? theme.hintColor() + : defaultHintColor(theme.brightness()); + } + } catch (Throwable noTheme) { + tint = null; + } + if (tint != null) { + style.color(tint); + } + // The ambient inputDecorationTheme's labelStyle, over the tint. + // + // It was never read, so a theme that states the colour of its labels did not get + // it and every field fell back to the generic hint colour. The Rally study sets + // one, and its login labels came out dark on a dark background where the + // reference reads them in light grey. + InputDecorationThemeData themed = inputDecorationTheme(); + if (themed != null && themed.getLabelStyle() != null) { + style = style.merge(themed.getLabelStyle()); + } + com.codename1.flutter.TextStyle explicit = d == null ? null : d.getHintStyle(); + return explicit == null ? style : style.merge(explicit); + } + + /// Flutter's ThemeData default when the theme names no hintColor: black38 + /// on a light theme, white70 on a dark one. + private static com.codename1.flutter.Color defaultHintColor( + com.codename1.flutter.Brightness brightness) { + return new com.codename1.flutter.Color( + brightness == com.codename1.flutter.Brightness.dark + ? 0xB3FFFFFFL : 0x61000000L); + } + + /** + * Flutter's {@code InputDecoration.applyDefaults}: every field the widget + * leaves unset falls back to the ambient inputDecorationTheme. + * + *

{@code filled} is a primitive on both sides, so "unset" and "false" + * cannot be told apart; a theme that asks for a fill therefore wins over a + * decoration that simply did not mention one, which is the case the studies + * exercise. Rally names its dark fill once on the theme rather than on each + * of its login fields, and without this they rendered as white blocks on a + * dark page.

+ */ + static boolean resolveFilled(InputDecoration d, InputDecorationThemeData themed) { + if (d != null && d.isFilled()) { + return true; + } + return themed != null && themed.isFilled(); + } + + /** The fill colour, the decoration's before the theme's. */ + static com.codename1.flutter.Color resolveFill(InputDecoration d, + InputDecorationThemeData themed) { + if (d != null && d.getFillColor() != null) { + return d.getFillColor(); + } + return themed == null ? null : themed.getFillColor(); + } + + /** The content padding, the decoration's before the theme's. */ + static com.codename1.flutter.EdgeInsetsGeometry resolvePadding(InputDecoration d, + InputDecorationThemeData themed) { + if (d != null && d.getContentPadding() != null) { + return d.getContentPadding(); + } + return themed == null ? null : themed.getContentPadding(); + } + + /// The ambient {@code inputDecorationTheme}, or null when there is none. + private InputDecorationThemeData inputDecorationTheme() { + try { + return Theme.of(this).inputDecorationTheme(); + } catch (Throwable noTheme) { + return null; + } + } + + /** One style's size, weight and colour onto one Codename One style. */ + /// What a translucent ink is composited over: the field's own fill when it has an + /// opaque one, else the surface the page is painted on. + private int backdropRgb() { + try { + com.codename1.flutter.Color fill = + resolveFill(textField().getDecoration(), inputDecorationTheme()); + if (fill != null && fill.alpha() == 255) { + return fill.rgb(); + } + ColorScheme cs = Theme.of(this).colorScheme(); + if (cs != null && cs.surface() != null) { + return cs.surface().rgb(); + } + } catch (Throwable noTheme) { + // fall through to white + } + return 0xFFFFFF; + } + + private static void applyOne(com.codename1.ui.plaf.Style target, + com.codename1.flutter.TextStyle ts, int backdropRgb) { + if (ts == null) { + return; + } + // Resolve the NAMED family first. Deriving from whatever the theme left + // on the component is what silently did nothing: a system font does not + // derive, so the size was dropped on the floor and the placeholder kept + // rendering half again too tall. A bundled TrueType face does derive. + com.codename1.ui.Font named = com.codename1.flutter.fonts.FontResolver.resolve( + ts.fontFamily(), ts.getFontWeight(), false); + if (ts.getFontSize() != null || ts.getFontWeight() != null || named != null) { + com.codename1.ui.Font base = named != null ? named : target.getFont(); + // Whether the face already carries the weight; see TextRenderElement. + boolean weighted = named != null; + if (named == null && ts.getFontSize() != null) { + // The theme's font here is a SYSTEM font, which does not derive, + // so the requested size was being dropped. See + // FontResolver.platformFace. + com.codename1.ui.Font platform = + com.codename1.flutter.fonts.FontResolver.platformFace( + ts.getFontWeight(), false); + if (platform != null) { + base = platform; + weighted = true; + } + } + if (base == null) { + base = com.codename1.ui.Font.getDefaultFont(); + } + if (base != null) { + float sizePx = ts.getFontSize() != null + ? (float) Dp.px(ts.getFontSize()) + : (base.getPixelSize() > 0 ? base.getPixelSize() : base.getHeight()); + int weight = !weighted + && ts.getFontWeight() != null && ts.getFontWeight().isBold() + ? com.codename1.ui.Font.STYLE_BOLD : com.codename1.ui.Font.STYLE_PLAIN; + try { + target.setFont(base.derive(sizePx, weight)); + } catch (Exception cannotDerive) { + // keep the base font + } + } + } + if (ts.getColor() != null) { + // A TRANSLUCENT ink has to be composited here. + // + // Codename One's Style carries an opaque foreground, so taking rgb() off a + // colour that stated an alpha paints it at FULL strength -- and Material states + // one constantly. The compose page asks for its Subject placeholder in the + // primary colour at half opacity and got it in solid navy, reading as a title + // rather than as a hint. + com.codename1.flutter.Color c = ts.getColor(); + if (c.alpha() < 255) { + c = com.codename1.flutter.Color.alphaBlend(c, + new com.codename1.flutter.Color(0xFF000000L | (backdropRgb & 0xFFFFFF))); + } + target.setFgColor(c.rgb()); + } + } + + /** + * The decoration's SURFACE: its fill, its outline and its content padding. + * + *

All three were accepted and discarded, so a field that asks to be a + * solid rounded block — which is what Crane's search form is, four purple + * capsules on a purple back layer — rendered as the theme's default + * outlined box on white. The decoration is the whole visual identity of a + * Material text field; ignoring it leaves the field looking like no design + * at all.

+ */ + private void applyDecoration(Component target, InputDecoration d) { + com.codename1.ui.plaf.Style all = target.getAllStyles(); + // Flutter resolves a decoration through InputDecoration.applyDefaults: + // each field the widget leaves unset falls back to the ambient + // inputDecorationTheme. That theme was held opaquely and never read, so + // Rally's login fields -- a dark fill named once on the theme rather + // than on each field -- rendered as white blocks on a dark page. + InputDecorationThemeData themed = inputDecorationTheme(); + if (resolveFilled(d, themed)) { + com.codename1.flutter.Color fill = resolveFill(d, themed); + if (fill == null) { + try { + fill = Theme.of(this).colorScheme().surfaceVariant(); + } catch (Throwable ignore) { + fill = null; + } + } + if (fill != null) { + all.setBgColor(fill.rgb()); + all.setBgTransparency(255); + } + } + // Which border, through the same fallbacks Flutter uses: the field's own, then the + // theme's enabled border, then the theme's border. + Object stated = d.getBorder(); + if (stated == null && themed != null) { + stated = themed.getEnabledBorder() != null ? themed.getEnabledBorder() : themed.getBorder(); + } + // A border that draws ITSELF wins over anything the runtime would + // approximate. Shrine's fields chamfer their corners, and matching that + // to the nearest rounded rectangle is what made them read as square. + // A border class this runtime does not know draws ITSELF: it is an + // application subclass with its own paint, and only it knows its shape + // (Shrine's CutCornersBorder chamfers its corners). + // + // Class identity, not reflection: ParparVM has no Class.getMethod, and + // asking for one does not fail at runtime -- it fails the iOS C compile + // with an undeclared-function error, which is how two device builds + // came to fail while the recording quietly used a stale app. + if (paintsItself(stated)) { + all.setBorder(new FlutterShapeBorderPainter( + (com.codename1.flutter.ShapeBorder) stated)); + com.codename1.ui.plaf.Border own = target.getUnselectedStyle().getBorder(); + if (own != null) { + target.getSelectedStyle().setBorder(own); + target.getPressedStyle().setBorder(own); + target.getDisabledStyle().setBorder(own); + } + return; + } + int radiusPx = stated instanceof com.codename1.flutter.InputBorder + ? outlineRadiusPx((com.codename1.flutter.InputBorder) stated) : 0; + if (radiusPx > 0) { + com.codename1.ui.plaf.RoundRectBorder b = com.codename1.ui.plaf.RoundRectBorder.create() + .cornerRadius(radiusPx / com.codename1.ui.Display.getInstance().convertToPixels(1f)) + .shadowOpacity(0); + // An OUTLINE, which is what the name says. It was built with strokeOpacity 0 -- + // a rounded background and no line -- so a field whose whole decoration IS its + // outline had none: Shrine's login states a CutCornersBorder on its theme and + // rendered as two labels loose on a white page. + com.codename1.flutter.BorderSide side = sideOf(stated); + if (side != null && side.color() != null && side.width() > 0) { + b = b.strokeColor(side.color().rgb()) + .strokeOpacity(side.color().alpha()) + .stroke((float) Math.max(1, Math.round( + com.codename1.flutter.rendering.Dp.px(side.width()))), false); + } else { + b = b.strokeOpacity(0); + } + all.setBorder(b); + } else if (stated == com.codename1.flutter.InputBorder.none) { + all.setBorder(com.codename1.ui.plaf.Border.createEmpty()); + } else if (stated == null || stated instanceof com.codename1.flutter.UnderlineInputBorder) { + // Flutter's DEFAULT is a rule under the field, not a box around it. Leaving the + // Codename One theme's own border in place drew a full outline on every field + // that states no border of its own -- Rally's login fields are a dark fill with + // a hairline beneath, and they came out as bright rectangles. + all.setBorder(com.codename1.ui.plaf.Border.createUnderlineBorder( + Math.max(1, (int) Math.round(com.codename1.flutter.rendering.Dp.px(1))), + underlineRgb())); + } + // The SAME border in every state. A Codename One theme gives a text field one + // border unselected and a different one -- often none -- selected, which is a + // reasonable default for a native-looking field and wrong for this one: a + // reference text field keeps its decoration whether it has focus or not, and only + // its caret and the highlight colour change. + // + // Left to the theme it looked like the box belonged to whichever field was NOT + // being used. Measured on Shrine's login, the outline's top and bottom edges were + // at y=1191 and y=1363 on arrival, and moved to y=979 and y=1151 -- the other + // field entirely -- on tapping into the password box. + com.codename1.ui.plaf.Border rest = target.getUnselectedStyle().getBorder(); + if (rest != null) { + target.getSelectedStyle().setBorder(rest); + target.getPressedStyle().setBorder(rest); + } + com.codename1.flutter.EdgeInsets pad = insetsOf(resolvePadding(d, themed)); + if (pad != null) { + all.setPaddingUnit(com.codename1.ui.plaf.Style.UNIT_TYPE_PIXELS); + all.setPadding((int) Math.round(com.codename1.flutter.rendering.Dp.px(pad.top())), + (int) Math.round(com.codename1.flutter.rendering.Dp.px(pad.bottom())), + (int) Math.round(com.codename1.flutter.rendering.Dp.px(pad.left())), + (int) Math.round(com.codename1.flutter.rendering.Dp.px(pad.right()))); + } + } + + /// The default underline's colour: onSurface at 38%, composited onto what it is drawn + /// over, because a Codename One border colour carries no alpha. + private int underlineRgb() { + // Whatever the decoration's own border side states, then BLACK. + // + // Not onSurface at 38%: Flutter's default BorderSide is opaque black, + // and an InputDecorator recolours it only when the field is focused, + // disabled or in error. Fading it made every resting underline in the + // app a mid grey where the reference draws a black rule -- measured + // (150,150,150) against (0,0,0) on the text-field demo. + InputDecoration d = textField().getDecoration(); + com.codename1.flutter.BorderSide side = sideOf(d == null ? null : d.getBorder()); + if (side == null) { + InputDecorationThemeData themed = inputDecorationTheme(); + if (themed != null) { + Object b = themed.getEnabledBorder() != null + ? themed.getEnabledBorder() : themed.getBorder(); + side = sideOf(b); + } + } + if (side != null && side.color() != null) { + return side.color().rgb(); + } + return 0x000000; + } + + /// The side an input border draws itself with, or null when it states none. + private static com.codename1.flutter.BorderSide sideOf(Object border) { + if (!(border instanceof com.codename1.flutter.InputBorder)) { + return null; + } + com.codename1.flutter.BorderSide side = + ((com.codename1.flutter.InputBorder) border).getBorderSide(); + // BorderSide.none is our default, and it states nothing -- Flutter's + // default is an ordinary black side. Treat "none" as unstated. + return side == null || side.color() == null || side.width() <= 0 ? null : side; + } + + /** The outline's corner radius in device pixels, or 0 when it has none. */ + private static int outlineRadiusPx(com.codename1.flutter.InputBorder border) { + if (!(border instanceof com.codename1.flutter.OutlineInputBorder)) { + return 0; + } + com.codename1.flutter.BorderRadius r = + ((com.codename1.flutter.OutlineInputBorder) border).borderRadius(); + if (r == null || r.topLeft() == null) { + return 0; + } + return (int) Math.round(com.codename1.flutter.rendering.Dp.px(r.topLeft().x())); + } + + private static com.codename1.flutter.EdgeInsets insetsOf( + com.codename1.flutter.EdgeInsetsGeometry g) { + return g instanceof com.codename1.flutter.EdgeInsets + ? (com.codename1.flutter.EdgeInsets) g : null; + } + + private void rebindController() { + TextEditingController c = textField().getController(); + if (c != boundController) { + if (boundController != null) { + boundController.unbind(this); + } + boundController = c; + if (c != null) { + c.bind(this); + } + } + } + + /** + * A user edit arrived: sync the controller (which notifies its + * listeners) and fire onChanged with the new string. Public so headless + * tests can drive the flow without a component. + */ + public void userEdited(String newText) { + // Enforced here too, not only through the native field's max size: text can + // arrive by paste or a platform editor that ignores it, and the value handed + // on must never exceed the limit. It used to reach the controller and + // onChanged whole, so a 14-character field accepted a 15th. + // Counted in user-perceived characters, as Flutter's maxLength is: cutting UTF-16 + // units split an emoji at the limit and handed half a surrogate pair on. + Long max = textField().getMaxLength(); + if (newText != null && max != null && max.longValue() >= 0 + && com.codename1.flutter.foundation.Characters.count(newText) > max.longValue()) { + newText = com.codename1.flutter.foundation.Characters.take(newText, max.longValue()); + if (field != null && !applying) { + applying = true; + try { + field.setText(newText); + } finally { + applying = false; + } + } + } + if (counterLabel != null) { + counterLabel.setText(com.codename1.flutter.foundation.Characters.count(newText) + + "/" + textField().getMaxLength()); + } + rebindController(); + if (boundController != null) { + boundController.valueFromComponent(newText, this); + } + Funcs.VoidFunc1 f = textField().getOnChanged(); + if (f != null) { + f.call(newText); + } + } + + /** + * The component's live text, or null when headless. + */ + String componentText() { + return field == null ? null : field.getText(); + } + + /** + * Push a programmatic controller value into the component (no + * data-changed feedback loop). + */ + void applyControllerText(String v) { + if (field == null) { + return; + } + applying = true; + try { + field.setText(v == null ? "" : v); + } finally { + applying = false; + } + } + + private static boolean eq(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Component c = component(); + double prefW = Dp.px(DEFAULT_WIDTH_LP); + double prefH = Dp.px(MIN_HEIGHT_LP); + if (c != null) { + Dimension d = c.getPreferredSize(); + prefW = Math.max(prefW, d.getWidth()); + prefH = Math.max(prefH, d.getHeight()); + } + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : prefW; + double h = Math.max(prefH, Dp.px(MIN_HEIGHT_LP)); + return constraints.constrain(new Size(w, h)); + } + + /** The FocusNode the widget gave this field, bridged to the editor through focusHost. */ + private com.codename1.flutter.FocusNode boundFocus; + + /** + * Moves the real focus when the app moves its FocusNode. The node used to keep + * a private flag only, so requestFocus neither placed the caret nor opened the + * keyboard while hasFocus reported true. + */ + private final com.codename1.flutter.FocusNode.Host focusHost = new com.codename1.flutter.FocusNode.Host() { + @Override + public void focusHost() { + // An asked-for focus is not the arrival focus: never hand it back. + focusReleased = true; + focusField(2); + } + + @Override + public void blurHost() { + com.codename1.ui.TextField tf = field; + if (tf == null) { + return; + } + if (tf.isEditing()) { + tf.stopEditing(); + } + com.codename1.ui.Form f = tf.getComponentForm(); + if (f != null && f.getFocused() == tf) { + f.setFocused(null); + } + } + }; + + private void bindFocus(com.codename1.flutter.FocusNode node) { + if (node == boundFocus) { + return; + } + if (boundFocus != null) { + boundFocus.detachHost(focusHost); + } + boundFocus = node; + if (node != null) { + node.attachHost(focusHost); + } + } + + /** + * Focuses the editor and starts editing it. A field that is not on a form yet + * -- focus requested in initState, before the page is shown -- cannot take the + * focus, so it tries again after the current event, a bounded number of times. + */ + private void focusField(final int retries) { + com.codename1.ui.TextField tf = field; + if (tf == null || !Display.isInitialized()) { + return; + } + if (tf.getComponentForm() == null) { + if (retries > 0) { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + focusField(retries - 1); + } + }); + } + return; + } + tf.requestFocus(); + tf.startEditingAsync(); + } + + /** True once the user has actually pressed on this field. */ + private boolean touched; + /** True once the arrival focus has been handed back. */ + private boolean focusReleased; + + /** + * Hands back the focus Codename One hands a field on arrival. + * + *

Done ONCE, and never after the user has touched the field, so tapping + * into it still focuses it and typing still works.

+ */ + private void releaseUnrequestedFocus(com.codename1.ui.TextField tf) { + if (focusReleased || touched) { + return; + } + try { + com.codename1.ui.Form f = tf.getComponentForm(); + if (f == null) { + return; + } + focusReleased = true; + if (f.getFocused() == tf) { + f.setFocused(null); + } + } catch (Throwable noForm) { + // not on a form yet: the next apply tries again + } + } + + /** The decoration's trailing glyph, or null. */ + /** The IconButton the suffix is, or wraps, or null. */ + private IconButton suffixButton() { + InputDecoration d = textField().getDecoration(); + Widget w = d == null ? null : d.getSuffixIcon(); + for (int depth = 0; depth < 6 && w != null; depth++) { + if (w instanceof IconButton) { + return (IconButton) w; + } + if (w instanceof com.codename1.flutter.widgets.Icon) { + return null; + } + w = com.codename1.flutter.WidgetPreview.step(w, this); + } + return null; + } + + private com.codename1.flutter.widgets.Icon suffixIcon() { + InputDecoration d = textField().getDecoration(); + Widget w = d == null ? null : d.getSuffixIcon(); + // Looked THROUGH: the demo's password toggle is an IconButton, not a + // bare Icon, and matching only the bare form left it with no glyph. + for (int depth = 0; depth < 6 && w != null; depth++) { + if (w instanceof com.codename1.flutter.widgets.Icon) { + return (com.codename1.flutter.widgets.Icon) w; + } + if (w instanceof IconButton) { + w = ((IconButton) w).getIcon(); + continue; + } + w = com.codename1.flutter.WidgetPreview.step(w, this); + } + return null; + } + + /** + * The field with Material's supporting line under it: the helper text on the + * leading side, the character counter on the trailing side. + * + *

Both were stored and never drawn, so the demo showed neither the + * {@code 0/14} under the phone number nor "Keep it short, this is just a + * demo." under the life story -- and everything below sat that line's height + * too high.

+ */ + private Component withHelper(com.codename1.ui.TextField tf, Component field) { + InputDecoration d = textField().getDecoration(); + String help = d == null ? null : d.getHelperText(); + Long max = textField().getMaxLength(); + if ((help == null || help.length() == 0) && max == null) { + return field; + } + com.codename1.ui.Container stack = new com.codename1.ui.Container( + new com.codename1.ui.layouts.BoxLayout( + com.codename1.ui.layouts.BoxLayout.Y_AXIS)); + stack.setUIID("Container"); + stack.getAllStyles().setBgTransparency(0); + stack.getAllStyles().setPadding(0, 0, 0, 0); + stack.getAllStyles().setMargin(0, 0, 0, 0); + stack.add(field); + com.codename1.ui.Container line = new com.codename1.ui.Container( + new com.codename1.ui.layouts.BorderLayout()); + line.setUIID("Container"); + line.getAllStyles().setBgTransparency(0); + line.getAllStyles().setPadding( + (int) Math.round(com.codename1.flutter.rendering.Dp.px(4)), 0, 0, 0); + line.getAllStyles().setMargin(0, 0, 0, 0); + if (help != null && help.length() > 0) { + helperLabel = supportLabel(help); + // CENTER, not WEST: a WEST cell takes its preferred width and the + // sentence was cut off mid-word ("...just a demc"). + line.add(com.codename1.ui.layouts.BorderLayout.CENTER, helperLabel); + } + if (max != null) { + counterLabel = supportLabel(counterText()); + line.add(com.codename1.ui.layouts.BorderLayout.EAST, counterLabel); + } + stack.add(line); + supportLine = stack; + return stack; + } + + private com.codename1.ui.Label supportLabel(String text) { + com.codename1.ui.Label l = new com.codename1.ui.Label(text == null ? "" : text); + l.setUIID("Container"); + l.getAllStyles().setBgTransparency(0); + l.getAllStyles().setPadding(0, 0, 0, 0); + l.getAllStyles().setMargin(0, 0, 0, 0); + applyOne(l.getAllStyles(), supportStyle(), backdropRgb()); + return l; + } + + /** Material's supporting text: bodySmall in onSurfaceVariant. */ + private com.codename1.flutter.TextStyle supportStyle() { + com.codename1.flutter.TextStyle st = new com.codename1.flutter.TextStyle(); + try { + ThemeData t = Theme.of(this); + if (t != null && t.textTheme() != null && t.textTheme().bodySmall() != null) { + st = st.merge(t.textTheme().bodySmall()); + } + ColorScheme cs = t == null ? null : t.colorScheme(); + if (cs != null && cs.onSurfaceVariant() != null) { + st.color(cs.onSurfaceVariant()); + } + } catch (Throwable noTheme) { + // an unthemed field still draws its supporting line + } + return st; + } + + private String counterText() { + Long max = textField().getMaxLength(); + if (max == null) { + return ""; + } + String t = componentText(); + return com.codename1.flutter.foundation.Characters.count(t) + "/" + max.longValue(); + } + + private com.codename1.ui.Container supportLine; + private com.codename1.ui.Label helperLabel; + private com.codename1.ui.Label counterLabel; + + /** + * Whether {@code border} is an application subclass that paints itself, + * rather than one of the shapes this runtime draws through a Codename One + * border. + */ + private static boolean paintsItself(Object border) { + if (!(border instanceof com.codename1.flutter.ShapeBorder) + || border == com.codename1.flutter.InputBorder.none) { + return false; + } + Class c = border.getClass(); + return c != com.codename1.flutter.OutlineInputBorder.class + && c != com.codename1.flutter.UnderlineInputBorder.class + && c != com.codename1.flutter.InputBorder.class; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFormField.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFormField.java new file mode 100644 index 00000000000..0aeb6343594 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextFormField.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.FormFieldSetter; +import com.codename1.flutter.widgets.FormFieldValidator; + +import dart.runtime.Funcs; + +/** + * A Material {@link TextField} wired to {@link com.codename1.flutter.widgets.Form} + * validation — Flutter's {@code TextFormField}. It renders as a {@code TextField} + * (this pass forwards the controller/decoration/obscure/enabled it can map); + * {@code validator}/{@code onSaved} are captured so a later pass can register the + * field with its enclosing form. All values are String-valued in new_gallery. + */ +public class TextFormField extends StatelessWidget { + + private TextEditingController controller; + private String initialValue; + private InputDecoration decoration; + private Object keyboardType; + private Object style; + private boolean obscureText; + private Boolean enabled; + private Object maxLines; + private Object minLines; + private Object maxLength; + private FormFieldValidator validator; + private FormFieldSetter onSaved; + private Funcs.VoidFunc1 onChanged; + private Funcs.VoidFunc1 onFieldSubmitted; + private Funcs.VoidFunc0 onEditingComplete; + private Object focusNode; + private Object textInputAction; + private Object textCapitalization; + private Object autovalidateMode; + private Object inputFormatters; + private Object autofillHints; + private Object autofocus; + private Object cursorColor; + private String restorationId; + + public void controller(TextEditingController v) { this.controller = v; } + public void initialValue(String v) { this.initialValue = v; } + public void decoration(InputDecoration v) { this.decoration = v; } + public void keyboardType(Object v) { this.keyboardType = v; } + public void style(Object v) { this.style = v; } + public void obscureText(boolean v) { this.obscureText = v; } + public void enabled(Boolean v) { this.enabled = v; } + public void maxLines(Object v) { this.maxLines = v; } + public void minLines(Object v) { this.minLines = v; } + public void maxLength(Object v) { this.maxLength = v; } + public void validator(FormFieldValidator v) { this.validator = v; } + public void onSaved(FormFieldSetter v) { this.onSaved = v; } + public void onChanged(Funcs.VoidFunc1 v) { this.onChanged = v; } + public void onFieldSubmitted(Funcs.VoidFunc1 v) { this.onFieldSubmitted = v; } + public void onEditingComplete(Funcs.VoidFunc0 v) { this.onEditingComplete = v; } + public void focusNode(Object v) { this.focusNode = v; } + public void textInputAction(Object v) { this.textInputAction = v; } + public void textCapitalization(Object v) { this.textCapitalization = v; } + public void autovalidateMode(Object v) { this.autovalidateMode = v; } + public void inputFormatters(Object v) { this.inputFormatters = v; } + public void autofillHints(Object v) { this.autofillHints = v; } + public void autofocus(Object v) { this.autofocus = v; } + public void cursorColor(Object v) { this.cursorColor = v; } + public void restorationId(String v) { this.restorationId = v; } + public void maxLengthEnforcement(Object v) { } + public void onTap(Object v) { } + public void buildCounter(Object v) { } + + @Override + public Widget build(BuildContext context) { + TextField field = new TextField(); + if (controller != null) { + field.controller(controller); + } + if (decoration != null) { + field.decoration(decoration); + } + field.obscureText(obscureText); + if (enabled != null) { + field.enabled(enabled); + } + if (onChanged != null) { + field.onChanged(onChanged); + } + // maxLength, maxLines and minLines were accepted here and dropped on the + // way to the field, so a form field could not count its characters or + // grow past one line however it was configured. The text-field demo is + // all TextFormField: its phone number asks for a 14-character counter + // and its life story for three lines. + if (maxLength instanceof Number) { + field.maxLength(((Number) maxLength).longValue()); + } + if (maxLines instanceof Number) { + field.maxLines(((Number) maxLines).longValue()); + } + if (minLines instanceof Number) { + field.minLines(((Number) minLines).longValue()); + } + if (keyboardType != null) { + field.keyboardType(keyboardType); + } + return field; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextSelectionThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextSelectionThemeData.java new file mode 100644 index 00000000000..903dd18d875 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextSelectionThemeData.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +/** + * Colours for text selection — Flutter's {@code TextSelectionThemeData}. + * + *

Held rather than applied: Codename One draws selection with its own theme colours, so + * these are recorded for the styling pass to pick up. Keeping the type is not cosmetic + * though — a theme that names it must still transpile, and three of the four studies set + * one.

+ */ +public class TextSelectionThemeData { + + private Color cursorColor; + private Color selectionColor; + private Color selectionHandleColor; + + public void cursorColor(Color v) { + this.cursorColor = v; + } + + public void selectionColor(Color v) { + this.selectionColor = v; + } + + public void selectionHandleColor(Color v) { + this.selectionHandleColor = v; + } + + public Color getCursorColor() { + return cursorColor; + } + + public Color getSelectionColor() { + return selectionColor; + } + + public Color getSelectionHandleColor() { + return selectionHandleColor; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextTheme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextTheme.java new file mode 100644 index 00000000000..0c0e3c73aa3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TextTheme.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.TextStyle; + +/** + * Material 3 default text styles. Each getter returns the configured override + * when {@link #copyWith} (or a builder) supplied one, otherwise a fresh + * {@link TextStyle} carrying the M3 default logical size for that role. Fresh + * instances are returned for the defaults because TextStyle is a mutable + * write-once config object; sharing would leak one call site's mutation. + */ +public class TextTheme { + + private TextStyle displayLarge; + private TextStyle displayMedium; + private TextStyle displaySmall; + private TextStyle headlineLarge; + private TextStyle headlineMedium; + private TextStyle headlineSmall; + private TextStyle titleLarge; + private TextStyle titleMedium; + private TextStyle titleSmall; + private TextStyle bodyLarge; + private TextStyle bodyMedium; + private TextStyle bodySmall; + private TextStyle labelLarge; + private TextStyle labelMedium; + private TextStyle labelSmall; + + /** + * This theme with {@code other}'s properties layered on top, role by role — + * Flutter's {@code TextTheme.merge}. Used to combine a type GEOMETRY (sizes, + * weights, tracking) with an INK theme (colours), which is how + * {@link Typography} composes its two halves. + */ + public TextTheme merge(TextTheme other) { + if (other == null) { + return this; + } + TextTheme t = new TextTheme(); + t.displayLarge = displayLarge().merge(other.displayLarge); + t.displayMedium = displayMedium().merge(other.displayMedium); + t.displaySmall = displaySmall().merge(other.displaySmall); + t.headlineLarge = headlineLarge().merge(other.headlineLarge); + t.headlineMedium = headlineMedium().merge(other.headlineMedium); + t.headlineSmall = headlineSmall().merge(other.headlineSmall); + t.titleLarge = titleLarge().merge(other.titleLarge); + t.titleMedium = titleMedium().merge(other.titleMedium); + t.titleSmall = titleSmall().merge(other.titleSmall); + t.bodyLarge = bodyLarge().merge(other.bodyLarge); + t.bodyMedium = bodyMedium().merge(other.bodyMedium); + t.bodySmall = bodySmall().merge(other.bodySmall); + t.labelLarge = labelLarge().merge(other.labelLarge); + t.labelMedium = labelMedium().merge(other.labelMedium); + t.labelSmall = labelSmall().merge(other.labelSmall); + return t; + } + + private static TextStyle sized(double size) { + TextStyle t = new TextStyle(); + t.fontSize(size); + return t; + } + + // ------------------------------------------------------------------ + // Named-parameter setters (the Dart constructor's named args and any + // {@code textTheme.copyWith(role: style)}-style overrides land here). + // ------------------------------------------------------------------ + + public void displayLarge(TextStyle v) { + this.displayLarge = v; + } + + public void displayMedium(TextStyle v) { + this.displayMedium = v; + } + + public void displaySmall(TextStyle v) { + this.displaySmall = v; + } + + public void headlineLarge(TextStyle v) { + this.headlineLarge = v; + } + + public void headlineMedium(TextStyle v) { + this.headlineMedium = v; + } + + public void headlineSmall(TextStyle v) { + this.headlineSmall = v; + } + + public void titleLarge(TextStyle v) { + this.titleLarge = v; + } + + public void titleMedium(TextStyle v) { + this.titleMedium = v; + } + + public void titleSmall(TextStyle v) { + this.titleSmall = v; + } + + public void bodyLarge(TextStyle v) { + this.bodyLarge = v; + } + + public void bodyMedium(TextStyle v) { + this.bodyMedium = v; + } + + public void bodySmall(TextStyle v) { + this.bodySmall = v; + } + + public void labelLarge(TextStyle v) { + this.labelLarge = v; + } + + public void labelMedium(TextStyle v) { + this.labelMedium = v; + } + + public void labelSmall(TextStyle v) { + this.labelSmall = v; + } + + public TextStyle displayLarge() { + return displayLarge != null ? displayLarge : sized(57); + } + + public TextStyle displayMedium() { + return displayMedium != null ? displayMedium : sized(45); + } + + public TextStyle displaySmall() { + return displaySmall != null ? displaySmall : sized(36); + } + + public TextStyle headlineLarge() { + return headlineLarge != null ? headlineLarge : sized(32); + } + + public TextStyle headlineMedium() { + return headlineMedium != null ? headlineMedium : sized(28); + } + + public TextStyle headlineSmall() { + return headlineSmall != null ? headlineSmall : sized(24); + } + + public TextStyle titleLarge() { + return titleLarge != null ? titleLarge : sized(22); + } + + public TextStyle titleMedium() { + return titleMedium != null ? titleMedium : sized(16); + } + + public TextStyle titleSmall() { + return titleSmall != null ? titleSmall : sized(14); + } + + public TextStyle bodyLarge() { + return bodyLarge != null ? bodyLarge : sized(16); + } + + public TextStyle bodyMedium() { + return bodyMedium != null ? bodyMedium : sized(14); + } + + public TextStyle bodySmall() { + return bodySmall != null ? bodySmall : sized(12); + } + + public TextStyle labelLarge() { + return labelLarge != null ? labelLarge : sized(14); + } + + public TextStyle labelMedium() { + return labelMedium != null ? labelMedium : sized(12); + } + + public TextStyle labelSmall() { + return labelSmall != null ? labelSmall : sized(11); + } + + /** + * Returns a copy with the supplied (non-null) roles overridden. Parameters + * follow the M3 role order declared in the Dart stub. + */ + public TextTheme copyWith(TextStyle displayLarge, TextStyle displayMedium, TextStyle displaySmall, + TextStyle headlineLarge, TextStyle headlineMedium, TextStyle headlineSmall, + TextStyle titleLarge, TextStyle titleMedium, TextStyle titleSmall, + TextStyle bodyLarge, TextStyle bodyMedium, TextStyle bodySmall, + TextStyle labelLarge, TextStyle labelMedium, TextStyle labelSmall) { + TextTheme c = new TextTheme(); + c.displayLarge = displayLarge != null ? displayLarge : this.displayLarge; + c.displayMedium = displayMedium != null ? displayMedium : this.displayMedium; + c.displaySmall = displaySmall != null ? displaySmall : this.displaySmall; + c.headlineLarge = headlineLarge != null ? headlineLarge : this.headlineLarge; + c.headlineMedium = headlineMedium != null ? headlineMedium : this.headlineMedium; + c.headlineSmall = headlineSmall != null ? headlineSmall : this.headlineSmall; + c.titleLarge = titleLarge != null ? titleLarge : this.titleLarge; + c.titleMedium = titleMedium != null ? titleMedium : this.titleMedium; + c.titleSmall = titleSmall != null ? titleSmall : this.titleSmall; + c.bodyLarge = bodyLarge != null ? bodyLarge : this.bodyLarge; + c.bodyMedium = bodyMedium != null ? bodyMedium : this.bodyMedium; + c.bodySmall = bodySmall != null ? bodySmall : this.bodySmall; + c.labelLarge = labelLarge != null ? labelLarge : this.labelLarge; + c.labelMedium = labelMedium != null ? labelMedium : this.labelMedium; + c.labelSmall = labelSmall != null ? labelSmall : this.labelSmall; + return c; + } + + /** + * Returns a copy in which every role's style has {@code bodyColor} applied + * to the body/label/title roles and {@code displayColor} to the + * display/headline roles, with an optional {@code fontFamily} and font-size + * scaling applied uniformly. Mirrors Flutter's {@code TextTheme.apply}. + * Parameter order matches the Dart stub. + */ + public TextTheme apply(String fontFamily, Double fontSizeFactor, Double fontSizeDelta, + Color displayColor, Color bodyColor, Object decoration, Object decorationColor) { + TextTheme c = new TextTheme(); + c.displayLarge = applyOne(displayLarge(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.displayMedium = applyOne(displayMedium(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.displaySmall = applyOne(displaySmall(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.headlineLarge = applyOne(headlineLarge(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.headlineMedium = applyOne(headlineMedium(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.headlineSmall = applyOne(headlineSmall(), displayColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.titleLarge = applyOne(titleLarge(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.titleMedium = applyOne(titleMedium(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.titleSmall = applyOne(titleSmall(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.bodyLarge = applyOne(bodyLarge(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.bodyMedium = applyOne(bodyMedium(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.bodySmall = applyOne(bodySmall(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.labelLarge = applyOne(labelLarge(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.labelMedium = applyOne(labelMedium(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + c.labelSmall = applyOne(labelSmall(), bodyColor, fontFamily, fontSizeFactor, fontSizeDelta); + return c; + } + + private static TextStyle applyOne(TextStyle base, Color color, String fontFamily, + Double fontSizeFactor, Double fontSizeDelta) { + return base.apply(color, null, fontFamily, fontSizeFactor, fontSizeDelta, null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Theme.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Theme.java new file mode 100644 index 00000000000..659a8f4b5e6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Theme.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Applies a {@link ThemeData} to a subtree — Flutter's {@code Theme} widget — + * and provides the static {@link #of(BuildContext)} lookup. As a widget it + * simply renders its {@code child}; the {@code data} it carries is what a + * descendant's {@code Theme.of(context)} resolves. When no {@code Theme} + * ancestor is present, {@link #of} falls back to the nearest + * {@link MaterialApp}'s effective theme (and a default {@link ThemeData} when + * there is none). + */ +public class Theme extends com.codename1.flutter.widgets.InheritedWidget { + + private ThemeData data; + + public Theme() { + } + + public void data(ThemeData v) { + this.data = v; + } + + public ThemeData getData() { + return data; + } + + @Override + public boolean updateShouldNotify(com.codename1.flutter.widgets.InheritedWidget oldWidget) { + return !(oldWidget instanceof Theme) || ((Theme) oldWidget).data != data; + } + + public static ThemeData of(BuildContext context) { + // An INHERITED lookup, so it is a hash lookup rather than a walk to the + // root, and so a widget that reads the theme is rebuilt when the theme + // changes. Theme.of is called by most themed widgets on every build; + // as an ancestor search it was one of the hottest paths in the runtime. + Theme t = context == null + ? null + : context.maybeDependOnInheritedWidgetOfExactType(Theme.class); + if (t != null && t.data != null) { + return t.data; + } + MaterialApp app = context == null + ? null + : context.findAncestorWidgetOfExactType(MaterialApp.class); + if (app != null) { + return app.effectiveTheme(); + } + return new ThemeData(); + } + + /** + * The brightness of the effective theme ({@code Theme.brightnessOf}). + */ + public static Brightness brightnessOf(BuildContext context) { + return of(context).brightness(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeData.java new file mode 100644 index 00000000000..f90b7cf12fe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeData.java @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.Color; + +/** + * Material theme configuration: a color scheme, text themes, brightness and the + * component sub-theme bundles. When no explicit color scheme is set one is + * derived from the default M3 seed honoring the brightness. M4 maps the ACTIVE + * ThemeData onto the CN1 UIManager through {@link ThemeDataAdapter}. + * + *

Named Dart constructor parameters map to setter methods; {@link #copyWith} + * returns a merged copy. Sub-theme bundles owned by other runtime areas + * (snackBarTheme, inputDecorationTheme, ...) are held opaquely as {@code Object}.

+ */ +public class ThemeData { + + private static final Color DEFAULT_SEED = new Color(0xFF6750A4); + + private ColorScheme colorScheme; + private TextTheme textTheme; + private TextTheme resolvedTextTheme; + private TextTheme primaryTextTheme = new TextTheme(); + private boolean useMaterial3 = true; + private Brightness brightness; + + private Color primaryColor; + private Color scaffoldBackgroundColor; + private Color canvasColor; + private Color cardColor; + private Color dividerColor; + private Color focusColor; + private Color highlightColor; + private Color splashColor; + private Color hintColor; + private Color disabledColor; + private Color shadowColor; + private Color indicatorColor; + private Color secondaryHeaderColor; + + private IconThemeData iconTheme; + private IconThemeData primaryIconTheme; + private AppBarTheme appBarTheme; + private ChipThemeData chipTheme; + private CheckboxThemeData checkboxTheme; + private CardTheme cardTheme; + private BottomAppBarThemeData bottomAppBarTheme; + private DividerThemeData dividerTheme; + private NavigationRailThemeData navigationRailTheme; + + private Object snackBarTheme; + private Object inputDecorationTheme; + private Object radioTheme; + private Object switchTheme; + private Object tooltipTheme; + private BottomSheetThemeData bottomSheetTheme; + private SliderThemeData sliderTheme; + private Object floatingActionButtonTheme; + private Object elevatedButtonTheme; + private Object textButtonTheme; + private Object outlinedButtonTheme; + private Object pageTransitionsTheme; + private Object visualDensity; + private Object typography; + private Object platform; + private Boolean applyElevationOverlayColor; + private String fontFamily; + + /** + * {@code ThemeData.dark}: a theme whose brightness is dark; the color + * scheme is derived from the default seed honoring that brightness. + */ + public static ThemeData dark(Boolean useMaterial3) { + ThemeData t = new ThemeData(); + t.brightness(Brightness.dark); + if (useMaterial3 != null) { + t.useMaterial3(useMaterial3); + } + return t; + } + + // ------------------------------------------------------------------ + // Named-parameter setters + // ------------------------------------------------------------------ + + public void colorScheme(ColorScheme v) { this.colorScheme = v; } + public void colorSchemeSeed(Color seed) { this.colorScheme = ColorScheme.fromSeed(seed, brightness); } + public void useMaterial3(boolean v) { this.useMaterial3 = v; } + public void brightness(Brightness v) { this.brightness = v; } + public void textTheme(TextTheme v) { this.textTheme = v; } + public void primaryTextTheme(TextTheme v) { this.primaryTextTheme = v; } + public void primaryColor(Color v) { this.primaryColor = v; } + public void scaffoldBackgroundColor(Color v) { this.scaffoldBackgroundColor = v; } + public void canvasColor(Color v) { this.canvasColor = v; } + public void cardColor(Color v) { this.cardColor = v; } + public void dividerColor(Color v) { this.dividerColor = v; } + public void focusColor(Color v) { this.focusColor = v; } + public void highlightColor(Color v) { this.highlightColor = v; } + public void splashColor(Color v) { this.splashColor = v; } + public void hintColor(Color v) { this.hintColor = v; } + public void disabledColor(Color v) { this.disabledColor = v; } + public void shadowColor(Color v) { this.shadowColor = v; } + public void indicatorColor(Color v) { this.indicatorColor = v; } + public void secondaryHeaderColor(Color v) { this.secondaryHeaderColor = v; } + public void iconTheme(IconThemeData v) { this.iconTheme = v; } + public void primaryIconTheme(IconThemeData v) { this.primaryIconTheme = v; } + public void appBarTheme(AppBarTheme v) { this.appBarTheme = v; } + public void chipTheme(ChipThemeData v) { this.chipTheme = v; } + public void checkboxTheme(CheckboxThemeData v) { this.checkboxTheme = v; } + public void cardTheme(CardTheme v) { this.cardTheme = v; } + public void bottomAppBarTheme(BottomAppBarThemeData v) { this.bottomAppBarTheme = v; } + public void dividerTheme(DividerThemeData v) { this.dividerTheme = v; } + public void navigationRailTheme(NavigationRailThemeData v) { this.navigationRailTheme = v; } + public void snackBarTheme(Object v) { this.snackBarTheme = v; } + public void inputDecorationTheme(Object v) { this.inputDecorationTheme = v; } + + /** + * {@code inputDecorationTheme}, when it is one this runtime understands. + * + *

Held opaquely because most of it is borders this port cannot draw, but + * the fill IS drawable and the studies depend on it: Rally's login fields + * are a dark fill on a dark page and rendered as white blocks without it.

+ */ + public InputDecorationThemeData inputDecorationTheme() { + return inputDecorationTheme instanceof InputDecorationThemeData + ? (InputDecorationThemeData) inputDecorationTheme : null; + } + public void radioTheme(Object v) { this.radioTheme = v; } + public void switchTheme(Object v) { this.switchTheme = v; } + public void tooltipTheme(Object v) { this.tooltipTheme = v; } + public void bottomSheetTheme(BottomSheetThemeData v) { this.bottomSheetTheme = v; } + public BottomSheetThemeData bottomSheetTheme() { return bottomSheetTheme != null ? bottomSheetTheme : new BottomSheetThemeData(); } + public void sliderTheme(SliderThemeData v) { this.sliderTheme = v; } + public SliderThemeData sliderTheme() { return sliderTheme != null ? sliderTheme : new SliderThemeData(); } + public void floatingActionButtonTheme(Object v) { this.floatingActionButtonTheme = v; } + public void elevatedButtonTheme(Object v) { this.elevatedButtonTheme = v; } + public void textButtonTheme(Object v) { this.textButtonTheme = v; } + public void outlinedButtonTheme(Object v) { this.outlinedButtonTheme = v; } + /// The text-selection colours a study sets ({@code TextSelectionThemeData}). Held for + /// the styling pass; selection rendering does not read it yet, but dropping the value + /// entirely made the setting look unsupported rather than pending. + private TextSelectionThemeData textSelectionTheme; + + public void textSelectionTheme(TextSelectionThemeData v) { + this.textSelectionTheme = v; + } + + public TextSelectionThemeData getTextSelectionTheme() { + return textSelectionTheme; + } + + public void pageTransitionsTheme(PageTransitionsTheme v) { this.pageTransitionsTheme = v; } + public void visualDensity(Object v) { this.visualDensity = v; } + public void typography(Object v) { this.typography = v; } + public void platform(Object v) { this.platform = v; } + public void applyElevationOverlayColor(boolean v) { this.applyElevationOverlayColor = v; } + public void fontFamily(String v) { this.fontFamily = v; } + + // ------------------------------------------------------------------ + // Getters + // ------------------------------------------------------------------ + + public boolean getUseMaterial3() { return useMaterial3; } + + /** The declared brightness, defaulting to light. */ + public Brightness brightness() { return brightness == null ? Brightness.light : brightness; } + + public ColorScheme colorScheme() { + if (colorScheme == null) { + colorScheme = ColorScheme.fromSeed(DEFAULT_SEED, brightness); + } + return colorScheme; + } + + /** + * The effective text theme: what {@code textTheme:} was given, otherwise the + * scale the {@code typography:} asks for, otherwise the Material 3 defaults. + * + *

A theme that names {@code Typography.material2018} is asking for the + * Material 2 type scale and inks, which differ from M3 in every size and in + * the colour of the display roles. Ignoring that (which is what returning a + * bare TextTheme did) rendered the gallery's demos in the wrong scale + * throughout, most visibly on its own typography page.

+ */ + public TextTheme textTheme() { + if (resolvedTextTheme == null) { + // No typography named means the Material 3 default, not "no type scale at + // all". An empty TextTheme leaves every role without a size or a line + // height, so text falls back to whatever the font measures -- which is a few + // logical pixels short per line, once per line, on every screen. + resolvedTextTheme = typography instanceof Typography + ? ((Typography) typography).resolve(brightness == Brightness.dark) + : Typography.material2021().resolve(brightness == Brightness.dark); + if (textTheme != null) { + // MERGED over the defaults, not used instead of them. A style handed in + // here states what it wants to change and inherits the rest, and the + // thing it most often does not state is the LINE HEIGHT: Reply restyles + // its roles with a font, a weight and a tracking, and Flutter keeps the + // type scale's height under them. Returning the app's styles raw dropped + // it, so every line was laid out at whatever the font measured -- three + // lines per mail card, 26 device pixels short each card, and a list that + // drifted further out of place the further down it went. + resolvedTextTheme = resolvedTextTheme.merge(textTheme); + } + } + return resolvedTextTheme; + } + public TextTheme primaryTextTheme() { return primaryTextTheme; } + public Color primaryColor() { return primaryColor; } + public Color scaffoldBackgroundColor() { return scaffoldBackgroundColor; } + public Color canvasColor() { return canvasColor; } + public Color cardColor() { return cardColor; } + public Color dividerColor() { return dividerColor; } + public Color focusColor() { return focusColor; } + public Color highlightColor() { return highlightColor; } + public Color splashColor() { return splashColor; } + public Color hintColor() { return hintColor; } + public Color disabledColor() { return disabledColor; } + public Color shadowColor() { return shadowColor; } + /** + * The ambient icon style, never null. + * + *

Flutter's {@code ThemeData()} fills every slot in, so app code reads + * {@code Theme.of(context).iconTheme} and calls {@code copyWith} on it + * without a null check — Shrine and Crane both build their theme as + * {@code _customIconTheme(base.iconTheme)}, which threw on a bare + * {@code ThemeData()} and took the whole study down with it.

+ */ + public IconThemeData iconTheme() { + if (iconTheme == null) { + iconTheme = defaultIconTheme(colorScheme().onSurface()); + } + return iconTheme; + } + + /** The icon style for surfaces painted in the primary colour, never null. */ + public IconThemeData primaryIconTheme() { + if (primaryIconTheme == null) { + primaryIconTheme = defaultIconTheme(colorScheme().onPrimary()); + } + return primaryIconTheme; + } + + /** Material's default icon: 24 logical pixels, fully opaque, in {@code color}. */ + private static IconThemeData defaultIconTheme(Color color) { + IconThemeData d = new IconThemeData(); + d.size(24); + d.opacity(1.0); + if (color != null) { + d.color(color); + } + return d; + } + public AppBarTheme appBarTheme() { return appBarTheme; } + public ChipThemeData chipTheme() { return chipTheme; } + public CheckboxThemeData checkboxTheme() { return checkboxTheme; } + public CardTheme cardTheme() { return cardTheme; } + public BottomAppBarThemeData bottomAppBarTheme() { return bottomAppBarTheme; } + public DividerThemeData dividerTheme() { return dividerTheme; } + public NavigationRailThemeData navigationRailTheme() { + return navigationRailTheme == null ? new NavigationRailThemeData() : navigationRailTheme; + } + public Object platform() { return platform; } + + private ThemeData shallowClone() { + ThemeData c = new ThemeData(); + c.colorScheme = colorScheme; + c.textTheme = textTheme; + c.primaryTextTheme = primaryTextTheme; + c.useMaterial3 = useMaterial3; + c.brightness = brightness; + c.primaryColor = primaryColor; + c.scaffoldBackgroundColor = scaffoldBackgroundColor; + c.canvasColor = canvasColor; + c.cardColor = cardColor; + c.dividerColor = dividerColor; + c.focusColor = focusColor; + c.highlightColor = highlightColor; + c.splashColor = splashColor; + c.hintColor = hintColor; + c.disabledColor = disabledColor; + c.shadowColor = shadowColor; + c.indicatorColor = indicatorColor; + c.secondaryHeaderColor = secondaryHeaderColor; + c.iconTheme = iconTheme; + c.primaryIconTheme = primaryIconTheme; + c.appBarTheme = appBarTheme; + c.chipTheme = chipTheme; + c.checkboxTheme = checkboxTheme; + c.cardTheme = cardTheme; + c.bottomAppBarTheme = bottomAppBarTheme; + c.dividerTheme = dividerTheme; + c.navigationRailTheme = navigationRailTheme; + c.snackBarTheme = snackBarTheme; + c.inputDecorationTheme = inputDecorationTheme; + c.radioTheme = radioTheme; + c.switchTheme = switchTheme; + c.tooltipTheme = tooltipTheme; + c.bottomSheetTheme = bottomSheetTheme; + c.floatingActionButtonTheme = floatingActionButtonTheme; + c.elevatedButtonTheme = elevatedButtonTheme; + c.textButtonTheme = textButtonTheme; + c.outlinedButtonTheme = outlinedButtonTheme; + c.pageTransitionsTheme = pageTransitionsTheme; + c.visualDensity = visualDensity; + c.typography = typography; + c.platform = platform; + c.applyElevationOverlayColor = applyElevationOverlayColor; + c.fontFamily = fontFamily; + return c; + } + + /** + * Returns a copy with the supplied (non-null) values overridden. Parameter + * order matches the Dart stub. + */ + public ThemeData copyWith(ColorScheme colorScheme, TextTheme textTheme, TextTheme primaryTextTheme, + Brightness brightness, Color primaryColor, Color scaffoldBackgroundColor, + Color canvasColor, Color cardColor, Color dividerColor, Color focusColor, + Color highlightColor, Color splashColor, Color hintColor, Color disabledColor, + IconThemeData iconTheme, AppBarTheme appBarTheme, ChipThemeData chipTheme, + CardTheme cardTheme, DividerThemeData dividerTheme, Object platform, + NavigationRailThemeData navigationRailTheme, + Boolean applyElevationOverlayColor, + BottomAppBarThemeData bottomAppBarTheme, BottomSheetThemeData bottomSheetTheme, + Object inputDecorationTheme, IconThemeData primaryIconTheme, + PageTransitionsTheme pageTransitionsTheme, Color indicatorColor, + TextSelectionThemeData textSelectionTheme, Object tabBarTheme, + Object snackBarTheme, Object tooltipTheme) { + ThemeData c = shallowClone(); + if (colorScheme != null) c.colorScheme = colorScheme; + if (textTheme != null) c.textTheme = textTheme; + if (primaryTextTheme != null) c.primaryTextTheme = primaryTextTheme; + if (brightness != null) c.brightness = brightness; + if (primaryColor != null) c.primaryColor = primaryColor; + if (scaffoldBackgroundColor != null) c.scaffoldBackgroundColor = scaffoldBackgroundColor; + if (canvasColor != null) c.canvasColor = canvasColor; + if (cardColor != null) c.cardColor = cardColor; + if (dividerColor != null) c.dividerColor = dividerColor; + if (focusColor != null) c.focusColor = focusColor; + if (highlightColor != null) c.highlightColor = highlightColor; + if (splashColor != null) c.splashColor = splashColor; + if (hintColor != null) c.hintColor = hintColor; + if (disabledColor != null) c.disabledColor = disabledColor; + if (iconTheme != null) c.iconTheme = iconTheme; + if (appBarTheme != null) c.appBarTheme = appBarTheme; + if (chipTheme != null) c.chipTheme = chipTheme; + if (cardTheme != null) c.cardTheme = cardTheme; + if (dividerTheme != null) c.dividerTheme = dividerTheme; + if (navigationRailTheme != null) c.navigationRailTheme = navigationRailTheme; + if (platform != null) c.platform = platform; + if (applyElevationOverlayColor != null) c.applyElevationOverlayColor = applyElevationOverlayColor; + // The sub-themes a study overrides when it re-skins the app. copyWith had no + // parameters for these, so a study's own bottom bar, sheet, input and tab styling + // was dropped on the floor and it rendered with the base Material theme. + if (bottomAppBarTheme != null) c.bottomAppBarTheme(bottomAppBarTheme); + if (bottomSheetTheme != null) c.bottomSheetTheme(bottomSheetTheme); + if (inputDecorationTheme != null) c.inputDecorationTheme(inputDecorationTheme); + if (primaryIconTheme != null) c.primaryIconTheme(primaryIconTheme); + if (pageTransitionsTheme != null) c.pageTransitionsTheme(pageTransitionsTheme); + if (indicatorColor != null) c.indicatorColor(indicatorColor); + if (textSelectionTheme != null) c.textSelectionTheme(textSelectionTheme); + if (snackBarTheme != null) c.snackBarTheme(snackBarTheme); + if (tooltipTheme != null) c.tooltipTheme(tooltipTheme); + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeDataAdapter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeDataAdapter.java new file mode 100644 index 00000000000..0c1d922c291 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ThemeDataAdapter.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.ui.Form; +import com.codename1.ui.plaf.Style; + +import java.util.HashMap; +import java.util.Map; + +/** + * Converts the ACTIVE {@link ThemeData} into a CN1 UIManager theme overlay + * for the Flutter* UIID namespace ({@code UIManager.addThemeProps}), plus + * direct styling of the Flutter-owned Form. + * + *

Theme isolation: the overlay only ever writes keys in the + * {@code Flutter*} UIID namespace — non-Flutter UIIDs (Label, Button, Form, + * Toolbar, ...) are never touched globally, so a Flutter subtree embedded in + * a regular CN1 app can't restyle the host. The Form and Toolbar backgrounds + * that Flutter DOES own (the runApp Form, the root Scaffold's Toolbar) are + * styled per-instance ({@link #applyToForm}, AppBarRenderElement) rather + * than through global theme constants.

+ * + *

Prop table (colors are CN1 theme hex strings):

+ *
    + *
  • colorScheme.surface — FlutterScaffold (the root canvas) bgColor, and + * Drawer/BottomNavigationBar backgrounds
  • + *
  • colorScheme.onSurface — FlutterText/FlutterRichText/FlutterIcon + * fgColor, FlutterIconButton fgColor
  • + *
  • colorScheme.primary/onPrimary — FlutterElevatedButton bg/fg; + * primary — FlutterTextButton/FlutterOutlinedButton fg
  • + *
  • colorScheme.surface/onSurface — FlutterAppBar bg/fg: a Material 3 app + * bar sits on the surface with an elevation tint rather than a saturated + * fill (this is the strip-mode bar; toolbar mode is styled per-instance + * by AppBarRenderElement, to the same default)
  • + *
+ * + *

State-metric invariance (see RenderElement.unifyStateMetrics): the + * overlay also writes the {@code sel#}/{@code press#}/{@code dis#} variants + * of every color so a focus/press state change never swaps in a stale + * base-theme color.

+ */ +public final class ThemeDataAdapter { + + private ThemeDataAdapter() { + } + + /** + * The pure prop table for a theme (headless-testable). Every key is in + * the Flutter* UIID namespace. + */ + public static Map themeProps(ThemeData t) { + ColorScheme cs = t.colorScheme(); + String surface = hex(cs.surface()); + String onSurface = hex(cs.onSurface()); + String primary = hex(cs.primary()); + String onPrimary = hex(cs.onPrimary()); + String inversePrimary = hex(cs.inversePrimary()); + + String surfaceVariant = hex(cs.surfaceVariant() != null + ? cs.surfaceVariant() : cs.surface()); + // Material's disabled tone: onSurface at 38% over the surface. + String onSurfaceFaded = hex(com.codename1.flutter.Color.alphaBlend( + new com.codename1.flutter.Color( + (0x61L << 24) | (cs.onSurface().rgb() & 0xFFFFFFL)), + new com.codename1.flutter.Color( + 0xFF000000L | (cs.surface().rgb() & 0xFFFFFF)))); + + Map p = new HashMap(); + + // The Flutter canvas (root host container) and full-bleed surfaces. + bg(p, "FlutterScaffold", surface); + bg(p, "FlutterDrawer", surface); + bg(p, "FlutterBottomNavigationBar", surface); + + // Content foregrounds. + fg(p, "FlutterText", onSurface); + fg(p, "FlutterRichText", onSurface); + fg(p, "FlutterIcon", onSurface); + fg(p, "FlutterListTile", onSurface); + + // Buttons (ButtonRenderElement also styles programmatically from + // Theme.of; these keep the UIID defaults consistent). + bg(p, "FlutterElevatedButton", primary); + fg(p, "FlutterElevatedButton", onPrimary); + fg(p, "FlutterTextButton", primary); + fg(p, "FlutterOutlinedButton", primary); + fg(p, "FlutterIconButton", onSurface); + + // Slider. Codename One paints the Material 3 look -- a thin rounded + // track with the active part in the accent and a round thumb -- only + // when the theme asks for it; without the constants it falls back to + // the legacy full-height fill, which is why the sliders demo came up as + // flat lavender bars with no thumb at all. The painter takes the + // inactive track from this style's background, the thumb from its + // foreground, and the active track from the *Full style. + bg(p, "FlutterSlider", surfaceVariant); + fg(p, "FlutterSlider", primary); + bg(p, "FlutterSliderFull", primary); + p.put("FlutterSlider.dis#bgColor", onSurfaceFaded); + p.put("FlutterSlider.dis#fgColor", onSurfaceFaded); + p.put("FlutterSliderFull.dis#bgColor", onSurfaceFaded); + + // App bar; the Material 3 ThemeData default background is surface + // (with an elevation tint), title/icons onSurface. + bg(p, "FlutterAppBar", surface); + fg(p, "FlutterAppBar", onSurface); + + // Switch: the CN1 Switch paints the thumb from the fgColor and the + // track from the bgColor, picking the selected style when ON and the + // unselected style when OFF (Switch.java:410/575 vs 431/634). Mirror + // Material 3: ON => primary track + onPrimary (white) thumb; + // OFF => a muted container track + outline (grey) thumb. + String onTrack = primary; + String onThumb = onPrimary; + String offTrack = hex(new Color(0xFFE7E0EC)); + String offThumb = hex(cs.outline()); + p.put("FlutterSwitch.sel#bgColor", onTrack); + p.put("FlutterSwitch.sel#fgColor", onThumb); + p.put("FlutterSwitch.sel#transparency", "255"); + p.put("FlutterSwitch.press#bgColor", onTrack); + p.put("FlutterSwitch.press#fgColor", onThumb); + p.put("FlutterSwitch.press#transparency", "255"); + p.put("FlutterSwitch.bgColor", offTrack); + p.put("FlutterSwitch.fgColor", offThumb); + p.put("FlutterSwitch.transparency", "255"); + + return p; + } + + /** + * Installs the theme's prop table as a UIManager overlay. Safe headless + * (logs and returns). + */ + public static void install(ThemeData t) { + try { + java.util.Hashtable h = + new java.util.Hashtable(themeProps(t)); + com.codename1.ui.plaf.UIManager.getInstance().addThemeProps(h); + } catch (Throwable err) { + log("could not install ThemeData overlay: " + err); + } + } + + /** + * Styles the Flutter-owned Form per-instance: the form and content pane + * backgrounds become colorScheme.surface. Instance styling (not theme + * constants) keeps non-Flutter UIIDs untouched globally. + */ + public static void applyToForm(Form f, ThemeData t) { + if (f == null) { + return; + } + try { + int surface = t.colorScheme().surface().rgb(); + paintSolid(f.getAllStyles(), surface); + paintSolid(f.getContentPane().getAllStyles(), surface); + } catch (Throwable err) { + log("could not style the Form from ThemeData: " + err); + } + } + + /** + * Solid-color background: BACKGROUND_NONE drops any theme background + * image/gradient that would otherwise paint OVER the bgColor. + */ + public static void paintSolid(Style s, int rgb) { + s.setBackgroundType(Style.BACKGROUND_NONE); + s.setBgColor(rgb); + s.setBgTransparency(255); + } + + /** + * Paints a Flutter {@link Color}, honoring its alpha. + * + *

{@code Colors.transparent} is {@code 0x00000000} — alpha 0 over black — + * and the gallery uses it for app bars and scaffolds that should show what + * is behind them. Painting only the RGB word turns every one of those into + * an opaque black band, so the alpha has to carry through: fully + * transparent means paint nothing at all.

+ */ + public static void paintColor(Style s, Color c) { + if (c == null) { + return; + } + int alpha = c.alpha(); + if (alpha <= 0) { + s.setBackgroundType(Style.BACKGROUND_NONE); + s.setBgTransparency(0); + return; + } + s.setBackgroundType(Style.BACKGROUND_NONE); + s.setBgColor(c.rgb()); + s.setBgTransparency(alpha); + } + + /** + * CN1 theme hex string for a color's 24-bit RGB portion. + */ + public static String hex(Color c) { + String s = Integer.toHexString(c.rgb()); + while (s.length() < 6) { + s = "0" + s; + } + return s; + } + + private static void bg(Map p, String uiid, String color) { + for (String state : STATES) { + p.put(uiid + "." + state + "bgColor", color); + p.put(uiid + "." + state + "transparency", "255"); + } + } + + private static void fg(Map p, String uiid, String color) { + for (String state : STATES) { + p.put(uiid + "." + state + "fgColor", color); + } + } + + private static final String[] STATES = {"", "sel#", "press#", "dis#"}; + + private static void log(String msg) { + try { + com.codename1.io.Log.p("Flutter runtime: " + msg); + } catch (Throwable t) { + // headless: Log has no storage backend + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Thumb.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Thumb.java new file mode 100644 index 00000000000..5d79b3470c1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Thumb.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * Identifies which thumb of a {@code RangeSlider} an interaction targets — + * Flutter's {@code Thumb} enum. The sliders demo references its constants when + * building custom range-slider semantics. + */ +public enum Thumb { + start, + end +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimeOfDay.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimeOfDay.java new file mode 100644 index 00000000000..fa6fa04c34d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimeOfDay.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; + +import dart.core.DateTime; + +/** + * A wall-clock time — hour and minute, no date ({@code TimeOfDay} in Flutter). + * new_gallery's picker demo builds one from a {@link DateTime}, compares + * instances for equality and renders it via {@link #format(BuildContext)}. + * + *

Named {@code hour:}/{@code minute:} constructor parameters map to same-named + * setters; {@link #fromDateTime(Object)} extracts the time-of-day from a + * {@code DateTime}.

+ */ +public class TimeOfDay { + + private int hour; + private int minute; + + public TimeOfDay() { + } + + // Named-parameter setters. Accept Dart's `int` (Java long) and narrow to the + // small hour/minute range. + public void hour(long v) { + this.hour = (int) v; + } + + public void minute(long v) { + this.minute = (int) v; + } + + public static TimeOfDay fromDateTime(Object time) { + TimeOfDay t = new TimeOfDay(); + if (time instanceof DateTime) { + DateTime dt = (DateTime) time; + t.hour = (int) dt.hour(); + t.minute = (int) dt.minute(); + } + return t; + } + + public static TimeOfDay now() { + return fromDateTime(DateTime.now()); + } + + public int hour() { + return hour; + } + + public int minute() { + return minute; + } + + /** Returns a copy with the supplied fields overridden (null keeps current). */ + public TimeOfDay replacing(Integer hour, Integer minute) { + TimeOfDay t = new TimeOfDay(); + t.hour = hour != null ? hour.intValue() : this.hour; + t.minute = minute != null ? minute.intValue() : this.minute; + return t; + } + + /** Formats using a 24-hour {@code HH:mm} pattern; localization is layered later. */ + public String format(BuildContext context) { + return pad(hour) + ":" + pad(minute); + } + + private static String pad(int v) { + return v < 10 ? "0" + v : Integer.toString(v); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TimeOfDay)) { + return false; + } + TimeOfDay other = (TimeOfDay) o; + return hour == other.hour && minute == other.minute; + } + + @Override + public int hashCode() { + return hour * 60 + minute; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimePickerDialog.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimePickerDialog.java new file mode 100644 index 00000000000..3d6a82062cc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TimePickerDialog.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Container; + +import dart.async.Future; + +/** + * A material time-picker dialog — Flutter's {@code TimePickerDialog}. This + * milestone renders a placeholder surface; the clock face and confirm/cancel + * flow land in a later pass. The initial time is retained. + */ +public class TimePickerDialog extends StatelessWidget { + + private String restorationId; + private TimeOfDay initialTime; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void initialTime(TimeOfDay v) { + this.initialTime = v; + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } + + /** Top-level {@code showTimePicker(...)} — shows the dialog and completes with the chosen time. */ + public static Future show(BuildContext context, TimeOfDay initialTime) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ToggleButtons.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ToggleButtons.java new file mode 100644 index 00000000000..d7b5a885604 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/ToggleButtons.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Row; + +import dart.core.DartList; + +/** + * A horizontal set of toggle buttons that share a selection state — Flutter's + * {@code ToggleButtons}. {@code isSelected} runs parallel to {@code children}; + * {@code onPressed} fires with the tapped index. This pass lays the children + * out in a {@link Row} and records styling for API shape; ripple, borders and + * selection painting are deferred. + */ +public class ToggleButtons extends StatelessWidget { + + private DartList children; + private DartList isSelected; + private Object onPressed; + private TextStyle textStyle; + private Object constraints; + private Color color; + private Color selectedColor; + private Color disabledColor; + private Color fillColor; + private Color focusColor; + private Color highlightColor; + private Color hoverColor; + private Color splashColor; + private boolean renderBorder = true; + private Color borderColor; + private Color selectedBorderColor; + private Color disabledBorderColor; + private Object borderRadius; + private Double borderWidth; + private Object direction; + + public void children(DartList v) { + this.children = v; + } + + public void isSelected(DartList v) { + this.isSelected = v; + } + + public void onPressed(dart.runtime.Funcs.VoidFunc1 v) { + this.onPressed = v; + } + + public void textStyle(TextStyle v) { + this.textStyle = v; + } + + public void constraints(Object v) { + this.constraints = v; + } + + public void color(Color v) { + this.color = v; + } + + public void selectedColor(Color v) { + this.selectedColor = v; + } + + public void disabledColor(Color v) { + this.disabledColor = v; + } + + public void fillColor(Color v) { + this.fillColor = v; + } + + public void focusColor(Color v) { + this.focusColor = v; + } + + public void highlightColor(Color v) { + this.highlightColor = v; + } + + public void hoverColor(Color v) { + this.hoverColor = v; + } + + public void splashColor(Color v) { + this.splashColor = v; + } + + public void renderBorder(boolean v) { + this.renderBorder = v; + } + + public void borderColor(Color v) { + this.borderColor = v; + } + + public void selectedBorderColor(Color v) { + this.selectedBorderColor = v; + } + + public void disabledBorderColor(Color v) { + this.disabledBorderColor = v; + } + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public void borderWidth(double v) { + this.borderWidth = v; + } + + public void direction(Object v) { + this.direction = v; + } + + public DartList getChildren() { + return children; + } + + public DartList getIsSelected() { + return isSelected; + } + + public Object getOnPressed() { + return onPressed; + } + + @Override + public Widget build(BuildContext context) { + Row row = new Row(); + row.children(children); + return row; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tooltip.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tooltip.java new file mode 100644 index 00000000000..0fa85946748 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Tooltip.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Element; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.HasChild; +import com.codename1.flutter.widgets.PassThroughRenderElement; + +/** + * A material tooltip that shows a label on long-press/hover. The label is + * retained but not yet shown; the child renders unchanged. See + * {@link PassThroughRenderElement}. + */ +public class Tooltip extends Widget implements HasChild { + + private String message; + private Object richMessage; + private double height; + private Object padding; + private Object margin; + private double verticalOffset; + private boolean preferBelow = true; + private boolean excludeFromSemantics; + private Object decoration; + private TextStyle textStyle; + private Object waitDuration; + private Object showDuration; + private Object triggerMode; + private Widget child; + + public void message(String v) { + this.message = v; + } + + public void richMessage(Object v) { + this.richMessage = v; + } + + public void height(double v) { + this.height = v; + } + + public void padding(Object v) { + this.padding = v; + } + + public void margin(Object v) { + this.margin = v; + } + + public void verticalOffset(double v) { + this.verticalOffset = v; + } + + public void preferBelow(boolean v) { + this.preferBelow = v; + } + + public void excludeFromSemantics(boolean v) { + this.excludeFromSemantics = v; + } + + public void decoration(Object v) { + this.decoration = v; + } + + public void textStyle(TextStyle v) { + this.textStyle = v; + } + + public void waitDuration(Object v) { + this.waitDuration = v; + } + + public void showDuration(Object v) { + this.showDuration = v; + } + + public void triggerMode(Object v) { + this.triggerMode = v; + } + + public void child(Widget v) { + this.child = v; + } + + public String getMessage() { + return message; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TooltipThemeData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TooltipThemeData.java new file mode 100644 index 00000000000..39ece68f674 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/TooltipThemeData.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.TextStyle; + +import dart.core.Duration; + +/** + * Material {@code TooltipThemeData}: write-once tooltip styling. Named Dart + * constructor parameters map to setter methods; unset values stay null. + */ +public class TooltipThemeData { + + private Double height; + private EdgeInsets padding; + private EdgeInsets margin; + private Double verticalOffset; + private Boolean preferBelow; + private Boolean excludeFromSemantics; + private Object decoration; + private TextStyle textStyle; + private Object textAlign; + private Duration waitDuration; + private Duration showDuration; + private Object triggerMode; + private Boolean enableFeedback; + + public void height(double v) { + this.height = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void margin(EdgeInsets v) { + this.margin = v; + } + + public void verticalOffset(double v) { + this.verticalOffset = v; + } + + public void preferBelow(boolean v) { + this.preferBelow = v; + } + + public void excludeFromSemantics(boolean v) { + this.excludeFromSemantics = v; + } + + public void decoration(Object v) { + this.decoration = v; + } + + public void textStyle(TextStyle v) { + this.textStyle = v; + } + + public void textAlign(Object v) { + this.textAlign = v; + } + + public void waitDuration(Duration v) { + this.waitDuration = v; + } + + public void showDuration(Duration v) { + this.showDuration = v; + } + + public void triggerMode(Object v) { + this.triggerMode = v; + } + + public void enableFeedback(boolean v) { + this.enableFeedback = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Typography.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Typography.java new file mode 100644 index 00000000000..43f3b3ff0c5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/Typography.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Colors; +import com.codename1.flutter.FontWeight; +import com.codename1.flutter.TextStyle; + +/** + * The set of geometry-specific {@link TextTheme}s for a Material design + * language — Flutter's {@code Typography}. A ThemeData is built from + * {@code Typography.material2018(...)}; its {@code englishLike}/{@code dense}/ + * {@code tall} themes are merged by script. This pass records the supplied + * themes; when none are given the getters return {@code null} and the caller's + * ThemeData falls back to its own defaults. + */ +public class Typography { + + private Object platform; + private TextTheme black; + private TextTheme white; + private TextTheme englishLike; + private TextTheme dense; + private TextTheme tall; + + private Typography() { + } + + /** + * Dart's {@code Typography.material2018(...)} factory. + * + *

Called from Dart as {@code Typography.material2018(platform: ...)} and + * nothing else, so every text theme arrives null and this used to record + * five nulls and change nothing. That is not what the caller asked for: a + * theme naming this typography is asking for the 2018 (Material 2) type + * scale, which is a different set of sizes, weights and INKS from the + * Material 3 defaults -- the gallery's demos are framed in it, so its + * typography page rendered every sample at roughly 60% of its size and in + * full black where the design is grey.

+ */ + public static Typography material2018(Object platform, TextTheme black, TextTheme white, + TextTheme englishLike, TextTheme dense, TextTheme tall) { + return build(platform, + black != null ? black : blackMountainView(), + white != null ? white : whiteMountainView(), + englishLike != null ? englishLike : englishLike2018(), + dense, tall); + } + + /** + * Flutter's {@code englishLike2018} geometry: size, weight and tracking per + * role, with no colour (the colour comes from the black/white theme this is + * merged with). + */ + public static TextTheme englishLike2018() { + TextTheme t = new TextTheme(); + t.displayLarge(style(96, FontWeight.w300, -1.5)); + t.displayMedium(style(60, FontWeight.w300, -0.5)); + t.displaySmall(style(48, FontWeight.w400, 0)); + t.headlineLarge(style(40, FontWeight.w400, 0.25)); + t.headlineMedium(style(34, FontWeight.w400, 0.25)); + t.headlineSmall(style(24, FontWeight.w400, 0)); + t.titleLarge(style(20, FontWeight.w500, 0.15)); + t.titleMedium(style(16, FontWeight.w400, 0.15)); + t.titleSmall(style(14, FontWeight.w500, 0.1)); + t.bodyLarge(style(16, FontWeight.w400, 0.5)); + t.bodyMedium(style(14, FontWeight.w400, 0.25)); + t.bodySmall(style(12, FontWeight.w400, 0.4)); + t.labelLarge(style(14, FontWeight.w500, 1.25)); + t.labelMedium(style(12, FontWeight.w400, 1.5)); + t.labelSmall(style(10, FontWeight.w400, 1.5)); + return t; + } + + /** + * Flutter's {@code englishLike2021} geometry -- the Material 3 type scale. + * + *

Every role here carries a LINE HEIGHT, which the 2018 scale does not, and that + * is the difference that shows. Without it a line of text is as tall as the font + * happens to be, and the error repeats once per line: the mail study's card is three + * lines of text, each about three logical pixels short, so every card came out 26 + * device pixels shorter than the reference and the whole list drifted upward the + * further down it went. Measured against the reference, the card's content is 128 + * logical pixels tall and ours was 119 -- 20 + 16 + 4 + 32 + 16 + 20 against the same + * sum with the three text heights taken from the font instead.

+ */ + public static TextTheme englishLike2021() { + TextTheme t = new TextTheme(); + t.displayLarge(style(57, FontWeight.w400, -0.25, 1.12)); + t.displayMedium(style(45, FontWeight.w400, 0, 1.16)); + t.displaySmall(style(36, FontWeight.w400, 0, 1.22)); + t.headlineLarge(style(32, FontWeight.w400, 0, 1.25)); + t.headlineMedium(style(28, FontWeight.w400, 0, 1.29)); + t.headlineSmall(style(24, FontWeight.w400, 0, 1.33)); + t.titleLarge(style(22, FontWeight.w400, 0, 1.27)); + t.titleMedium(style(16, FontWeight.w500, 0.15, 1.50)); + t.titleSmall(style(14, FontWeight.w500, 0.1, 1.43)); + t.labelLarge(style(14, FontWeight.w500, 0.1, 1.43)); + t.labelMedium(style(12, FontWeight.w500, 0.5, 1.33)); + t.labelSmall(style(11, FontWeight.w500, 0.5, 1.45)); + t.bodyLarge(style(16, FontWeight.w400, 0.5, 1.50)); + t.bodyMedium(style(14, FontWeight.w400, 0.25, 1.43)); + t.bodySmall(style(12, FontWeight.w400, 0.4, 1.33)); + return t; + } + + /** The Material 3 default: the 2021 scale with the brightness's inks. */ + public static Typography material2021() { + return build(null, blackMountainView(), whiteMountainView(), + englishLike2021(), null, null); + } + + /** Flutter's {@code blackMountainView} inks: display roles grey, body roles near-black. */ + public static TextTheme blackMountainView() { + return inks(Colors.black54, Colors.black87, Colors.black); + } + + /** Flutter's {@code whiteMountainView} inks, for a dark theme. */ + public static TextTheme whiteMountainView() { + return inks(Colors.white70, Colors.white, Colors.white); + } + + private static TextTheme inks(Color display, Color body, Color emphasis) { + TextTheme t = new TextTheme(); + t.displayLarge(ink(display)); + t.displayMedium(ink(display)); + t.displaySmall(ink(display)); + t.headlineLarge(ink(display)); + t.headlineMedium(ink(display)); + t.headlineSmall(ink(body)); + t.titleLarge(ink(body)); + t.titleMedium(ink(body)); + t.titleSmall(ink(emphasis)); + t.bodyLarge(ink(body)); + t.bodyMedium(ink(body)); + t.bodySmall(ink(display)); + t.labelLarge(ink(body)); + t.labelMedium(ink(body)); + t.labelSmall(ink(emphasis)); + return t; + } + + private static TextStyle style(double size, FontWeight weight, double tracking) { + TextStyle t = new TextStyle(); + t.fontSize(size); + t.fontWeight(weight); + t.letterSpacing(tracking); + return t; + } + + /// The same, with the line height the 2021 scale states for the role. + private static TextStyle style(double size, FontWeight weight, double tracking, + double height) { + TextStyle t = style(size, weight, tracking); + t.height(height); + return t; + } + + private static TextStyle ink(Color c) { + TextStyle t = new TextStyle(); + t.color(c); + return t; + } + + /** + * The text theme a {@link ThemeData} should use when it names this + * typography and no explicit textTheme: the geometry, with the ink for the + * requested brightness layered on top — Flutter's + * {@code defaultTextTheme.merge(...)}. + */ + public TextTheme resolve(boolean dark) { + TextTheme geometry = englishLike != null ? englishLike : englishLike2018(); + TextTheme colours = dark + ? (white != null ? white : whiteMountainView()) + : (black != null ? black : blackMountainView()); + return geometry.merge(colours); + } + + /** Dart's {@code Typography.material2014(...)} factory. */ + public static Typography material2014(Object platform, TextTheme black, TextTheme white, + TextTheme englishLike, TextTheme dense, TextTheme tall) { + return build(platform, black, white, englishLike, dense, tall); + } + + private static Typography build(Object platform, TextTheme black, TextTheme white, + TextTheme englishLike, TextTheme dense, TextTheme tall) { + Typography t = new Typography(); + t.platform = platform; + t.black = black; + t.white = white; + t.englishLike = englishLike; + t.dense = dense; + t.tall = tall; + return t; + } + + public TextTheme black() { + return black; + } + + public TextTheme white() { + return white; + } + + public TextTheme englishLike() { + return englishLike; + } + + public TextTheme dense() { + return dense; + } + + public TextTheme tall() { + return tall; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/UserAccountsDrawerHeader.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/UserAccountsDrawerHeader.java new file mode 100644 index 00000000000..ea481bea033 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/UserAccountsDrawerHeader.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; + +/** + * A Material drawer header showing the signed-in account — Flutter's + * {@code UserAccountsDrawerHeader}. This pass renders the account picture, name + * and email stacked in a {@link Column}; the themed background, details arrow and + * other-account switching are captured for a later render pass. + */ +public class UserAccountsDrawerHeader extends StatelessWidget { + + private Object decoration; + private Object margin; + private Widget currentAccountPicture; + private DartList otherAccountsPictures; + private Widget accountName; + private Widget accountEmail; + private Object onDetailsPressed; + private Object arrowColor; + + public void decoration(Object v) { this.decoration = v; } + public void margin(Object v) { this.margin = v; } + public void currentAccountPicture(Widget v) { this.currentAccountPicture = v; } + public void otherAccountsPictures(DartList v) { this.otherAccountsPictures = v; } + public void accountName(Widget v) { this.accountName = v; } + public void accountEmail(Widget v) { this.accountEmail = v; } + public void onDetailsPressed(Object v) { this.onDetailsPressed = v; } + public void arrowColor(Object v) { this.arrowColor = v; } + + @Override + public Widget build(BuildContext context) { + DartList children = new DartList(); + if (currentAccountPicture != null) { + children.add(currentAccountPicture); + } + if (accountName != null) { + children.add(accountName); + } + if (accountEmail != null) { + children.add(accountEmail); + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.start); + col.children(children); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VerticalDivider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VerticalDivider.java new file mode 100644 index 00000000000..bf19342af3b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VerticalDivider.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.ColoredBox; +import com.codename1.flutter.widgets.SizedBox; + +/** + * A thin vertical line, the vertical sibling of {@link Divider} — Flutter's + * {@code VerticalDivider}. Occupies {@code width} horizontally and paints a + * line {@code thickness} wide in {@code color}. This pass renders a full-height + * box of the given width, filled when a color is supplied. + */ +public class VerticalDivider extends StatelessWidget { + + private Double width; + private Double thickness; + private Color color; + + public void width(double v) { + this.width = v; + } + + public void thickness(double v) { + this.thickness = v; + } + + public void indent(double v) { + } + + public void endIndent(double v) { + } + + public void color(Color v) { + this.color = v; + } + + @Override + public Widget build(BuildContext context) { + SizedBox box = new SizedBox(); + box.width(width != null ? width : 16.0); + if (color != null) { + ColoredBox cb = new ColoredBox(); + cb.color(color); + box.child(cb); + } + return box; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VisualDensity.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VisualDensity.java new file mode 100644 index 00000000000..1e22d732911 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/VisualDensity.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * A per-axis density adjustment applied to a component's compactness, + * mirroring Flutter's {@code VisualDensity}. Values are in abstract density + * units in the range [-4, 4] where 0 is the un-adjusted baseline. + */ +public class VisualDensity { + + /** The standard, un-adjusted density. */ + public static final VisualDensity standard = new VisualDensity(0.0, 0.0); + /** A looser density for pointer-first platforms. */ + public static final VisualDensity comfortable = new VisualDensity(-1.0, -1.0); + /** A tighter density. */ + public static final VisualDensity compact = new VisualDensity(-2.0, -2.0); + /** + * The platform-appropriate default (compact on desktop, standard on + * touch). This pass has no adaptive backend, so it aliases + * {@link #standard}. + */ + public static final VisualDensity adaptivePlatformDensity = standard; + + private double horizontal; + private double vertical; + + public VisualDensity() { + } + + public VisualDensity(double horizontal, double vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + + public void horizontal(double v) { + this.horizontal = v; + } + + public void vertical(double v) { + this.vertical = v; + } + + public double getHorizontal() { + return horizontal; + } + + public double getVertical() { + return vertical; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetState.java new file mode 100644 index 00000000000..762fcac935d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetState.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +/** + * The Material-3 rename of {@link MaterialState} (identical members). Newer + * Flutter aliases the whole {@code MaterialStateX} family to + * {@code WidgetStateX}; both spellings appear in real apps. + */ +public enum WidgetState { + hovered, focused, pressed, dragged, selected, scrolledUnder, disabled, error +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetStateProperty.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetStateProperty.java new file mode 100644 index 00000000000..e809f1fa395 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/material/WidgetStateProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; + +import dart.core.DartSet; +import dart.runtime.Funcs; + +/** + * The Material-3 rename of {@link MaterialStateProperty} (identical shape). + * Newer Flutter aliases {@code MaterialStateProperty} to + * {@code WidgetStateProperty}; both spellings resolve. + */ +public class WidgetStateProperty { + + private final Funcs.Func1, Color> resolver; + private final Object constant; + private final boolean isConstant; + + private WidgetStateProperty(Funcs.Func1, Color> resolver, + Object constant, boolean isConstant) { + this.resolver = resolver; + this.constant = constant; + this.isConstant = isConstant; + } + + /** A property that is {@code value} in every state. */ + public static WidgetStateProperty all(Object value) { + return new WidgetStateProperty(null, value, true); + } + + /** A property computed from the active state set on each read. */ + public static WidgetStateProperty resolveWith(Funcs.Func1, Color> resolver) { + return new WidgetStateProperty(resolver, null, false); + } + + /** Evaluates this property for {@code states}. */ + public Object resolve(DartSet states) { + if (isConstant) { + return constant; + } + return resolver == null ? null : resolver.call(states); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/DialogRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/DialogRoute.java new file mode 100644 index 00000000000..a6569ecaccf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/DialogRoute.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A modal route that displays a dialog above the current page — Flutter's + * {@code DialogRoute}. Pushed onto the {@link Navigator}; the {@code builder} + * produces the dialog content lazily when the route is shown. This pass records + * the builder, barrier appearance and settings for API shape; the actual modal + * presentation is handled by the navigation layer. + * + * @param the value the route completes with when popped + */ +public class DialogRoute extends Route { + + private BuildContext context; + private Funcs.Func1 builder; + private Object settings; + private Color barrierColor; + private boolean barrierDismissible = true; + private String barrierLabel; + private boolean useSafeArea = true; + + public void context(BuildContext v) { + this.context = v; + } + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void settings(Object v) { + this.settings = v; + } + + public void barrierColor(Color v) { + this.barrierColor = v; + } + + public void barrierDismissible(boolean v) { + this.barrierDismissible = v; + } + + public void barrierLabel(String v) { + this.barrierLabel = v; + } + + public void useSafeArea(boolean v) { + this.useSafeArea = v; + } + + public void themes(Object v) { + } + + public void anchorPoint(Object v) { + } + + public void traversalEdgeBehavior(Object v) { + } + + public Funcs.Func1 getBuilder() { + return builder; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/MaterialPageRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/MaterialPageRoute.java new file mode 100644 index 00000000000..2c4737d889c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/MaterialPageRoute.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A route whose page is produced by a {@code WidgetBuilder}. Pushed with + * {@link Navigator#push}; the builder runs lazily when the route's element + * tree mounts, receiving a BuildContext inside the NEW page's tree. + * + *

The type parameter {@code T} is the route's result type (the value a + * {@code Navigator.pop(result)} returns). It is phantom in this runtime but + * lets transpiled {@code MaterialPageRoute} subclasses and type arguments + * resolve.

+ * + * @param the route's pop-result type + */ +public class MaterialPageRoute extends Route { + + private Funcs.Func1 builder; + private Object maintainState; + private Object fullscreenDialog; + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + /** Flutter's {@code maintainState} — whether the route stays mounted when covered. */ + public void maintainState(Object v) { + this.maintainState = v; + } + + /** Flutter's {@code fullscreenDialog} — whether the route is a full-screen modal. */ + public void fullscreenDialog(Object v) { + this.fullscreenDialog = v; + } + + @Override + public boolean isFullscreenDialog() { + return Boolean.TRUE.equals(fullscreenDialog); + } + + public Funcs.Func1 getBuilder() { + return builder; + } + + @Override + public Widget buildPage(BuildContext context) { + Funcs.Func1 b = getBuilder(); + return b == null ? null : b.call(context); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Navigator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Navigator.java new file mode 100644 index 00000000000..e14b717bbdc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Navigator.java @@ -0,0 +1,812 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Dialogs; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.Toolbar; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Flutter route stack, mapped onto CN1 Forms: every {@link #push} mounts + * the route's widget tree in a NEW Form (the same mounting pattern as + * {@code FlutterUI.runApp}) and shows it; {@link #pop} unmounts that route's + * element tree and returns to the previous Form via {@code showBack()}. + * + *

M3 keeps ONE static stack per app process. The base route (the + * {@code runApp} form) is implicit and never on the stack, so popping with + * an empty stack — popping the last route — is a no-op. {@code pop} first + * consults the dialog stack ({@link Dialogs}): a {@code Navigator.pop} + * inside an open dialog's action dismisses that dialog, matching Flutter's + * dialogs-are-routes behavior.

+ * + *

When the pushed tree's root is a {@link + * com.codename1.flutter.material.Scaffold} its root-mode mounting binds a + * CN1 Toolbar to the new Form; the navigator then wires the Toolbar's BACK + * arrow to {@code pop}.

+ * + *

Headless (no Display): the stack bookkeeping still runs — no Forms are + * created and the route's builder is not invoked (it would run on mount).

+ */ +public class Navigator extends StatelessWidget { + + private static final List stack = new ArrayList(); + + // --- Nested Navigator widget (Flutter's embeddable Navigator) -------------- + // A Navigator can also be used AS a widget (Reply's mail navigator, several + // demos): it owns a private route table via onGenerateRoute/initialRoute. This + // pass records the configuration and renders the initial route's page so the + // subtree has content; the private route stack is deferred. + private Object navigatorKey; + private String initialRoute; + private dart.runtime.Funcs.Func1 onGenerateRoute; + private dart.runtime.Funcs.Func1 onUnknownRoute; + private String restorationScopeId; + private Object observers; + private Object pages; + private Object onPopPage; + + public Navigator() { + } + + public Navigator(com.codename1.flutter.Key key) { + key(key); + } + + public void navigatorKey(Object v) { + this.navigatorKey = v; + } + + public void initialRoute(String v) { + this.initialRoute = v; + } + + public void onGenerateRoute(dart.runtime.Funcs.Func1 v) { + this.onGenerateRoute = v; + } + + public void onUnknownRoute(dart.runtime.Funcs.Func1 v) { + this.onUnknownRoute = v; + } + + public void restorationScopeId(String v) { + this.restorationScopeId = v; + } + + public void observers(Object v) { + this.observers = v; + } + + public void pages(Object v) { + this.pages = v; + } + + public void onPopPage(Object v) { + this.onPopPage = v; + } + + @Override + public com.codename1.flutter.Widget build(com.codename1.flutter.BuildContext context) { + if (onGenerateRoute != null) { + RouteSettings settings = new RouteSettings(); + settings.name(initialRoute); + Object route = onGenerateRoute.call(settings); + if (route instanceof Route) { + com.codename1.flutter.Widget page = ((Route) route).buildPage(context); + if (page != null) { + return page; + } + com.codename1.flutter.FlutterErrorReport.noRoute( + String.valueOf(initialRoute), + "a nested Navigator's " + route.getClass().getName() + + " built no page"); + } else if (route != null) { + com.codename1.flutter.FlutterErrorReport.noRoute( + String.valueOf(initialRoute), + "onGenerateRoute returned a " + route.getClass().getName() + + ", which is not a Route"); + } + } + return null; + } + + /** + * Pushes the route: builds its page in a new Form and shows it. + * + * @return the route's result: completes when the route is popped, with the value + * given to {@code pop}, or null -- Flutter's {@code Future}. It used to + * answer nothing, so {@code await Navigator.push(...)} could not wait for + * the page to close. + */ + public static dart.async.Future push(BuildContext context, MaterialPageRoute route) { + RouteEntry e = new RouteEntry(route); + if (Display.isInitialized()) { + // The form to come BACK to, which during a pop's transition is not the + // form on screen. + // + // Display.getCurrent() still answers the page being left until the + // transition that replaces it finishes, so a push issued while a pop is + // still animating recorded the popped page as its own previous form -- + // and that page then never left. Driven route to route, the first study + // opened sat behind every screen for the rest of the run, and popping + // back from any of them returned to it rather than to the page beneath. + e.previousForm = returningTo != null + ? returningTo : Display.getInstance().getCurrent(); + RenderHost host = FlutterUI.mountInNewForm(new RouteWidget(route), pushingElement(context)); + e.form = host.form(); + e.rootElement = host.rootElement(); + Toolbar tb = e.form.getToolbar(); + if (tb != null && host.isFormToolbarBound()) { + // a root Scaffold bound its AppBar to the Form Toolbar; give + // that bar the material back arrow + tb.setBackCommand("", new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + pop(null); + } + }); + } + // The unbound case -- a page that draws its own AppBar in-canvas, as the + // gallery's demo pages do -- needs the Form's Toolbar hidden, or the page + // carries two bars and the Flutter canvas shrinks by the toolbar inset. + // FlutterUI.mountInNewForm does that for every Form it builds, this one + // included, so there is nothing to do here. + stack.add(e); + // Without this the Form replaces the screen outright and the page is fully + // painted on the first frame after the tap. Flutter always animates a route in. + RouteTransitions.apply(e.form, route); + e.form.show(); + } else { + stack.add(e); + } + return e.popped.future(); + } + + /** + * Whether a pop would close anything: an open dialog -- which pop dismisses first, + * as the route above the page -- or a pushed page. The page stack alone was checked, + * so with a dialog over the base route maybePop answered false without closing it + * and a system-back handler could not dismiss the dialog. + */ + static boolean poppable() { + return com.codename1.flutter.material.Dialogs.openDialogCount() > 0 || !stack.isEmpty(); + } + + /** + * Pops the topmost dialog if one is open, else the topmost pushed route. + * Popping the last (base) route is a no-op. + */ + public static void pop(BuildContext context) { + pop(context, null); + } + + /** + * Flutter's {@code Navigator.pop(context, result)}: pops as {@link #pop(BuildContext)} + * does and completes the popped route's future with {@code result}. + */ + public static void pop(BuildContext context, Object result) { + if (Dialogs.popTopDialog(result)) { + return; + } + if (stack.isEmpty()) { + // the base runApp route: never popped + return; + } + RouteEntry e = stack.remove(stack.size() - 1); + e.popped.complete(result); + if (e.previousForm != null) { + // The way out mirrors the way in. showBack() plays the transition of the form + // being RETURNED TO, in reverse -- and that form carries whatever animation + // brought IT on screen, which has nothing to do with the route being dismissed. + // Leaving it alone is why closing the compose page slid the inbox in from the + // side after the compose page had grown out of the button: the push was a + // container transform and the pop was the inbox's own page transition. + RouteTransitions.apply(e.previousForm, e.route); + // Tear the popped page down only once the transition that shows the page + // BEHIND it has finished. + // + // The transition photographs the outgoing Form the moment it starts, so + // unmounting first handed it a Form with nothing left in it: the container + // transform folded a blank card back into the button while the reference + // folds away the page you were reading. onShowCompleted fires after the + // animation queue drains, which is exactly when the popped page stops being + // on screen. + unmountWhenShown(e.previousForm, e.rootElement); + returningTo = e.previousForm; + e.previousForm.showBack(); + } else if (e.rootElement != null) { + FlutterUI.unmountTree(e.rootElement); + } + } + + /// The form a pop is on its way back to, for as long as that is still in + /// flight. Null at rest. + private static Form returningTo; + + /// Unmounts {@code tree} the first time {@code form} finishes being shown. + private static void unmountWhenShown(Form form, final Element tree) { + if (tree == null) { + return; + } + final Form f = form; + final ActionListener[] once = + new ActionListener[1]; + once[0] = new ActionListener() { + @Override + public void actionPerformed(ActionEvent ev) { + f.removeShowListener(once[0]); + if (returningTo == f) { + returningTo = null; + } + FlutterUI.unmountTree(tree); + } + }; + form.addShowListener(once[0]); + } + + /** + * The number of pushed routes (the implicit base route not included). + */ + public static int stackSize() { + return stack.size(); + } + + // --- Named routes --------------------------------------------------------- + // Flutter resolves a route NAME against the app's table: the `routes` map + // first, then onGenerateRoute, then onUnknownRoute. MaterialApp publishes + // its table here on build so a pushNamed from anywhere below can resolve it + // — the gallery reaches every one of its demos this way + // (Navigator.of(context).restorablePushNamed('/demo/')). + + /** + * One app's route table: the {@code routes} map plus the two callbacks. + * + *

Per app, not per process. A study inside the gallery is a whole + * {@code MaterialApp} of its own, and while one global table was kept the + * study's table replaced the gallery's the moment the study built — + * permanently, since popping back does not rebuild the outer app. After + * one visit to Reply, every other study route resolved to nothing ("no + * route for '/shrine'") and re-entering Reply mounted its page under a + * table, and a root scope, that belonged to the wrong app.

+ */ + public static final class RouteTable { + + private final Object routes; + private final Funcs.Func1 generate; + private final Funcs.Func1 unknown; + + RouteTable(Object routes, Funcs.Func1 generate, + Funcs.Func1 unknown) { + this.routes = routes; + this.generate = generate; + this.unknown = unknown; + } + + /** + * Flutter's order: the {@code routes} map first, then + * {@code onGenerateRoute}, then {@code onUnknownRoute}. + */ + @SuppressWarnings("unchecked") + Route resolve(RouteSettings settings) { + if (routes instanceof java.util.Map) { + Object builder = ((java.util.Map) routes).get(settings.name()); + if (builder instanceof Funcs.Func1) { + MaterialPageRoute route = new MaterialPageRoute(); + route.builder((Funcs.Func1) builder); + route.settings(settings); + return route; + } + } + Route r = generate != null ? generate.call(settings) : null; + if (r == null && unknown != null) { + r = unknown.call(settings); + } + return r; + } + } + + /** + * Implemented by a widget that publishes a route table — {@code MaterialApp} + * and anything else that behaves like an app root. The table hangs off the + * widget so an ancestor walk finds the nearest one. + */ + public interface RouteTableHost { + + void routeTable(RouteTable table); + + RouteTable routeTable(); + } + + /** The outermost app's table: what a context-less push resolves against. */ + private static RouteTable rootTable; + + /** + * Publishes an app's route table; called by MaterialApp on build. + * + * @return whether this is the ROOT app — no other app above it — which is + * also what decides who owns the process-wide root scope + */ + public static boolean installRouteTable(BuildContext owner, Object routes, + Funcs.Func1 onGenerateRoute, + Funcs.Func1 onUnknownRoute) { + RouteTable table = new RouteTable(routes, onGenerateRoute, onUnknownRoute); + Element self = owner instanceof Element ? (Element) owner : null; + if (self != null && self.widget() instanceof RouteTableHost) { + ((RouteTableHost) self.widget()).routeTable(table); + } + boolean root = self == null || hostAbove(self.ancestor()) == null; + if (root) { + rootTable = table; + } + return root; + } + + /** Publishes a table with no owning element — tests and bare trees. */ + public static void installRouteTable(Object routes, + Funcs.Func1 onGenerateRoute, + Funcs.Func1 onUnknownRoute) { + installRouteTable(null, routes, onGenerateRoute, onUnknownRoute); + } + + /** Test / hot-restart hook: forgets the installed route table. */ + public static void resetRouteTable() { + rootTable = null; + } + + /** The nearest route-table host at or above {@code e}, or null. */ + private static RouteTableHost hostAbove(Element e) { + while (e != null) { + if (e.widget() instanceof RouteTableHost + && ((RouteTableHost) e.widget()).routeTable() != null) { + return (RouteTableHost) e.widget(); + } + e = e.ancestor(); + } + return null; + } + + /** + * Resolves a route name against the table of the nearest app above + * {@code context}, or — for a push from outside the tree — the root app's. + * Null when nothing claims the name. + */ + public static Route resolveRoute(BuildContext context, String name, Object arguments) { + RouteSettings settings = new RouteSettings(); + settings.name(name); + settings.arguments(arguments); + + RouteTableHost host = context instanceof Element + ? hostAbove((Element) context) : null; + Route r = host != null ? host.routeTable().resolve(settings) : null; + if (r == null && rootTable != null + && (host == null || host.routeTable() != rootTable)) { + // A nested app that does not know the name: fall through to the + // app that owns the whole process, which is where a name it has + // never heard of ('/demo/banner' from inside a study) belongs. + r = rootTable.resolve(settings); + } + return r; + } + + /** Resolves against the root app's table. */ + public static Route resolveRoute(String name, Object arguments) { + return resolveRoute(null, name, arguments); + } + + /** + * Resolves a route name and pushes it, returning whether anything was + * pushed. An unresolvable name is reported rather than thrown: a dead link + * in one corner of an app should not take the app down — but it IS a + * failure, and a screen that never opened must not read as a screen that + * opened cleanly. + */ + public static boolean pushNamed(BuildContext context, String name, Object arguments) { + return pushNamedForResult(context, name, arguments) != null; + } + + /** + * {@link #pushNamed(BuildContext, String, Object)} answering the route's result + * future, or null when nothing was pushed. + */ + static dart.async.Future pushNamedForResult(BuildContext context, String name, Object arguments) { + Route route = resolveRoute(context, name, arguments); + if (route instanceof MaterialPageRoute) { + // Name the screen so any error it raises reports where it happened. + com.codename1.flutter.FlutterErrorReport.route(name); + return push(context, (MaterialPageRoute) route); + } + com.codename1.flutter.FlutterErrorReport.noRoute(name, route == null ? null + : "(unsupported route type " + route.getClass().getName() + ")"); + return null; + } + + /** + * pushReplacementNamed: the route is resolved FIRST, and the current one is popped + * (completing with {@code result}, as Flutter's is) only when there is a replacement + * to show. Popping first sent the user back a page whenever the name was unknown or + * resolved to an unsupported route -- the replacement failed and the page it was to + * replace was gone. + */ + static dart.async.Future replaceNamed(BuildContext context, String name, Object arguments, + Object result) { + Route route = resolveRoute(context, name, arguments); + if (!(route instanceof MaterialPageRoute)) { + com.codename1.flutter.FlutterErrorReport.noRoute(name, route == null ? null + : "(unsupported route type " + route.getClass().getName() + ")"); + return nothingPushed(); + } + pop(context, result); + com.codename1.flutter.FlutterErrorReport.route(name); + return push(context, (MaterialPageRoute) route); + } + + /** + * What a push answers when nothing could be pushed. An already-completed null: a + * caller awaiting it must not hang on a page that never opened, and the failure has + * been reported where it happened. + */ + private static dart.async.Future nothingPushed() { + return dart.async.Future.value(null); + } + + private static dart.async.Future orNothing(dart.async.Future pushed) { + return pushed != null ? pushed : nothingPushed(); + } + + /** A push of any route: a MaterialPageRoute is shown, anything else is reported. */ + private static dart.async.Future pushAny(BuildContext context, Object route) { + if (route instanceof MaterialPageRoute) { + return push(context, (MaterialPageRoute) route); + } + com.codename1.flutter.FlutterErrorReport.noRoute(null, route == null ? "(null route)" + : "(unsupported route type " + route.getClass().getName() + ")"); + return nothingPushed(); + } + + /** + * Test / hot-restart hook: forgets all pushed routes without unmounting. + */ + public static void reset() { + stack.clear(); + rootScopeContext = null; + } + + /** + * The single navigator handle for this process. Its {@code pop} delegates + * to {@link Navigator#pop}; the restoration-push helpers are no-ops that + * return an informational id (restoration is not persisted). + */ + private static final NavigatorState STATE = new NavigatorState() { + @Override + public void pop(Object result) { + Navigator.pop(null, result); + } + + @Override + public dart.async.Future pushNamed(String routeName, Object arguments) { + return orNothing(pushNamedForResult(null, routeName, arguments)); + } + + @Override + public String restorablePushNamed(String routeName, Object arguments) { + Navigator.pushNamed(null, routeName, arguments); + // restoration is not persisted; the id is informational + return routeName == null ? "" : routeName; + } + + @Override + public dart.async.Future pushReplacementNamed(String routeName, Object arguments, Object result) { + return replaceNamed(null, routeName, arguments, result); + } + + @Override + public dart.async.Future push(Object route) { + return pushAny(null, route); + } + + @Override + public String restorablePush( + Funcs.Func2 routeBuilder, Object arguments) { + return Navigator.restorablePush(null, routeBuilder, arguments); + } + + @Override + public dart.async.Future maybePop(Object result) { + if (!poppable()) { + return dart.async.Future.value(Boolean.FALSE); + } + Navigator.pop(null, result); + return dart.async.Future.value(Boolean.TRUE); + } + + @Override + public boolean canPop() { + return poppable(); + } + + @Override + public void popUntil(Funcs.Func1, Boolean> predicate) { + // Pop down to the first route the predicate accepts. With the base + // runApp route off the stack, an always-false predicate unwinds to it. + while (!stack.isEmpty()) { + Route top = (Route) stack.get(stack.size() - 1).route; + Boolean stop; + try { + stop = predicate == null ? null : predicate.call(top); + } catch (Throwable thrownByPredicate) { + // KEEP UNWINDING, and report it. + // + // A predicate that throws used to take the whole pop with it and leave + // the user on the page they were trying to leave -- and popUntil is + // what the back-to-gallery button calls, so a throw there was a locked + // app with no way out. Unwinding one route too far is a page the user + // can walk back into; a screen with no exit is not. + com.codename1.flutter.FlutterErrorReport.record(thrownByPredicate); + stop = null; + } + if (stop != null && stop.booleanValue()) { + return; + } + Navigator.pop(null); + } + } + }; + + /** + * The nearest navigator's mutable state ({@code Navigator.of(context)}). + * There is one navigator per process, but the handle REMEMBERS the calling + * context: a route pushed through it inherits that context's scopes (see + * {@code Element.contextFallback}), which is what makes Theme.of and + * Localizations.of resolve inside the pushed page. + */ + public static NavigatorState of(BuildContext context, Boolean rootNavigator) { + if (context == null) { + return STATE; + } + return new BoundState(context); + } + + /** The element a push should inherit from, or null when unknown. Public so a dialog, + * which Flutter also builds under the navigator, mounts under the same scopes; and + * RouteInheritanceTest pins the rule without mounting and showing a Form. */ + public static com.codename1.flutter.Element pushingElement(BuildContext context) { + // A route inherits from the NAVIGATOR, never from the widget that pushed it. + // In Flutter the route's subtree is built by the Navigator and sits directly + // under it, so the only ancestors it ever sees are the ones above the + // Navigator -- MaterialApp's Theme, MediaQuery and Localizations, and whatever + // the app wrapped MaterialApp in (the studies put their providers there). + // Whichever button happened to be tapped contributes nothing. + // + // Inheriting from the tapping widget instead is not a harmless approximation, + // because scopes on the way down SUBTRACT things. A vertical scroll view wraps + // its content in MediaQuery.removePadding(top, bottom) -- it has consumed the + // display cutout itself, so its children must not inset for it a second time. + // A route pushed from a row inside that list then inherited "the top inset is + // already spent", its SafeArea resolved to zero, and the new page drew its + // title underneath the clock. That is why tapping a mail row landed under the + // status bar while the compose button -- a Scaffold slot, outside the list -- + // came out correctly: same widget code, different ancestor chain. + // + // "The navigator" is the NEAREST one, exactly as Navigator.of(context) resolves + // it. A study is a MaterialApp in its own right, wrapped by the providers it + // needs, so a route it pushes has to mount under ITS scope: mounting under the + // outermost app's instead climbs out of the study's MultiProvider, and the + // page's first Provider.of comes back null. + for (com.codename1.flutter.Element e = context instanceof com.codename1.flutter.Element + ? (com.codename1.flutter.Element) context : null; e != null; e = e.parent()) { + if (e.widget() instanceof RootScope) { + return e; + } + } + // Nothing above the pushing context claims a navigator position, so fall back to + // the outermost app's - which is where a context-less push belongs anyway. + if (rootScopeContext != null && rootScopeContext.isMounted()) { + return rootScopeContext; + } + // No MaterialApp at all (a bare FlutterUI.wrap tree, or a push that arrives + // before the app root mounts). The pushing context is then the best ancestor + // available, subtractive scopes and all. + if (context instanceof com.codename1.flutter.Element) { + return (com.codename1.flutter.Element) context; + } + // A push from outside the tree entirely - a deep link, a notification tap, a + // test harness: the showing tree's root is all there is. + BuildContext showing = com.codename1.flutter.FlutterUI.currentContext(); + return showing instanceof com.codename1.flutter.Element + ? (com.codename1.flutter.Element) showing : null; + } + + /// Where the app's root navigator sits - below MaterialApp's Theme, MediaQuery and + /// Localizations, above the app content. Established by {@link RootScope}. + private static com.codename1.flutter.Element rootScopeContext; + + /** + * Marks the app's root navigator position in the element tree. MaterialApp inserts + * one below its localizations scope; its only job is to remember its own context so + * a context-less {@code pushNamed} can inherit from the right place. + */ + public static final class RootScope extends com.codename1.flutter.StatelessWidget { + + private final Widget child; + private final boolean root; + + public RootScope(Widget child) { + this(child, true); + } + + /** + * @param root whether this scope belongs to the OUTERMOST app. A study + * is a MaterialApp of its own and inserts a scope too; if + * that one claimed the process-wide position, then once the + * study was popped the remembered context was unmounted and + * every later context-less push mounted its page above the + * app's Theme, MediaQuery and Localizations — where + * {@code GalleryLocalizations.of(context)!} is null. + */ + public RootScope(Widget child, boolean root) { + this.child = child; + this.root = root; + } + + @Override + public Widget build(BuildContext context) { + if (root && context instanceof com.codename1.flutter.Element) { + rootScopeContext = (com.codename1.flutter.Element) context; + } + return child; + } + } + + /** A {@link NavigatorState} that pushes on behalf of a specific context. */ + private static final class BoundState extends NavigatorState { + + private final BuildContext context; + + BoundState(BuildContext context) { + this.context = context; + } + + @Override + public void pop(Object result) { + Navigator.pop(context, result); + } + + @Override + public dart.async.Future pushNamed(String routeName, Object arguments) { + return orNothing(pushNamedForResult(context, routeName, arguments)); + } + + @Override + public String restorablePushNamed(String routeName, Object arguments) { + Navigator.pushNamed(context, routeName, arguments); + return routeName == null ? "" : routeName; + } + + @Override + public dart.async.Future pushReplacementNamed(String routeName, Object arguments, Object result) { + return replaceNamed(context, routeName, arguments, result); + } + + @Override + public dart.async.Future push(Object route) { + return pushAny(context, route); + } + + @Override + public String restorablePush( + Funcs.Func2 routeBuilder, Object arguments) { + return Navigator.restorablePush(context, routeBuilder, arguments); + } + + @Override + public dart.async.Future maybePop(Object result) { + if (!poppable()) { + return dart.async.Future.value(Boolean.FALSE); + } + Navigator.pop(context, result); + return dart.async.Future.value(Boolean.TRUE); + } + + @Override + public boolean canPop() { + return poppable(); + } + + @Override + public void popUntil(Funcs.Func1, Boolean> predicate) { + STATE.popUntil(predicate); + } + } + + /** + * Restoration-aware push. Restoration is not persisted here, so the route + * is pushed immediately when it is a {@link MaterialPageRoute} and an empty + * (informational) restoration id is returned. + */ + public static String restorablePush(BuildContext context, + dart.runtime.Funcs.Func2 routeBuilder, Object arguments) { + Object route = routeBuilder != null ? routeBuilder.call(context, arguments) : null; + if (route instanceof MaterialPageRoute) { + push(context, (MaterialPageRoute) route); + } + return ""; + } + + /** + * Pops the topmost route if one exists, returning whether anything was + * popped ({@code Navigator.maybePop}). + */ + public static boolean maybePop(BuildContext context) { + if (!poppable()) { + return false; + } + pop(context); + return true; + } + + private static final class RouteEntry { + final MaterialPageRoute route; + /** Completed by the pop that removes this route, with that pop's result. */ + final dart.async.Completer popped = new dart.async.Completer(); + Form form; + Form previousForm; + Element rootElement; + + RouteEntry(MaterialPageRoute route) { + this.route = route; + } + } + + /** + * Adapter mounting a route's WidgetBuilder as a widget tree root: the + * builder runs during the first build, receiving a BuildContext that + * lives inside the new page's element tree. + */ + static final class RouteWidget extends StatelessWidget { + + private final Route route; + + RouteWidget(Route route) { + this.route = route; + } + + @Override + public Widget build(BuildContext context) { + return route.buildPage(context); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/NavigatorState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/NavigatorState.java new file mode 100644 index 00000000000..62ee4cddccf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/NavigatorState.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +/** + * The mutable state of a {@code Navigator} ({@code NavigatorState} in Flutter), + * as reached via {@code Navigator.of(context)} or a {@code GlobalKey}. + * Only the restoration-related push surface exercised by the gallery is modelled + * here; each restorable push returns an opaque restoration id (a no-op string in + * this implementation). The routing itself is handled by {@link Navigator}. + */ +public abstract class NavigatorState { + + /** + * Push a route created by {@code routeBuilder}, returning a restoration id. + * Restoration is not persisted, so the returned id is informational only. + * {@code routeBuilder} is a {@code Route Function(BuildContext, Object?)}. + */ + public String restorablePush( + dart.runtime.Funcs.Func2 routeBuilder, + Object arguments) { + return ""; + } + + public String restorablePushNamed(String routeName, Object arguments) { + return routeName == null ? "" : routeName; + } + + public void pop(Object result) { + } + + /** + * Pop routes until {@code predicate} accepts the top route + * ({@code NavigatorState.popUntil}). No route stack is kept, so this is a no-op. + * {@code predicate} is a {@code bool Function(Route)}. + */ + public void popUntil(dart.runtime.Funcs.Func1, Boolean> predicate) { + } + + /** Push a named route ({@code NavigatorState.pushNamed}); completes with its pop result. */ + public dart.async.Future pushNamed(String routeName, Object arguments) { + return dart.async.Future.value(null); + } + + /** Replace the current route ({@code NavigatorState.pushReplacementNamed}). */ + public dart.async.Future pushReplacementNamed(String routeName, Object arguments, Object result) { + return dart.async.Future.value(null); + } + + /** Pop if possible ({@code NavigatorState.maybePop}). */ + public dart.async.Future maybePop(Object result) { + return dart.async.Future.value(Boolean.FALSE); + } + + /** + * Whether the navigator can pop the current route ({@code NavigatorState.canPop}). + * This minimal model keeps no route stack, so it reports {@code false}. + */ + public boolean canPop() { + return false; + } + + /** + * Push the given route onto the navigator ({@code NavigatorState.push}); the + * returned future completes with the route's pop result. Navigator's own states + * override this; the base answers an already-completed null. + */ + public dart.async.Future push(Object route) { + return dart.async.Future.value(null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/PageRouteBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/PageRouteBuilder.java new file mode 100644 index 00000000000..6b06a3a9d84 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/PageRouteBuilder.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.Color; + +import dart.core.Duration; + +/** + * A {@link Route} whose page and transition are supplied by builder callbacks — + * Flutter's {@code PageRouteBuilder}. {@code pageBuilder} produces the + * destination widget and {@code transitionsBuilder} wraps it in the animated + * transition; both receive the {@code (context, animation, secondaryAnimation)} + * triple. This pass records the callbacks and route configuration; driving the + * transition animation is deferred. + * + * @param the value the route completes with when popped + */ +public class PageRouteBuilder extends Route { + + private RouteSettings settings; + private Object pageBuilder; + private Object transitionsBuilder; + private Duration transitionDuration; + private Duration reverseTransitionDuration; + private boolean opaque = true; + private boolean barrierDismissible; + private Color barrierColor; + private String barrierLabel; + private boolean maintainState = true; + private boolean fullscreenDialog; + + public void settings(RouteSettings v) { + this.settings = v; + } + + public void pageBuilder(dart.runtime.Funcs.Func3, + com.codename1.flutter.animation.Animation, com.codename1.flutter.Widget> v) { + this.pageBuilder = v; + } + + public void transitionsBuilder(dart.runtime.Funcs.Func4, + com.codename1.flutter.animation.Animation, + com.codename1.flutter.Widget, com.codename1.flutter.Widget> v) { + this.transitionsBuilder = v; + } + + public void transitionDuration(Duration v) { + this.transitionDuration = v; + } + + public void reverseTransitionDuration(Duration v) { + this.reverseTransitionDuration = v; + } + + public void opaque(boolean v) { + this.opaque = v; + } + + public void barrierDismissible(boolean v) { + this.barrierDismissible = v; + } + + public void barrierColor(Color v) { + this.barrierColor = v; + } + + public void barrierLabel(String v) { + this.barrierLabel = v; + } + + public void maintainState(boolean v) { + this.maintainState = v; + } + + public void fullscreenDialog(boolean v) { + this.fullscreenDialog = v; + } + + public RouteSettings getSettings() { + return settings; + } + + public Object getPageBuilder() { + return pageBuilder; + } + + public Object getTransitionsBuilder() { + return transitionsBuilder; + } + + public Duration getTransitionDuration() { + return transitionDuration; + } + + @Override + @SuppressWarnings("unchecked") + public com.codename1.flutter.Widget buildPage(com.codename1.flutter.BuildContext context) { + if (!(pageBuilder instanceof dart.runtime.Funcs.Func3)) { + return null; + } + // The two animations a page builder is handed; a route shown without a + // transition is at its end state. + return (com.codename1.flutter.Widget) ((dart.runtime.Funcs.Func3) + pageBuilder).call(context, null, null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RestorableRouteFuture.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RestorableRouteFuture.java new file mode 100644 index 00000000000..e244d6e32b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RestorableRouteFuture.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.RestorableProperty; + +import dart.runtime.Funcs; + +/** + * A restorable object that can imperatively push a route and complete with its + * result ({@code RestorableRouteFuture} in Flutter). The gallery constructs it + * with an {@code onPresent} callback (which pushes a route on a + * {@link NavigatorState}) and an optional {@code onComplete} callback, then calls + * {@link #present(Object)} from button handlers. + * + *

Route restoration is not persisted; this holds the callbacks and drives them + * within a single session. Because the surrounding {@code Navigator} static + * helpers (owned by the navigation category) are needed to actually resolve a + * {@link NavigatorState}, this is a minimal, API-complete implementation.

+ * + * @param the result type produced when the pushed route completes + */ +public class RestorableRouteFuture extends RestorableProperty { + + private Funcs.Func2 onPresent; + private Funcs.VoidFunc1 onComplete; + private boolean present; + + public RestorableRouteFuture() { + } + + /** Named constructor parameter {@code onPresent:} — pushes the route. */ + public void onPresent(Funcs.Func2 callback) { + this.onPresent = callback; + } + + /** Named constructor parameter {@code onComplete:} — receives the result. */ + public void onComplete(Funcs.VoidFunc1 callback) { + this.onComplete = callback; + } + + /** Imperatively present the route. Optional argument is forwarded to onPresent. */ + public void present(Object arguments) { + this.present = true; + notifyListeners(); + } + + public boolean isPresent() { + return present; + } + + public String route() { + return null; + } + + // ------------------------------------------------------------------ + // Framework plumbing + // ------------------------------------------------------------------ + + /** Deliver a route result to the onComplete callback (single-session use). */ + @SuppressWarnings("unchecked") + void complete(Object result) { + this.present = false; + if (onComplete != null) { + onComplete.call((T) result); + } + notifyListeners(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Route.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Route.java new file mode 100644 index 00000000000..a9b867516e1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/Route.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +/** + * Base type of a navigable route — Flutter's {@code Route}. + * + *

{@link #buildPage} is the one thing every route has to answer, and having + * it here is what stops the navigator caring which kind of route it holds. It + * used to ask {@code instanceof MaterialPageRoute} and build nothing for + * anything else, so a nested navigator handed a Cupertino route — as the + * Cupertino navigation-bar demo does — rendered a blank screen with nothing + * reported. + * + * @param the value type the route completes with when popped + */ +public abstract class Route { + + private RouteSettings settings; + + /** + * Flutter's {@code Route.settings}. Accepts an {@code Object} because super-parameter + * forwarding erases the argument type to {@code dynamic}; only a {@link RouteSettings} is + * retained. + */ + public void settings(Object v) { + this.settings = (v instanceof RouteSettings) ? (RouteSettings) v : null; + } + + /** + * Never null, as Flutter's is not: a route built without settings gets an empty + * one whose name is null. + * + *

Returning null here LOCKED THE APP. Every study is wrapped in a back-to-gallery + * button whose tap is {@code popUntil(route.settings().name() == '/')}, so a route on + * the stack with no settings threw out of the predicate, the pop never finished, and + * the only way out of the study was gone. A route reached in a way that sets no + * settings -- a showModalPopup, a route pushed by builder rather than by name -- was + * enough to arm it.

+ */ + public RouteSettings settings() { + if (settings == null) { + settings = new RouteSettings(); + } + return settings; + } + + /** + * The page this route displays, or null when it cannot build one. + * + *

Every concrete route overrides this; a route that does not is a gap + * worth reporting rather than a blank screen, which is why the navigator + * reports a null page.

+ */ + /** + * How long this route's entrance runs, or -1 to take the platform's page duration. + * Flutter reads this off the route ({@code Route.transitionDuration}); a route with + * motion of its own -- a container transform, a custom PageRouteBuilder -- sets it. + */ + public int transitionMillis() { + return -1; + } + + /** + * Whether this route enters as a full-screen modal, which Flutter animates up from the + * bottom edge rather than in from the side. + */ + public boolean isFullscreenDialog() { + return false; + } + + /** + * The name of the component this route grows out of, or null. + * + *

Set for a container transform: the transition grows the incoming page from the + * bounds of the thing that was tapped, so it needs to be able to find that thing in + * the outgoing Form.

+ */ + public String containerTransformSource() { + return null; + } + + /** + * Whether this route is an expanding container transform rather than a page push. The + * real effect grows the tapped card into the page; a route is a Form of its own here, + * so the closest honest approximation is a cross-fade, which at least reads as the + * same surface changing rather than a new page arriving from off-screen. + */ + public boolean isContainerTransform() { + return false; + } + + public com.codename1.flutter.Widget buildPage(com.codename1.flutter.BuildContext context) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteSettings.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteSettings.java new file mode 100644 index 00000000000..d4da926d1fe --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteSettings.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +/** + * The data with which a route was pushed ({@code RouteSettings} in Flutter): its + * name and optional arguments. new_gallery's {@code onGenerateRoute} matches on + * {@link #name()} to build the right page. + */ +public class RouteSettings { + + private String name; + private Object arguments; + + public RouteSettings() { + } + + // Named-parameter setters. + public void name(String v) { + this.name = v; + } + + public void arguments(Object v) { + this.arguments = v; + } + + public String name() { + return name; + } + + public Object arguments() { + return arguments; + } + + /** Returns a copy with the supplied fields overridden (null keeps current). */ + public RouteSettings copyWith(String name, Object arguments) { + RouteSettings c = new RouteSettings(); + c.name = name != null ? name : this.name; + c.arguments = arguments != null ? arguments : this.arguments; + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteTransitions.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteTransitions.java new file mode 100644 index 00000000000..25e2adc4964 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/navigation/RouteTransitions.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.TargetPlatform; +import com.codename1.flutter.foundation.FoundationLib; +import com.codename1.ui.Form; +import com.codename1.ui.animations.CommonTransitions; +import com.codename1.ui.animations.Transition; + +/** + * Gives a pushed route the motion Flutter would give it. + * + *

A route here is a Form of its own, and a Form shown without a transition simply + * replaces what was on screen. That is what every page push did: the new page was fully + * painted on the first frame after the tap, with nothing in between. Flutter never does + * that -- {@code MaterialPageRoute} always has a transition, chosen by + * {@code PageTransitionsTheme} from the target platform.

+ * + *

The mapping below follows Flutter's own default table + * ({@code PageTransitionsTheme._defaultBuilders}) and its durations:

+ * + *
    + *
  • iOS and macOS use {@code CupertinoPageTransitionsBuilder}, 500ms, the page + * entering from the trailing edge. Codename One's horizontal slide is the same + * gesture; what it does not reproduce is the outgoing page's parallax, since it + * moves both pages at one rate.
  • + *
  • Android, Windows and Linux use {@code ZoomPageTransitionsBuilder}, 300ms, a + * fade with a slight scale. A cross-fade keeps the fade and drops the scale.
  • + *
  • A full-screen dialog enters from the bottom edge on every platform.
  • + *
  • A container transform ({@code OpenContainer}) grows the tapped card into the + * page. Across two Forms the honest approximation is a cross-fade over the route's own + * duration -- it reads as one surface becoming another rather than as a page arriving + * from off-screen, which is the part that matters.
  • + *
+ * + *

Only the entering transition is set. Codename One plays it in reverse for + * {@code showBack()}, which is what {@link Navigator#pop} uses, so the way back out of a + * route mirrors the way in without a second mapping to keep in step.

+ */ +final class RouteTransitions { + + /** {@code CupertinoRouteTransitionMixin.kTransitionDuration}. */ + private static final int CUPERTINO_PAGE_MS = 500; + + /** {@code ZoomPageTransitionsBuilder.transitionDuration}. */ + private static final int ZOOM_PAGE_MS = 300; + + private RouteTransitions() { + } + + /** + * Sets the transition the route asks for on the Form that carries it. Called before + * the Form is shown; a Form with no transition set replaces the screen outright. + */ + static void apply(Form form, Route route) { + if (form == null || route == null) { + return; + } + Transition t = forRoute(route); + if (t != null) { + form.setTransitionInAnimator(t); + } + } + + static Transition forRoute(Route route) { + return forRoute(route, FoundationLib.defaultTargetPlatform); + } + + /** The platform is a parameter so both branches of the table can be pinned by a test. */ + static Transition forRoute(Route route, TargetPlatform platform) { + int ms = route.transitionMillis(); + if (route.isContainerTransform()) { + String source = route.containerTransformSource(); + if (source != null) { + // The page grows out of the bounds of what was tapped and folds back into + // it on the way out, which is what makes a card feel like it BECAME the + // page rather than being replaced by one. + // + // Not BubbleTransition, which was the closest thing to hand and is a + // circular reveal: a hole opening in the screen, always round, always from + // the centre of the destination. Material's container transform is a + // rounded RECTANGLE travelling from the tapped bounds with its corners + // straightening and the two contents crossing over inside it, which is a + // different shape and a different anchor. + return com.codename1.ui.animations.ContainerTransformTransition.create( + source, ms > 0 ? ms : ZOOM_PAGE_MS); + } + // Nothing to grow from -- the tapped surface has no component of its own. + // A cross-fade at least reads as one surface becoming another. + return CommonTransitions.createFade(ms > 0 ? ms : ZOOM_PAGE_MS); + } + if (route.isFullscreenDialog()) { + // Up from the bottom edge: SLIDE_VERTICAL with forward false, since forward + // moves the incoming page down rather than up. + return CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, false, + ms > 0 ? ms : platformPageMillis(platform)); + } + if (usesCupertinoPageTransition(platform)) { + // forward=FALSE is the push. CommonTransitions names the direction after the + // OUTGOING page -- paintSlideAtPosition moves the source by +position when + // forward is true, so the destination comes in from the leading edge, which is + // the way BACK. A push brings the new page in from the trailing edge, and + // showBack() plays this in reverse for the pop. + // Not a slide: a slide holds the two pages a screen apart and moves the pair, + // and this platform's push moves them different distances on different + // curves -- the arriving page crosses the whole screen, the one it covers + // drifts a third of it. + return com.codename1.ui.animations.CupertinoPageTransition.create( + ms > 0 ? ms : CUPERTINO_PAGE_MS); + } + return CommonTransitions.createFade(ms > 0 ? ms : ZOOM_PAGE_MS); + } + + private static boolean usesCupertinoPageTransition(TargetPlatform p) { + return p == TargetPlatform.iOS || p == TargetPlatform.macOS; + } + + private static int platformPageMillis(TargetPlatform p) { + return usesCupertinoPageTransition(p) ? CUPERTINO_PAGE_MS : ZOOM_PAGE_MS; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BorderDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BorderDirectional.java new file mode 100644 index 00000000000..2965118ff19 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BorderDirectional.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.painting; + +/** + * A box border whose sides are resolved against the ambient text direction + * ({@code start}/{@code end} rather than {@code left}/{@code right}) — Flutter's + * {@code BorderDirectional}. The settings demo draws a leading rule with a + * {@code start} side. The sides are held as {@code Object} because a + * {@code BorderSide} is supplied by the widget layer; this pass retains them for + * the box decoration to paint. + */ +public class BorderDirectional { + + private Object top; + private Object bottom; + private Object start; + private Object end; + + public void top(Object v) { + this.top = v; + } + + public void bottom(Object v) { + this.bottom = v; + } + + public void start(Object v) { + this.start = v; + } + + public void end(Object v) { + this.end = v; + } + + public Object getTop() { + return top; + } + + public Object getBottom() { + return bottom; + } + + public Object getStart() { + return start; + } + + public Object getEnd() { + return end; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BoxPainter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BoxPainter.java new file mode 100644 index 00000000000..332e237d8a0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/BoxPainter.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.painting; + +import com.codename1.flutter.Canvas; +import com.codename1.flutter.ImageConfiguration; +import com.codename1.flutter.Offset; + +/** + * The object a {@code Decoration} produces to paint itself — Flutter's + * {@code BoxPainter}. A decoration returns one from {@code createBoxPainter}; + * the render layer calls {@link #paint} with the {@link Canvas}, the top-left + * {@link Offset} of the box and an {@link ImageConfiguration} carrying the box + * size. The tab-indicator and Rally pie-chart decorations subclass this to draw + * custom borders. The optional repaint {@code onChanged} callback is captured by + * the decoration; {@link #dispose()} releases any held resources. + */ +public abstract class BoxPainter { + + /** + * Paints the decoration onto {@code canvas}. The box occupies the rectangle + * whose top-left is {@code offset} and whose size is + * {@code configuration.size}. + */ + public abstract void paint(Canvas canvas, Offset offset, ImageConfiguration configuration); + + /** Releases resources held by this painter. */ + public void dispose() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/ExactAssetImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/ExactAssetImage.java new file mode 100644 index 00000000000..47e0d69d36d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/painting/ExactAssetImage.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.painting; + +import com.codename1.flutter.ImageProvider; + +/** + * An {@link ImageProvider} that loads a bundled asset at an exact device-pixel + * scale — Flutter's {@code ExactAssetImage}. Unlike {@code AssetImage} it does + * not pick a resolution-appropriate variant; the named {@code scale} identifies + * the density the asset was authored for. The product thumbnails use it to load + * the pre-scaled catalog images. + */ +public class ExactAssetImage extends ImageProvider { + + private final String assetName; + private double scale = 1.0; + private String packageName; + private Object bundle; + + public ExactAssetImage(String assetName) { + this.assetName = assetName; + } + + public void scale(double v) { + this.scale = v; + } + + /** + * Named parameter setter for the Dart {@code package:} parameter. The + * transpiler escapes the reserved word {@code package} to {@code package_}. + */ + public void package_(String v) { + this.packageName = v; + } + + public void bundle(Object v) { + this.bundle = v; + } + + public String getAssetName() { + return assetName; + } + + public double getScale() { + return scale; + } + + public String getPackage() { + return packageName; + } + + /** + * The classpath-relative asset path, honoring the optional package + * qualifier ({@code packages//}). + */ + public String resolvedName() { + if (packageName != null && !assetName.startsWith("packages/")) { + return "packages/" + packageName + "/" + assetName; + } + return assetName; + } + + @Override + public String sourceKey() { + return "asset:" + resolvedName() + "@" + scale; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ClampingScrollSimulation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ClampingScrollSimulation.java new file mode 100644 index 00000000000..31ac3c5ee93 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ClampingScrollSimulation.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * A friction {@link Simulation} clamped to a scroll range — Flutter's + * {@code ClampingScrollSimulation}. Models the deceleration of a fling on the + * Android-style clamping scroll physics. This pass captures the parameters and + * reports the resting {@code position}. + */ +public class ClampingScrollSimulation extends Simulation { + + private double position; + private double velocity; + private Double friction; + + public ClampingScrollSimulation() { + } + + public void position(double v) { + this.position = v; + } + + public void velocity(double v) { + this.velocity = v; + } + + public void friction(Double v) { + this.friction = v; + } + + public double getPosition() { + return position; + } + + public double getVelocity() { + return velocity; + } + + public Double getFriction() { + return friction; + } + + @Override + public double x(double time) { + return position; + } + + @Override + public double dx(double time) { + return 0.0; + } + + @Override + public boolean isDone(double time) { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ScrollSpringSimulation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ScrollSpringSimulation.java new file mode 100644 index 00000000000..77cee0b1d0a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/ScrollSpringSimulation.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * A spring {@link Simulation} that carries a scrollable from a start offset to + * an end offset under a {@link SpringDescription} — Flutter's + * {@code ScrollSpringSimulation}. Used by the home carousel's snapping physics + * to settle onto an item after a fling. The spring is captured for API shape; + * this pass models the endpoint (the settled position is {@code end}). + * + * @see com.codename1.flutter.widgets.ScrollPhysics + */ +public class ScrollSpringSimulation extends Simulation { + + private final Object spring; + private final double start; + private final double end; + private final double velocity; + + public ScrollSpringSimulation(Object spring, double start, double end, double velocity) { + this.spring = spring; + this.start = start; + this.end = end; + this.velocity = velocity; + } + + public Object getSpring() { + return spring; + } + + public double getStart() { + return start; + } + + public double getEnd() { + return end; + } + + public double getVelocity() { + return velocity; + } + + @Override + public double x(double time) { + return end; + } + + @Override + public double dx(double time) { + return 0.0; + } + + @Override + public boolean isDone(double time) { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Simulation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Simulation.java new file mode 100644 index 00000000000..059577f7e31 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Simulation.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * The base of a one-dimensional physics simulation over time — Flutter's + * {@code Simulation}. Subclasses model a value that evolves with time: + * {@link #x(double)} is the position at {@code time} seconds, {@link #dx(double)} + * the velocity, and {@link #isDone(double)} whether the simulation has settled. + * The {@code tolerance} the simulation settles within is captured for API shape. + */ +public abstract class Simulation { + + private Tolerance tolerance = Tolerance.defaultTolerance; + + public void tolerance(Tolerance v) { + this.tolerance = v; + } + + public Tolerance tolerance() { + return tolerance; + } + + /** The position of the object at {@code time} seconds. */ + public abstract double x(double time); + + /** The velocity of the object at {@code time} seconds. */ + public abstract double dx(double time); + + /** Whether the simulation is done (settled) at {@code time} seconds. */ + public abstract boolean isDone(double time); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringDescription.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringDescription.java new file mode 100644 index 00000000000..0a307a3a2a6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringDescription.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * Structural parameters of a spring — Flutter's {@code SpringDescription}: + * {@code mass}, {@code stiffness} and {@code damping}. Fed to a + * {@code ScrollSpringSimulation} to model overscroll / fling settling. + */ +public class SpringDescription { + + private double mass; + private double stiffness; + private double damping; + + public SpringDescription() { + } + + public void mass(double v) { + this.mass = v; + } + + public void stiffness(double v) { + this.stiffness = v; + } + + public void damping(double v) { + this.damping = v; + } + + public double getMass() { + return mass; + } + + public double getStiffness() { + return stiffness; + } + + public double getDamping() { + return damping; + } + + /** + * {@code SpringDescription.withDampingRatio}: builds a spring from a mass, + * stiffness and damping ratio (1.0 = critically damped). + */ + public static SpringDescription withDampingRatio(double mass, double stiffness, double ratio) { + SpringDescription s = new SpringDescription(); + s.mass(mass); + s.stiffness(stiffness); + s.damping(ratio * 2.0 * Math.sqrt(mass * stiffness)); + return s; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringSimulation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringSimulation.java new file mode 100644 index 00000000000..c6b0feef7d7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/SpringSimulation.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * A {@link Simulation} of a spring settling from a start offset to an end + * offset under a {@link SpringDescription} — Flutter's {@code SpringSimulation} + * (the base of {@link ScrollSpringSimulation}). This pass models the endpoint; + * the settled position is {@code end}. + */ +public class SpringSimulation extends Simulation { + + private final SpringDescription spring; + private final double start; + private final double end; + private final double velocity; + + public SpringSimulation(SpringDescription spring, double start, double end, double velocity) { + this.spring = spring; + this.start = start; + this.end = end; + this.velocity = velocity; + } + + public SpringDescription getSpring() { + return spring; + } + + public double getStart() { + return start; + } + + public double getEnd() { + return end; + } + + public double getVelocity() { + return velocity; + } + + @Override + public double x(double time) { + return end; + } + + @Override + public double dx(double time) { + return 0.0; + } + + @Override + public boolean isDone(double time) { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Tolerance.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Tolerance.java new file mode 100644 index 00000000000..d89c2d5f12e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/physics/Tolerance.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.physics; + +/** + * The error tolerances a physics simulation settles within — Flutter's + * {@code Tolerance}: {@code distance}, {@code time} and {@code velocity} + * thresholds below which a simulation is considered to have come to rest. The + * home carousel physics compares the fling velocity against + * {@link #velocity()}. + */ +public class Tolerance { + + /** Flutter's {@code Tolerance.defaultTolerance}. */ + public static final Tolerance defaultTolerance = new Tolerance(1e-3, 1e-3, 1e-3); + + private double distance = 1e-3; + private double time = 1e-3; + private double velocity = 1e-3; + + public Tolerance() { + } + + public Tolerance(double distance, double time, double velocity) { + this.distance = distance; + this.time = time; + this.velocity = velocity; + } + + public void distance(double v) { + this.distance = v; + } + + public void time(double v) { + this.time = v; + } + + public void velocity(double v) { + this.velocity = v; + } + + public double distance() { + return distance; + } + + public double time() { + return time; + } + + public double velocity() { + return velocity; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProvider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProvider.java new file mode 100644 index 00000000000..d7d51381fda --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +/** + * provider's {@code ChangeNotifierProvider}: a + * {@link Provider} specialised for {@code ChangeNotifier} values. The gallery + * uses the {@code .value} form inside a {@code MultiProvider}; disposal of a + * created notifier is not modeled in this pass. + */ +public class ChangeNotifierProvider extends Provider { + + /** + * Subscribes to the model and rebuilds this subtree when it notifies. + * + *

Without this the provider read its value once and nothing ever listened, so + * {@code notifyListeners()} changed nothing on screen and every control whose job is + * to set a field on the model did nothing at all.

+ */ + @Override + public com.codename1.flutter.Element createElement() { + return new ChangeNotifierProviderElement(this); + } + + /** {@link Provider#providedType(Class)}, answering this type so a chain keeps it. */ + @Override + public ChangeNotifierProvider providedType(Class v) { + super.providedType(v); + return this; + } + + /** The {@code ChangeNotifierProvider.value(value: ...)} named constructor. */ + public static ChangeNotifierProvider value(Key key, Object value, Widget child) { + ChangeNotifierProvider p = new ChangeNotifierProvider(); + p.key(key); + p.value(value); + p.child(child); + return p; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProviderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProviderElement.java new file mode 100644 index 00000000000..61d6f633581 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ChangeNotifierProviderElement.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.Listenable; + +import dart.runtime.Funcs; + +/** + * Element for a provider whose value is a {@link Listenable}: subscribes on mount and + * rebuilds its subtree on every notification. + * + *

Without this a provider was a plain widget that read its value once. Calling + * {@code notifyListeners()} on the model then changed nothing on screen, and every + * control whose whole job is to set a field on it did nothing at all -- the mail study's + * search button, its mailbox switcher, its starring and deleting. They were not + * unwired: the handler ran, the model changed, and no one was listening.

+ */ +public class ChangeNotifierProviderElement extends ProviderElement { + + private Listenable listened; + + private final Funcs.VoidFunc0 handler = new Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + // And everything that READ the model. Rebuilding only this element achieves + // nothing: its build hands back the same child widget instance and + // reconciliation returns early. + rebuildProviderDependents(); + } + }; + + public ChangeNotifierProviderElement(StatelessWidget widget) { + super(widget); + } + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + } + + @Override + public void update(Widget newWidget) { + // ProviderElement.update rebuilds the dependents when this brings a different + // model; the subscription moves to the new one either way. + unsubscribe(); + super.update(newWidget); + subscribe(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + private void subscribe() { + Object value = widget() instanceof Provider ? ((Provider) widget()).getValue() : null; + if (value instanceof Listenable) { + listened = (Listenable) value; + listened.addListener(handler); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(handler); + listened = null; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Consumer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Consumer.java new file mode 100644 index 00000000000..00e9c951e6d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Consumer.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * provider's {@code Consumer}: rebuilds via {@code builder(context, value, + * child)} with the nearest ancestor-provided value. + * + *

The Dart type argument {@code T} is threaded in by the transpiler as a type token + * ({@link #providedType}), so the right model is found with several in scope. Without it + * the lookup took the NEAREST provided value of any type, which is only ever correct by + * luck — see {@link Selector} for what that cost.

+ */ +public class Consumer extends StatelessWidget { + + private Funcs.Func3 builder; + private Widget child; + private Class providedType = Object.class; + + /** + * The model type this consumer reads — the Dart {@code T}, emitted by the transpiler. + * Defaults to {@code Object}: the nearest provider of any type. + */ + public void providedType(Class v) { + this.providedType = v == null ? Object.class : v; + } + + public void builder(Funcs.Func3 v) { + this.builder = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + @SuppressWarnings("unchecked") + public Widget build(BuildContext context) { + T value = (T) context.providerValueOfType(providedType); + return builder == null ? child : builder.call(context, value, child); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/MultiProvider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/MultiProvider.java new file mode 100644 index 00000000000..416aa1b84a2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/MultiProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import java.util.List; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * provider's {@code MultiProvider}: nests its {@code providers} around + * {@code child} so each becomes an ancestor of the app subtree. The list order + * is outermost-first (Flutter semantics), so the first provider ends up highest + * in the tree. + */ +public class MultiProvider extends StatelessWidget { + + private List providers; + private Widget child; + + public void providers(List v) { + this.providers = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + Widget acc = child; + if (providers != null) { + for (int i = providers.size() - 1; i >= 0; i--) { + SingleChildWidget p = providers.get(i); + if (p != null) { + p.child(acc); + acc = p; + } + } + } + return acc; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Provider.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Provider.java new file mode 100644 index 00000000000..68559ed785a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Provider.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.InheritedValueProvider; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +/** + * provider's {@code Provider}: publishes {@code value} to its subtree by + * runtime type. {@link #of(BuildContext, boolean, Class)} walks the element tree + * for the nearest provider whose value is assignable to the requested type — the + * Dart {@code Provider.of(context)} witness is threaded in as {@code type} by + * the transpiler. + * + *

The {@code create} form (a lazily-invoked factory) is accepted for API + * shape but not exercised by the gallery, which uses the {@code .value} form.

+ */ +public class Provider extends SingleChildWidget implements InheritedValueProvider { + + protected Object value; + protected Object create; + protected boolean lazy = true; + /** The Dart {@code T} of {@code Provider}, when the transpiler knows it; else null. */ + protected Class providedType; + + public void value(Object v) { + this.value = v; + } + + public void create(Object v) { + this.create = v; + } + + public void lazy(boolean v) { + this.lazy = v; + } + + public Object getValue() { + return value; + } + + /** + * The provider's declared type ({@code Provider}), threaded in by the transpiler as + * Java erases it. With it the provider matches a lookup for T even while its value is + * null; without it only a non-null value can say what it provides. Answers this + * provider, so it chains onto a {@code .value(...)} named constructor. + */ + public Provider providedType(Class v) { + this.providedType = v; + return this; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new ProviderElement(this); + } + + @Override + public Object providedValueFor(Class type) { + return providesType(type) ? value : null; + } + + @Override + public boolean providesType(Class type) { + if (type == null) { + return false; + } + if (value != null) { + return type.isInstance(value); + } + // A null value matches by the declared type. Not for an Object lookup -- the + // default of a Consumer whose type was not known -- which means "the nearest + // model", and a null is no model: it would shadow the real one further out. + return providedType != null && type != Object.class && type.isAssignableFrom(providedType); + } + + /** The {@code Provider.value(value: ...)} named constructor. */ + public static Provider value(Key key, Object value, Widget child) { + Provider p = new Provider(); + p.key(key); + p.value(value); + p.child(child); + return p; + } + + /** {@code Provider.of(context, listen: ...)}. */ + @SuppressWarnings("unchecked") + public static T of(BuildContext context, boolean listen, Class type) { + return (T) context.providerValueOfType(type, listen); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ProviderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ProviderElement.java new file mode 100644 index 00000000000..93a7e4d8c48 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/ProviderElement.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.StatelessElement; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.DartRuntime; + +/** + * Element for a {@link Provider}: when a rebuild hands it a different value, the widgets + * that read the old one rebuild, as provider's updateShouldNotify (by default, + * {@code previous != next}) has them do. Swapping the value alone did nothing on + * screen whenever the provider's child was the same widget instance -- reconciliation + * skips an identical child, and nothing told a Consumer or Selector below it that the + * value it showed had been replaced. + */ +public class ProviderElement extends StatelessElement { + + public ProviderElement(StatelessWidget widget) { + super(widget); + } + + @Override + public void update(Widget newWidget) { + Object before = valueOf(widget()); + super.update(newWidget); + if (!DartRuntime.eq(before, valueOf(widget()))) { + rebuildProviderDependents(); + } + } + + private static Object valueOf(Widget w) { + return w instanceof Provider ? ((Provider) w).getValue() : null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Selector.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Selector.java new file mode 100644 index 00000000000..e0b82676eb6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/Selector.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * provider's {@code Selector}: rebuilds only when a selected slice + * {@code S} of a provided value {@code A} changes. {@code selector(context, a)} + * extracts the slice and {@code builder(context, s, child)} renders it. + * + *

The Dart type argument {@code A} is threaded in by the transpiler as a type token + * ({@link #providedType}), so the right model is found even when several are in scope. + * Without it the lookup took the NEAREST provided value of any type: Reply has both its + * localizations and its EmailStore above the Selector, got the localizations, and failed — + * with a ClassCastException on the desktop and, because the cast is unchecked there, a + * wrong object that flowed on until an unrelated switch matched nothing on iOS.

+ * + * @param the provided value type + * @param the selected slice type + */ +public class Selector extends StatelessWidget { + + private Funcs.Func2 selector; + private Funcs.Func3 builder; + private Object shouldRebuild; + private Widget child; + private Class providedType = Object.class; + + /** + * The model type this selector reads — the Dart {@code A}, emitted by the transpiler. + * Defaults to {@code Object}, which resolves to the nearest provider of any type and is + * only correct when a single value is in scope. + */ + public void providedType(Class v) { + this.providedType = v == null ? Object.class : v; + } + + public void selector(Funcs.Func2 v) { + this.selector = v; + } + + public void builder(Funcs.Func3 v) { + this.builder = v; + } + + public void shouldRebuild(Object v) { + this.shouldRebuild = v; + } + + public void child(Widget v) { + this.child = v; + } + + /** The model this selector reads, from the provider above {@code context}. */ + @SuppressWarnings("unchecked") + S select(BuildContext context) { + A value = (A) context.providerValueOfType(providedType); + return selector == null ? (S) value : selector.call(context, value); + } + + /** Whether {@code next} differs from {@code previous} enough to rebuild. */ + @SuppressWarnings({"unchecked", "rawtypes"}) + boolean changed(S previous, S next) { + if (shouldRebuild instanceof Funcs.Func2) { + return Boolean.TRUE.equals(((Funcs.Func2) shouldRebuild).call(previous, next)); + } + return !deepEquals(previous, next); + } + + Widget buildFor(BuildContext context, S selected) { + return builder == null ? child : builder.call(context, selected, child); + } + + /** Uncached: the element ({@link SelectorElement}) decides when to call this. */ + @Override + public Widget build(BuildContext context) { + return buildFor(context, select(context)); + } + + /** + * An element that remembers ITS last selection. The cache used to live on the + * widget, which one instance mounted in two places shares: the second place got the + * first one's subtree, and each overwrote the other's selection. + */ + @Override + public com.codename1.flutter.Element createElement() { + return new SelectorElement(this); + } + + /** provider's default test: DeepCollectionEquality, over lists, sets and maps. */ + @SuppressWarnings("rawtypes") + static boolean deepEquals(Object a, Object b) { + if (a == b) { + return true; + } + if (a instanceof java.util.List && b instanceof java.util.List) { + java.util.List la = (java.util.List) a; + java.util.List lb = (java.util.List) b; + if (la.size() != lb.size()) { + return false; + } + for (int i = 0; i < la.size(); i++) { + if (!deepEquals(la.get(i), lb.get(i))) { + return false; + } + } + return true; + } + if (a instanceof java.util.Map && b instanceof java.util.Map) { + java.util.Map ma = (java.util.Map) a; + java.util.Map mb = (java.util.Map) b; + if (ma.size() != mb.size()) { + return false; + } + for (Object k : ma.keySet()) { + if (!mb.containsKey(k) || !deepEquals(ma.get(k), mb.get(k))) { + return false; + } + } + return true; + } + if (a instanceof java.util.Set && b instanceof java.util.Set) { + java.util.Set sa = (java.util.Set) a; + java.util.Set sb = (java.util.Set) b; + return sa.size() == sb.size() && sa.containsAll(sb); + } + return dart.runtime.DartRuntime.eq(a, b); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SelectorElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SelectorElement.java new file mode 100644 index 00000000000..30a9e6f5778 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SelectorElement.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.StatelessElement; +import com.codename1.flutter.Widget; + +/** + * The element of a {@link Selector}: rebuilds its subtree only when the selected slice + * changed, or when it was given a new Selector configuration -- provider's rule. The + * last selection and the child built for it are kept here, per mount point. + */ +public class SelectorElement extends StatelessElement { + + private boolean built; + private Widget builtFor; + private S lastSelected; + private Widget lastChild; + + public SelectorElement(Selector widget) { + super(widget); + } + + @Override + @SuppressWarnings("unchecked") + protected Widget build() { + Selector w = (Selector) widget(); + S selected = w.select(this); + if (built && builtFor == w && !w.changed(lastSelected, selected)) { + // Handing back the same child instance lets reconciliation skip the subtree. + return lastChild; + } + Widget out = w.buildFor(this, selected); + built = true; + builtFor = w; + lastSelected = selected; + lastChild = out; + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SingleChildWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SingleChildWidget.java new file mode 100644 index 00000000000..c6f3f37d125 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/provider/SingleChildWidget.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * provider's SingleChildWidget: the base of the composable provider widgets + * (the element type held in a {@code MultiProvider}'s {@code providers} list). + * It renders its single {@code child}; subclasses add the value they publish. + */ +public class SingleChildWidget extends StatelessWidget { + + protected Widget child; + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/BoxConstraints.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/BoxConstraints.java new file mode 100644 index 00000000000..cb7bda7526a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/BoxConstraints.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.EdgeInsets; + +/** + * Flutter's box constraints: a min/max range for each axis. + * Constraints flow down the render tree, sizes flow back up. + * {@code Double.POSITIVE_INFINITY} marks an unbounded max. + * + *

The framework treats instances as immutable values; the no-arg + * constructor and the void setters exist only for transpiled Dart code + * ({@code BoxConstraints(minWidth: ..., maxWidth: ...)} becomes allocate + + * setter calls). Values arriving from Dart are logical pixels — widgets that + * consume them (ConstrainedBox) convert to device pixels.

+ */ +public final class BoxConstraints { + + private double minWidth; + private double maxWidth; + private double minHeight; + private double maxHeight; + + /** + * Dart-facing constructor: all named parameters optional, defaulting to + * the unconstrained range (0..∞ on both axes). + */ + public BoxConstraints() { + this(0, Double.POSITIVE_INFINITY, 0, Double.POSITIVE_INFINITY); + } + + public BoxConstraints(double minWidth, double maxWidth, double minHeight, double maxHeight) { + this.minWidth = minWidth; + this.maxWidth = maxWidth; + this.minHeight = minHeight; + this.maxHeight = maxHeight; + } + + /** Named parameter setter for the Dart {@code minWidth:} parameter. */ + public void minWidth(double v) { + this.minWidth = v; + } + + /** Named parameter setter for the Dart {@code maxWidth:} parameter. */ + public void maxWidth(double v) { + this.maxWidth = v; + } + + /** Named parameter setter for the Dart {@code minHeight:} parameter. */ + public void minHeight(double v) { + this.minHeight = v; + } + + /** Named parameter setter for the Dart {@code maxHeight:} parameter. */ + public void maxHeight(double v) { + this.maxHeight = v; + } + + /** + * Constraints that force exactly the given size. + */ + public static BoxConstraints tight(double width, double height) { + return new BoxConstraints(width, width, height, height); + } + + /** + * Constraints that allow any size up to the given maximums. + */ + public static BoxConstraints loose(double maxWidth, double maxHeight) { + return new BoxConstraints(0, maxWidth, 0, maxHeight); + } + + public double minWidth() { + return minWidth; + } + + public double maxWidth() { + return maxWidth; + } + + public double minHeight() { + return minHeight; + } + + public double maxHeight() { + return maxHeight; + } + + public boolean hasBoundedWidth() { + return maxWidth != Double.POSITIVE_INFINITY; + } + + public boolean hasBoundedHeight() { + return maxHeight != Double.POSITIVE_INFINITY; + } + + public boolean hasTightWidth() { + return minWidth >= maxWidth; + } + + public boolean hasTightHeight() { + return minHeight >= maxHeight; + } + + public boolean isTight() { + return hasTightWidth() && hasTightHeight(); + } + + public double constrainWidth(double width) { + return clamp(width, minWidth, maxWidth); + } + + public double constrainHeight(double height) { + return clamp(height, minHeight, maxHeight); + } + + /** + * The size closest to {@code size} that satisfies these constraints. + */ + public Size constrain(Size size) { + return new Size(constrainWidth(size.width()), constrainHeight(size.height())); + } + + /** + * Constrains a size while keeping its aspect ratio, as Flutter's + * {@code constrainSizeAndAttemptToPreserveAspectRatio} does. + * + *

{@link #constrain} clamps the two axes independently, which throws the + * ratio away: a picture whose natural size is wider than the box comes back + * with the box's width and its own height. That is how an image laid out + * under a width-driven fit ended up in a box taller than its content, with + * the artwork centred in the slack and everything below it pushed down.

+ * + *

Each clamp here carries the other axis with it, and the order matters: + * width, then height, then the minimums, so a later clamp corrects an + * earlier one rather than being overwritten by it. A tight box has only one + * answer and keeps no ratio.

+ */ + public Size constrainSizeAndAttemptToPreserveAspectRatio(Size size) { + if (isTight()) { + return smallest(); + } + double width = size.width(); + double height = size.height(); + if (width <= 0 || height <= 0) { + return constrain(size); + } + double aspectRatio = width / height; + if (width > maxWidth()) { + width = maxWidth(); + height = width / aspectRatio; + } + if (height > maxHeight()) { + height = maxHeight(); + width = height * aspectRatio; + } + if (width < minWidth()) { + width = minWidth(); + height = width / aspectRatio; + } + if (height < minHeight()) { + height = minHeight(); + width = height * aspectRatio; + } + return new Size(constrainWidth(width), constrainHeight(height)); + } + + public Size smallest() { + return new Size(constrainWidth(0), constrainHeight(0)); + } + + public Size biggest() { + return new Size(constrainWidth(Double.POSITIVE_INFINITY), constrainHeight(Double.POSITIVE_INFINITY)); + } + + /** + * New constraints with the given edges removed, never going below zero + * (Flutter's BoxConstraints.deflate). The insets are interpreted in the + * same unit as these constraints. + */ + public BoxConstraints deflate(EdgeInsets edges) { + double horizontal = edges.left() + edges.right(); + double vertical = edges.top() + edges.bottom(); + double deflatedMinWidth = Math.max(0, minWidth - horizontal); + double deflatedMinHeight = Math.max(0, minHeight - vertical); + return new BoxConstraints( + deflatedMinWidth, + Math.max(deflatedMinWidth, maxWidth - horizontal), + deflatedMinHeight, + Math.max(deflatedMinHeight, maxHeight - vertical)); + } + + /** + * These constraints (the "additional" ones, e.g. a ConstrainedBox's) with + * every value clamped into the given bounds — Flutter's + * {@code BoxConstraints.enforce}: the result respects {@code bounds} while + * getting as close to these constraints as possible. + */ + public BoxConstraints enforce(BoxConstraints bounds) { + return new BoxConstraints( + clamp(minWidth, bounds.minWidth, bounds.maxWidth), + clamp(maxWidth, bounds.minWidth, bounds.maxWidth), + clamp(minHeight, bounds.minHeight, bounds.maxHeight), + clamp(maxHeight, bounds.minHeight, bounds.maxHeight)); + } + + /** + * New constraints with the minimum extents removed. + */ + public BoxConstraints loosen() { + return new BoxConstraints(0, maxWidth, 0, maxHeight); + } + + /** + * New constraints with the given dimensions (when non-null) tightened as + * close to the requested value as these constraints allow (Flutter's + * BoxConstraints.tighten). + */ + public BoxConstraints tighten(Double width, Double height) { + double minW = width == null ? minWidth : clamp(width, minWidth, maxWidth); + double maxW = width == null ? maxWidth : clamp(width, minWidth, maxWidth); + double minH = height == null ? minHeight : clamp(height, minHeight, maxHeight); + double maxH = height == null ? maxHeight : clamp(height, minHeight, maxHeight); + return new BoxConstraints(minW, maxW, minH, maxH); + } + + private static double clamp(double v, double min, double max) { + if (v < min) { + return min; + } + if (v > max) { + return max; + } + return v; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BoxConstraints)) { + return false; + } + BoxConstraints c = (BoxConstraints) o; + return c.minWidth == minWidth && c.maxWidth == maxWidth + && c.minHeight == minHeight && c.maxHeight == maxHeight; + } + + @Override + public int hashCode() { + long bits = com.codename1.flutter.ValueHash.bits(minWidth); + bits = bits * 31 + com.codename1.flutter.ValueHash.bits(maxWidth); + bits = bits * 31 + com.codename1.flutter.ValueHash.bits(minHeight); + bits = bits * 31 + com.codename1.flutter.ValueHash.bits(maxHeight); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "BoxConstraints(" + minWidth + "<=w<=" + maxWidth + ", " + minHeight + "<=h<=" + maxHeight + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/CustomPainter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/CustomPainter.java new file mode 100644 index 00000000000..456057037db --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/CustomPainter.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Canvas; +import com.codename1.flutter.semantics.SemanticsBuilderCallback; + +/** + * Base class an application implements to paint directly onto a {@link Canvas} + * — Flutter's {@code CustomPainter}. Subclasses override {@link #paint} to draw + * and {@link #shouldRepaint} to decide when a re-paint is required. The + * optional {@code repaint} listenable (a Listenable that triggers repaints) is + * captured for API shape. + */ +public abstract class CustomPainter { + + private Object repaint; + + public CustomPainter() { + } + + public void repaint(Object v) { + this.repaint = v; + } + + /** + * Draws this painter's content within a box of the given {@code size}. + */ + public abstract void paint(Canvas canvas, Size size); + + /** + * Whether a repaint is needed when the delegate is replaced by + * {@code oldDelegate}. Dart subclasses narrow the parameter type + * ({@code covariant}), which becomes an overload rather than an override in + * Java; this default keeps the base concrete so those subclasses compile. + */ + public boolean shouldRepaint(CustomPainter oldDelegate) { + return true; + } + + /** + * Returns the callback that produces this painter's accessibility nodes, or + * {@code null} when the painter contributes no custom semantics — Flutter's + * {@code CustomPainter.semanticsBuilder}. Painters that annotate their + * drawing for screen readers (e.g. the Rally line chart) override this to + * return a {@link SemanticsBuilderCallback}. + */ + public SemanticsBuilderCallback semanticsBuilder() { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Dp.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Dp.java new file mode 100644 index 00000000000..f8ecfd0bdd4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Dp.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.ui.Display; + +/** + * Flutter logical pixel to CN1 device pixel conversion. + * + *

Flutter's logical pixel is defined as roughly 1/160 inch (a Material dp, + * i.e. 0.15875mm). When a CN1 Display is available, logical values are + * converted through {@code Display.convertToPixels} using that physical + * definition; without a Display (headless unit tests) the scale is 1, so + * logical values and pixels coincide.

+ */ +public final class Dp { + + private static final double MM_PER_LP = 25.4 / 160.0; + private static double cachedScale = -1; + + private Dp() { + } + + /** + * Device pixels per Flutter logical pixel. + */ + public static double scale() { + if (!Display.isInitialized()) { + return 1; + } + if (cachedScale <= 0) { + // Ask the platform for its OWN scale factor first. Flutter's logical pixel is + // the platform's logical pixel, so where the platform reports one it is the + // right answer by definition - and it is not always what the density bucket + // implies. On iOS the bucket for a modern iPhone is DENSITY_560, which maps to + // 3.5, while UIScreen.scale is 3: everything rendered 7/6 too large against + // native Flutter on the same device. + cachedScale = Display.getInstance().getDevicePixelRatio(); + if (cachedScale <= 0) { + cachedScale = bucketScale(Display.getInstance().getDeviceDensity()); + } + if (cachedScale <= 0) { + // unknown bucket: fall back to physical measurement + int px = Display.getInstance().convertToPixels((float) (MM_PER_LP * 100)); + cachedScale = px / 100.0; + } + if (cachedScale <= 0) { + cachedScale = 1; + } + } + return cachedScale; + } + + /** + * Flutter/Android-style devicePixelRatio per CN1 density bucket. + * Flutter buckets its devicePixelRatio exactly like Android dp buckets + * (mdpi=1, hdpi=1.5, xhdpi=2, xxhdpi=3, xxxhdpi=4), so mapping CN1's + * density constants beats measuring physical millimeters — a 256dpi + * panel is an xhdpi/2.0 device, not a 1.6 one. + */ + private static double bucketScale(int density) { + switch (density) { + case Display.DENSITY_VERY_LOW: + return 0.5; + case Display.DENSITY_LOW: + return 0.75; + case Display.DENSITY_MEDIUM: + return 1.0; + case Display.DENSITY_HIGH: + return 1.5; + case Display.DENSITY_VERY_HIGH: + return 2.0; + case Display.DENSITY_HD: + return 3.0; + case Display.DENSITY_560: + return 3.5; + case Display.DENSITY_2HD: + return 4.0; + case Display.DENSITY_4K: + return 5.0; + default: + return -1; + } + } + + /** + * Converts logical pixels to (fractional) device pixels. + */ + public static double px(double lp) { + return lp * scale(); + } + + /** + * Converts logical pixels to millimeters (for CN1 APIs that take mm sizes, + * e.g. FontImage.createMaterial). + */ + public static float mm(double lp) { + return (float) (lp * MM_PER_LP); + } + + /** + * Test hook / hot-reload hook: forgets the cached scale. + */ + public static void resetCache() { + cachedScale = -1; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/FlutterRootLayout.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/FlutterRootLayout.java new file mode 100644 index 00000000000..6f72febb1b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/FlutterRootLayout.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.RenderElement; +import com.codename1.ui.Container; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.layouts.Layout; +import com.codename1.ui.plaf.Style; + +/** + * The CN1 layout installed on the single flat container hosting a Flutter + * subtree. CN1 sees one container whose children are the subtree's leaf + * components (Labels, buttons ...); this layout runs the Flutter constraint + * pass on the render-element tree and writes the resulting absolute bounds + * onto those components. + */ +public class FlutterRootLayout extends Layout { + + private final RenderHost host; + + public FlutterRootLayout(RenderHost host) { + this.host = host; + } + + public RenderHost host() { + return host; + } + + /// How many times a root has been laid out, and what that cost. + /// + /// Read the PROFILE, not the count. Start-up runs 47 passes, which looks + /// alarming until the split shows one pass of ~549ms and 46 of ~1ms: the + /// count is a red herring and the cost is a single full layout, which is + /// where the transpiled widget tree is actually constructed (LayoutBuilder + /// builds during layout, as Flutter's does). + private static int rootPasses; + private static long rootMs; + private static long rootFirstMs = -1; + private static long rootWorstMs; + private static int rootDepth; + private static int rootMaxDepth; + + /// The box the first few passes were given, in device pixels. + /// + /// A pass that runs against the WRONG box is not merely wasted: LayoutBuilder + /// builds during layout, so the whole widget tree is constructed against + /// that box — and an adaptive app asks the box which layout it is, so a + /// provisional size builds the wrong application. + private static final StringBuilder rootBoxes = new StringBuilder(); + private static int rootBoxesRecorded; + + private static void noteBox(BoxConstraints c) { + if (rootBoxesRecorded >= 6 || c == null) { + return; + } + rootBoxesRecorded++; + if (rootBoxes.length() > 0) { + rootBoxes.append(' '); + } + rootBoxes.append((int) c.maxWidth()).append('x').append((int) c.maxHeight()); + } + + /** Root layout passes so far, and their cost profile. */ + public static String rootLayoutCost() { + return rootPasses + " root pass(es) in " + rootMs + "ms (first=" + rootFirstMs + + "ms worst=" + rootWorstMs + "ms maxNesting=" + rootMaxDepth + + " boxes=" + rootBoxes + ")"; + } + + /** + * Whether a root layout pass is running right now, anywhere in the app. + * + *

Layout is not re-entrant. While a pass is walking the tree, elements + * are being built and mounted underneath it (LayoutBuilder builds during + * layout, as Flutter's does), so anything that asks for a FRESH pass at + * that moment walks a tree that is half-replaced: ancestors are already + * detached from the elements still being laid out, and every + * {@code .of(context)} lookup made from inside the new pass answers + * "nothing here". The gallery's splash crashed exactly this way on the + * native build -- a PositionedTransition ticked while the first frame was + * laying out, forced a second pass from inside the first, and the backdrop + * then failed a {@code GalleryOptions.of(context)!} whose provider was + * four levels above a parent pointer that had already been cleared.

+ * + *

Requests that arrive during a pass are recorded and run once the + * outermost pass finishes, so nothing is silently dropped.

+ */ + public static boolean inLayout() { + return rootDepth > 0; + } + + private static final java.util.List PENDING = + new java.util.ArrayList(); + + /** Records a relayout request that arrived while a pass was in progress. */ + static void deferRevalidate(RenderHost h) { + if (h != null && !PENDING.contains(h)) { + PENDING.add(h); + } + } + + private static void runDeferred() { + if (PENDING.isEmpty()) { + return; + } + // Bounded: a deferred pass can itself defer, and without a ceiling two + // hosts that invalidate each other would spin here forever. + for (int round = 0; round < 4 && !PENDING.isEmpty(); round++) { + java.util.List due = new java.util.ArrayList(PENDING); + PENDING.clear(); + for (int i = 0; i < due.size(); i++) { + due.get(i).revalidate(); + } + } + PENDING.clear(); + } + + @Override + public void layoutContainer(Container parent) { + RenderElement root = host.rootRenderElement(); + if (root == null) { + return; + } + long t0 = System.currentTimeMillis(); + rootPasses++; + rootDepth++; + rootMaxDepth = Math.max(rootMaxDepth, rootDepth); + try { + layoutRoot(parent, root); + } finally { + long took = System.currentTimeMillis() - t0; + // Only top-level passes are added up: a nested pass is already + // inside its parent's elapsed time, and counting both makes the + // total look like multiples of the work actually done. + if (rootDepth == 1) { + rootMs += took; + } + if (rootFirstMs < 0) { + rootFirstMs = took; + } + rootWorstMs = Math.max(rootWorstMs, took); + rootDepth--; + } + if (rootDepth == 0) { + runDeferred(); + } + } + + /// How many times a single pass may re-run because it invalidated itself. Two extra + /// attempts is enough for the case this exists for -- one subtree that builds late, + /// and its parents remeasured once around it -- and a bound means a widget that + /// dirties itself unconditionally degrades to a stale frame rather than a hang. + private static final int SETTLE_ATTEMPTS = 3; + + private void layoutRoot(Container parent, RenderElement root) { + Style s = parent.getStyle(); + BoxConstraints box = constraintsFor(parent); + noteBox(box); + // A pass can invalidate itself. A LayoutBuilder sits out a speculative measurement + // and inflates its subtree on a later one, underneath the performLayout of the box + // that contains it -- so by the time this pass finishes, the offsets it stored are + // for a set of children that has since changed. Running once and trusting the + // result left Reply's mail list with every card at the list's origin and a pane of + // zero size, which reads on screen as an empty page, and no later pass repaired it + // because they all hit the same clean cache. + // + // So: run, and if the tree says it is still dirty, run again with what it now + // knows. This settles on the second pass in practice. + for (int attempt = 0; attempt < SETTLE_ATTEMPTS; attempt++) { + root.layout(box); + root.position(s.getPaddingLeftNoRTL(), s.getPaddingTop()); + if (!root.needsLayout()) { + return; + } + } + } + + /** + * The constraints this pass hands the subtree. By default the pane's own box, which + * is right for a top-level host: CN1 owns that container's size. + * + *

A nested host (an effect's pane) overrides this, because there the Flutter pass + * already decided the subtree's constraints and the pane's component size may not + * reflect them yet.

+ */ + protected BoxConstraints constraintsFor(Container parent) { + Style s = parent.getStyle(); + int width = parent.getLayoutWidth() - parent.getSideGap() - s.getHorizontalPadding(); + int height = parent.getLayoutHeight() - parent.getBottomGap() - s.getVerticalPadding(); + if (width < 0) { + width = 0; + } + if (height < 0) { + height = 0; + } + return BoxConstraints.tight(width, height); + } + + @Override + public Dimension getPreferredSize(Container parent) { + RenderElement root = host.rootRenderElement(); + if (root == null) { + return new Dimension(0, 0); + } + // Dry pass with loose unbounded constraints. It goes through dryLayout, which keeps + // its own cache slot: these constraints differ from layoutContainer's tight ones, so + // sharing one slot made the two passes evict each other on every box in the tree. + Size sz = root.dryLayout(BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + Style s = parent.getStyle(); + int w = (int) Math.ceil(sz.width()) + s.getHorizontalPadding(); + int h = (int) Math.ceil(sz.height()) + s.getVerticalPadding(); + return new Dimension(w, h); + } + + @Override + public boolean isOverlapSupported() { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/GraphicsCanvas.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/GraphicsCanvas.java new file mode 100644 index 00000000000..3c63e45f81e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/GraphicsCanvas.java @@ -0,0 +1,822 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Canvas; +import com.codename1.flutter.Color; +import com.codename1.flutter.Gradient; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Paint; +import com.codename1.flutter.PaintingStyle; +import com.codename1.flutter.Path; +import com.codename1.flutter.RRect; +import com.codename1.flutter.Radius; +import com.codename1.flutter.Rect; +import com.codename1.flutter.StrokeCap; +import com.codename1.flutter.StrokeJoin; +import com.codename1.ui.Graphics; +import com.codename1.ui.Stroke; +import com.codename1.ui.geom.GeneralPath; +import com.codename1.ui.geom.Rectangle; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link Canvas} that draws onto a Codename One {@link Graphics} — the + * backend behind every {@code CustomPainter} in a transpiled app. + * + *

Coordinates

+ * Flutter painters work in LOGICAL pixels; CN1 draws in device pixels. The + * canvas therefore starts with a base transform of {@code scale(devicePixelRatio)} + * composed with the component's origin, and the painter is handed a {@link Size} + * in logical pixels — so a painter written against Flutter's coordinate system + * lands at the right physical size on any density. + * + *

Transforms

+ * The transform stack is kept HERE, as a 2x3 affine matrix, and every + * coordinate is mapped through it before reaching Graphics. That avoids + * depending on CN1's optional {@code Transform} support, which is not available + * on every port — a painter must not silently draw untransformed geometry + * because a platform lacks a capability. + */ +public class GraphicsCanvas extends Canvas { + + /** Affine matrix [a c e; b d f] — the same layout as Flutter's Matrix4 2D subset. */ + private double a = 1, b = 0, c = 0, d = 1, e = 0, f = 0; + + private final List stack = new ArrayList(); + private final Graphics g; + private final boolean shapes; + + public GraphicsCanvas(Graphics g, int originX, int originY, double devicePixelRatio) { + this.g = g; + this.shapes = g.isShapeSupported(); + translate(originX / (devicePixelRatio == 0 ? 1 : devicePixelRatio), + originY / (devicePixelRatio == 0 ? 1 : devicePixelRatio)); + scale(devicePixelRatio, devicePixelRatio); + } + + // ------------------------------------------------------------------ + // Transform stack + // ------------------------------------------------------------------ + + /** + * Saves the transform AND the clip. Only the transform used to be saved, while + * clipRect/clipRRect/clipPath narrow the Graphics clip itself, so a clip set between + * save() and restore() stayed in force after the restore and cropped every later + * drawing command of the painter. + */ + @Override + public void save() { + stack.add(new double[] {a, b, c, d, e, f}); + g.pushClip(); + } + + @Override + public void saveLayer(Rect bounds, Paint paint) { + // No offscreen compositing: the layer's blend/opacity is not modelled, + // but the transform must still nest correctly. + save(); + } + + @Override + public void restore() { + if (stack.isEmpty()) { + return; + } + double[] m = stack.remove(stack.size() - 1); + a = m[0]; b = m[1]; c = m[2]; d = m[3]; e = m[4]; f = m[5]; + g.popClip(); + } + + @Override + public void translate(double dx, double dy) { + e += a * dx + c * dy; + f += b * dx + d * dy; + } + + @Override + public void scale(double sx, double sy) { + a *= sx; b *= sx; + c *= sy; d *= sy; + } + + @Override + public void rotate(double radians) { + double cos = Math.cos(radians); + double sin = Math.sin(radians); + double na = a * cos + c * sin; + double nb = b * cos + d * sin; + double nc = c * cos - a * sin; + double nd = d * cos - b * sin; + a = na; b = nb; c = nc; d = nd; + } + + @Override + public void skew(double sx, double sy) { + double na = a + c * sy; + double nb = b + d * sy; + double nc = c + a * sx; + double nd = d + b * sx; + a = na; b = nb; c = nc; d = nd; + } + + private float mapX(double x, double y) { + return (float) (a * x + c * y + e); + } + + private float mapY(double x, double y) { + return (float) (b * x + d * y + f); + } + + /** The transform's average scale — used for stroke widths and radii. */ + private double avgScale() { + return (Math.sqrt(a * a + b * b) + Math.sqrt(c * c + d * d)) / 2; + } + + // ------------------------------------------------------------------ + // Drawing + // ------------------------------------------------------------------ + + @Override + public void drawRect(Rect rect, Paint paint) { + GeneralPath p = new GeneralPath(); + appendRect(p, rect.left(), rect.top(), rect.right(), rect.bottom()); + emit(p, paint); + } + + @Override + public void drawRRect(RRect rrect, Paint paint) { + GeneralPath p = new GeneralPath(); + appendRRect(p, rrect); + emit(p, paint); + } + + @Override + public void drawCircle(Offset center, double radius, Paint paint) { + drawOval(Rect.fromCircle(center, radius), paint); + } + + @Override + public void drawOval(Rect rect, Paint paint) { + GeneralPath p = new GeneralPath(); + appendOval(p, rect.left(), rect.top(), rect.right(), rect.bottom()); + emit(p, paint); + } + + @Override + public void drawLine(Offset p1, Offset p2, Paint paint) { + GeneralPath p = new GeneralPath(); + p.moveTo(mapX(p1.dx(), p1.dy()), mapY(p1.dx(), p1.dy())); + p.lineTo(mapX(p2.dx(), p2.dy()), mapY(p2.dx(), p2.dy())); + // a line has no interior: always stroked, whatever the paint style says + strokeShape(p, paint); + } + + @Override + public void drawArc(Rect rect, double startAngle, double sweepAngle, boolean useCenter, Paint paint) { + GeneralPath p = new GeneralPath(); + appendArc(p, rect, startAngle, sweepAngle, useCenter); + emit(p, paint); + } + + @Override + public void drawPath(Path path, Paint paint) { + GeneralPath p = toGeneralPath(path); + emit(p, paint); + } + + /** + * Fills a triangle mesh — {@code Canvas.drawVertices}. + * + *

Codename One has no mesh primitive, so each triangle is filled as a + * path. A mesh whose vertices all share one colour is filled as a single + * polygon instead: it is the same picture without the hairline seams that + * antialiased abutting triangles leave, and it is the shape the + * 2D-transformations demo actually draws — one flat hexagon per board + * point, as a triangle fan. + * + *

Per-vertex colour interpolation (Gouraud shading) is not modelled; a + * multi-coloured triangle takes its first vertex's colour. The blend mode is + * ignored — Codename One composites source-over. + * + *

Points go through {@link #mapX}/{@link #mapY} like every other + * primitive here: a painter works in logical pixels and the canvas carries + * the device-pixel-ratio scale, so a path built from raw coordinates comes + * out at a third of its size on a 3x screen. + * + *

This used to draw nothing at all, so the demo's entire board — the only + * content on that screen — was invisible against its background. + */ + @Override + public void drawVertices(Object vertices, Object blendMode, Paint paint) { + if (!(vertices instanceof com.codename1.flutter.Vertices)) { + return; + } + com.codename1.flutter.Vertices v = (com.codename1.flutter.Vertices) vertices; + List points = order(v); + if (points.size() < 3) { + return; + } + List colors = colorsOf(v, points.size()); + + if (v.getMode() == com.codename1.flutter.VertexMode.triangleFan && uniform(colors)) { + GeneralPath p = new GeneralPath(); + p.moveTo(mapX(points.get(0).dx(), points.get(0).dy()), + mapY(points.get(0).dx(), points.get(0).dy())); + for (int i = 1; i < points.size(); i++) { + p.lineTo(mapX(points.get(i).dx(), points.get(i).dy()), + mapY(points.get(i).dx(), points.get(i).dy())); + } + p.closePath(); + fillShape(p, meshPaint(paint, colors.isEmpty() ? null : colors.get(0))); + return; + } + + for (int t = 0; t + 2 < triangleLimit(v, points.size()); t += triangleStep(v)) { + int a; + int b; + int c; + switch (v.getMode()) { + case triangleFan: + a = 0; + b = t + 1; + c = t + 2; + break; + case triangleStrip: + a = t; + b = t + 1; + c = t + 2; + break; + default: + a = t; + b = t + 1; + c = t + 2; + break; + } + if (c >= points.size()) { + break; + } + GeneralPath p = new GeneralPath(); + p.moveTo(mapX(points.get(a).dx(), points.get(a).dy()), + mapY(points.get(a).dx(), points.get(a).dy())); + p.lineTo(mapX(points.get(b).dx(), points.get(b).dy()), + mapY(points.get(b).dx(), points.get(b).dy())); + p.lineTo(mapX(points.get(c).dx(), points.get(c).dy()), + mapY(points.get(c).dx(), points.get(c).dy())); + p.closePath(); + fillShape(p, meshPaint(paint, colors.isEmpty() ? null : colors.get(a))); + } + } + + /** The mesh's positions, resolved through its index buffer when it has one. */ + private static List order(com.codename1.flutter.Vertices v) { + List out = new ArrayList(); + dart.core.DartList positions = v.getPositions(); + if (positions == null) { + return out; + } + dart.core.DartList indices = v.getIndices(); + if (indices == null || indices.isEmpty()) { + for (Offset o : positions) { + out.add(o); + } + return out; + } + for (Integer i : indices) { + if (i != null && i.intValue() >= 0 && i.intValue() < positions.size()) { + out.add(positions.get(i.intValue())); + } + } + return out; + } + + private static List colorsOf(com.codename1.flutter.Vertices v, int count) { + List out = new ArrayList(); + dart.core.DartList colors = v.getColors(); + if (colors == null) { + return out; + } + for (int i = 0; i < count; i++) { + out.add(i < colors.size() ? colors.get(i) : null); + } + return out; + } + + private static boolean uniform(List colors) { + if (colors.isEmpty()) { + return true; + } + Color first = colors.get(0); + for (Color c : colors) { + if (c == null || first == null) { + if (c != first) { + return false; + } + } else if (c.value() != first.value()) { + return false; + } + } + return true; + } + + /** The paint to fill a triangle with: the mesh's vertex colour wins over the Paint's. */ + private static Paint meshPaint(Paint paint, Color vertexColor) { + if (vertexColor == null) { + return paint; + } + Paint p = new Paint(); + p.color(vertexColor); + return p; + } + + private static int triangleLimit(com.codename1.flutter.Vertices v, int count) { + return count; + } + + private static int triangleStep(com.codename1.flutter.Vertices v) { + return v.getMode() == com.codename1.flutter.VertexMode.triangles ? 3 : 1; + } + + @Override + public void drawColor(Color color, Object blendMode) { + if (color == null) { + return; + } + int alpha = g.getAlpha(); + g.setColor(color.rgb()); + g.setAlpha(color.alpha()); + g.fillRect(g.getClipX(), g.getClipY(), g.getClipWidth(), g.getClipHeight()); + g.setAlpha(alpha); + } + + @Override + public void drawShadow(Path path, Color color, double elevation, boolean transparentOccluder) { + // Approximated as a solid silhouette offset by the elevation; CN1's + // real shadow machinery is bound to Style-driven borders, not to a + // free-form path. + if (color == null) { + return; + } + save(); + translate(0, elevation); + Paint p = new Paint(); + p.color(color); + p.style(PaintingStyle.fill); + drawPath(path, p); + restore(); + } + + @Override + public void clipRect(Rect rect) { + g.clipRect((int) Math.floor(mapX(rect.left(), rect.top())), + (int) Math.floor(mapY(rect.left(), rect.top())), + (int) Math.ceil(rect.width() * avgScale()), + (int) Math.ceil(rect.height() * avgScale())); + } + + @Override + public void clipRRect(RRect rrect) { + // CN1 clips to rectangles; the corner rounding is dropped rather than + // clipping nothing at all. + clipRect(rrect.outerRect()); + } + + @Override + public void clipPath(Path path) { + if (shapes) { + // NOTE: on iOS this does nothing -- see EffectRenderElement.paintRoundClipped + // for the probe. A Canvas.clipPath is therefore inert there, and the subtree + // paints unclipped. Left as it is because nothing in the gallery calls + // clipPath, so there is no shape to check a fix against; when something does, + // it needs the layer treatment the clip widgets now use. + g.setClip(toGeneralPath(path)); + } + } + + // ------------------------------------------------------------------ + // Path construction (device space) + // ------------------------------------------------------------------ + + private GeneralPath toGeneralPath(Path path) { + GeneralPath p = new GeneralPath(); + double cx = 0; + double cy = 0; + // Whether the path has a current point, which decides how an arcTo with + // forceMoveTo=false attaches: it JOINS the current point with a line, and only + // starts a subpath of its own when there is nothing to join to. + boolean hasCurrent = false; + for (Path.Segment s : path.segments()) { + double[] v = s.coords; + if ("moveTo".equals(s.verb)) { + p.moveTo(mapX(v[0], v[1]), mapY(v[0], v[1])); + cx = v[0]; cy = v[1]; hasCurrent = true; + } else if ("lineTo".equals(s.verb)) { + p.lineTo(mapX(v[0], v[1]), mapY(v[0], v[1])); + cx = v[0]; cy = v[1]; hasCurrent = true; + } else if ("cubicTo".equals(s.verb)) { + p.curveTo(mapX(v[0], v[1]), mapY(v[0], v[1]), + mapX(v[2], v[3]), mapY(v[2], v[3]), + mapX(v[4], v[5]), mapY(v[4], v[5])); + cx = v[4]; cy = v[5]; hasCurrent = true; + } else if ("quadraticBezierTo".equals(s.verb) || "conicTo".equals(s.verb)) { + // a conic is approximated by its quadratic control polygon + p.quadTo(mapX(v[0], v[1]), mapY(v[0], v[1]), + mapX(v[2], v[3]), mapY(v[2], v[3])); + cx = v[2]; cy = v[3]; hasCurrent = true; + } else if ("arcTo".equals(s.verb)) { + Rect oval = Rect.fromLTRB(v[0], v[1], v[2], v[3]); + boolean forceMoveTo = v[6] != 0; + appendArc(p, oval, v[4], v[5], false, forceMoveTo || !hasCurrent); + double end = v[4] + v[5]; + cx = oval.center().dx() + oval.width() / 2 * Math.cos(end); + cy = oval.center().dy() + oval.height() / 2 * Math.sin(end); + hasCurrent = true; + } else if ("arcToPoint".equals(s.verb)) { + double[] arc = arcToPoint(cx, cy, v[0], v[1], v[2], v[5] != 0, v[6] != 0, + ARC_SEGMENTS); + if (arc == null) { + p.lineTo(mapX(v[0], v[1]), mapY(v[0], v[1])); + } else { + for (int i = 0; i < arc.length; i += 2) { + p.lineTo(mapX(arc[i], arc[i + 1]), mapY(arc[i], arc[i + 1])); + } + } + cx = v[0]; cy = v[1]; hasCurrent = true; + } else if ("addRect".equals(s.verb)) { + appendRect(p, v[0], v[1], v[2], v[3]); + hasCurrent = true; + } else if ("addOval".equals(s.verb) || "addRRect".equals(s.verb)) { + appendOval(p, v[0], v[1], v[2], v[3]); + hasCurrent = true; + } else if ("close".equals(s.verb)) { + p.closePath(); + } + } + return p; + } + + /// The points along a circular arc from (x0,y0) to (x1,y1), in the path's own + /// coordinates, EXCLUDING the start and including the end. + /// + /// Flutter's {@code arcToPoint} names an arc by its END POINT and a radius, the way + /// SVG does. Both callers used to replace it with a straight line -- the note here + /// said that "keeps the outline closed", which it does, but a chord is not an arc: a + /// bottom app bar's notch is two quadratics either side of one of these, so the curve + /// went down, cut straight across, and came back up. It reads as a dimple with a bump + /// in it, which is exactly what it is. + /// + /// Solved as SVG does (endpoint to centre parameterisation) for the circular case, + /// which is the only one Flutter's own notch strategies use, and flattened: a filled + /// path is flattened by the rasteriser anyway, and the ports flatten clip paths + /// themselves. + /// + /// @param segments how many line segments to approximate with; 1 gives back the chord + /// @return {x, y} pairs, or null when the arc is degenerate and the chord is right + public static double[] arcToPoint(double x0, double y0, double x1, double y1, + double radius, boolean largeArc, boolean clockwise, int segments) { + double dx = (x0 - x1) / 2; + double dy = (y0 - y1) / 2; + double half = Math.sqrt(dx * dx + dy * dy); + if (half <= 0 || segments < 2) { + return null; + } + double r = Math.max(Math.abs(radius), half); + // The centre lies off the chord's midpoint, perpendicular to it. Which side is + // what largeArc and clockwise choose between. + double coef = Math.sqrt(Math.max(0, (r * r - half * half))) / half; + double sign = largeArc != clockwise ? 1 : -1; + double cx = (x0 + x1) / 2 + sign * coef * dy; + double cy = (y0 + y1) / 2 - sign * coef * dx; + double a0 = Math.atan2(y0 - cy, x0 - cx); + double a1 = Math.atan2(y1 - cy, x1 - cx); + double sweep = a1 - a0; + // Normalise the sweep into the direction asked for. + if (clockwise && sweep < 0) { + sweep += 2 * Math.PI; + } else if (!clockwise && sweep > 0) { + sweep -= 2 * Math.PI; + } + double[] out = new double[segments * 2]; + for (int i = 1; i <= segments; i++) { + double a = a0 + sweep * i / segments; + out[(i - 1) * 2] = cx + r * Math.cos(a); + out[(i - 1) * 2 + 1] = cy + r * Math.sin(a); + } + return out; + } + + /** Segments enough that an arc reads as a curve at any size a notch or badge uses. */ + public static final int ARC_SEGMENTS = 24; + + private void appendRect(GeneralPath p, double l, double t, double r, double b) { + p.moveTo(mapX(l, t), mapY(l, t)); + p.lineTo(mapX(r, t), mapY(r, t)); + p.lineTo(mapX(r, b), mapY(r, b)); + p.lineTo(mapX(l, b), mapY(l, b)); + p.closePath(); + } + + private void appendRRect(GeneralPath p, RRect rr) { + Rect r = rr.outerRect(); + double rad = radius(rr); + if (rad <= 0) { + appendRect(p, r.left(), r.top(), r.right(), r.bottom()); + return; + } + double l = r.left(); + double t = r.top(); + double ri = r.right(); + double bo = r.bottom(); + rad = Math.min(rad, Math.min(r.width(), r.height()) / 2); + double k = rad * KAPPA; + p.moveTo(mapX(l + rad, t), mapY(l + rad, t)); + p.lineTo(mapX(ri - rad, t), mapY(ri - rad, t)); + curve(p, ri - rad + k, t, ri, t + rad - k, ri, t + rad); + p.lineTo(mapX(ri, bo - rad), mapY(ri, bo - rad)); + curve(p, ri, bo - rad + k, ri - rad + k, bo, ri - rad, bo); + p.lineTo(mapX(l + rad, bo), mapY(l + rad, bo)); + curve(p, l + rad - k, bo, l, bo - rad + k, l, bo - rad); + p.lineTo(mapX(l, t + rad), mapY(l, t + rad)); + curve(p, l, t + rad - k, l + rad - k, t, l + rad, t); + p.closePath(); + } + + private static double radius(RRect rr) { + Radius tl = rr.tlRadius(); + return tl == null ? 0 : tl.x(); + } + + /** Bezier constant for approximating a quarter circle. */ + private static final double KAPPA = 0.5522847498307933; + + private void appendOval(GeneralPath p, double l, double t, double r, double b) { + double cx = (l + r) / 2; + double cy = (t + b) / 2; + double rx = (r - l) / 2; + double ry = (b - t) / 2; + double kx = rx * KAPPA; + double ky = ry * KAPPA; + p.moveTo(mapX(cx, t), mapY(cx, t)); + curve(p, cx + kx, t, r, cy - ky, r, cy); + curve(p, r, cy + ky, cx + kx, b, cx, b); + curve(p, cx - kx, b, l, cy + ky, l, cy); + curve(p, l, cy - ky, cx - kx, t, cx, t); + p.closePath(); + } + + private void curve(GeneralPath p, double x1, double y1, double x2, double y2, double x3, double y3) { + p.curveTo(mapX(x1, y1), mapY(x1, y1), mapX(x2, y2), mapY(x2, y2), mapX(x3, y3), mapY(x3, y3)); + } + + /** Flattens the arc into line segments — enough for chart arcs and gauges. */ + private void appendArc(GeneralPath p, Rect rect, double startAngle, double sweepAngle, boolean useCenter) { + appendArc(p, rect, startAngle, sweepAngle, useCenter, true); + } + + /** + * Appends an arc. When {@code startsNewSubpath} is false the arc is JOINED to whatever + * the path already ends at, with a line to its start point - Flutter's + * {@code arcTo(..., forceMoveTo: false)}. That join is what turns two opposing + * half-circle arcs into one stadium outline instead of two separate discs. + */ + private void appendArc(GeneralPath p, Rect rect, double startAngle, double sweepAngle, + boolean useCenter, boolean startsNewSubpath) { + double cx = rect.center().dx(); + double cy = rect.center().dy(); + double rx = rect.width() / 2; + double ry = rect.height() / 2; + int steps = Math.max(2, (int) Math.ceil(Math.abs(sweepAngle) / (Math.PI / 36))); + if (useCenter) { + p.moveTo(mapX(cx, cy), mapY(cx, cy)); + } + for (int i = 0; i <= steps; i++) { + double ang = startAngle + sweepAngle * i / steps; + double x = cx + rx * Math.cos(ang); + double y = cy + ry * Math.sin(ang); + if (i == 0 && !useCenter && startsNewSubpath) { + p.moveTo(mapX(x, y), mapY(x, y)); + } else { + p.lineTo(mapX(x, y), mapY(x, y)); + } + } + if (useCenter) { + p.closePath(); + } + } + + // ------------------------------------------------------------------ + // Paint application + // ------------------------------------------------------------------ + + private void emit(GeneralPath p, Paint paint) { + if (paint != null && paint.style() == PaintingStyle.stroke) { + strokeShape(p, paint); + } else { + fillShape(p, paint); + } + } + + private void applyColor(Paint paint) { + Color col = paint == null ? null : paint.color(); + if (col == null) { + g.setColor(0); + g.setAlpha(255); + return; + } + g.setColor(col.rgb()); + g.setAlpha(col.alpha()); + } + + private void fillShape(GeneralPath p, Paint paint) { + if (fillWithGradient(p, paint)) { + return; + } + int alpha = g.getAlpha(); + applyColor(paint); + if (shapes) { + g.fillShape(p); + } + g.setAlpha(alpha); + } + + /** + * Fills {@code p} with the Paint's gradient shader, if it has one, by clipping to the + * shape and running Codename One's linear gradient across its bounding box. Returns + * false when there is no gradient to paint, leaving the solid path to the caller. + * + *

Without this a shaded Paint carries no {@code color} at all and everything it + * draws comes out the default black - which is what the gallery's settings icon did, + * its pink and teal sticks both painting black.

+ */ + private boolean fillWithGradient(GeneralPath p, Paint paint) { + if (paint == null || !(paint.shader() instanceof Gradient.GradientShader)) { + return false; + } + Gradient gradient = ((Gradient.GradientShader) paint.shader()).gradient(); + int[] ramp = gradient.colorRamp(); + if (ramp.length == 0 || !shapes) { + return false; + } + Rectangle bounds = p.getBounds(); + if (bounds.getWidth() <= 0 || bounds.getHeight() <= 0) { + return false; + } + boolean vertical = isVertical(gradient); + int alpha = g.getAlpha(); + int clipX = g.getClipX(); + int clipY = g.getClipY(); + int clipW = g.getClipWidth(); + int clipH = g.getClipHeight(); + try { + if (p.isRectangle()) { + // The ramp fills the whole path, so no clipping is needed at all. + g.setAlpha((ramp[0] >>> 24) & 0xff); + g.fillLinearGradient(ramp[0] & 0xffffff, ramp[ramp.length - 1] & 0xffffff, + bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), + !vertical); + return true; + } + // A SHAPE clip does not confine fillLinearGradient on every port. On iOS it + // is simply ignored -- Graphics.isShapeClipSupported() answers true, the clip + // is installed, and the ramp still fills the path's bounding BOX. The + // gallery's settings icon is two stadium-shaped sticks painted that way, and + // on the device they came out as square blocks while the desktop, where the + // clip is honoured, drew them correctly. That is the shape of defect this + // whole exercise keeps turning up: the sweep cannot see it. + // + // So do not ask a shape clip to do it. Fill the SHAPE once per band of the + // ramp, with a RECTANGULAR clip confining each fill to its band -- a rect + // clip and fillShape are honoured everywhere. The shape is then exact on + // every port, and the ramp is quantised rather than absent. + int span = vertical ? bounds.getHeight() : bounds.getWidth(); + int bands = Math.max(8, Math.min(span, 48)); + for (int i = 0; i < bands; i++) { + int from = span * i / bands; + int to = span * (i + 1) / bands; + if (to <= from) { + continue; + } + int bandX = vertical ? bounds.getX() : bounds.getX() + from; + int bandY = vertical ? bounds.getY() + from : bounds.getY(); + int bandW = vertical ? bounds.getWidth() : to - from; + int bandH = vertical ? to - from : bounds.getHeight(); + int argb = rampAt(ramp, (i + 0.5) / bands); + g.setClip(clipX, clipY, clipW, clipH); + g.clipRect(bandX, bandY, bandW, bandH); + if (g.getClipWidth() <= 0 || g.getClipHeight() <= 0) { + continue; + } + g.setAlpha((argb >>> 24) & 0xff); + g.setColor(argb & 0xffffff); + g.fillShape(p); + } + } finally { + g.setClip(clipX, clipY, clipW, clipH); + g.setAlpha(alpha); + } + return true; + } + + /// The ramp's colour at {@code t} in 0..1, with the stops spread evenly. + /// + /// Even spacing is Flutter's own default when a gradient declares no {@code stops}, + /// and it is what the previous code assumed far more crudely: it read the first and + /// last entries and ignored everything between them, so a three-stop gradient lost + /// its middle colour entirely. + static int rampAt(int[] ramp, double t) { + if (ramp.length == 1) { + return ramp[0]; + } + if (t <= 0) { + return ramp[0]; + } + if (t >= 1) { + return ramp[ramp.length - 1]; + } + double pos = t * (ramp.length - 1); + int i = (int) pos; + if (i >= ramp.length - 1) { + return ramp[ramp.length - 1]; + } + double f = pos - i; + int a = ramp[i]; + int b = ramp[i + 1]; + return (lerpChannel(a, b, f, 24) << 24) | (lerpChannel(a, b, f, 16) << 16) + | (lerpChannel(a, b, f, 8) << 8) | lerpChannel(a, b, f, 0); + } + + private static int lerpChannel(int a, int b, double t, int shift) { + int ca = (a >>> shift) & 0xff; + int cb = (b >>> shift) & 0xff; + return (int) Math.round(ca + (cb - ca) * t) & 0xff; + } + + /** Whether the gradient runs top-to-bottom rather than left-to-right. */ + private static boolean isVertical(Gradient gradient) { + Object begin = gradient.getBegin(); + Object end = gradient.getEnd(); + if (!(begin instanceof Alignment) || !(end instanceof Alignment)) { + return false; // Flutter's default is centerLeft -> centerRight + } + double dx = Math.abs(((Alignment) end).x() - ((Alignment) begin).x()); + double dy = Math.abs(((Alignment) end).y() - ((Alignment) begin).y()); + return dy > dx; + } + + private void strokeShape(GeneralPath p, Paint paint) { + int alpha = g.getAlpha(); + applyColor(paint); + if (shapes) { + double w = paint == null || paint.strokeWidth() <= 0 ? 1 : paint.strokeWidth(); + g.drawShape(p, new Stroke((float) (w * avgScale()), capOf(paint), joinOf(paint), + (float) (paint == null ? 4 : paint.strokeMiterLimit()))); + } + g.setAlpha(alpha); + } + + private static int capOf(Paint paint) { + StrokeCap c = paint == null ? null : paint.strokeCap(); + if (c == StrokeCap.round) { + return Stroke.CAP_ROUND; + } + if (c == StrokeCap.square) { + return Stroke.CAP_SQUARE; + } + return Stroke.CAP_BUTT; + } + + private static int joinOf(Paint paint) { + StrokeJoin j = paint == null ? null : paint.strokeJoin(); + if (j == StrokeJoin.bevel) { + return Stroke.JOIN_BEVEL; + } + if (j == StrokeJoin.round) { + return Stroke.JOIN_ROUND; + } + return Stroke.JOIN_MITER; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HitTestBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HitTestBehavior.java new file mode 100644 index 00000000000..e5178c8d8dc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HitTestBehavior.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +/** + * How a target participates in hit testing — Flutter's {@code HitTestBehavior}. + */ +public enum HitTestBehavior { + deferToChild, opaque, translucent +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HorizontalScrollRootLayout.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HorizontalScrollRootLayout.java new file mode 100644 index 00000000000..acbe487de66 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/HorizontalScrollRootLayout.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.RenderElement; +import com.codename1.ui.Container; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.plaf.Style; + +/** + * {@link ScrollRootLayout}'s horizontal twin: the content is laid out with a + * tight viewport HEIGHT and an unbounded horizontal main axis, and the content + * extent is reported as the preferred size so CN1's tensile scrolling takes + * over once the content is wider than the viewport. + * + *

Used by horizontally scrolling Flutter boundaries — {@code PageView}, and + * {@code ListView(scrollDirection: Axis.horizontal)}.

+ */ +public class HorizontalScrollRootLayout extends FlutterRootLayout { + + /** + * The content height used by the last real layout pass. As in the vertical + * case, getPreferredSize must measure at the SAME extent layoutContainer + * used, or the two passes alternate and the UI bounces. + */ + private int lastLayoutHeight = -1; + + public HorizontalScrollRootLayout(RenderHost host) { + super(host); + } + + @Override + public void layoutContainer(Container parent) { + RenderElement root = host().rootRenderElement(); + if (root == null) { + return; + } + Style s = parent.getStyle(); + int height = parent.getHeight() - s.getVerticalPadding(); + if (height < 0) { + height = 0; + } + lastLayoutHeight = height; + root.layout(contentConstraints(height)); + root.position(s.getPaddingLeftNoRTL(), s.getPaddingTop()); + } + + @Override + public Dimension getPreferredSize(Container parent) { + RenderElement root = host().rootRenderElement(); + if (root == null) { + return new Dimension(0, 0); + } + Style s = parent.getStyle(); + int height = lastLayoutHeight > 0 ? lastLayoutHeight : parent.getHeight() - s.getVerticalPadding(); + Size sz = root.layout(height > 0 + ? contentConstraints(height) + : BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + int w = (int) Math.ceil(sz.width()) + s.getHorizontalPadding(); + int h = (int) Math.ceil(sz.height()) + s.getVerticalPadding(); + return new Dimension(w, h); + } + + /** + * Tight viewport height, unbounded width — the Flutter viewport contract + * for a horizontal scrollable. + */ + public static BoxConstraints contentConstraints(double height) { + return new BoxConstraints(0, Double.POSITIVE_INFINITY, height, height); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/PaintingContext.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/PaintingContext.java new file mode 100644 index 00000000000..baf6caf8c8b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/PaintingContext.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Canvas; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Rect; + +/** + * The canvas + child-painting handle passed to {@code RenderObject.paint} — + * Flutter's {@code PaintingContext}. The sliders demo's custom slider shapes + * read {@link #canvas()} to draw the thumb / value indicator. + */ +public class PaintingContext { + + private final Canvas canvas; + private final Rect estimatedBounds; + + public PaintingContext() { + this(new Canvas(), Rect.zero); + } + + public PaintingContext(Canvas canvas, Rect estimatedBounds) { + this.canvas = canvas; + this.estimatedBounds = estimatedBounds; + } + + /** The canvas onto which painting should be done. */ + public Canvas canvas() { + return canvas; + } + + /** Paints a child render object at the given offset (no-op stub). */ + public void paintChild(RenderObject child, Offset offset) { + } + + /** An estimate of the bounds within which painting will happen. */ + public Rect estimatedBounds() { + return estimatedBounds; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderBox.java new file mode 100644 index 00000000000..fd5da0767fd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderBox.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Offset; +import com.codename1.flutter.rendering.Dp; + +/** + * A render object laid out with the box protocol (a Cartesian size) — Flutter's + * {@code RenderBox}. + * + *

Backed by the live layout when it is obtained from + * {@code BuildContext.findRenderObject()}: the size and the global position + * come from the Codename One component the element owns, converted to the + * LOGICAL pixels a Flutter caller expects. + * + *

It used to answer zero and the identity mapping, which is worse than + * unimplemented — a caller positioning something by these numbers puts it in + * the top-left corner with no indication anything went wrong. The gallery's + * feature-discovery highlight centres itself this way. + */ +public class RenderBox extends RenderObject { + + private Size size = Size.ZERO; + /** The element this box reports for, when it was obtained from the live tree. */ + private com.codename1.flutter.RenderElement element; + + public RenderBox() { + } + + public RenderBox(com.codename1.flutter.RenderElement element) { + this.element = element; + } + + /** The size of this box after layout, in logical pixels. */ + public Size size() { + if (element == null) { + return size; + } + double scale = scale(); + Size s = element.size(); + return s == null ? Size.ZERO : new Size(s.width() / scale, s.height() / scale); + } + + /** Named setter used by the runtime once layout is known. */ + public void size(Size v) { + this.size = v == null ? Size.ZERO : v; + } + + /** Whether this box has been through layout and has a valid size. */ + public boolean hasSize() { + return size() != null; + } + + /** + * Converts a point from this box's local coordinate space to the global + * (screen) space, optionally relative to {@code ancestor}. + */ + public Offset localToGlobal(Offset point, RenderObject ancestor) { + Offset p = point == null ? Offset.zero : point; + if (element == null) { + return p; + } + // The layout pass writes every box's position absolutely within the + // host, so the element already knows where it is; no component needed + // (many elements own none). + // + // ...but "within the host" is not the screen when the subtree lives in a + // host of its own. A root Scaffold's app bar is hosted in the Codename + // One Toolbar, so its boxes are positioned from the toolbar's origin, + // and anything asking where it is on SCREEN was told where it is inside + // the bar. The gallery's feature discovery asks exactly that to place + // its coach mark: the circle came out low enough to change which branch + // of its own radius rule it took, and so the wrong size as well as the + // wrong place. Adding the host container's absolute position makes the + // answer global, which is what the name says. + double scale = scale(); + // The host's own origin is NOT added here. + // + // It was, on the theory that a Toolbar-hosted subtree reports positions + // from the bar's origin rather than the screen's. Measured on the + // device, that double-counted: the feature discovery's centre came back + // 162 logical pixels below the icon it was asking about, which then also + // put its background circle in the wrong size bracket. The layout pass + // already writes absolute positions within the form, so the element's + // own x/y are the global answer. + return new Offset(p.dx() + element.x() / scale, + p.dy() + element.y() / scale); + } + + /** + * Converts a point from global (screen) space to this box's local space, + * optionally relative to {@code ancestor}. + */ + public Offset globalToLocal(Offset point, RenderObject ancestor) { + Offset p = point == null ? Offset.zero : point; + if (element == null) { + return p; + } + double scale = scale(); + return new Offset(p.dx() - element.x() / scale, p.dy() - element.y() / scale); + } + + private static double scale() { + double s = Dp.scale(); + return s <= 0 ? 1 : s; + } + + /** + * The transform mapping this object's coordinate space to {@code ancestor} + * (a 4x4 matrix in Flutter). Returned opaque for this milestone. + */ + public Object getTransformTo(RenderObject ancestor) { + return null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderHost.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderHost.java new file mode 100644 index 00000000000..46e53359666 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderHost.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Toolbar; +import com.codename1.ui.layouts.Layout; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * The bridge between a Flutter element subtree and the single flat CN1 + * {@link Container} that hosts its leaf components. Every element carries a + * reference to its host; render elements attach/detach their CN1 component + * here and the host's {@link FlutterRootLayout} drives the constraint pass. + * + *

All CN1 references are optional so the reconciler and layout algorithms + * can be exercised in headless unit tests (no Display, no components).

+ */ +public class RenderHost { + + private Container container; + private Form form; + private Toolbar toolbar; + private boolean toolbarTitleHost; + private Funcs.Func0 rootSupplier; + + public Container container() { + return container; + } + + public void container(Container container) { + this.container = container; + } + + /** + * The CN1 Form hosting this subtree; only set on the main host created by + * {@code FlutterUI.runApp} (null under {@code FlutterUI.wrap} and tests). + */ + public Form form() { + return form; + } + + public void form(Form form) { + this.form = form; + } + + /** + * The Toolbar this host renders into, when this host is the title area of + * a root Scaffold's app bar. + */ + public Toolbar toolbar() { + return toolbar; + } + + public void toolbar(Toolbar toolbar) { + this.toolbar = toolbar; + } + + private boolean formToolbarBound; + + /** + * Whether a root-mode Scaffold in this host's tree claimed the Form's + * Toolbar for its AppBar. + * + *

A route whose Scaffold is nested (inside a ColoredBox, say) renders + * its AppBar as an in-canvas strip instead, and then the Form must show no + * Toolbar at all — otherwise the page carries two bars and CN1's toolbar + * inset shrinks the Flutter canvas.

+ */ + public boolean isFormToolbarBound() { + return formToolbarBound; + } + + public void formToolbarBound(boolean v) { + this.formToolbarBound = v; + } + + public boolean isToolbarTitleHost() { + return toolbarTitleHost; + } + + public void toolbarTitleHost(boolean v) { + this.toolbarTitleHost = v; + } + + /** + * Supplies the root element of the subtree this host displays. A supplier + * (not a fixed element) because reconciliation may replace the element. + */ + public void rootSupplier(Funcs.Func0 supplier) { + this.rootSupplier = supplier; + } + + public void rootElement(final Element e) { + this.rootSupplier = new Funcs.Func0() { + @Override + public Element call() { + return e; + } + }; + } + + public Element rootElement() { + return rootSupplier == null ? null : rootSupplier.call(); + } + + /** + * The first render element at or below the root element — the entry point + * of the layout pass. + */ + public RenderElement rootRenderElement() { + return RenderElement.findRenderElement(rootElement()); + } + + // ------------------------------------------------------------------ + // Component plumbing (no-ops when headless) + // ------------------------------------------------------------------ + + private final List attachOrder = new ArrayList(); + private int insertionCursor = -1; + + /** + * The component-owning render elements attached to this host, in flat + * container order (mirrors the CN1 container's child order at runtime; + * observable headless for unit tests). + */ + public List attachOrder() { + return attachOrder; + } + + /// The element at the end of the attach order, or null when nothing is attached. + public RenderElement lastAttached() { + int n = attachOrder.size(); + return n == 0 ? null : attachOrder.get(n - 1); + } + + /** + * Points the attach cursor at a flat-container index: subsequent + * {@link #attach} calls insert sequentially there instead of appending. + * Used by {@code Element.updateChild} to drop a replacement subtree's + * components into the slots the replaced subtree occupied, keeping the + * container's z-order aligned with element-tree order. Returns the + * previous cursor for restoration via {@link #endInsertion}. + */ + public int beginInsertion(int index) { + int prev = insertionCursor; + insertionCursor = index; + return prev; + } + + public void endInsertion(int previous) { + insertionCursor = previous; + } + + /** + * The flat-container index of the first component in the given element + * subtree that is attached to this host, or -1. + */ + public int firstAttachIndex(Element subtree) { + if (subtree == null) { + return -1; + } + if (subtree instanceof RenderElement) { + int i = attachOrder.indexOf(subtree); + if (i >= 0) { + return i; + } + } + final int[] found = {-1}; + subtree.visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + if (found[0] < 0) { + found[0] = firstAttachIndex(c); + } + } + }); + return found[0]; + } + + /** + * Attaches an element's component at the insertion cursor (advancing it) + * or, without an active cursor, at the end — mount order is depth-first, + * so appending preserves element-tree order for fresh subtrees. + */ + public void attach(RenderElement owner) { + if (owner == null) { + return; + } + int index = insertionCursor >= 0 + ? Math.min(insertionCursor, attachOrder.size()) + : attachOrder.size(); + attachOrder.add(index, owner); + Component c = owner.component(); + if (container != null && c != null) { + container.addComponent(Math.min(index, container.getComponentCount()), c); + } + if (insertionCursor >= 0) { + insertionCursor = index + 1; + } + } + + public void detach(RenderElement owner) { + if (owner == null) { + return; + } + int index = attachOrder.indexOf(owner); + if (index >= 0) { + attachOrder.remove(index); + if (insertionCursor > index) { + insertionCursor--; + } + } + Component c = owner.component(); + if (c == null) { + return; + } + // Remove it from WHATEVER container holds it, not only from this host's own. + // + // An element's component does not always sit in the container of the host that + // attached it: a scroll view puts its content pane inside its ScrollPane, and + // other effects re-parent as well. Removing it only when the parent happened to + // be this host's container meant an unmounted element could keep a live + // component on screen -- deleting a row from a list left the whole previous + // content pane attached beside the new one, so every surviving row drew twice + // and the deleted row was still there. + com.codename1.ui.Container parent = c.getParent(); + if (parent != null) { + parent.removeComponent(c); + } + } + + /** + * Moves the given attach entries (a block of already-attached elements, + * e.g. one parent's child subtrees flattened in NEW tree order) so they + * appear in that order, both in {@link #attachOrder} and in the CN1 + * container — keyed reconciliation can match surviving children at new + * indices, and without this their components would keep the OLD paint + * order. Entries not in {@code desired} are untouched; the block is + * re-inserted at the first index it currently occupies. + */ + public void reorderToTreeOrder(List desired) { + if (desired.size() < 2) { + return; + } + java.util.Set members = new java.util.HashSet(desired); + List current = new ArrayList(); + for (RenderElement r : attachOrder) { + if (members.contains(r)) { + current.add(r); + } + } + if (current.equals(desired)) { + return; + } + int insertAt = attachOrder.indexOf(current.get(0)); + attachOrder.removeAll(current); + attachOrder.addAll(insertAt, desired); + if (container != null) { + for (int i = 0; i < desired.size(); i++) { + Component c = desired.get(i).component(); + if (c != null && c.getParent() == container) { + container.removeComponent(c); + int target = Math.min(insertAt + i, container.getComponentCount()); + container.addComponent(target, c); + } + } + } + } + + /** + * Lays out THIS host's subtree after a build changed it. + * + *

Deliberately not {@code revalidate()}. In Codename One a finished layout is + * finished; revalidate goes to the Form root and lays the whole hierarchy out again, + * which for a Flutter build flush is the wrong scope by a wide margin - a setState on + * one leaf would re-lay out the toolbar, the side menu and every other container on the + * form. Marking this container's own subtree and calling {@code layoutContainer()} does + * only the work that a change inside this host can possibly have affected.

+ * + *

The Flutter pass this triggers is tight against the host's own bounds, so the host + * does not change size and its parent has nothing to redo.

+ */ + public void revalidate() { + if (container == null) { + return; + } + // Never from inside a pass. A relayout requested mid-pass walks a tree + // that is currently being replaced; see FlutterRootLayout.inLayout. + if (FlutterRootLayout.inLayout()) { + FlutterRootLayout.deferRevalidate(this); + return; + } + // Run OUR constraint pass and nothing else. It writes every component's bounds + // absolutely, so none of Codename One's own layout machinery has to participate. + // + // What must NOT happen here is invalidating preferred sizes. + // setShouldCalcPreferredSize(true) - which is also the first thing revalidate() does + // - recurses down every child container and throws away CN1's cached measurements, + // so every Label re-measures its text. Measured on the gallery home, that was the + // whole of the cost: the Flutter constraint pass itself is ~2ms across ~4900 boxes, + // while the frame was ~200ms. A component whose content actually changed invalidates + // itself (Label.setText does), so blanket-invalidating a subtree only discards + // measurements that were still valid. + Layout layout = container.getLayout(); + if (layout != null) { + layout.layoutContainer(container); + } + container.repaint(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderObject.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderObject.java new file mode 100644 index 00000000000..c66240a225c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/RenderObject.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Rect; + +/** + * The base of the render tree — Flutter's {@code RenderObject}. new_gallery + * reaches one via {@code BuildContext.findRenderObject()} and casts it to + * {@link RenderBox}. This is a structural stub: the Codename One runtime lays + * out with its own {@link com.codename1.flutter.RenderElement} tree, so the + * geometry accessors return neutral values until a later rendering milestone + * wires them to the live layout. + */ +public class RenderObject { + + /** Whether this render object is attached to the render tree. */ + public boolean attached() { + return false; + } + + /** The bounds painted by this object, in its own coordinate space. */ + public Rect paintBounds() { + return Rect.zero; + } + + /** The bounds used for semantics, in its own coordinate space. */ + public Rect semanticBounds() { + return Rect.zero; + } + + /** Marks this object as needing a repaint (no-op in this milestone). */ + public void markNeedsPaint() { + } + + /** Marks this object as needing layout (no-op in this milestone). */ + public void markNeedsLayout() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollDirection.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollDirection.java new file mode 100644 index 00000000000..bff80d79156 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollDirection.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +/** + * The user-scroll direction reported by a UserScrollNotification — Flutter's + * {@code ScrollDirection}. + */ +public enum ScrollDirection { + idle, forward, reverse +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollRootLayout.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollRootLayout.java new file mode 100644 index 00000000000..1bf6bea00cc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/ScrollRootLayout.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.RenderElement; +import com.codename1.ui.Container; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.plaf.Style; + +/** + * The CN1 layout installed on a scrollable Flutter boundary container (the + * pane owned by SingleChildScrollView/ListView/GridView). Unlike + * {@link FlutterRootLayout} — which forces the subtree into the container's + * exact bounds — this lays the content out with a tight cross axis (the + * viewport width) and an unbounded vertical main axis, and reports the + * content extent as the preferred size so CN1's own tensile scrolling takes + * over when the content is taller than the viewport. + */ +public class ScrollRootLayout extends FlutterRootLayout { + + /** + * The content width used by the last real layout pass. getPreferredSize + * must lay content out at the SAME width layoutContainer used (it can't + * compute the side gap itself — see the recursion note below); otherwise + * the two passes alternate between different widths and the UI "bounces" + * whenever CN1 interleaves scroll-size and layout computations. + */ + private int lastLayoutWidth = -1; + + public ScrollRootLayout(RenderHost host) { + super(host); + } + + @Override + public void layoutContainer(Container parent) { + RenderElement root = host().rootRenderElement(); + if (root == null) { + return; + } + Style s = parent.getStyle(); + int width = parent.getLayoutWidth() - parent.getSideGap() - s.getHorizontalPadding(); + if (width < 0) { + width = 0; + } + lastLayoutWidth = width; + root.layout(contentConstraints(width)); + root.position(s.getPaddingLeftNoRTL(), s.getPaddingTop()); + } + + @Override + public Dimension getPreferredSize(Container parent) { + RenderElement root = host().rootRenderElement(); + if (root == null) { + return new Dimension(0, 0); + } + Style s = parent.getStyle(); + // Width preference order: + // 1. the width the real layout pass used (keeps both passes + // consistent — inconsistent widths make the UI bounce); + // 2. Component.getWidth() — NOT Container.getLayoutWidth(), which + // falls back to getPreferredW() pre-layout and re-enters this + // method; likewise getSideGap() routes through isScrollableY -> + // getScrollDimension -> calcPreferredSize and recurses. + int width = lastLayoutWidth > 0 ? lastLayoutWidth : parent.getWidth() - s.getHorizontalPadding(); + Size sz = root.layout(width > 0 + ? contentConstraints(width) + : BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + int w = (int) Math.ceil(sz.width()) + s.getHorizontalPadding(); + int h = (int) Math.ceil(sz.height()) + s.getVerticalPadding(); + return new Dimension(w, h); + } + + /** + * Tight viewport width, unbounded height — the Flutter viewport contract + * for a vertical scrollable. + */ + public static BoxConstraints contentConstraints(double width) { + return new BoxConstraints(width, width, 0, Double.POSITIVE_INFINITY); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Size.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Size.java new file mode 100644 index 00000000000..dd684180bd1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/Size.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Offset; + +/** + * An immutable width/height pair, in the same unit as the constraints that + * produced it (device pixels at runtime, raw logical values in unit tests). + */ +public final class Size { + + public static final Size ZERO = new Size(0, 0); + + private final double width; + private final double height; + + public Size(double width, double height) { + this.width = width; + this.height = height; + } + + /** {@code Size.fromRadius}: a square that bounds a circle of {@code radius}. */ + public static Size fromRadius(double radius) { + return new Size(radius * 2, radius * 2); + } + + /** {@code Size.fromHeight}: a fixed height, unbounded width. */ + public static Size fromHeight(double height) { + return new Size(Double.POSITIVE_INFINITY, height); + } + + /** {@code Size.fromWidth}: a fixed width, unbounded height. */ + public static Size fromWidth(double width) { + return new Size(width, Double.POSITIVE_INFINITY); + } + + public double width() { + return width; + } + + public double height() { + return height; + } + + /** {@code Size.shortestSide}: the lesser of {@link #width()} and {@link #height()}. */ + public double shortestSide() { + return Math.min(width, height); + } + + /** {@code Size.longestSide}: the greater of {@link #width()} and {@link #height()}. */ + public double longestSide() { + return Math.max(width, height); + } + + /** + * {@code Size.center}: the offset to the center of this size, given a + * top-left {@code origin}. + */ + public Offset center(Offset origin) { + return new Offset(origin.dx() + width / 2, origin.dy() + height / 2); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Size)) { + return false; + } + Size s = (Size) o; + return s.width == width && s.height == height; + } + + @Override + public int hashCode() { + long bits = com.codename1.flutter.ValueHash.bits(width) * 31 + com.codename1.flutter.ValueHash.bits(height); + return (int) (bits ^ (bits >>> 32)); + } + + @Override + public String toString() { + return "Size(" + width + ", " + height + ")"; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegate.java new file mode 100644 index 00000000000..ac82ccce3ca --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegate.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +/** + * Base type for a sliver-grid layout strategy — Flutter's + * {@code SliverGridDelegate}. + */ +public abstract class SliverGridDelegate { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithFixedCrossAxisCount.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithFixedCrossAxisCount.java new file mode 100644 index 00000000000..9a82ed40372 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithFixedCrossAxisCount.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +/** + * Lays a grid out with a fixed number of tiles across the cross axis — + * Flutter's {@code SliverGridDelegateWithFixedCrossAxisCount}. Signature-only. + */ +public class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate { + + private long crossAxisCount; + private double mainAxisSpacing; + private double crossAxisSpacing; + private double childAspectRatio = 1.0; + private double mainAxisExtent; + + public void crossAxisCount(long v) { this.crossAxisCount = v; } + public void mainAxisSpacing(double v) { this.mainAxisSpacing = v; } + public void crossAxisSpacing(double v) { this.crossAxisSpacing = v; } + public void childAspectRatio(double v) { this.childAspectRatio = v; } + public void mainAxisExtent(double v) { this.mainAxisExtent = v; } + + public long getCrossAxisCount() { return crossAxisCount; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithMaxCrossAxisExtent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithMaxCrossAxisExtent.java new file mode 100644 index 00000000000..09d295b09b0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/SliverGridDelegateWithMaxCrossAxisExtent.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +/** + * Lays a grid out with tiles no wider than a maximum cross-axis extent — + * Flutter's {@code SliverGridDelegateWithMaxCrossAxisExtent}. Signature-only. + */ +public class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate { + + private double maxCrossAxisExtent; + private double mainAxisSpacing; + private double crossAxisSpacing; + private double childAspectRatio = 1.0; + private double mainAxisExtent; + + public void maxCrossAxisExtent(double v) { this.maxCrossAxisExtent = v; } + public void mainAxisSpacing(double v) { this.mainAxisSpacing = v; } + public void crossAxisSpacing(double v) { this.crossAxisSpacing = v; } + public void childAspectRatio(double v) { this.childAspectRatio = v; } + public void mainAxisExtent(double v) { this.mainAxisExtent = v; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/TextPainter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/TextPainter.java new file mode 100644 index 00000000000..2511da73103 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/rendering/TextPainter.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.Canvas; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextDirection; + +/** + * Lays out and paints a span of styled text — Flutter's {@code TextPainter}. + * Used directly by custom painters: configure it with a {@code text} span and a + * {@code textDirection}, call {@link #layout}, read {@link #width}/{@link + * #height}/{@link #size}, then {@link #paint} onto a {@link Canvas}. This pass + * captures the configuration and reports a zero-size layout; real text measuring + * and glyph painting are deferred to the text layer. + */ +public class TextPainter { + + private Object text; + private TextDirection textDirection; + private TextAlign textAlign; + private Double textScaleFactor; + private Integer maxLines; + private String ellipsis; + private double width; + private double height; + + public TextPainter() { + } + + public void text(Object v) { + this.text = v; + } + + public void textDirection(TextDirection v) { + this.textDirection = v; + } + + public void textAlign(TextAlign v) { + this.textAlign = v; + } + + public void textScaleFactor(double v) { + this.textScaleFactor = v; + } + + public void maxLines(int v) { + this.maxLines = v; + } + + public void ellipsis(String v) { + this.ellipsis = v; + } + + public void textWidthBasis(Object v) { + } + + public void strutStyle(Object v) { + } + + public void locale(Object v) { + } + + /** + * Computes the visual layout within the given width bounds. A null bound + * means the Flutter default (0 / infinity). + */ + public void layout(Double minWidth, Double maxWidth) { + // Measurement deferred; dimensions remain zero for this pass. + } + + /** + * Paints the laid-out text with its top-left at {@code offset} (an Offset). + */ + public void paint(Canvas canvas, Object offset) { + } + + public Size size() { + return new Size(width, height); + } + + public double width() { + return width; + } + + public double height() { + return height; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerBinding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerBinding.java new file mode 100644 index 00000000000..9e3b7f1ccc5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerBinding.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.scheduler; + +import com.codename1.ui.Display; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** + * The singleton that drives frame scheduling — Flutter's {@code SchedulerBinding}. + * new_gallery reaches it through {@code SchedulerBinding.instance} to register + * post-frame callbacks (e.g. feature-discovery overlays that must measure a + * widget after its first layout). + * + *

Callbacks are dispatched on the Codename One EDT after the current event + * loop via {@link Display#callSerially}, which is the closest analogue to + * "after the current frame". The {@code FrameTiming} argument Flutter passes is + * approximated with a zero {@link Duration}.

+ */ +public final class SchedulerBinding { + + /** + * Dart's {@code SchedulerBinding.instance}. A Dart {@code static get} + * accessor is emitted as a Java static field reference, so this is a field + * rather than a method. + */ + public static final SchedulerBinding instance = new SchedulerBinding(); + + private int nextCallbackId = 1; + + private SchedulerBinding() { + } + + /** Dart's {@code addPostFrameCallback}: run {@code callback} after this frame. */ + public void addPostFrameCallback(final Funcs.VoidFunc1 callback) { + dispatch(callback); + } + + /** + * Dart's {@code scheduleFrameCallback}: schedule a transient frame callback + * and return its id. {@code rescheduling} is accepted for API shape. + */ + public int scheduleFrameCallback(final Object callback, Boolean rescheduling) { + dispatch(callback); + return nextCallbackId++; + } + + /** Dart's {@code scheduleFrame}: request a new frame. A no-op in this pass. */ + public void scheduleFrame() { + } + + @SuppressWarnings("unchecked") + private void dispatch(final Object callback) { + if (callback == null) { + return; + } + Runnable r = new Runnable() { + @Override + public void run() { + if (callback instanceof Funcs.VoidFunc1) { + ((Funcs.VoidFunc1) callback).call(Duration.zero); + } else if (callback instanceof Runnable) { + ((Runnable) callback).run(); + } + } + }; + try { + if (Display.isInitialized()) { + Display.getInstance().callSerially(r); + } else { + r.run(); + } + } catch (Throwable t) { + // Best effort: a callback throwing must not abort scheduling. + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerLib.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerLib.java new file mode 100644 index 00000000000..4f70e0065ff --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scheduler/SchedulerLib.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.scheduler; + +/** + * Top-level members of Flutter's {@code package:flutter/scheduler.dart} library + * that the app references directly, mirrored as Java statics. + * + *

{@code timeDilation} slows every {@code AnimationController} in the app by + * the given factor. It is a mutable top-level {@code double} in Flutter + * (default {@code 1.0}); the transpiler routes both reads and writes of the + * bare {@code timeDilation} identifier to this field.

+ */ +public final class SchedulerLib { + + private SchedulerLib() { + } + + /** Flutter's {@code scheduler.timeDilation}; 1.0 == real time. */ + public static double timeDilation = 1.0; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/Model.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/Model.java new file mode 100644 index 00000000000..bd212db8708 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/Model.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.scopedmodel; + +import com.codename1.flutter.foundation.ChangeNotifier; + +/** + * scoped_model's {@code Model}: the base class application models extend + * ({@code class AppStateModel extends Model}). It is a {@link ChangeNotifier}, + * so {@code notifyListeners()} / {@code addListener} / {@code removeListener} + * come from the notifier's default methods. + */ +public class Model implements ChangeNotifier { + + /** Held here, not in the shared map, so this goes when the object does (see ChangeNotifier). */ + private final java.util.List changeNotifierListeners = + new java.util.ArrayList(); + + @Override + public java.util.List changeNotifierListeners$() { + return changeNotifierListeners; + } + +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModel.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModel.java new file mode 100644 index 00000000000..8d3416e9793 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModel.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.scopedmodel; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.InheritedValueProvider; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * scoped_model's {@code ScopedModel}: publishes {@code model} + * to its subtree by runtime type and renders its {@code child}. + * {@link #of(BuildContext, boolean, Class)} walks the tree for the nearest + * matching model — the Dart {@code ScopedModel.of(context)} witness is + * threaded in as {@code type} by the transpiler. + */ +public class ScopedModel extends StatelessWidget implements InheritedValueProvider { + + private Object model; + private Widget child; + + public void model(Object v) { + this.model = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getModel() { + return model; + } + + @Override + public Object providedValueFor(Class type) { + return model != null && type.isInstance(model) ? model : null; + } + + @Override + public Widget build(BuildContext context) { + return child; + } + + /** {@code ScopedModel.of(context, rebuildOnChange: ...)}. */ + @SuppressWarnings("unchecked") + public static T of(BuildContext context, boolean rebuildOnChange, Class type) { + return (T) context.providerValueOfType(type); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModelDescendant.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModelDescendant.java new file mode 100644 index 00000000000..5b30633f1ef --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/scopedmodel/ScopedModelDescendant.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.scopedmodel; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * scoped_model's {@code ScopedModelDescendant}: rebuilds via + * {@code builder(context, child, model)} with the nearest ancestor + * {@link ScopedModel}'s model. + * + *

Known limitation (this pass): the class-level {@code } is dropped by the + * transpiler, so the nearest provided value of ANY type is passed. Correct when + * a single model is in scope (the gallery's shrine study).

+ */ +public class ScopedModelDescendant extends StatelessWidget { + + private Funcs.Func3 builder; + private boolean rebuildOnChange = true; + private Widget child; + + public void builder(Funcs.Func3 v) { + this.builder = v; + } + + public void rebuildOnChange(boolean v) { + this.rebuildOnChange = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + @SuppressWarnings("unchecked") + public Widget build(BuildContext context) { + T model = (T) context.providerValueOfType(Object.class); + return builder == null ? child : builder.call(context, child, model); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/CustomPainterSemantics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/CustomPainterSemantics.java new file mode 100644 index 00000000000..a53cbff4a81 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/CustomPainterSemantics.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.semantics; + +import com.codename1.flutter.Key; +import com.codename1.flutter.Rect; + +/** + * A single accessibility node emitted by a {@code CustomPainter} — Flutter's + * {@code CustomPainterSemantics}. It maps a {@link Rect} region of the painted + * surface to a set of semantic {@code properties} (a + * {@code SemanticsProperties}). The Rally line chart emits one per data group so + * screen readers can announce each day's balance. {@code transform} and + * {@code tags} are captured for API shape. + */ +public class CustomPainterSemantics { + + private Rect rect; + private Object properties; + private Object transform; + private Object tags; + private Key key; + + public CustomPainterSemantics() { + } + + public void rect(Rect v) { + this.rect = v; + } + + public void properties(Object v) { + this.properties = v; + } + + public void transform(Object v) { + this.transform = v; + } + + public void tags(Object v) { + this.tags = v; + } + + public void key(Key v) { + this.key = v; + } + + public Rect getRect() { + return rect; + } + + public Object getProperties() { + return properties; + } + + public Key getKey() { + return key; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/OrdinalSortKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/OrdinalSortKey.java new file mode 100644 index 00000000000..636487e6384 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/OrdinalSortKey.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.semantics; + +/** + * A sort key that orders semantics nodes numerically — Flutter's + * {@code OrdinalSortKey}. Nodes with a lower {@code order} are traversed + * first; an optional {@code name} scopes the ordering to a named group. This + * pass captures the values for API shape; the accessibility traversal order is + * applied by the semantics layer later. + */ +public class OrdinalSortKey { + + private final double order; + private String name; + + public OrdinalSortKey(double order) { + this.order = order; + } + + public void name(String v) { + this.name = v; + } + + public double getOrder() { + return order; + } + + public String getName() { + return name; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsBuilderCallback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsBuilderCallback.java new file mode 100644 index 00000000000..53d16baccaa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsBuilderCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.semantics; + +import com.codename1.flutter.rendering.Size; + +import dart.core.DartList; + +/** + * The signature of a {@code CustomPainter}'s {@code semanticsBuilder} — Flutter's + * {@code SemanticsBuilderCallback} typedef, + * {@code List Function(Size size)}. Given the current + * paint {@code size}, it returns the accessibility nodes the painter exposes. + * Declared as a single-abstract-method interface so transpiled painters can + * supply it with a lambda. + */ +public interface SemanticsBuilderCallback { + DartList call(Size size); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsService.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsService.java new file mode 100644 index 00000000000..bc044ba2913 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/semantics/SemanticsService.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.semantics; + +import com.codename1.flutter.TextDirection; + +/** + * Fires accessibility announcements — Flutter's {@code SemanticsService}. The + * settings panel announces its open/close state; this runtime records the last + * announcement so the platform accessibility layer (or a test) can observe it, + * but performs no screen-reader I/O in this pass. + */ +public abstract class SemanticsService { + + private static volatile String lastAnnouncement; + private static volatile String lastTooltip; + + private SemanticsService() { + } + + /** + * Dart's {@code SemanticsService.announce(message, textDirection, + * {assertiveness})}. Records the message for observation. + */ + public static void announce(String message, TextDirection textDirection, Object assertiveness) { + lastAnnouncement = message; + } + + /** Dart's {@code SemanticsService.tooltip(message)}. */ + public static void tooltip(String message) { + lastTooltip = message; + } + + /** The most recently announced message, or null. */ + public static String lastAnnouncement() { + return lastAnnouncement; + } + + /** The most recently announced tooltip, or null. */ + public static String lastTooltip() { + return lastTooltip; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/AutofillHints.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/AutofillHints.java new file mode 100644 index 00000000000..81f8ab74678 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/AutofillHints.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * The set of autofill hint strings a text input can advertise to the platform + * autofill service — Flutter's {@code AutofillHints}. Each constant is the + * canonical hint token; only a representative subset is modelled here. + */ +public final class AutofillHints { + + private AutofillHints() { + } + + public static final String username = "username"; + public static final String password = "password"; + public static final String newPassword = "newPassword"; + public static final String email = "email"; + public static final String name = "name"; + public static final String givenName = "givenName"; + public static final String familyName = "familyName"; + public static final String telephoneNumber = "telephoneNumber"; + public static final String oneTimeCode = "oneTimeCode"; + public static final String streetAddressLine1 = "streetAddressLine1"; + public static final String streetAddressLine2 = "streetAddressLine2"; + public static final String postalCode = "postalCode"; + public static final String creditCardNumber = "creditCardNumber"; + public static final String countryName = "countryName"; +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/Clipboard.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/Clipboard.java new file mode 100644 index 00000000000..434fe67bc61 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/Clipboard.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import com.codename1.ui.Display; +import dart.async.Future; + +/** + * System clipboard access, mirroring Flutter's {@code Clipboard}. Backed by + * CN1's {@code Display.copyToClipboard}/{@code getPasteDataFromClipboard}. + */ +public abstract class Clipboard { + + private Clipboard() { + } + + public static Future setData(ClipboardData data) { + if (data != null && Display.isInitialized()) { + Display.getInstance().copyToClipboard(data.text()); + } + return Future.value((Object) null); + } + + public static Future getData(String format) { + Object contents = Display.isInitialized() ? Display.getInstance().getPasteDataFromClipboard() : null; + return Future.value((Object) new ClipboardData(contents == null ? null : contents.toString())); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/ClipboardData.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/ClipboardData.java new file mode 100644 index 00000000000..b6b7dd611e7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/ClipboardData.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * Payload for {@link Clipboard}, mirroring Flutter's {@code ClipboardData}. + */ +public class ClipboardData { + + private String text; + + /** Named-parameter constructor {@code ClipboardData({text})}. */ + public ClipboardData(String text) { + this.text = text; + } + + /** Allocate-then-setters form for the {@code ClipboardData(text: ...)} named constructor. */ + public ClipboardData() { + } + + public void text(String v) { + this.text = v; + } + + public String text() { + return text; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/FilteringTextInputFormatter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/FilteringTextInputFormatter.java new file mode 100644 index 00000000000..7616e142043 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/FilteringTextInputFormatter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import com.codename1.flutter.TextEditingValue; + +/** + * Filters edited text against a pattern — Flutter's + * {@code FilteringTextInputFormatter}. new_gallery uses the {@link #digitsOnly} + * preset; the {@link #allow}/{@link #deny} factories and + * {@link #singleLineFormatter} are provided for API fidelity. This milestone + * captures the API shape; the actual character filtering is deferred, so the + * default {@link #formatEditUpdate} passes the edit through unchanged. + */ +public class FilteringTextInputFormatter extends TextInputFormatter { + + /** Allows only decimal digits ({@code 0-9}). */ + public static final FilteringTextInputFormatter digitsOnly = new FilteringTextInputFormatter(); + + /** Collapses newlines so the field stays single-line. */ + public static final FilteringTextInputFormatter singleLineFormatter = new FilteringTextInputFormatter(); + + public FilteringTextInputFormatter() { + } + + /** Dart's {@code FilteringTextInputFormatter.allow} named constructor. */ + public static FilteringTextInputFormatter allow(Object filterPattern, String replacementString) { + return new FilteringTextInputFormatter(); + } + + /** Dart's {@code FilteringTextInputFormatter.deny} named constructor. */ + public static FilteringTextInputFormatter deny(Object filterPattern, String replacementString) { + return new FilteringTextInputFormatter(); + } + + @Override + public TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + return newValue; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyDownEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyDownEvent.java new file mode 100644 index 00000000000..a17fdb0fd78 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyDownEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * A key-press event, mirroring Flutter's {@code KeyDownEvent}. Matched with + * {@code event is KeyDownEvent} in key handlers. + */ +public class KeyDownEvent extends KeyEvent { + + public KeyDownEvent(LogicalKeyboardKey logicalKey, PhysicalKeyboardKey physicalKey, String character) { + super(logicalKey, physicalKey, character); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEvent.java new file mode 100644 index 00000000000..d74ca872c5e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEvent.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import dart.core.Duration; + +/** + * Base class for a keyboard event in Flutter's modern {@code HardwareKeyboard} + * API ({@code KeyEvent}). The {@code Focus.onKeyEvent} / {@code KeyboardListener} + * callbacks receive one of the concrete subclasses ({@link KeyDownEvent}, + * {@link KeyUpEvent}, {@link KeyRepeatEvent}); code switches on the runtime type + * with {@code event is KeyDownEvent} and reads {@link #logicalKey()}. + */ +public abstract class KeyEvent { + + final LogicalKeyboardKey logicalKey; + final PhysicalKeyboardKey physicalKey; + final String character; + final Duration timeStamp; + + KeyEvent(LogicalKeyboardKey logicalKey, PhysicalKeyboardKey physicalKey, String character) { + this.logicalKey = logicalKey; + this.physicalKey = physicalKey; + this.character = character; + this.timeStamp = null; + } + + /** The logical (layout-dependent) key for this event. */ + public LogicalKeyboardKey logicalKey() { + return logicalKey; + } + + /** The physical (scan-code) key for this event. */ + public PhysicalKeyboardKey physicalKey() { + return physicalKey; + } + + /** The character produced, or {@code null} for non-printable keys. */ + public String character() { + return character; + } + + /** The event time, relative to an arbitrary epoch. */ + public Duration timeStamp() { + return timeStamp; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEventResult.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEventResult.java new file mode 100644 index 00000000000..3d7ef47e957 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyEventResult.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * The result a key handler reports to the focus system, mirroring Flutter's + * {@code KeyEventResult}. Returned from {@code Focus.onKeyEvent} handlers so + * {@code return KeyEventResult.handled}/{@code ignored} transpiles directly. + */ +public enum KeyEventResult { + /** The key event was handled; stop propagation. */ + handled, + /** The key event was not handled; continue propagation. */ + ignored, + /** Handled here, but skip remaining handlers in this node. */ + skipRemainingHandlers +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyRepeatEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyRepeatEvent.java new file mode 100644 index 00000000000..e3e985f6ef4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyRepeatEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * A key auto-repeat event, mirroring Flutter's {@code KeyRepeatEvent}. Matched + * with {@code event is KeyRepeatEvent} in key handlers. + */ +public class KeyRepeatEvent extends KeyEvent { + + public KeyRepeatEvent(LogicalKeyboardKey logicalKey, PhysicalKeyboardKey physicalKey, String character) { + super(logicalKey, physicalKey, character); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyUpEvent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyUpEvent.java new file mode 100644 index 00000000000..9c235acd2b6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/KeyUpEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * A key-release event, mirroring Flutter's {@code KeyUpEvent}. + */ +public class KeyUpEvent extends KeyEvent { + + public KeyUpEvent(LogicalKeyboardKey logicalKey, PhysicalKeyboardKey physicalKey) { + super(logicalKey, physicalKey, null); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LengthLimitingTextInputFormatter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LengthLimitingTextInputFormatter.java new file mode 100644 index 00000000000..2c876480dfb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LengthLimitingTextInputFormatter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import com.codename1.flutter.TextEditingValue; + +/** + * Truncates edited text to a maximum length — Flutter's + * {@code LengthLimitingTextInputFormatter}. API-shape only for this milestone; + * the default {@link #formatEditUpdate} passes the edit through unchanged. + */ +public class LengthLimitingTextInputFormatter extends TextInputFormatter { + + private Long maxLength; + private MaxLengthEnforcement maxLengthEnforcement; + + public LengthLimitingTextInputFormatter(Long maxLength) { + this.maxLength = maxLength; + } + + public void maxLengthEnforcement(MaxLengthEnforcement v) { + this.maxLengthEnforcement = v; + } + + public Long getMaxLength() { + return maxLength; + } + + public MaxLengthEnforcement getMaxLengthEnforcement() { + return maxLengthEnforcement; + } + + @Override + public TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + return newValue; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LogicalKeyboardKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LogicalKeyboardKey.java new file mode 100644 index 00000000000..9d922452c4d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/LogicalKeyboardKey.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * A logical (layout-dependent) keyboard key, mirroring Flutter's + * {@code LogicalKeyboardKey}. The static constants are singletons, so the + * {@code event.logicalKey == LogicalKeyboardKey.enter} comparisons that + * new_gallery performs resolve by reference identity. + * + *

Key ids follow Flutter's logical key-id numbering where practical; the + * exact numeric value is unimportant for Codename One since only identity + * comparisons are used.

+ */ +public class LogicalKeyboardKey { + + private final long keyId; + private final String keyLabel; + private final String debugName; + + LogicalKeyboardKey(long keyId, String keyLabel, String debugName) { + this.keyId = keyId; + this.keyLabel = keyLabel; + this.debugName = debugName; + } + + /** The unique logical key id. */ + public long keyId() { + return keyId; + } + + /** The printable label for the key, or {@code null} for non-printables. */ + public String keyLabel() { + return keyLabel; + } + + /** A human-readable name for debugging. */ + public String debugName() { + return debugName; + } + + public static final LogicalKeyboardKey arrowUp = new LogicalKeyboardKey(0x100000301L, null, "Arrow Up"); + public static final LogicalKeyboardKey arrowDown = new LogicalKeyboardKey(0x100000303L, null, "Arrow Down"); + public static final LogicalKeyboardKey arrowLeft = new LogicalKeyboardKey(0x100000302L, null, "Arrow Left"); + public static final LogicalKeyboardKey arrowRight = new LogicalKeyboardKey(0x100000304L, null, "Arrow Right"); + public static final LogicalKeyboardKey enter = new LogicalKeyboardKey(0x100000013L, null, "Enter"); + public static final LogicalKeyboardKey numpadEnter = new LogicalKeyboardKey(0x20000020eL, null, "Numpad Enter"); + public static final LogicalKeyboardKey escape = new LogicalKeyboardKey(0x100000009L, null, "Escape"); + public static final LogicalKeyboardKey tab = new LogicalKeyboardKey(0x100000009L + 1, null, "Tab"); + public static final LogicalKeyboardKey space = new LogicalKeyboardKey(0x00000000020L, " ", "Space"); + public static final LogicalKeyboardKey backspace = new LogicalKeyboardKey(0x100000008L, null, "Backspace"); + public static final LogicalKeyboardKey delete = new LogicalKeyboardKey(0x10000007fL, null, "Delete"); + public static final LogicalKeyboardKey home = new LogicalKeyboardKey(0x100000306L, null, "Home"); + public static final LogicalKeyboardKey end = new LogicalKeyboardKey(0x100000305L, null, "End"); + public static final LogicalKeyboardKey pageUp = new LogicalKeyboardKey(0x100000308L, null, "Page Up"); + public static final LogicalKeyboardKey pageDown = new LogicalKeyboardKey(0x100000307L, null, "Page Down"); + public static final LogicalKeyboardKey shift = new LogicalKeyboardKey(0x1000700e1L, null, "Shift"); + public static final LogicalKeyboardKey control = new LogicalKeyboardKey(0x1000700e0L, null, "Control"); + public static final LogicalKeyboardKey meta = new LogicalKeyboardKey(0x1000700e3L, null, "Meta"); + public static final LogicalKeyboardKey alt = new LogicalKeyboardKey(0x1000700e2L, null, "Alt"); + + // Accessor forms for the `external static ... get name` stubs, in case the + // emitter routes property reads through zero-arg methods. + public static LogicalKeyboardKey arrowUp() { return arrowUp; } + public static LogicalKeyboardKey arrowDown() { return arrowDown; } + public static LogicalKeyboardKey arrowLeft() { return arrowLeft; } + public static LogicalKeyboardKey arrowRight() { return arrowRight; } + public static LogicalKeyboardKey enter() { return enter; } + public static LogicalKeyboardKey numpadEnter() { return numpadEnter; } + public static LogicalKeyboardKey escape() { return escape; } + public static LogicalKeyboardKey tab() { return tab; } + public static LogicalKeyboardKey space() { return space; } + public static LogicalKeyboardKey backspace() { return backspace; } + public static LogicalKeyboardKey delete() { return delete; } + public static LogicalKeyboardKey home() { return home; } + public static LogicalKeyboardKey end() { return end; } + public static LogicalKeyboardKey pageUp() { return pageUp; } + public static LogicalKeyboardKey pageDown() { return pageDown; } + public static LogicalKeyboardKey shift() { return shift; } + public static LogicalKeyboardKey control() { return control; } + public static LogicalKeyboardKey meta() { return meta; } + public static LogicalKeyboardKey alt() { return alt; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/MaxLengthEnforcement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/MaxLengthEnforcement.java new file mode 100644 index 00000000000..57c03f6edce --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/MaxLengthEnforcement.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * How a text field's {@code maxLength} is enforced — Flutter's + * {@code MaxLengthEnforcement}. new_gallery's text_field_demo passes + * {@link #none} to disable enforcement while still showing the counter. + */ +public enum MaxLengthEnforcement { + /** No enforcement; text may exceed {@code maxLength}. */ + none, + /** Prevent input beyond {@code maxLength}. */ + enforced, + /** Enforce only once the IME composing region resolves. */ + truncateAfterCompositionEnds +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/PhysicalKeyboardKey.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/PhysicalKeyboardKey.java new file mode 100644 index 00000000000..8c6bc3497a6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/PhysicalKeyboardKey.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * A physical (scan-code) keyboard key, mirroring Flutter's + * {@code PhysicalKeyboardKey}. Present so {@code KeyEvent.physicalKey} resolves; + * new_gallery never compares against it. + */ +public class PhysicalKeyboardKey { + + private final int usbHidUsage; + private final String debugName; + + public PhysicalKeyboardKey(int usbHidUsage) { + this(usbHidUsage, null); + } + + public PhysicalKeyboardKey(int usbHidUsage, String debugName) { + this.usbHidUsage = usbHidUsage; + this.debugName = debugName; + } + + public int usbHidUsage() { + return usbHidUsage; + } + + public String debugName() { + return debugName; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemChrome.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemChrome.java new file mode 100644 index 00000000000..5ef8548475f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemChrome.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import java.util.List; + +/** + * System chrome controls, mirroring Flutter's {@code SystemChrome}. No-ops: + * CN1 owns orientation and overlay behaviour through its own APIs. + */ +public abstract class SystemChrome { + + private SystemChrome() { + } + + public static void setSystemUIOverlayStyle(SystemUiOverlayStyle style) { + // no-op + } + + public static void setPreferredOrientations(List orientations) { + // no-op + } + + public static void setEnabledSystemUIMode(Object mode) { + // no-op + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemUiOverlayStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemUiOverlayStyle.java new file mode 100644 index 00000000000..166cc6c0536 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/SystemUiOverlayStyle.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * Describes the status/navigation-bar appearance, mirroring Flutter's + * {@code SystemUiOverlayStyle}. Inert here (CN1 manages system chrome + * separately); the {@code light}/{@code dark} presets are provided for API + * shape. + */ +public final class SystemUiOverlayStyle { + + /** Overlays suited to a light (bright) background. */ + public static final SystemUiOverlayStyle light = new SystemUiOverlayStyle("light"); + + /** Overlays suited to a dark background. */ + public static final SystemUiOverlayStyle dark = new SystemUiOverlayStyle("dark"); + + private final String name; + + private SystemUiOverlayStyle(String name) { + this.name = name; + } + + @Override + public String toString() { + return "SystemUiOverlayStyle." + name; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextCapitalization.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextCapitalization.java new file mode 100644 index 00000000000..78e9d5a8313 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextCapitalization.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * How the soft keyboard auto-capitalizes text — Flutter's + * {@code TextCapitalization}. + */ +public enum TextCapitalization { + none, words, sentences, characters +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputAction.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputAction.java new file mode 100644 index 00000000000..b73cb142c80 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputAction.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * The action button on the soft keyboard — Flutter's {@code TextInputAction}. + */ +public enum TextInputAction { + none, unspecified, done, go, search, send, next, previous, + continueAction, join, route, emergencyCall, newline +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputFormatter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputFormatter.java new file mode 100644 index 00000000000..a5de0565a2d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputFormatter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import com.codename1.flutter.TextEditingValue; + +/** + * The base class every text input formatter extends — Flutter's + * {@code TextInputFormatter}. Transpiled Dart may subclass this (as + * new_gallery's {@code _UsNumberTextInputFormatter} does) and override + * {@link #formatEditUpdate}. The default implementation is the identity + * transform (the new value passes through unchanged). + */ +public class TextInputFormatter { + + public TextInputFormatter() { + } + + /** + * Transforms an edit from {@code oldValue} to {@code newValue}, returning + * the value that should actually be applied. The default is the identity. + */ + public TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + return newValue; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputType.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputType.java new file mode 100644 index 00000000000..2ba18702b75 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/TextInputType.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +/** + * The kind of soft keyboard for a text field — Flutter's {@code TextInputType}. + * Flutter models these as static const instances; the constant names are what + * matter for the app, so an enum with the matching names is sufficient here. + */ +public enum TextInputType { + text, multiline, number, phone, datetime, emailAddress, url, + visiblePassword, name, streetAddress, none +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java new file mode 100644 index 00000000000..de5e121026e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/services/UrlLauncher.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.services; + +import com.codename1.ui.Display; + +import dart.async.Future; +import dart.core.DartUri; + +/** + * The top-level functions of the {@code url_launcher} package new_gallery calls + * to open external links (the About / settings pages). Mirrored onto Codename + * One's {@link Display#execute(String)} and {@link Display#canExecute(String)}. + * + *

Each returns an already-completed {@link Future} so a non-awaited call + * transpiles and runs to completion; {@code launchUrl} additionally fires the + * native open. The {@code mode}/{@code webOnlyWindowName} options are accepted + * for API shape.

+ */ +public final class UrlLauncher { + + private UrlLauncher() { + } + + /** {@code url_launcher}'s {@code launchUrl(url)}. */ + public static Future launchUrl(DartUri url, Object mode, Object webOnlyWindowName) { + return launchUrlString(url == null ? null : url.toString(), mode, webOnlyWindowName); + } + + /** {@code url_launcher}'s {@code canLaunchUrl(url)}. */ + public static Future canLaunchUrl(DartUri url) { + return canLaunchUrlString(url == null ? null : url.toString()); + } + + /** {@code url_launcher}'s {@code launchUrlString(urlString)}. */ + public static Future launchUrlString(String urlString, Object mode, Object webOnlyWindowName) { + boolean ok = false; + if (urlString != null) { + try { + if (Display.isInitialized()) { + Display.getInstance().execute(urlString); + ok = true; + } + } catch (Throwable t) { + ok = false; + } + } + return Future.value(ok); + } + + /** {@code url_launcher}'s {@code canLaunchUrlString(urlString)}. */ + public static Future canLaunchUrlString(String urlString) { + boolean can = false; + if (urlString != null) { + try { + if (Display.isInitialized()) { + Boolean b = Display.getInstance().canExecute(urlString); + // A null result means "unknown"; treat it as launchable. + can = b == null || b.booleanValue(); + } + } catch (Throwable t) { + can = false; + } + } + return Future.value(can); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/util/AsciiUtil.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/util/AsciiUtil.java new file mode 100644 index 00000000000..3ed125a8ded --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/util/AsciiUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.util; + +/** + * ASCII string helpers — package:collection's {@code compareAsciiUpperCase}. + * The settings page sorts locale display names with it. + */ +public final class AsciiUtil { + + private AsciiUtil() { + } + + /** + * Dart's {@code compareAsciiUpperCase(a, b)}: compares two strings by + * upper-casing ASCII letters only (a-z -> A-Z), leaving all other code + * units untouched. Returns a negative, zero, or positive int like + * {@link String#compareTo}. + */ + public static long compareAsciiUpperCase(String a, String b) { + if (a == null) { + return b == null ? 0 : -1; + } + if (b == null) { + return 1; + } + int len = Math.min(a.length(), b.length()); + for (int i = 0; i < len; i++) { + int ca = toUpperAscii(a.charAt(i)); + int cb = toUpperAscii(b.charAt(i)); + if (ca != cb) { + return ca - cb; + } + } + return a.length() - b.length(); + } + + private static int toUpperAscii(char c) { + if (c >= 'a' && c <= 'z') { + return c - ('a' - 'A'); + } + return c; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Matrix4.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Matrix4.java new file mode 100644 index 00000000000..3cbafa4100c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Matrix4.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.vectormath; + +import dart.core.DartList; + +/** + * A 4x4 column-major transform matrix — {@code package:vector_math_64}'s + * {@code Matrix4}. new_gallery builds one (via {@link #identity()}, + * {@link #rotationZ(double)}, {@link #translationValues}, ...) and hands it to a + * {@code Transform} as its {@code transform} argument, or reads {@link #storage()}. + * + *

The 16 entries are stored column-major, matching vector_math so + * {@code storage[12..14]} hold the translation and the transpiler's index math + * stays valid.

+ */ +public class Matrix4 { + + private final double[] m = new double[16]; + + private Matrix4() { + } + + private static Matrix4 zero() { + return new Matrix4(); + } + + /** vector_math's {@code Matrix4.identity()}. */ + public static Matrix4 identity() { + Matrix4 r = new Matrix4(); + r.m[0] = 1.0; + r.m[5] = 1.0; + r.m[10] = 1.0; + r.m[15] = 1.0; + return r; + } + + /** vector_math's {@code Matrix4.rotationX(radians)}. */ + public static Matrix4 rotationX(double radians) { + Matrix4 r = identity(); + double c = Math.cos(radians); + double s = Math.sin(radians); + r.m[5] = c; + r.m[6] = s; + r.m[9] = -s; + r.m[10] = c; + return r; + } + + /** vector_math's {@code Matrix4.rotationY(radians)}. */ + public static Matrix4 rotationY(double radians) { + Matrix4 r = identity(); + double c = Math.cos(radians); + double s = Math.sin(radians); + r.m[0] = c; + r.m[2] = -s; + r.m[8] = s; + r.m[10] = c; + return r; + } + + /** vector_math's {@code Matrix4.rotationZ(radians)}. */ + public static Matrix4 rotationZ(double radians) { + Matrix4 r = identity(); + double c = Math.cos(radians); + double s = Math.sin(radians); + r.m[0] = c; + r.m[1] = s; + r.m[4] = -s; + r.m[5] = c; + return r; + } + + /** vector_math's {@code Matrix4.translationValues(x, y, z)}. */ + public static Matrix4 translationValues(double x, double y, double z) { + Matrix4 r = identity(); + r.m[12] = x; + r.m[13] = y; + r.m[14] = z; + return r; + } + + /** vector_math's {@code Matrix4.diagonal3Values(x, y, z)}. */ + public static Matrix4 diagonal3Values(double x, double y, double z) { + Matrix4 r = new Matrix4(); + r.m[0] = x; + r.m[5] = y; + r.m[10] = z; + r.m[15] = 1.0; + return r; + } + + /** + * vector_math's {@code storage}: the matrix's own column-major entries, so + * {@code matrix.storage[12] = 20} moves the matrix. It used to build a copy on + * every call, and writes through it were silently lost. + */ + public DartList storage() { + if (storageView == null) { + storageView = dart.core.DartDoubleList.view(m); + } + return storageView; + } + + private DartList storageView; + + /** + * Component-wise interpolation of two matrices, which is what Flutter's + * {@code Matrix4Tween} does. + * + *

It lives here rather than in the tween because the storage does: doing it + * through {@code storage()} would build two lists per frame for an animation that + * runs per frame.

+ */ + public static Matrix4 lerp(Matrix4 a, Matrix4 b, double t) { + if (a == null && b == null) { + return null; + } + if (a == null) { + a = zero(); + } + if (b == null) { + b = zero(); + } + Matrix4 r = new Matrix4(); + for (int i = 0; i < 16; i++) { + r.m[i] = a.m[i] + (b.m[i] - a.m[i]) * t; + } + return r; + } + + /** A copy of this matrix — vector_math's {@code clone()}. */ + public Matrix4 clone() { + Matrix4 r = new Matrix4(); + System.arraycopy(m, 0, r.m, 0, m.length); + return r; + } + + /** vector_math's {@code setEntry(row, col, value)} (column-major storage). */ + public void setEntry(int row, int col, double value) { + m[col * 4 + row] = value; + } + + /** vector_math's {@code setRotationZ(radians)}. */ + public void setRotationZ(double radians) { + double c = Math.cos(radians); + double s = Math.sin(radians); + m[0] = c; + m[1] = s; + m[4] = -s; + m[5] = c; + } + + // vector_math's translate(x, [y, z]) — arity overloads for the transpiler. + + public void translate(double x) { + translate(x, 0.0, 0.0); + } + + public void translate(double x, double y) { + translate(x, y, 0.0); + } + + public void translate(double x, double y, double z) { + m[12] += m[0] * x + m[4] * y + m[8] * z; + m[13] += m[1] * x + m[5] * y + m[9] * z; + m[14] += m[2] * x + m[6] * y + m[10] * z; + m[15] += m[3] * x + m[7] * y + m[11] * z; + } + + // vector_math's scale(x, [y, z]) — arity overloads for the transpiler. + + public void scale(double x) { + scale(x, x, x); + } + + public void scale(double x, double y) { + scale(x, y, 1.0); + } + + public void scale(double x, double y, double z) { + for (int i = 0; i < 4; i++) { + m[i] *= x; + m[4 + i] *= y; + m[8 + i] *= z; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Vector3.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Vector3.java new file mode 100644 index 00000000000..33312cc9a3b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/vectormath/Vector3.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.vectormath; + +/** + * A 3-component double vector from {@code package:vector_math} + * ({@code Vector3}). new_gallery's transformations demo uses it for hex-grid cube + * coordinates, reading {@link #x()} / {@link #y()} / {@link #z()}. + */ +public class Vector3 { + + private double x; + private double y; + private double z; + + public Vector3(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public static Vector3 zero() { + return new Vector3(0, 0, 0); + } + + public static Vector3 all(double value) { + return new Vector3(value, value, value); + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + public double z() { + return z; + } + + public void x(double v) { + this.x = v; + } + + public void y(double v) { + this.y = v; + } + + public void z(double v) { + this.z = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Align.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Align.java new file mode 100644 index 00000000000..ec4051f294e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Align.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Aligns its child within itself per an {@link Alignment} (default center). + * Expands to the incoming constraints when they are bounded, otherwise sizes + * to the child. + */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class Align extends Widget implements HasChild { + + private Alignment alignment; + private Widget child; + private Double widthFactor; + private Double heightFactor; + + public void widthFactor(Double v) { + this.widthFactor = v; + } + + public void heightFactor(Double v) { + this.heightFactor = v; + } + + public Double getWidthFactor() { + return widthFactor; + } + + public Double getHeightFactor() { + return heightFactor; + } + + public void alignment(Alignment v) { + this.alignment = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Alignment getAlignment() { + return alignment; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new AlignRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java new file mode 100644 index 00000000000..d05ad06d556 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlignRenderElement.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Flutter's RenderPositionedBox: loosens the incoming constraints for the + * child, expands itself to the bounded axes and positions the child by the + * configured alignment. Owns no CN1 component. + */ +public class AlignRenderElement extends SingleChildRenderElement { + + public AlignRenderElement(Align widget) { + super(widget); + } + + private Alignment alignment() { + Alignment a = ((Align) widget()).getAlignment(); + return a == null ? Alignment.center : a; + } + + @Override + protected Widget childWidget() { + return ((Align) widget()).getChild(); + } + + /** + * Flutter shrink-wraps an axis when a factor is given for it, or when that axis is + * unbounded; otherwise the box expands to fill. + */ + private static boolean shrinkWraps(Double factor, boolean bounded) { + return factor != null || !bounded; + } + + private static double factored(Double factor, double childExtent) { + return childExtent * (factor == null ? 1.0 : factor.doubleValue()); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Align self0 = (Align) widget(); + Double wf = self0.getWidthFactor(); + Double hf = self0.getHeightFactor(); + boolean shrinkW = shrinkWraps(wf, constraints.hasBoundedWidth()); + boolean shrinkH = shrinkWraps(hf, constraints.hasBoundedHeight()); + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size( + shrinkW ? 0 : constraints.maxWidth(), + shrinkH ? 0 : constraints.maxHeight())); + } + Size cs = child.layout(constraints.loosen()); + // A factor scales the box to a FRACTION of the child, which is how an expand/collapse + // animates: heightFactor runs 0 -> 1 while the child keeps its full size, and the + // enclosing ClipRect hides the part that does not fit yet. + double w = shrinkW ? factored(wf, cs.width()) : constraints.maxWidth(); + double h = shrinkH ? factored(hf, cs.height()) : constraints.maxHeight(); + Size self = constraints.constrain(new Size(w, h)); + Alignment a = alignment(); + setChildOffset(child, + Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(a.y(), self.height(), cs.height())); + return self; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlwaysScrollableScrollPhysics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlwaysScrollableScrollPhysics.java new file mode 100644 index 00000000000..dee24ec62d9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AlwaysScrollableScrollPhysics.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Scroll physics that always lets the user scroll, even when the content fits — + * Flutter's {@code AlwaysScrollableScrollPhysics}. + */ +public class AlwaysScrollableScrollPhysics extends ScrollPhysics { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedList.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedList.java new file mode 100644 index 00000000000..379b1e23306 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedList.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.AlwaysStoppedAnimation; +import com.codename1.flutter.animation.Animation; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A scrollable list that animates item insertion and removal — Flutter's + * {@code AnimatedList}. This milestone eagerly materializes + * {@code itemBuilder(context, index, animation)} for {@code 0..initialItemCount-1} + * with a fully-arrived ({@code 1.0}) animation, laid out linearly; the + * per-item enter/exit transitions driven through {@link AnimatedListState} are + * deferred. + */ +public class AnimatedList extends StatelessWidget { + + private Funcs.Func3, Widget> itemBuilder; + private long initialItemCount; + private boolean shrinkWrap; + + public void itemBuilder(Funcs.Func3, Widget> v) { + this.itemBuilder = v; + } + + public void initialItemCount(long v) { + this.initialItemCount = v; + } + + public void scrollDirection(Object v) { + } + + public void reverse(boolean v) { + } + + public void controller(Object v) { + } + + public void primary(Object v) { + } + + public void physics(Object v) { + } + + public void shrinkWrap(boolean v) { + this.shrinkWrap = v; + } + + public void padding(Object v) { + } + + public void clipBehavior(Clip v) { + } + + public static AnimatedListState of(BuildContext context) { + return new AnimatedListState(); + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + Animation arrived = new AlwaysStoppedAnimation(1.0); + if (itemBuilder != null) { + for (long i = 0; i < initialItemCount; i++) { + Widget w = itemBuilder.call(context, i, arrived); + if (w != null) { + kids.add(w); + } + } + } + ListView list = new ListView(); + list.children(kids); + if (shrinkWrap) { + list.shrinkWrap(true); + } + return list; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedListState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedListState.java new file mode 100644 index 00000000000..6e9b801af5c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnimatedListState.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Animation; + +import dart.core.Duration; +import dart.runtime.Funcs; + +/** + * The mutable state of an {@link AnimatedList} — Flutter's + * {@code AnimatedListState}, reached through a {@code GlobalKey}. Exposes the + * imperative {@code insertItem}/{@code removeItem} operations. In this milestone + * the list rebuilds from its item count on each frame rather than running + * per-item insert/remove transitions, so these record the intent without a + * flight animation. + */ +public class AnimatedListState { + + public void insertItem(long index, Duration duration) { + // structural insert is reflected on the next rebuild; transition deferred + } + + public void removeItem(long index, + Funcs.Func2, Widget> builder, + Duration duration) { + // structural remove is reflected on the next rebuild; transition deferred + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnnotatedRegion.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnnotatedRegion.java new file mode 100644 index 00000000000..e879a7aba0d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AnnotatedRegion.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Annotates the layer tree with a value (e.g. a {@code SystemUiOverlayStyle}) + * over the region its child occupies. The value is retained but not yet + * applied; the child renders unchanged. See {@link PassThroughRenderElement}. + * + * @param the annotation value type (e.g. SystemUiOverlayStyle) + */ +public class AnnotatedRegion extends Widget implements HasChild { + + private Widget child; + private Object value; + private boolean sized = true; + + public void child(Widget v) { + this.child = v; + } + + public void value(Object v) { + this.value = v; + } + + public void sized(boolean v) { + this.sized = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AspectRatio.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AspectRatio.java new file mode 100644 index 00000000000..d58df057924 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AspectRatio.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Forces its {@code child} to a specific aspect ratio — Flutter's {@code AspectRatio}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class AspectRatio extends Widget implements HasChild { + + private double aspectRatio; + private Widget child; + + public void aspectRatio(double v) { this.aspectRatio = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AsyncSnapshot.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AsyncSnapshot.java new file mode 100644 index 00000000000..fa71f0e4f3d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/AsyncSnapshot.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * An immutable snapshot of interaction with an asynchronous computation, handed + * to a {@code FutureBuilder} / {@code StreamBuilder} builder, mirroring + * Flutter's {@code AsyncSnapshot}. The about page reads {@link #hasData()} + * and {@link #data()}. + * + * @param the type of the async value + */ +public class AsyncSnapshot { + + private final ConnectionState connectionState; + private final T data; + private final Object error; + private final Object stackTrace; + + public AsyncSnapshot() { + this(ConnectionState.none, null, null, null); + } + + public AsyncSnapshot(ConnectionState connectionState, T data, Object error, Object stackTrace) { + this.connectionState = connectionState; + this.data = data; + this.error = error; + this.stackTrace = stackTrace; + } + + public ConnectionState connectionState() { + return connectionState; + } + + public T data() { + return data; + } + + public Object error() { + return error; + } + + public Object stackTrace() { + return stackTrace; + } + + public boolean hasData() { + return data != null; + } + + public boolean hasError() { + return error != null; + } + + /** + * Flutter's {@code requireData}: the data, or the stored error rethrown, or a + * StateError when there is neither. Returning null let a builder carry on and + * fail later at an unrelated dereference, with the original asynchronous error + * lost. + */ + public T requireData() { + if (data != null) { + return data; + } + if (error != null) { + throw dart.runtime.DartRuntime.asError(error); + } + throw new dart.core.StateError("Snapshot has neither data nor error"); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BouncingScrollPhysics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BouncingScrollPhysics.java new file mode 100644 index 00000000000..9f2160f87be --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BouncingScrollPhysics.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Scroll physics that let the content be dragged past its edges against a rubber band, and + * spring back when released — Flutter's {@code BouncingScrollPhysics}, and the iOS default. + * + *

The rubber band is the whole feel of an overscroll, and it is not a constant: the + * further past the edge you already are, the less each pixel of finger moves the content, + * so the list gets progressively harder to pull. Codename One's own tensile drag is a + * different model, which is why an overscroll here used to feel unlike the same gesture in + * Flutter even though everything else about the scroll matched.

+ * + *

Every number below is pinned by the {@code flutter_scroll_physics} behavioral case, + * whose expectations are generated by running the same Dart on the Flutter SDK — so this + * is measured against Flutter rather than tuned by eye.

+ */ +public class BouncingScrollPhysics extends ScrollPhysics { + + @Override + public ScrollPhysics applyTo(ScrollPhysics ancestor) { + BouncingScrollPhysics p = new BouncingScrollPhysics(); + p.parent(buildParent(ancestor)); + return p; + } + + /** + * How much of a drag survives at a given overscroll, as a fraction of the viewport. + * Quadratic, so resistance builds smoothly rather than at a threshold. + */ + public double frictionFactor(double overscrollFraction) { + return 0.52 * Math.pow(1 - overscrollFraction, 2); + } + + @Override + public double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { + if (!position.outOfRange()) { + // In range there is no rubber band at all - resistance here would just feel + // like a list that does not track the finger. + return offset; + } + double overscrollPastStart = + Math.max(position.minScrollExtent() - position.pixels(), 0.0); + double overscrollPastEnd = + Math.max(position.pixels() - position.maxScrollExtent(), 0.0); + double overscrollPast = Math.max(overscrollPastStart, overscrollPastEnd); + // A drag that carries the content back towards the range is measured from where it + // ENDS rather than where it starts, so returning resists less than leaving. That + // asymmetry is why a Flutter overscroll lets go easily instead of feeling sticky. + boolean easing = (overscrollPastStart > 0.0 && offset < 0.0) + || (overscrollPastEnd > 0.0 && offset > 0.0); + double friction = easing + ? frictionFactor((overscrollPast - Math.abs(offset)) + / position.viewportDimension()) + : frictionFactor(overscrollPast / position.viewportDimension()); + double direction = offset < 0 ? -1.0 : (offset > 0 ? 1.0 : 0.0); + return direction * applyFriction(overscrollPast, Math.abs(offset), friction); + } + + /** + * Applies {@code gamma} to the part of the drag spent outside the content, and none to + * whatever is left over. A drag long enough to cross back inside is only damped for the + * stretch where it was actually overscrolled. + */ + private static double applyFriction(double extentOutside, double absDelta, double gamma) { + double total = 0.0; + if (extentOutside > 0) { + double deltaToLimit = extentOutside / gamma; + if (absDelta < deltaToLimit) { + return absDelta * gamma; + } + total += extentOutside; + absDelta -= deltaToLimit; + } + return total + absDelta; + } + + /** A bouncing scrollable always accepts a drag: it can always overscroll. */ + @Override + public boolean shouldAcceptUserOffset(ScrollMetrics position) { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Builder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Builder.java new file mode 100644 index 00000000000..ed98161dca1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Builder.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A stateless helper whose {@code build} is delegated to a closure — Flutter's + * {@code Builder}. Useful to obtain a {@link BuildContext} below the current + * widget. + */ +public class Builder extends Widget { + + private Funcs.Func1 builder; + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public Funcs.Func1 getBuilder() { + return builder; + } + + @Override + public Element createElement() { + return new BuilderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BuilderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BuilderElement.java new file mode 100644 index 00000000000..bed778daa8f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/BuilderElement.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.ComposedElement; +import com.codename1.flutter.Widget; + +/** + * Element for {@link Builder}: rebuilding invokes the builder closure with this + * element as the {@link com.codename1.flutter.BuildContext} and reconciles the + * single resulting child. + */ +public class BuilderElement extends ComposedElement { + + public BuilderElement(Builder widget) { + super(widget); + } + + @Override + protected Widget build() { + return ((Builder) widget()).getBuilder().call(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Center.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Center.java new file mode 100644 index 00000000000..73a0473bb42 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Center.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Centers its child within itself. Expands to the incoming constraints when + * they are bounded, otherwise sizes to the child. + */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class Center extends Widget implements HasChild { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new CenterRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CenterRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CenterRenderElement.java new file mode 100644 index 00000000000..41f22595858 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CenterRenderElement.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Flutter's RenderPositionedBox with a fixed center alignment: loosens the + * incoming constraints for the child, expands itself to the bounded axes and + * positions the child by alignment. Owns no CN1 component. + */ +public class CenterRenderElement extends SingleChildRenderElement { + + public CenterRenderElement(Center widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((Center) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + } + Size cs = child.layout(constraints.loosen()); + double w = constraints.hasBoundedWidth() ? constraints.maxWidth() : cs.width(); + double h = constraints.hasBoundedHeight() ? constraints.maxHeight() : cs.height(); + Size self = constraints.constrain(new Size(w, h)); + setChildOffset(child, + Alignment.along(0, self.width(), cs.width()), + Alignment.along(0, self.height(), cs.height())); + return self; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClampingScrollPhysics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClampingScrollPhysics.java new file mode 100644 index 00000000000..7f5c659b8c6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClampingScrollPhysics.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Scroll physics that clamps at the content edges (the Android default) — + * Flutter's {@code ClampingScrollPhysics}. + */ +public class ClampingScrollPhysics extends ScrollPhysics { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOval.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOval.java new file mode 100644 index 00000000000..3789147e209 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOval.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Clips its child to an oval. See {@link ClipOvalRenderElement}. + */ +public class ClipOval extends Widget implements HasChild { + + private Object clipper; + private Clip clipBehavior = Clip.antiAlias; + private Widget child; + + public void clipper(Object v) { + this.clipper = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** {@code Clip.none} means do not clip, so it must not get a clipping pane. */ + @Override + public Element createElement() { + if (clipBehavior == Clip.none) { + return new PassThroughRenderElement(this); + } + return new ClipOvalRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOvalRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOvalRenderElement.java new file mode 100644 index 00000000000..05bf78658f6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipOvalRenderElement.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; +import com.codename1.ui.geom.GeneralPath; + +/** + * Clips its subtree to an ellipse inscribed in its box — Flutter's + * {@code ClipOval}, and what makes an avatar round. + * + *

It was a pass-through, so every circular portrait in the app rendered as + * a square photograph sitting on top of a round background: the mail study's + * sender avatars, the contact rows, the profile chips.

+ */ +public class ClipOvalRenderElement extends ClipRectRenderElement { + + private GeneralPath path; + private int pathX = Integer.MIN_VALUE; + private int pathY = Integer.MIN_VALUE; + private int pathW = -1; + private int pathH = -1; + + public ClipOvalRenderElement(Widget widget) { + super(widget); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + int w = pane.getWidth(); + int h = pane.getHeight(); + if (w <= 0 || h <= 0) { + paintChildren.paint(g); + return; + } + if (paintShapeClipped(g, pane, paintChildren, pathFor(pane.getX(), pane.getY(), w, h))) { + return; + } + com.codename1.flutter.FlutterErrorReport.unimplemented("ClipOval", + "this platform cannot clip to a shape, so the subtree paints square"); + paintChildren.paint(g); + } + + /// The circle-through-Beziers constant: the control-point offset, as a fraction of + /// the radius, that makes a cubic segment match a quarter arc. + private static final double KAPPA = 0.5522847498307933; + + /** The inscribed ellipse, rebuilt only when the box changes. */ + private GeneralPath pathFor(int x, int y, int w, int h) { + // Reused rather than rebuilt while the box is unchanged; see + // ClipRRectRenderElement for why that matters. The origin is part of + // the key because the path is in parent-relative coordinates. + if (path != null && pathX == x && pathY == y && pathW == w && pathH == h) { + return path; + } + path = new GeneralPath(); + pathX = x; + pathY = y; + pathW = w; + pathH = h; + float rx = w / 2f; + float ry = h / 2f; + float cx = x + rx; + float cy = y + ry; + float ox = (float) (rx * KAPPA); + float oy = (float) (ry * KAPPA); + path.moveTo(cx - rx, cy); + path.curveTo(cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry); + path.curveTo(cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy); + path.curveTo(cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry); + path.curveTo(cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy); + path.closePath(); + return path; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRect.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRect.java new file mode 100644 index 00000000000..b85bf21cff1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRect.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Clips its child with a rounded rectangle. See + * {@link ClipRRectRenderElement}. + */ +public class ClipRRect extends Widget implements HasChild { + + private Object borderRadius; + private Object clipper; + private Clip clipBehavior = Clip.antiAlias; + private Widget child; + + public void borderRadius(Object v) { + this.borderRadius = v; + } + + public Object getBorderRadius() { + return borderRadius; + } + + public Clip getClipBehavior() { + return clipBehavior; + } + + public void clipper(Object v) { + this.clipper = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** {@code Clip.none} means do not clip, so it must not get a clipping pane. */ + @Override + public Element createElement() { + if (clipBehavior == Clip.none) { + return new PassThroughRenderElement(this); + } + return new ClipRRectRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRectRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRectRenderElement.java new file mode 100644 index 00000000000..fb577cc689d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRRectRenderElement.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BorderRadius; +import com.codename1.flutter.Radius; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Dp; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; +import com.codename1.ui.geom.GeneralPath; + +/** + * Clips its subtree to a ROUNDED rectangle — Flutter's {@code ClipRRect}. + * + *

It used to be a pass-through, so every corner the design rounds came out + * square: the gallery frames each demo in a card with a 10dp top radius, the + * mail study clips its avatars, and the study cards on the home screen are + * rounded. None of it appeared.

+ * + *

The nested pane from {@link EffectRenderElement} already confines the + * subtree to a rectangle; the corners need a shaped clip on top, which is only + * available where the port supports one. Where it is not, the rectangular pane + * clip stands — square corners, but never content spilling out.

+ * + *

The path is rebuilt only when the box or the radii change. Building a + * GeneralPath per paint is how an earlier version of this runtime put the event + * thread inside the garbage collector for the duration of every frame.

+ */ +public class ClipRRectRenderElement extends ClipRectRenderElement { + + private GeneralPath path; + private int pathX = Integer.MIN_VALUE; + private int pathY = Integer.MIN_VALUE; + private int pathW = -1; + private int pathH = -1; + private double pathTl; + private double pathTr; + private double pathBr; + private double pathBl; + + public ClipRRectRenderElement(Widget widget) { + super(widget); + } + + private BorderRadius radius() { + Widget w = widget(); + if (!(w instanceof ClipRRect)) { + return null; + } + Object r = ((ClipRRect) w).getBorderRadius(); + return r instanceof BorderRadius ? (BorderRadius) r : null; + } + + private static double px(Radius r) { + return r == null ? 0 : Dp.px(r.x()); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + BorderRadius radius = radius(); + int w = pane.getWidth(); + int h = pane.getHeight(); + if (radius == null || w <= 0 || h <= 0) { + paintChildren.paint(g); + return; + } + double tl = px(radius.topLeft()); + double tr = px(radius.topRight()); + double br = px(radius.bottomRight()); + double bl = px(radius.bottomLeft()); + if (tl <= 0 && tr <= 0 && br <= 0 && bl <= 0) { + paintChildren.paint(g); + return; + } + // PARENT-RELATIVE, not absolute: a Graphics being painted through has + // already accumulated its ancestors' translation, which is why the whole + // of Codename One draws with getX(). Clipping with the absolute position + // added that offset a second time, and the clip then landed somewhere + // else entirely -- the subtree was still laid out, still had components, + // and painted nothing. + int ax = pane.getX(); + int ay = pane.getY(); + GeneralPath p = pathFor(ax, ay, w, h, tl, tr, br, bl); + // The port's rounded-image path takes ONE radius, so it can draw this only when + // the four agree. Where they do not, the shape clip is the only mechanism -- and + // on iOS that does nothing at all, so say so rather than drawing square in + // silence. See EffectRenderElement.paintRoundClipped. + if (!shapeClipSupported(g)) { + // No shaped clip at all on this port: paint the subtree plainly and + // still finish the corners. The wedge fill needs fillShape, not a + // shaped CLIP, so it works where setClip(Shape) does not. + paintChildren.paint(g); + paintCornerWedges(g, pane, ax, ay, w, h, tl, tr, br, bl); + return; + } + if (paintShapeClipped(g, pane, paintChildren, p)) { + // The clip holds for the outermost paint and degrades to a rectangle + // once a child component paints (Component.paintComponent restores + // it from four ints, deliberately). So the corners are finished HERE + // instead: the four wedges between the box and its rounded outline + // are painted over in the colour behind the box. That is one draw we + // own completely -- clip set, drawn, restored -- and it needs neither + // a shaped clip that survives the subtree nor a mutable-image detour. + paintCornerWedges(g, pane, ax, ay, w, h, tl, tr, br, bl); + return; + } + paintChildren.paint(g); + paintCornerWedges(g, pane, ax, ay, w, h, tl, tr, br, bl); + } + + /// The wedge path (box minus rounded box), cached against the geometry that + /// produced it -- it only changes when the box or the radii do. + private GeneralPath wedges; + private String wedgeKey; + + /** + * Fills the four corner wedges with the colour behind this box. + * + *

Exact when what sits behind the box is a flat colour, which is the + * case this exists for -- a Material card on a page. When the backdrop is + * not a flat opaque colour there is nothing safe to paint, so the corners + * are left as the subtree drew them rather than covered with a guess.

+ */ + private void paintCornerWedges(Graphics g, Container pane, int x, int y, int w, int h, + double tl, double tr, double br, double bl) { + int backdrop = backdropRgb(pane); + if (backdrop < 0) { + return; + } + String key = x + "," + y + "," + w + "," + h + "," + + (int) tl + "," + (int) tr + "," + (int) br + "," + (int) bl; + if (wedges == null || !key.equals(wedgeKey)) { + GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + path.moveTo(x, y); + path.lineTo(x + w, y); + path.lineTo(x + w, y + h); + path.lineTo(x, y + h); + path.closePath(); + // The rounded outline as a second subpath: under the even-odd rule + // it becomes a hole, so a fill paints only the corners. + path.append(pathFor(x, y, w, h, tl, tr, br, bl), false); + wedges = path; + wedgeKey = key; + } + int priorColor = g.getColor(); + boolean priorAa = g.isAntiAliased(); + g.setAntiAliased(true); + g.setColor(backdrop); + g.pushClip(); + try { + g.clipRect(x, y, w, h); + g.fillShape(wedges); + } finally { + g.popClip(); + g.setColor(priorColor); + g.setAntiAliased(priorAa); + } + } + + /** + * The opaque colour painted behind {@code c}, or -1 when the nearest + * ancestor that paints anything is not a flat opaque fill. + */ + private static int backdropRgb(Container c) { + com.codename1.ui.Component cur = c == null ? null : c.getParent(); + while (cur != null) { + com.codename1.ui.plaf.Style st = cur.getStyle(); + if (st != null && st.getBgTransparency() == (byte) 0xff + && st.getBgImage() == null) { + return st.getBgColor(); + } + cur = cur.getParent(); + } + return -1; + } + + private static boolean shapeClipSupported(Graphics g) { + try { + return Display.isInitialized() && g.isShapeClipSupported(); + } catch (Throwable t) { + return false; + } + } + + private GeneralPath pathFor(int x, int y, int w, int h, + double tl, double tr, double br, double bl) { + // REUSED, not rebuilt. The path is geometry, and the geometry only + // changes when the box does; rebuilding it on every paint puts a fresh + // GeneralPath in front of the collector on every frame of every + // animation that crosses a rounded card, which is how an earlier + // version of this runtime parked the event thread inside the GC. + // The origin participates because the path is in parent-relative + // coordinates, which move when an ancestor scrolls. + if (path != null && pathX == x && pathY == y && pathW == w && pathH == h + && pathTl == tl && pathTr == tr && pathBr == br && pathBl == bl) { + return path; + } + path = new GeneralPath(); + pathX = x; + pathY = y; + pathW = w; + pathH = h; + pathTl = tl; + pathTr = tr; + pathBr = br; + pathBl = bl; + // A radius can never exceed half the box, or opposite corners overlap and + // the outline crosses itself. + double max = Math.min(w, h) / 2.0; + tl = Math.min(tl, max); + tr = Math.min(tr, max); + br = Math.min(br, max); + bl = Math.min(bl, max); + float left = x; + float top = y; + float right = x + w; + float bottom = y + h; + path.moveTo(left + tl, top); + path.lineTo(right - tr, top); + if (tr > 0) { + path.quadTo(right, top, right, top + tr); + } + path.lineTo(right, bottom - br); + if (br > 0) { + path.quadTo(right, bottom, right - br, bottom); + } + path.lineTo(left + bl, bottom); + if (bl > 0) { + path.quadTo(left, bottom, left, bottom - bl); + } + path.lineTo(left, top + tl); + if (tl > 0) { + path.quadTo(left, top, left + tl, top); + } + path.closePath(); + return path; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRect.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRect.java new file mode 100644 index 00000000000..3c2ea42e076 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRect.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Clips its child to a rectangle. See {@link ClipRectRenderElement}. + */ +public class ClipRect extends Widget implements HasChild { + + private Object clipper; + private Clip clipBehavior = Clip.hardEdge; + private Widget child; + + public void clipper(Object v) { + this.clipper = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public Clip getClipBehavior() { + return clipBehavior; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** + * {@code Clip.none} means DO NOT CLIP, so it must not get the clipping pane at all — + * that pane clips regardless of what the behaviour says, which is why asking for no + * clipping used to cut the content anyway and merely report that it had. + */ + @Override + public Element createElement() { + if (clipBehavior == Clip.none) { + return new PassThroughRenderElement(this); + } + return new ClipRectRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRectRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRectRenderElement.java new file mode 100644 index 00000000000..6ce4e6ce3e1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ClipRectRenderElement.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Clips its subtree to its own bounds, which is what makes an expand/collapse animation + * read as one: {@code ClipRect(child: Align(heightFactor: t, child: ...))} shrinks the box + * while the child keeps its full size, and everything past the box has to disappear. + * + *

The clip itself costs nothing to apply. Codename One already confines a component's + * paint - its own and its children's - to its bounds before calling + * {@code paint} (see {@code Component.internalPaintImpl}). The work is getting the subtree + * to be that component's children at all: the render tree is otherwise flat, every element + * absolutely positioned as a sibling in one host. {@link EffectRenderElement} supplies the + * nested pane that makes the subtree genuinely nested, so this element only has to exist, + * not to paint anything special.

+ */ +public class ClipRectRenderElement extends EffectRenderElement { + + + public ClipRectRenderElement(Widget widget) { + super(widget); + } + + @Override + protected Widget effectChild() { + return ((HasChild) widget()).getChild(); + } + + /** The clip mode this widget asks for, defaulting to a hard edge as Flutter's ClipRect does. */ + private Clip behavior() { + Widget w = widget(); + Clip c = w instanceof ClipRect ? ((ClipRect) w).getClipBehavior() : null; + return c == null ? Clip.hardEdge : c; + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + // Clip.none never reaches here: ClipRect gives it a pass-through element instead, + // since this element's pane clips whatever the behaviour asks for. + paintChildren.paint(g); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBox.java new file mode 100644 index 00000000000..890fcf1e775 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBox.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Paints a solid color behind its child — Flutter's {@code ColoredBox}. Sizes + * to the child, or fills the incoming constraints when childless. + */ +public class ColoredBox extends Widget { + + private Color color; + private Widget child; + + public void color(Color v) { + this.color = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Color getColor() { + return color; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new ColoredBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBoxRenderElement.java new file mode 100644 index 00000000000..40c57e7e651 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ColoredBoxRenderElement.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Color; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; + +/** + * Render element for {@link ColoredBox}: a CN1 Container (UIID "FlutterBox") + * filled with the color, covering the element bounds. The child's components + * attach after the face in tree order, so they paint on top. Sizes to the + * child, or fills the bounded incoming axes when childless. + */ +public class ColoredBoxRenderElement extends SingleChildRenderElement { + + public ColoredBoxRenderElement(ColoredBox widget) { + super(widget); + } + + private ColoredBox box() { + return (ColoredBox) widget(); + } + + @Override + protected Widget childWidget() { + return box().getChild(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + return null; + } + com.codename1.ui.Container face = new com.codename1.ui.Container(); + face.setUIID("FlutterBox"); + face.getAllStyles().setPadding(0, 0, 0, 0); + face.getAllStyles().setMargin(0, 0, 0, 0); + applyStyle(face); + return face; + } + + @Override + protected void updateComponent(Component c) { + applyStyle(c); + } + + private void applyStyle(Component face) { + try { + if (box().getColor() != null) { + face.getAllStyles().setBgColor(box().getColor().rgb()); + face.getAllStyles().setBgTransparency(box().getColor().alpha()); + } else { + face.getAllStyles().setBgTransparency(0); + } + } catch (Exception err) { + // styling best-effort + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Column.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Column.java new file mode 100644 index 00000000000..f54ca6a0a73 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Column.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * A vertical Flex. + */ +public class Column extends Flex { + + @Override + public boolean isVertical() { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConnectionState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConnectionState.java new file mode 100644 index 00000000000..e5c3123a994 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConnectionState.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * The connection state of an async computation feeding an + * {@link AsyncSnapshot}, mirroring Flutter's {@code ConnectionState}. + */ +public enum ConnectionState { + /** Not connected to any asynchronous computation. */ + none, + /** Connected, awaiting interaction. */ + waiting, + /** Connected and actively producing values. */ + active, + /** Connected to a terminated asynchronous computation. */ + done +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBox.java new file mode 100644 index 00000000000..03953b27d93 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBox.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; + +/** + * Imposes additional {@link BoxConstraints} (logical pixels) on its child, + * intersected with the incoming constraints. + */ +public class ConstrainedBox extends Widget { + + private BoxConstraints constraints; + private Widget child; + + public void constraints(BoxConstraints v) { + this.constraints = v; + } + + public void child(Widget v) { + this.child = v; + } + + public BoxConstraints getConstraints() { + return constraints; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new ConstrainedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBoxRenderElement.java new file mode 100644 index 00000000000..887888e9fef --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ConstrainedBoxRenderElement.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +/** + * Flutter's RenderConstrainedBox: the widget's additional constraints + * (converted from logical to device pixels) are + * {@link BoxConstraints#enforce(BoxConstraints) enforced} within the incoming + * ones and imposed on the child. Owns no CN1 component. + */ +public class ConstrainedBoxRenderElement extends SingleChildRenderElement { + + public ConstrainedBoxRenderElement(ConstrainedBox widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((ConstrainedBox) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + BoxConstraints additional = ((ConstrainedBox) widget()).getConstraints(); + BoxConstraints inner = additional == null + ? constraints + : toPx(additional).enforce(constraints); + RenderElement child = renderChild(); + if (child == null) { + return inner.constrain(Size.ZERO); + } + Size cs = child.layout(inner); + setChildOffset(child, 0, 0); + return cs; + } + + private static BoxConstraints toPx(BoxConstraints lp) { + return new BoxConstraints( + px(lp.minWidth()), px(lp.maxWidth()), + px(lp.minHeight()), px(lp.maxHeight())); + } + + private static double px(double v) { + return v == Double.POSITIVE_INFINITY ? v : Dp.px(v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Container.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Container.java new file mode 100644 index 00000000000..a41a1afa0f1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Container.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; + +/** + * A convenience widget that combines painting, positioning and sizing — + * Flutter's {@code Container}. Applies (in order) margin, decoration/color, + * additional constraints + explicit width/height, padding and alignment around + * an optional child. + * + *

Loosely-typed properties ({@code alignment}, {@code padding}, + * {@code margin}, {@code decoration}) accept their several Flutter value types; + * the render element interprets the ones it supports + * ({@link com.codename1.flutter.Alignment}/{@link com.codename1.flutter.AlignmentDirectional}, + * {@link com.codename1.flutter.EdgeInsets}, {@link com.codename1.flutter.BoxDecoration}).

+ */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class Container extends Widget implements HasChild { + + private Object alignment; + private Object padding; + private Color color; + private Object decoration; + private Object foregroundDecoration; + private Double width; + private Double height; + private BoxConstraints constraints; + private Object margin; + private Object transform; + private Object transformAlignment; + private Clip clipBehavior = Clip.none; + private Widget child; + + public void alignment(Object v) { + this.alignment = v; + } + + public void padding(Object v) { + this.padding = v; + } + + public void color(Color v) { + this.color = v; + } + + public void decoration(Object v) { + this.decoration = v; + } + + public void foregroundDecoration(Object v) { + this.foregroundDecoration = v; + } + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void constraints(BoxConstraints v) { + this.constraints = v; + } + + public void margin(Object v) { + this.margin = v; + } + + public void transform(Object v) { + this.transform = v; + } + + public void transformAlignment(Object v) { + this.transformAlignment = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getAlignment() { + return alignment; + } + + public Object getPadding() { + return padding; + } + + public Color getColor() { + return color; + } + + public Object getDecoration() { + return decoration; + } + + public Double getWidth() { + return width; + } + + public Double getHeight() { + return height; + } + + public BoxConstraints getConstraints() { + return constraints; + } + + public Object getMargin() { + return margin; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new ContainerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ContainerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ContainerRenderElement.java new file mode 100644 index 00000000000..15fafdd119a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ContainerRenderElement.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.AlignmentDirectional; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; + +/** + * Flutter's RenderContainer composite. Layout order: margin deflates the + * incoming constraints; explicit width/height and additional constraints + * tighten the box; padding insets the child; alignment positions the child + * within the padded box (and expands the box to the bounded axes when set). + * A CN1 Container face (UIID "FlutterBox") paints the color/decoration behind + * the child, covering the box minus the margin band. + */ +public class ContainerRenderElement extends SingleChildRenderElement { + + private EdgeInsets marginPx = EdgeInsets.all(0); + private Size contentSize = Size.ZERO; + + public ContainerRenderElement(Container widget) { + super(widget); + } + + private Container container() { + return (Container) widget(); + } + + @Override + protected Widget childWidget() { + return container().getChild(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + return null; + } + if (!FlutterBoxStyle.paints(container().getColor(), container().getDecoration())) { + return null; + } + Container c = container(); + com.codename1.ui.Container face = new com.codename1.ui.Container(); + face.setUIID("FlutterBox"); + face.getAllStyles().setPadding(0, 0, 0, 0); + face.getAllStyles().setMargin(0, 0, 0, 0); + FlutterBoxStyle.apply(face, c.getColor(), c.getDecoration()); + return face; + } + + @Override + protected void updateComponent(Component c) { + FlutterBoxStyle.apply(c, container().getColor(), container().getDecoration()); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Container w = container(); + marginPx = toPx(resolveInsets(w.getMargin())); + EdgeInsets paddingPx = toPx(resolveInsets(w.getPadding())); + + BoxConstraints box = constraints.deflate(marginPx); + if (w.getConstraints() != null) { + box = additionalToPx(w.getConstraints()).enforce(box); + } + Double widthPx = w.getWidth() == null ? null : Double.valueOf(Dp.px(w.getWidth())); + Double heightPx = w.getHeight() == null ? null : Double.valueOf(Dp.px(w.getHeight())); + box = box.tighten(widthPx, heightPx); + + Alignment align = resolveAlignment(w.getAlignment()); + RenderElement child = renderChild(); + + double contentW; + double contentH; + if (child == null) { + contentW = box.hasBoundedWidth() ? box.maxWidth() : paddingPx.horizontal(); + contentH = box.hasBoundedHeight() ? box.maxHeight() : paddingPx.vertical(); + contentSize = box.constrain(new Size(contentW, contentH)); + } else { + BoxConstraints inner = box.deflate(paddingPx); + if (align != null) { + inner = inner.loosen(); + } + Size cs = child.layout(inner); + contentW = cs.width() + paddingPx.horizontal(); + contentH = cs.height() + paddingPx.vertical(); + if (align != null) { + if (box.hasBoundedWidth()) { + contentW = box.maxWidth(); + } + if (box.hasBoundedHeight()) { + contentH = box.maxHeight(); + } + } + contentSize = box.constrain(new Size(contentW, contentH)); + double innerW = contentSize.width() - paddingPx.horizontal(); + double innerH = contentSize.height() - paddingPx.vertical(); + double dx = align == null ? 0 : Alignment.along(align.x(), innerW, cs.width()); + double dy = align == null ? 0 : Alignment.along(align.y(), innerH, cs.height()); + setChildOffset(child, + marginPx.left() + paddingPx.left() + dx, + marginPx.top() + paddingPx.top() + dy); + } + return constraints.constrain(new Size( + contentSize.width() + marginPx.horizontal(), + contentSize.height() + marginPx.vertical())); + } + + @Override + public void position(int x, int y) { + super.position(x, y); + Component face = component(); + if (face != null) { + face.setX(x + (int) Math.round(marginPx.left())); + face.setY(y + (int) Math.round(marginPx.top())); + face.setWidth(Math.max(0, (int) Math.round(contentSize.width()))); + face.setHeight(Math.max(0, (int) Math.round(contentSize.height()))); + } + } + + // ------------------------------------------------------------------ + + private static EdgeInsets resolveInsets(Object o) { + return o instanceof EdgeInsets ? (EdgeInsets) o : EdgeInsets.all(0); + } + + private static EdgeInsets toPx(EdgeInsets lp) { + return EdgeInsets.only(Dp.px(lp.left()), Dp.px(lp.top()), Dp.px(lp.right()), Dp.px(lp.bottom())); + } + + private static Alignment resolveAlignment(Object o) { + if (o instanceof Alignment) { + return (Alignment) o; + } + if (o instanceof AlignmentDirectional) { + return ((AlignmentDirectional) o).resolve(); + } + return null; + } + + private static BoxConstraints additionalToPx(BoxConstraints lp) { + return new BoxConstraints( + pxInf(lp.minWidth()), pxInf(lp.maxWidth()), + pxInf(lp.minHeight()), pxInf(lp.maxHeight())); + } + + private static double pxInf(double v) { + return v == Double.POSITIVE_INFINITY ? v : Dp.px(v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaint.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaint.java new file mode 100644 index 00000000000..4394039edfc --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaint.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.CustomPainter; +import com.codename1.flutter.rendering.Size; + +/** + * Provides a canvas for a {@link CustomPainter} to paint on, behind and/or in + * front of an optional {@code child} — Flutter's {@code CustomPaint}. The + * painters run against a Codename One {@code Graphics} through + * {@link com.codename1.flutter.rendering.GraphicsCanvas}; see + * {@link CustomPaintRenderElement}. + */ +public class CustomPaint extends Widget { + + private CustomPainter painter; + private CustomPainter foregroundPainter; + private Size size; + private Widget child; + + public void painter(CustomPainter v) { + this.painter = v; + } + + public void foregroundPainter(CustomPainter v) { + this.foregroundPainter = v; + } + + public void size(Size v) { + this.size = v; + } + + public void isComplex(boolean v) { + } + + public void willChange(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + public CustomPainter getPainter() { + return painter; + } + + /** The painter drawn OVER the child — Flutter's {@code foregroundPainter}. */ + public CustomPainter getForegroundPainter() { + return foregroundPainter; + } + + /** The box the painter asks for when there is no child, in logical pixels. */ + public Size getSize() { + return size; + } + + public Widget getChild() { + return child; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new CustomPaintRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaintRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaintRenderElement.java new file mode 100644 index 00000000000..d8e55a24bf9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomPaintRenderElement.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.CustomPainter; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.GraphicsCanvas; +import com.codename1.flutter.rendering.Size; +import com.codename1.io.Log; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; + +/** + * Runs a {@link CustomPainter} onto the screen — the render element behind + * {@link CustomPaint}. + * + *

The element owns a CN1 component whose {@code paint} drives + * {@code painter.paint(canvas, size)} through a {@link GraphicsCanvas}. The + * painter is handed a size in LOGICAL pixels and a canvas pre-scaled by the + * device pixel ratio, so a painter written against Flutter's coordinate system + * draws at the right physical size on any density.

+ * + *

Both painters are supported: {@code painter} draws behind the child, + * {@code foregroundPainter} over it. The child's own components are ordinary + * siblings attached after the backdrop, so they paint on top of it.

+ */ +public class CustomPaintRenderElement extends SingleChildRenderElement { + + public CustomPaintRenderElement(CustomPaint widget) { + super(widget); + } + + private CustomPaint paintWidget() { + return (CustomPaint) widget(); + } + + @Override + protected Widget childWidget() { + return paintWidget().getChild(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + return new PainterSurface(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child != null) { + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } + // No child: Flutter uses CustomPaint.size (logical pixels), falling + // back to the largest the constraints allow. + Size preferred = paintWidget().getSize(); + if (preferred != null) { + return constraints.constrain(new Size( + Dp.px(preferred.width()), Dp.px(preferred.height()))); + } + return constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + } + + /** The component that hands its Graphics to the painters. */ + private final class PainterSurface extends Container { + + PainterSurface() { + setUIID("FlutterCustomPaint"); + getAllStyles().setPadding(0, 0, 0, 0); + getAllStyles().setMargin(0, 0, 0, 0); + getAllStyles().setBgTransparency(0); + } + + @Override + public void paint(Graphics g) { + run(g, paintWidget().getPainter()); + super.paint(g); + run(g, paintWidget().getForegroundPainter()); + } + + /** + * Gives the painter back the ground an ancestor Transform took away. + * + *

Flutter does not clip a CustomPainter to the box it was handed -- + * an ancestor ClipRect does that -- so a painter may draw well outside + * its own size. Codename One clips every component to its bounds, and + * when an ancestor Transform has shifted the origin those bounds move + * with it, so the part of the drawing the shift brings into view is cut + * off instead. The 2D-transformations demo centres a board wider than + * the screen by translating it, and lost a strip down the right-hand + * side exactly as wide as the shift.

+ * + *

The shift is recoverable: a graphics being painted through has + * accumulated its ancestors' offsets, so without a transform its + * translation plus this component's parent-relative position is its + * absolute position. Whatever that identity is out by IS the transform. + * Clip to the box this component occupies ON SCREEN rather than to the + * one the shift moved it to.

+ */ + private void unclipFromAncestorTransform(Graphics g) { + int[] box = CustomPaintRenderElement.onScreenClip(g.getTranslateX(), g.getTranslateY(), + getX(), getY(), getAbsoluteX(), getAbsoluteY(), + getWidth(), getHeight()); + if (box != null) { + g.setClip(box[0], box[1], box[2], box[3]); + } + } + + private void run(Graphics g, CustomPainter painter) { + if (painter == null) { + return; + } + double dpr = Dp.scale(); + if (dpr <= 0) { + dpr = 1; + } + int clipX = g.getClipX(); + int clipY = g.getClipY(); + int clipW = g.getClipWidth(); + int clipH = g.getClipHeight(); + int color = g.getColor(); + int alpha = g.getAlpha(); + try { + unclipFromAncestorTransform(g); + // the painter's box, in the logical pixels it expects + Size logical = new Size(getWidth() / dpr, getHeight() / dpr); + // The origin is this component's PARENT-RELATIVE position, because a Graphics + // being painted through has already accumulated its ancestors' translation + // (Container.paintChildren translates by getX()/getY() on the way down) - which + // is why the whole of Codename One draws with getX(), not getAbsoluteX(). Using + // the absolute position here added the ancestors' offset a second time and + // pushed the drawing outside the bounds this component clips to, so the painter + // ran and nothing appeared. + painter.paint(new GraphicsCanvas(g, getX(), getY(), dpr), logical); + } catch (Throwable t) { + // One misbehaving painter must not take the whole frame down — + // but it must be REPORTED. A painter that throws leaves the + // screen looking merely empty, and a log line is invisible to + // the sweep: the 2D-transformations demo drew no board at all + // and every check said the route was fine. + com.codename1.flutter.FlutterErrorReport.unimplemented( + painter.getClass().getName(), + "its paint() threw " + t + "; nothing was drawn"); + } finally { + g.setClip(clipX, clipY, clipW, clipH); + g.setColor(color); + g.setAlpha(alpha); + } + } + } + + /** + * The component's ON-SCREEN box in the coordinates the graphics is + * currently painting in, or null when no ancestor transform has moved + * it and the clip already in force is the right one. + * + *

Without a transform, a graphics being painted through has + * accumulated exactly the ancestors' offsets, so its translation plus + * this component's parent-relative position is its absolute position. + * Whatever that identity is out by IS the transform's shift.

+ */ + static int[] onScreenClip(int translateX, int translateY, int x, int y, + int absoluteX, int absoluteY, int width, int height) { + int extraX = translateX + x - absoluteX; + int extraY = translateY + y - absoluteY; + if (extraX == 0 && extraY == 0) { + return null; + } + return new int[] {x - extraX, y - extraY, width, height}; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomScrollView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomScrollView.java new file mode 100644 index 00000000000..c1d5748b6b3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/CustomScrollView.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * A scroll view built from a list of slivers — Flutter's + * {@code CustomScrollView}. Modeled as a {@link ListView} whose children are + * the {@code slivers} (each sliver composes into a box widget); the fine-grained + * sliver scroll protocol is deferred. + */ +public class CustomScrollView extends StatelessWidget { + + private DartList slivers; + private boolean shrinkWrap; + + public void slivers(DartList v) { + this.slivers = v; + } + + public void controller(Object v) { + } + + public void scrollDirection(Object v) { + } + + public void reverse(boolean v) { + } + + public void shrinkWrap(boolean v) { + this.shrinkWrap = v; + } + + public void physics(Object v) { + } + + public void cacheExtent(double v) { + } + + public void primary(Object v) { + } + + public void clipBehavior(Clip v) { + } + + @Override + public Widget build(BuildContext context) { + ListView list = new ListView(); + list.children(slivers != null ? slivers : new DartList()); + if (shrinkWrap) { + list.shrinkWrap(true); + } + return list; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Debug.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Debug.java new file mode 100644 index 00000000000..1e1afdee06f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Debug.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; + +/** + * Debug assertion helpers from Flutter's widgets layer. In release-style + * transpiled output these are inert and always succeed. + */ +public abstract class Debug { + + private Debug() { + } + + public static boolean debugCheckHasDirectionality(BuildContext context) { + return true; + } + + public static boolean debugCheckHasMediaQuery(BuildContext context) { + return true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBox.java new file mode 100644 index 00000000000..78aab538d67 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBox.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Paints a {@link com.codename1.flutter.Decoration} (a + * {@link com.codename1.flutter.BoxDecoration} in practice) around its child — + * Flutter's {@code DecoratedBox}. Sizes to the child. + */ +public class DecoratedBox extends Widget { + + private Object decoration; + private Object position; + private Widget child; + + public void decoration(Object v) { + this.decoration = v; + } + + public void position(Object v) { + this.position = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getDecoration() { + return decoration; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new DecoratedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBoxRenderElement.java new file mode 100644 index 00000000000..c512c7a859a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DecoratedBoxRenderElement.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; + +/** + * Render element for {@link DecoratedBox}: a CN1 Container (UIID "FlutterBox") + * styled from the decoration, covering the element bounds behind the child. + * Sizes to the child, or fills the bounded incoming axes when childless. + */ +public class DecoratedBoxRenderElement extends SingleChildRenderElement { + + public DecoratedBoxRenderElement(DecoratedBox widget) { + super(widget); + } + + private DecoratedBox box() { + return (DecoratedBox) widget(); + } + + @Override + protected Widget childWidget() { + return box().getChild(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + return null; + } + com.codename1.ui.Container face = new com.codename1.ui.Container(); + face.setUIID("FlutterBox"); + face.getAllStyles().setPadding(0, 0, 0, 0); + face.getAllStyles().setMargin(0, 0, 0, 0); + FlutterBoxStyle.apply(face, null, box().getDecoration()); + return face; + } + + @Override + protected void updateComponent(Component c) { + FlutterBoxStyle.apply(c, null, box().getDecoration()); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DefaultTextStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DefaultTextStyle.java new file mode 100644 index 00000000000..14032639c82 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DefaultTextStyle.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; + +/** + * The default {@link TextStyle} for descendant {@code Text} widgets that do not + * supply their own — Flutter's {@code DefaultTextStyle}, an + * {@link InheritedWidget}. + * + *

This is the mechanism a container uses to style the text inside it without + * touching each {@code Text}: an app bar sets one default and its title picks up + * the colour and weight. While {@link #of(BuildContext)} returned an empty + * fallback, none of that reached the text, so a themed bar rendered its title in + * the default ink.

+ */ +public class DefaultTextStyle extends InheritedWidget { + + private TextStyle style; + private TextAlign textAlign; + private Boolean softWrap; + private Object overflow; + private Integer maxLines; + + public void style(TextStyle v) { + this.style = v; + } + + public void textAlign(TextAlign v) { + this.textAlign = v; + } + + public void softWrap(boolean v) { + this.softWrap = v; + } + + public void overflow(Object v) { + this.overflow = v; + } + + public void maxLines(int v) { + this.maxLines = v; + } + + public TextStyle getStyle() { + return style; + } + + public TextAlign getTextAlign() { + return textAlign; + } + + public Boolean getSoftWrap() { + return softWrap; + } + + public Integer getMaxLines() { + return maxLines; + } + + public Object getOverflow() { + return overflow; + } + + /** + * Nearest ancestor DefaultTextStyle — Flutter's {@code + * DefaultTextStyle.of(context)}, or an empty one when nothing above sets a + * default (its {@code getStyle()} is then null, meaning "inherit"). + */ + public static DefaultTextStyle of(BuildContext context) { + if (context != null) { + DefaultTextStyle d = + context.maybeDependOnInheritedWidgetOfExactType(DefaultTextStyle.class); + if (d != null) { + return d; + } + } + return new DefaultTextStyle(); + } + + /** Convenience for the runtime's own wrapping: a default style over a child. */ + public static DefaultTextStyle wrap(TextStyle style, Widget child) { + DefaultTextStyle d = new DefaultTextStyle(); + d.style(style); + d.child(child); + return d; + } + + @Override + public boolean updateShouldNotify(com.codename1.flutter.widgets.InheritedWidget oldWidget) { + if (!(oldWidget instanceof DefaultTextStyle)) { + return true; + } + return ((DefaultTextStyle) oldWidget).style != style; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Directionality.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Directionality.java new file mode 100644 index 00000000000..f700ae391c1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Directionality.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.TextDirection; +import com.codename1.flutter.Widget; + +/** + * Establishes the reading direction for its subtree, mirroring Flutter's + * {@code Directionality}. Layout-transparent in this runtime: it simply wraps + * its child; the recorded {@link TextDirection} is available for later + * bidi-aware rendering. + */ +public class Directionality extends Widget { + + private TextDirection textDirection; + private Widget child; + + public void textDirection(TextDirection v) { + this.textDirection = v; + } + + public void child(Widget v) { + this.child = v; + } + + public TextDirection getTextDirection() { + return textDirection; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new DirectionalityRenderElement(this); + } + + /** + * Dart's {@code Directionality.of(context)}: the ambient text direction. + * This runtime does not scope directionality through the element tree, so + * the default LTR reading direction is reported. + */ + public static TextDirection of(BuildContext context) { + return TextDirection.ltr; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DirectionalityRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DirectionalityRenderElement.java new file mode 100644 index 00000000000..c8ed3ba97a9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DirectionalityRenderElement.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Layout-transparent host for {@link Directionality}: passes the incoming + * constraints straight to the child and reports the child's size at the + * origin. Owns no CN1 component. + */ +public class DirectionalityRenderElement extends SingleChildRenderElement { + + public DirectionalityRenderElement(Directionality widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((Directionality) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size(0, 0)); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissDirection.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissDirection.java new file mode 100644 index 00000000000..ae00de7f51c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissDirection.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * The direction in which a {@link Dismissible} can be dismissed — Flutter's + * {@code DismissDirection}. + */ +public enum DismissDirection { + vertical, horizontal, endToStart, startToEnd, up, down, none +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Dismissible.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Dismissible.java new file mode 100644 index 00000000000..faaf0330d89 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Dismissible.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A widget that can be dismissed by dragging — Flutter's {@code Dismissible}. + * This milestone renders the {@code child}; the swipe-to-dismiss gesture, the + * reveal of {@code background}/{@code secondaryBackground} and the resize + * animation are deferred. {@code onDismissed} carries a {@link DismissDirection}. + */ +public class Dismissible extends Widget { + + private Widget child; + private Widget background; + private Widget secondaryBackground; + private Funcs.VoidFunc1 onDismissed; + private Funcs.Func1 confirmDismiss; + private Funcs.VoidFunc0 onResize; + private Object direction; + private Object dismissThresholds; + + public void child(Widget v) { + this.child = v; + } + + public void background(Widget v) { + this.background = v; + } + + public void secondaryBackground(Widget v) { + this.secondaryBackground = v; + } + + public void confirmDismiss(Funcs.Func1 v) { + this.confirmDismiss = v; + } + + public void onResize(Funcs.VoidFunc0 v) { + this.onResize = v; + } + + public void onUpdate(Object v) { + } + + public void onDismissed(Funcs.VoidFunc1 v) { + this.onDismissed = v; + } + + public void direction(Object v) { + this.direction = v; + } + + public void resizeDuration(Object v) { + } + + public void dismissThresholds(Object v) { + this.dismissThresholds = v; + } + + public void movementDuration(Object v) { + } + + public void crossAxisEndOffset(double v) { + } + + public void dragStartBehavior(Object v) { + } + + public void behavior(Object v) { + } + + public Widget getChild() { + return child; + } + + public Widget getBackground() { + return background; + } + + public Widget getSecondaryBackground() { + return secondaryBackground; + } + + public Funcs.VoidFunc1 getOnDismissed() { + return onDismissed; + } + + public Funcs.Func1 getConfirmDismiss() { + return confirmDismiss; + } + + public Object getDismissThresholds() { + return dismissThresholds; + } + + public Object getDirection() { + return direction; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new DismissibleRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissibleRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissibleRenderElement.java new file mode 100644 index 00000000000..da851e94c24 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/DismissibleRenderElement.java @@ -0,0 +1,615 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Form; +import com.codename1.ui.animations.Motion; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +/** + * Swipe to dismiss: the child follows the finger, a background shows behind it, and past + * a threshold it leaves and the callback fires. + * + *

The widget used to render its child and drop everything else, so a full swipe across + * a mail row in the Reply study changed 0.0% of the screen.

+ * + *

Why this listens on the FORM rather than overriding the component's pointer + * methods. The rows live in a list that scrolls vertically. Codename One decides who + * owns a gesture before a component sees it, and a horizontal drag inside a vertical + * scroller is claimed by the scroller -- so the row is never asked. A form pointer + * listener runs BEFORE that decision and can consume the event, which is the same hook + * {@code Tabs} uses to swipe between tabs inside a scrollable form. Consuming is what + * stops the list scrolling underneath the swipe.

+ * + *

The axis is decided once per gesture, on the first movement past the touch slop, and + * never revisited: a swipe that starts horizontal stays a swipe even if the finger + * wanders, and one that starts vertical is left to the list for the rest of the gesture. + * Deciding per packet makes a diagonal drag fight itself.

+ */ +public class DismissibleRenderElement extends RenderElement { + + /// Fraction of the width a drag must cross to dismiss, when the widget names none. + private static final double DEFAULT_THRESHOLD = 0.4; + + /// How far a finger may travel before the gesture commits to an axis. + private static final double SLOP_LP = 18; + + /// How long the child takes to leave, and to spring back. + private static final int DISMISS_MS = 200; + private static final int RESTORE_MS = 200; + + private com.codename1.flutter.Element backgroundEl; + private com.codename1.flutter.Element secondaryEl; + private com.codename1.flutter.Element childEl; + private RenderElement background; + private RenderElement secondary; + private RenderElement child; + + private double dragX; + /// The offset currently applied to the child's components. + private int appliedX; + private boolean pressedInside; + private boolean owning; + private boolean decided; + private int pressX; + private int pressY; + + private Motion slide; + private double slideFrom; + private double slideTo; + private boolean dismissing; + /// Set once the row has left: it must not be laid out again. + private boolean gone; + private DismissDirection dismissingTo; + private com.codename1.ui.animations.Animation clock; + + private ActionListener pressListener; + private ActionListener dragListener; + private ActionListener releaseListener; + private Form listeningOn; + + public DismissibleRenderElement(Widget widget) { + super(widget); + } + + private Dismissible dismissible() { + return (Dismissible) widget(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + childEl = updateChild(childEl, dismissible().getChild(), 0); + backgroundEl = updateChild(backgroundEl, dismissible().getBackground(), 1); + secondaryEl = updateChild(secondaryEl, dismissible().getSecondaryBackground(), 2); + if (gone) { + // UNMOUNT it, rather than merely skipping its layout. A dismissed row that is + // still mounted is still rebuilt, and it rebuilds with ancestors on their way + // out -- its provider lookup then answers null and the build that captured it + // fails later, inside a LayoutBuilder, far from here. + childEl = updateChild(childEl, null, 0); + backgroundEl = updateChild(backgroundEl, null, 1); + secondaryEl = updateChild(secondaryEl, null, 2); + child = null; + background = null; + secondary = null; + return constraints.smallest(); + } + child = RenderElement.findRenderElement(childEl); + background = RenderElement.findRenderElement(backgroundEl); + secondary = RenderElement.findRenderElement(secondaryEl); + if (child == null) { + return constraints.smallest(); + } + Size size = child.layout(constraints); + // The backgrounds fill exactly what the child occupies: they are what shows + // through as it moves off, so anything else leaves a gap at the edge. + BoxConstraints exact = BoxConstraints.tight(size.width(), size.height()); + if (background != null) { + background.layout(exact); + } + if (secondary != null) { + secondary.layout(exact); + } + return size; + } + + /** + * Visits the three children. + * + *

{@code Element.visitChildren} is EMPTY by default, so an element that owns + * children and does not override it owns them invisibly: the walk that unmounts a + * subtree never reaches them. A dismissed row was deactivated and its child, + * background and secondary background stayed mounted with their components still in + * the scene -- every surviving row then drew its new content over the old, an + * orphaned subtree rebuilt with its ancestors gone and its provider lookup answered + * null, and the inner lists inside those rows were never unmounted at all.

+ */ + @Override + public void visitChildren(dart.runtime.Funcs.VoidFunc1 + visitor) { + if (childEl != null) { + visitor.call(childEl); + } + if (backgroundEl != null) { + visitor.call(backgroundEl); + } + if (secondaryEl != null) { + visitor.call(secondaryEl); + } + } + + @Override + protected void positionChildren(int x, int y) { + if (background != null) { + background.position(x, y); + } + if (secondary != null) { + secondary.position(x, y); + } + if (child != null) { + child.position(x, y); + } + // Layout has just re-placed everything at rest, so whatever was applied is gone. + appliedX = 0; + applyDrag(); + attach(); + applyVisibility(); + } + + /// Only the background the swipe is heading towards is shown; both at rest would + /// paint one over the other and the wrong one would win. + private void applyVisibility() { + Component b = background == null ? null : background.component(); + Component s = secondary == null ? null : secondary.component(); + if (b != null) { + b.setVisible(dragX > 0); + } + if (s != null) { + s.setVisible(dragX < 0); + } + } + + // ------------------------------------------------------------------ + // Gesture + // ------------------------------------------------------------------ + + /// The first real component under a render element. + /// + /// A wrapper owns none of its own, so asking the child directly answers null and the + /// listeners were never attached -- the swipe did nothing because nothing was + /// listening for it. + private static Component firstComponentOf(RenderElement e) { + if (e == null) { + return null; + } + Component own = e.component(); + if (own != null) { + return own; + } + for (RenderElement c : e.renderChildren()) { + Component found = firstComponentOf(c); + if (found != null) { + return found; + } + } + return null; + } + + /// The outermost components of a subtree. + /// + /// Only these may be moved: a Codename One component's x is relative to its parent, + /// so shifting a nested one as well would move it twice. + private static void topComponents(RenderElement e, java.util.List out) { + if (e == null) { + return; + } + Component c = e.component(); + if (c != null) { + out.add(c); + return; + } + for (RenderElement k : e.renderChildren()) { + topComponents(k, out); + } + } + + /// Slides the child to the current drag offset, by the DIFFERENCE since last time. + /// Re-placing it from the element's own origin does not work: a render element inside + /// a scrolling list reports (0,0), so that put the row in the corner. + private void applyDrag() { + int want = (int) Math.round(dragX); + int delta = want - appliedX; + if (delta != 0) { + java.util.List tops = new java.util.ArrayList(); + topComponents(child, tops); + for (int i = 0; i < tops.size(); i++) { + Component c = tops.get(i); + c.setX(c.getX() + delta); + } + appliedX = want; + } + } + + private Component self() { + return firstComponentOf(child); + } + + /// Whether a press landed on this row, tested against the CHILD COMPONENT's absolute + /// bounds rather than the render element's. + /// + /// A render element inside a scrolling list reports (0,0) for its own position -- the + /// placement lives on the Codename One components -- so hit-testing the element put + /// every row's rectangle at the origin and the wrong rows answered. + /// The row's extent on screen: the union of every component under it. + /// + /// Neither end of the tree answers this on its own. The render element reports (0,0) + /// for its position inside a scrolling list -- the placement lives on the Codename + /// One components -- and the first component going down is a leaf, a label inside the + /// card rather than the card. Hit-testing either one picked the wrong rows. + private int[] rowBounds() { + int[] b = new int[] {Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MIN_VALUE, Integer.MIN_VALUE}; + union(child, b); + return b[0] == Integer.MAX_VALUE ? null : b; + } + + private static void union(RenderElement e, int[] b) { + if (e == null) { + return; + } + Component c = e.component(); + if (c != null && c.getWidth() > 0 && c.getHeight() > 0) { + int l = c.getAbsoluteX(); + int t = c.getAbsoluteY(); + b[0] = Math.min(b[0], l); + b[1] = Math.min(b[1], t); + b[2] = Math.max(b[2], l + c.getWidth()); + b[3] = Math.max(b[3], t + c.getHeight()); + } + for (RenderElement k : e.renderChildren()) { + union(k, b); + } + } + + private boolean withinRow(int x, int y) { + int[] b = rowBounds(); + return b != null && x >= b[0] && x < b[2] && y >= b[1] && y < b[3]; + } + + private void attach() { + Component anchor = self(); + Form f = anchor == null ? null : anchor.getComponentForm(); + if (f == null || f == listeningOn) { + return; + } + detach(); + listeningOn = f; + pressListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (dismissing) { + return; + } + pressedInside = withinRow(evt.getX(), evt.getY()); + owning = false; + decided = false; + pressX = evt.getX(); + pressY = evt.getY(); + } + }; + dragListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (!pressedInside || dismissing) { + return; + } + double dx = evt.getX() - pressX; + double dy = evt.getY() - pressY; + if (!decided) { + double slop = Dp.px(SLOP_LP); + if (Math.abs(dx) < slop && Math.abs(dy) < slop) { + return; + } + // Decided ONCE, and for the whole gesture. + decided = true; + owning = Math.abs(dx) > Math.abs(dy) && allows(dx); + } + if (!owning) { + return; + } + dragX = dx; + evt.consume(); + repaintRow(); + } + }; + releaseListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (!pressedInside) { + return; + } + pressedInside = false; + if (!owning || dismissing) { + return; + } + owning = false; + evt.consume(); + settle(); + } + }; + f.addPointerPressedListener(pressListener); + f.addPointerDraggedListener(dragListener); + f.addPointerReleasedListener(releaseListener); + } + + private void detach() { + if (listeningOn == null) { + return; + } + listeningOn.removePointerPressedListener(pressListener); + listeningOn.removePointerDraggedListener(dragListener); + listeningOn.removePointerReleasedListener(releaseListener); + listeningOn = null; + } + + /// Whether the widget accepts a swipe going this way at all. + private boolean allows(double dx) { + Object d = dismissible().getDirection(); + if (d == DismissDirection.none) { + return false; + } + if (d == DismissDirection.startToEnd) { + return dx > 0; + } + if (d == DismissDirection.endToStart) { + return dx < 0; + } + // horizontal, or unstated: Flutter's default is horizontal. + return true; + } + + private DismissDirection directionOf(double dx) { + return dx < 0 ? DismissDirection.endToStart : DismissDirection.startToEnd; + } + + /// The fraction of the width this direction must cross, from {@code dismissThresholds}. + private double thresholdFor(DismissDirection dir) { + Object t = dismissible().getDismissThresholds(); + if (t instanceof java.util.Map) { + Object v = ((java.util.Map) t).get(dir); + if (v instanceof Number) { + return ((Number) v).doubleValue(); + } + } + return DEFAULT_THRESHOLD; + } + + private void settle() { + int[] b = rowBounds(); + double width = b == null ? (size() == null ? 0 : size().width()) : (b[2] - b[0]); + if (width <= 0) { + animateTo(0, RESTORE_MS, false, null); + return; + } + final DismissDirection dir = directionOf(dragX); + boolean past = Math.abs(dragX) / width >= thresholdFor(dir); + if (!past) { + animateTo(0, RESTORE_MS, false, null); + return; + } + final double target = dragX < 0 ? -width : width; + confirm(dir, new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Boolean ok) { + if (ok.booleanValue()) { + animateTo(target, DISMISS_MS, true, dir); + } else { + animateTo(0, RESTORE_MS, false, null); + } + } + }); + } + + /** + * {@code confirmDismiss}, which may veto -- and usually answers with a Future, from + * an async callback or a dialog. That future used to count as consent, because only a + * Boolean was looked at: an asynchronous "no", or a failed confirmation, dismissed the + * row at once and ran onDismissed anyway. A Future now decides when it completes (an + * error vetoes), and the row holds where it was dragged until then, as in Flutter. No + * callback is consent; otherwise only {@code true} is. The answer is a + * {@code Future}, and Flutter reads it as {@code result ?? false}: a null -- + * a dialog closed without choosing -- used to count as consent and dismissed the row. + * Package visible for DismissibleConfirmTest. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + void confirm(DismissDirection dir, final dart.runtime.Funcs.VoidFunc1 decide) { + dart.runtime.Funcs.Func1 c = dismissible().getConfirmDismiss(); + if (c == null) { + decide.call(Boolean.TRUE); + return; + } + Object answer; + try { + answer = c.call(dir); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + decide.call(Boolean.FALSE); + return; + } + if (answer instanceof dart.async.Future) { + ((dart.async.Future) answer).then(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Object v) { + onEdt(decide, Boolean.valueOf(Boolean.TRUE.equals(v))); + } + }).catchError(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Object error) { + com.codename1.flutter.FlutterErrorReport.record(error); + onEdt(decide, Boolean.FALSE); + } + }); + return; + } + decide.call(Boolean.valueOf(Boolean.TRUE.equals(answer))); + } + + private static void onEdt(final dart.runtime.Funcs.VoidFunc1 decide, final Boolean ok) { + if (com.codename1.ui.Display.isInitialized() && !com.codename1.ui.CN.isEdt()) { + com.codename1.ui.CN.callSerially(new Runnable() { + @Override + public void run() { + decide.call(ok); + } + }); + } else { + decide.call(ok); + } + } + + private void animateTo(double target, int duration, boolean dismiss, DismissDirection dir) { + slideFrom = dragX; + slideTo = target; + dismissing = dismiss; + dismissingTo = dir; + slide = Motion.createEaseInOutMotion(0, 1000, duration); + slide.start(); + startClock(); + } + + private void startClock() { + Component anchor = self(); + final Form f = anchor == null ? null : anchor.getComponentForm(); + if (f == null || clock != null) { + return; + } + clock = new com.codename1.ui.animations.Animation() { + @Override + public boolean animate() { + if (slide == null) { + stopClock(f); + return false; + } + double t = slide.getValue() / 1000.0; + dragX = slideFrom + (slideTo - slideFrom) * t; + repaintRow(); + if (slide.isFinished()) { + slide = null; + stopClock(f); + if (dismissing) { + dismissing = false; + fireDismissed(dismissingTo); + } + // The repaint belongs AFTER the application has removed the item, not + // here: the callback is deferred to the next frame, so repainting now + // paints the list as it still is, and the state it ends up in is never + // painted at all. That left the rows that moved up showing their old + // text under their new text. + } + // Repaint the ROW, never the screen: returning true here makes the whole + // form flush every frame, which shows as a flicker behind the swipe. + return false; + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + } + }; + f.registerAnimated(clock); + } + + private void stopClock(Form f) { + if (clock != null && f != null) { + f.deregisterAnimated(clock); + } + clock = null; + } + + /// Tells the application the row is gone, on the NEXT frame rather than this one. + /// + /// The callback removes the item, which rebuilds the list -- and this runs from an + /// animation tick, in the middle of the frame that is still laying this very row out. + /// Removing it there left the dying subtree being laid out with its ancestors already + /// gone, and the provider lookup inside it returned null: "Cannot invoke + /// EmailStore.isEmailStarred because emailStore is null". Handing it to the next + /// frame lets this one finish first. + private void fireDismissed(final DismissDirection dir) { + final dart.runtime.Funcs.VoidFunc1 cb = + dismissible().getOnDismissed(); + if (cb == null) { + return; + } + gone = true; + detach(); + com.codename1.ui.Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + try { + cb.call(dir); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + // A frame later again, and a REVALIDATE rather than a repaint. Removing + // the item schedules its own rebuild, so repainting here paints the list + // as it still is; and the rows that move up leave their old pixels behind, + // which only a full re-layout and repaint clears. + com.codename1.ui.Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + com.codename1.ui.Form now = com.codename1.ui.Display.getInstance() + .getCurrent(); + if (now != null) { + now.revalidate(); + now.repaint(); + } + } + }); + } + }); + } + + private void repaintRow() { + applyDrag(); + applyVisibility(); + Component c = self(); + if (c != null) { + c.repaint(); + } + Component b = firstComponentOf(background); + if (b != null) { + b.repaint(); + } + Component s = firstComponentOf(secondary); + if (s != null) { + s.repaint(); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/EffectRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/EffectRenderElement.java new file mode 100644 index 00000000000..fe8835a466c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/EffectRenderElement.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.FlutterRootLayout; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; + +import dart.runtime.Funcs; + +/** + * Base for widgets that change how their subtree PAINTS without changing its layout — + * Opacity, Transform and their relatives. + * + *

The rest of the render tree is flat: every element's component is a sibling in one + * host container, positioned absolutely. That is fast, but it means an ancestor cannot + * wrap its descendants in a paint effect, because they are not its children. So an + * effect element owns a nested container with its own {@link RenderHost} — the same + * device the scrollables use — which makes its subtree genuinely nested and therefore + * something it can paint through.

+ * + *

Layout is untouched: the child is measured against the incoming constraints and the + * effect element takes exactly the child's size. Flutter's Opacity and Transform do not + * affect layout either.

+ */ +public abstract class EffectRenderElement extends RenderElement { + + private Element content; + private RenderHost innerHost; + + protected EffectRenderElement(Widget widget) { + super(widget); + } + + /** The widget this effect applies to. */ + protected abstract Widget effectChild(); + + /** + * Paints the effect's subtree into a Graphics of the caller's choosing. + * + *

The target is a parameter rather than the {@code Graphics} the effect was + * handed, because an effect that needs a matrix has to paint the subtree into an + * offscreen layer first - see {@link #layer}.

+ */ + protected interface Subtree { + void paint(Graphics target); + } + + /** + * Applies the effect and paints the subtree. Implementations must leave the + * Graphics as they found it — a frame paints many components through the same one. + */ + protected abstract void paintWithEffect(Graphics g, Container pane, Subtree paintChildren); + + private RenderHost innerHost() { + if (innerHost == null) { + innerHost = new RenderHost(); + innerHost.rootSupplier(new Funcs.Func0() { + @Override + public Element call() { + return content; + } + }); + } + return innerHost; + } + + @Override + protected RenderHost hostForChild(int slot) { + return innerHost(); + } + + @Override + public void mount(Element parent, int slot) { + // The pane is a container INSIDE the same Form, so its host has to know + // about that Form: a subtree that asks its host for the Form -- a + // Scaffold deciding whether it owns the toolbar, a dialog looking for + // somewhere to open -- would otherwise be told there is none simply + // because it happens to sit under a paint effect. + // Walk up for it. A nested host does not necessarily carry the Form -- an effect + // inside another effect takes its parent's inner host, whose form may itself be + // unset -- and once one link in that chain is null every host below it is too. + // Anything that then asks its host which Form it is in gets no answer: the safe + // area comes back as zero and the page lays out under the status bar. + com.codename1.ui.Form form = null; + for (Element e = parent; e != null && form == null; e = e.parent()) { + RenderHost h = e.host(); + if (h != null) { + form = h.form(); + } + } + if (form == null && host() != null) { + form = host().form(); + } + if (form != null) { + innerHost().form(form); + } + super.mount(parent, slot); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + EffectPane pane = new EffectPane(innerHost()); + innerHost().container(pane); + return pane; + } + + @Override + protected void syncChildren() { + content = updateChild(content, effectChild(), 0); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (content != null) { + visitor.call(content); + } + } + + /// The constraints the Flutter pass last gave this effect. + /// + /// The nested pane carries its own {@link FlutterRootLayout}, and Codename One runs + /// that layout independently, deriving constraints from the pane's CURRENT component + /// size. That size is only the Flutter-assigned one after {@code position} has written + /// it; before then it is whatever CN1 last put there — for a fresh subtree, its + /// preferred size. So the subtree could be laid out against a height it was never + /// given: the gallery's study card was measured at its unconstrained 272dp instead of + /// the carousel's 240dp viewport, which is why its caption was clipped and its bottom + /// corners came out square. + /// + /// Remembering the real constraints and handing them to the nested pass removes the + /// disagreement: the Flutter pass owns this subtree's geometry, and CN1's pass must + /// reproduce it rather than re-derive it. + private BoxConstraints lastConstraints; + + BoxConstraints effectConstraints() { + return lastConstraints; + } + + /** + * The render element of the single child, or null before it is mounted. + * + *

Exposed so a subclass can lay the child out on its own terms -- a + * {@link FittedBoxRenderElement} has to measure it UNBOUNDED, which is the + * whole difference between text that shrinks to fit and text that wraps.

+ */ + protected final RenderElement effectRenderChild() { + return findRenderElement(content); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + lastConstraints = constraints; + RenderElement c = findRenderElement(content); + if (c == null) { + return constraints.smallest(); + } + Size cs = c.layout(constraints); + c.position(0, 0); // inside our pane, the child sits at the origin + return constraints.constrain(cs); + } + + /** + * An effect is a PAINT wrapper: {@link #performLayout} takes its size straight + * from the child, so no configuration of the effect itself - a Transform's + * scale, a Material's colour or elevation - can move anything. + * + *

This matters most where it is animated. The gallery's carousel rebuilds a + * Transform per card per scroll frame; treating that as a layout change marked + * every ancestor up to the Scaffold dirty and relayed out the whole page on + * each frame, which is what made dragging the carousel stutter.

+ */ + @Override + protected boolean updateAffectsLayout() { + return false; + } + + @Override + protected void positionChildren(int x, int y) { + // The pane's own layout places the subtree in pane coordinates; nothing to do + // here, and positioning the child again in host coordinates would double-offset it. + } + + /// The offscreen this effect's subtree is rendered into, reused across frames. + private com.codename1.ui.Image layerImage; + + /// Renders the subtree into an offscreen image the size of {@code pane}, and hands + /// it back for the caller to draw wherever the effect wants it. + /// + /// An effect that needs a MATRIX -- a scale or a rotation -- cannot simply set one + /// on the Graphics and let Codename One walk the subtree, because the two disagree + /// about units once a matrix is in play. Component bounds are device pixels, but + /// `Graphics#getClipX` reports the clip in the matrix's own coordinates, and + /// `Container#paint`'s `g.translate(getX(), getY())` moves the origin by + /// `getX() / scale` user units rather than by `getX()`. `Component`'s + /// paint-time cull compares those two directly, so a child can be dropped for + /// being outside a clip it is in fact inside: the gallery's carousel scales the + /// card either side of the current page, and the 1019px translate to reach the + /// next card became 1171 user units, moving the clip clear of the card's bounds. + /// The card was not clipped or misplaced -- it was never painted at all, which is + /// why the next card never peeked in the way it does in the reference. + /// + /// Rendering to a layer and transforming the RESULT sidesteps that entirely: the + /// subtree paints through an untransformed Graphics, so every unit downstream is + /// the device pixel Codename One expects, and a pure scale then needs no matrix + /// support at all -- it is one `drawImage` into a destination rectangle. It is + /// also what Flutter does, where Transform is a layer rather than a paint mode. + /// + /// The buffer is kept and cleared rather than reallocated: a carousel drag scales + /// a card on every frame, and a fresh full-size ARGB image per frame is exactly + /// the allocation rate that drives a collection mid-drag. + /// + /// @return the layer, or null when the pane has no area to render into + protected final com.codename1.ui.Image layer(Container pane, Subtree subtree) { + return layer(pane, subtree, pane.getWidth(), pane.getHeight()); + } + + /// As {@link #layer(Container, Subtree)}, for an effect whose subtree does not have + /// the pane's own shape. A quarter-turned box is the case that needs it: its pane + /// reports the child's footprint with the axes swapped, so a layer the size of the + /// pane would cut the child in half before it was ever turned. + protected final com.codename1.ui.Image layer(Container pane, Subtree subtree, int w, int h) { + if (w <= 0 || h <= 0 || !(pane instanceof EffectPane)) { + return null; + } + if (layerImage == null || layerImage.getWidth() != w || layerImage.getHeight() != h) { + layerImage = com.codename1.ui.Image.createImage(w, h, 0); + } else { + layerImage.getGraphics().clearRect(0, 0, w, h); + } + // The subtree paints with the pane parked at the origin, so what lands in the + // image is exactly the pane's own box -- no translate to unwind afterwards. + ((EffectPane) pane).paintAtOrigin(layerImage.getGraphics()); + return layerImage; + } + + /// Paints the subtree clipped to {@code shape}. + /// + /// Uses `pushClip`/`popClip`, which is the idiom the ports support and the one the + /// shaped-clipping tests in `scripts/hellocodenameone` use. Saving `getClipX/Y/W/H` + /// and restoring with the four-int `setClip` -- which is what this used to do -- + /// cannot express a shape: it degrades whatever the ancestors had established to its + /// BOUNDING BOX. Nest two shaped clips and the outer one stops holding, which is + /// what made a shaped subtree look like it was layered wrongly rather than simply + /// unclipped. + /// + /// @return false when the port cannot clip to a shape at all, so the caller can + /// report it rather than drawing square in silence + protected final boolean paintShapeClipped(Graphics g, Container pane, Subtree subtree, + com.codename1.ui.geom.GeneralPath shape) { + if (shape == null) { + return false; + } + // NOTE: no isShapeClipSupported gate. The subtree is rendered to a + // buffer below and the rounding is applied to that buffer, either by + // clipping the target graphics where the port supports it or by MASKING + // the buffer where it does not. Gating on shape-clip support meant the + // iOS pipeline -- which reports no support for a live paint but does + // support it while a transition paints into an image -- drew the demo + // cards with rounded corners during the transition and square ones the + // moment it settled. + // INTERSECT, do not replace. setClip(Shape) installs the shape outright, so + // whatever the ancestors had established was simply discarded -- and the nearest + // ancestor that matters is usually a scroller. A clipped subtree inside a list + // therefore kept painting after it had scrolled out of the viewport: the mail + // avatars, which are the only clipped thing in a mail row, drew over the status + // bar as the list moved under it, and that band is not repainted by the scroll, + // so they smeared. + // + // Re-intersecting with the incoming rectangle after installing the shape is what + // clipRect is for. Note the incoming clip is read as a RECTANGLE, so a shaped + // clip an ancestor established is honoured only to its bounding box -- weaker + // than exact, and far better than dropping it. + int cx = g.getClipX(); + int cy = g.getClipY(); + int cw = g.getClipWidth(); + int ch = g.getClipHeight(); + // One application, around the whole subtree. + // + // NOTE: a shaped clip does NOT survive a subtree here, and that is by + // design in Codename One -- Component.paintComponent and + // paintInternalImpl save the clip as four ints and restore it with + // setClip(x,y,w,h), which degrades a shape to its bounding box for + // every sibling after the first. Making those pairs shape-aware is the + // change that is deliberately commented out in Component, because it + // alters the paint contract for every existing application. + // + // Two things were tried here and are recorded so they are not tried + // again. Rendering the subtree to a buffer and masking it rounds + // correctly on the desktop but fails on iOS -- a mutable-image detour + // mid-paint leaves a stale scissor (issue #5171, + // MutableImageClipReadbackTest) and createMask can fault for + // image-scale buffers (SimdLargeAllocaTest). Painting each child + // separately with the clip re-installed per child loses what + // Container.paint does around its child loop, and measured 13.97% wrong + // across the sweep against 2.54%. + g.pushClip(); + try { + g.setClip(shape); + g.clipRect(cx, cy, cw, ch); + subtree.paint(g); + } finally { + g.popClip(); + } + return true; + } + + /** The nested container: lays the subtree out at its own bounds and paints it through the effect. */ + private final class EffectPane extends Container { + + EffectPane(RenderHost host) { + super(new FlutterRootLayout(host) { + @Override + protected BoxConstraints constraintsFor(com.codename1.ui.Container parent) { + BoxConstraints c = effectConstraints(); + return c != null ? c : super.constraintsFor(parent); + } + }); + setUIID("FlutterEffect"); + stylesToSet(this).setPadding(0, 0, 0, 0); + stylesToSet(this).setMargin(0, 0, 0, 0); + stylesToSet(this).setBgTransparency(0); + } + + @Override + public void paint(final Graphics g) { + final Container self = this; + paintWithEffect(g, self, new Subtree() { + @Override + public void paint(Graphics target) { + EffectPane.super.paint(target); + } + }); + } + + /** Paints the subtree with this pane parked at the origin, for {@link #layer}. */ + void paintAtOrigin(Graphics target) { + int x = getX(); + int y = getY(); + setX(0); + setY(0); + try { + EffectPane.super.paint(target); + } finally { + setX(x); + setY(y); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeFocus.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeFocus.java new file mode 100644 index 00000000000..652d7b82909 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeFocus.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Excludes its subtree from focus traversal — Flutter's {@code ExcludeFocus}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class ExcludeFocus extends Widget implements HasChild { + + private boolean excluding = true; + private Widget child; + + public void excluding(boolean v) { this.excluding = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeSemantics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeSemantics.java new file mode 100644 index 00000000000..52003fb1030 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExcludeSemantics.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Drops the semantics of its child subtree. Renders the child unchanged for + * this milestone. See {@link PassThroughRenderElement}. + */ +public class ExcludeSemantics extends Widget implements HasChild { + + private boolean excluding = true; + private Widget child; + + public void excluding(boolean v) { + this.excluding = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Expanded.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Expanded.java new file mode 100644 index 00000000000..a2581481049 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Expanded.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Marks a child of Row/Column as flexible: it receives a share of the free + * main-axis space proportional to its flex factor (default 1). Only has an + * effect when its render element sits directly below a Flex. + */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class Expanded extends Widget implements HasChild { + + private Widget child; + private long flex = 1; + + public void child(Widget v) { + this.child = v; + } + + public void flex(long v) { + this.flex = v; + } + + public Widget getChild() { + return child; + } + + public long getFlex() { + return flex; + } + + @Override + public Element createElement() { + return new ExpandedRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExpandedRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExpandedRenderElement.java new file mode 100644 index 00000000000..835407f86f4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ExpandedRenderElement.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Pass-through box carrying the flex factor read by {@link FlexRenderElement}. + * The Flex parent hands it tight main-axis constraints; it forwards them to + * its child unchanged. + */ +public class ExpandedRenderElement extends SingleChildRenderElement { + + public ExpandedRenderElement(Expanded widget) { + super(widget); + } + + public long flex() { + return ((Expanded) widget()).getFlex(); + } + + @Override + protected Widget childWidget() { + return ((Expanded) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.smallest(); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FadeInImage.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FadeInImage.java new file mode 100644 index 00000000000..9d11ddb7d97 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FadeInImage.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.ImageProvider; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.Duration; + +/** + * Shows a {@code placeholder} image while the target {@code image} loads, then + * cross-fades to it — Flutter's {@code FadeInImage}. This pass reserves the + * box (when width/height are given) and holds both providers; the decode and + * fade animation are deferred to the image layer. + */ +public class FadeInImage extends StatelessWidget { + + private ImageProvider placeholder; + private ImageProvider image; + private Duration fadeInDuration; + private Duration fadeOutDuration; + private Double width; + private Double height; + private Object fit; + + public void placeholder(ImageProvider v) { + this.placeholder = v; + } + + public void image(ImageProvider v) { + this.image = v; + } + + public void fadeInDuration(Duration v) { + this.fadeInDuration = v; + } + + public void fadeOutDuration(Duration v) { + this.fadeOutDuration = v; + } + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void fit(Object v) { + this.fit = v; + } + + public void alignment(Object v) { + } + + public void repeat(Object v) { + } + + public void placeholderFit(Object v) { + } + + /** Whether the image is hidden from semantics — Flutter's {@code excludeFromSemantics}. */ + public void excludeFromSemantics(boolean v) { + } + + public ImageProvider getImage() { + return image; + } + + @Override + public Widget build(BuildContext context) { + // Render the target image directly (the cross-fade from the placeholder is + // deferred). Falls back to the placeholder, then an empty box, when absent. + ImageProvider shown = image != null ? image : placeholder; + if (shown != null) { + Image img = new Image(); + img.image(shown); + if (width != null) { + img.width(width); + } + if (height != null) { + img.height(height); + } + if (fit != null) { + img.fit(fit); + } + return img; + } + SizedBox box = new SizedBox(); + if (width != null) { + box.width(width); + } + if (height != null) { + box.height(height); + } + return box; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBox.java new file mode 100644 index 00000000000..57be53aade5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBox.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Scales and positions its {@code child} within itself — Flutter's {@code FittedBox}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class FittedBox extends Widget implements HasChild { + + private Object fit; + private Object alignment; + private Object clipBehavior; + private Widget child; + + public void fit(Object v) { this.fit = v; } + public void alignment(Object v) { this.alignment = v; } + public void clipBehavior(Object v) { this.clipBehavior = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** How the child is scaled into the box; Flutter defaults to {@code contain}. */ + public Object getFit() { + return fit; + } + + /** Where the scaled child sits in the box; Flutter defaults to the centre. */ + public Object getAlignment() { + return alignment; + } + + @Override + public Element createElement() { + return new FittedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBoxRenderElement.java new file mode 100644 index 00000000000..aa31d681136 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FittedBoxRenderElement.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.AlignmentDirectional; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Scales a subtree to fit the box it is given - Flutter's {@code FittedBox}. + * + *

The part that matters is the LAYOUT, not the scaling: Flutter's + * {@code RenderFittedBox} lays its child out against fully unbounded + * constraints, so the child takes the size it would like to be, and only then + * is that size scaled into the available box. A {@code Text} inside one + * therefore measures on a single line and is shrunk, and never wraps.

+ * + *

This used to be a pass-through, which handed the child the box's own + * constraints. The gallery's card titles are + * {@code FittedBox(fit: BoxFit.scaleDown)} around a 24sp headline, so + * "Top 10 Cities to Visit in Tamil Nadu" wrapped onto a second line at full + * size instead of staying on one line at 80% of it -- a card that was visibly + * the wrong shape, and the route's worst difference against the reference.

+ * + *

The scaling itself reuses the layer mechanism {@code Transform} already + * uses: the subtree renders once into an offscreen image and that image is + * drawn scaled. A raster scale is slightly softer than scaling glyph outlines, + * which is what Flutter does, but it is the mechanism this runtime has on every + * port, and being softer by a fraction of a pixel is not in the same class of + * error as laying the text out at the wrong size.

+ */ +public class FittedBoxRenderElement extends EffectRenderElement { + + private Size natural = Size.ZERO; + private double scaleX = 1; + private double scaleY = 1; + private double offsetX; + private double offsetY; + + public FittedBoxRenderElement(Widget widget) { + super(widget); + } + + private FittedBox box() { + Widget w = widget(); + return w instanceof FittedBox ? (FittedBox) w : null; + } + + @Override + protected Widget effectChild() { + FittedBox b = box(); + return b == null ? null : b.getChild(); + } + + /** + * A FittedBox's own configuration decides its size, unlike the paint-only + * effects this shares a base class with, so a rebuild has to re-run layout. + */ + @Override + protected boolean updateAffectsLayout() { + return true; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement c = effectRenderChild(); + scaleX = 1; + scaleY = 1; + offsetX = 0; + offsetY = 0; + if (c == null) { + natural = constraints.smallest(); + return natural; + } + natural = c.layout(new BoxConstraints()); + c.position(0, 0); + if (natural.width() <= 0 || natural.height() <= 0) { + return constraints.constrain(natural); + } + Size self = constrainPreservingAspect(constraints, natural); + double[] dest = ImageRenderElement.fittedSize( + fit(), self.width(), self.height(), natural.width(), natural.height()); + scaleX = dest[0] / natural.width(); + scaleY = dest[1] / natural.height(); + Alignment a = alignment(); + offsetX = Alignment.along(a.x(), self.width(), dest[0]); + offsetY = Alignment.along(a.y(), self.height(), dest[1]); + return self; + } + + /** + * Gives the pane the child's NATURAL box rather than the scaled one. + * + *

Codename One clips every component to its own rectangle, and the + * subtree below this one was laid out at its natural size, so a pane sized + * to the scaled result would crop the child before it could be scaled -- + * the layer would capture a fragment. The parent still lays out against the + * scaled size, which is what {@link #performLayout} returned.

+ */ + @Override + public void position(int x, int y) { + super.position(x, y); + Component pane = component(); + if (pane != null) { + pane.setWidth(Math.max(0, (int) Math.round(natural.width()))); + pane.setHeight(Math.max(0, (int) Math.round(natural.height()))); + } + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + if (scaleX == 1 && scaleY == 1 && offsetX == 0 && offsetY == 0) { + paintChildren.paint(g); + return; + } + int w = pane.getWidth(); + int h = pane.getHeight(); + int dw = (int) Math.round(w * scaleX); + int dh = (int) Math.round(h * scaleY); + if (dw <= 0 || dh <= 0) { + return; + } + com.codename1.ui.Image rendered = layer(pane, paintChildren); + if (rendered == null) { + // No layer available (a headless pane): drawing the subtree at its + // natural size overflows, but showing it is closer than showing + // nothing, and it is what the pass-through did for years. + paintChildren.paint(g); + return; + } + g.drawImage(rendered, + pane.getX() + (int) Math.round(offsetX), + pane.getY() + (int) Math.round(offsetY), + dw, dh); + } + + // ------------------------------------------------------------------ + + private com.codename1.flutter.BoxFit fit() { + FittedBox b = box(); + Object f = b == null ? null : b.getFit(); + return f instanceof com.codename1.flutter.BoxFit + ? (com.codename1.flutter.BoxFit) f + : com.codename1.flutter.BoxFit.contain; + } + + private Alignment alignment() { + FittedBox b = box(); + Object a = b == null ? null : b.getAlignment(); + if (a instanceof Alignment) { + return (Alignment) a; + } + if (a instanceof AlignmentDirectional) { + return ((AlignmentDirectional) a).resolve(); + } + return Alignment.center; + } + + /** + * Flutter's {@code BoxConstraints.constrainSizeAndAttemptToPreserveAspectRatio}: + * the size unchanged when it already fits, otherwise the largest one with the + * same aspect ratio that does. + */ + static Size constrainPreservingAspect(BoxConstraints c, Size size) { + if (c.isTight()) { + return new Size(c.minWidth(), c.minHeight()); + } + double width = size.width(); + double height = size.height(); + if (width <= 0 || height <= 0) { + return c.constrain(size); + } + double aspect = width / height; + if (width > c.maxWidth()) { + width = c.maxWidth(); + height = width / aspect; + } + if (height > c.maxHeight()) { + height = c.maxHeight(); + width = height * aspect; + } + if (width < c.minWidth()) { + width = c.minWidth(); + height = width / aspect; + } + if (height < c.minHeight()) { + height = c.minHeight(); + width = height * aspect; + } + return new Size(c.constrainWidth(width), c.constrainHeight(height)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FixedScrollMetrics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FixedScrollMetrics.java new file mode 100644 index 00000000000..a1923bd3b87 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FixedScrollMetrics.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * An immutable snapshot of a scrollable's extents — Flutter's {@code FixedScrollMetrics}. + * + *

Unlike a live {@link ScrollPosition} this describes a moment rather than tracking + * one, which is what makes the physics testable: {@code applyPhysicsToUserOffset} is a + * pure function of the metrics you hand it, so it can be sampled exactly without a + * scrollable, a viewport or a frame.

+ */ +public class FixedScrollMetrics extends ScrollMetrics { + + private com.codename1.flutter.AxisDirection axisDirection = + com.codename1.flutter.AxisDirection.down; + private double devicePixelRatio; + + public FixedScrollMetrics() { + // The named setters below stand in for Dart's named parameters; until one is + // called the extents are simply absent, as they are on a scrollable that has not + // laid out yet. + } + + public void minScrollExtent(double v) { + this.minScrollExtent = v; + this.hasContentDimensions = true; + } + + public void maxScrollExtent(double v) { + this.maxScrollExtent = v; + this.hasContentDimensions = true; + } + + public void pixels(double v) { + this.pixels = v; + this.hasPixels = true; + } + + public void viewportDimension(double v) { + this.viewportDimension = v; + this.hasViewportDimension = true; + } + + public void axisDirection(com.codename1.flutter.AxisDirection v) { + this.axisDirection = v; + } + + public void devicePixelRatio(double v) { + this.devicePixelRatio = v; + } + + public com.codename1.flutter.AxisDirection getAxisDirection() { + return axisDirection; + } + + public double getDevicePixelRatio() { + return devicePixelRatio; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flex.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flex.java new file mode 100644 index 00000000000..b51a7de7e29 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flex.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.MainAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Shared configuration of {@link Column} and {@link Row}. + */ +public abstract class Flex extends Widget { + + private DartList children; + private MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start; + private CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center; + private MainAxisSize mainAxisSize = MainAxisSize.max; + + public void children(DartList v) { + this.children = v; + } + + public void mainAxisAlignment(MainAxisAlignment v) { + this.mainAxisAlignment = v == null ? MainAxisAlignment.start : v; + } + + public void crossAxisAlignment(CrossAxisAlignment v) { + this.crossAxisAlignment = v == null ? CrossAxisAlignment.center : v; + } + + public void mainAxisSize(MainAxisSize v) { + this.mainAxisSize = v == null ? MainAxisSize.max : v; + } + + public DartList getChildren() { + return children; + } + + public MainAxisAlignment getMainAxisAlignment() { + return mainAxisAlignment; + } + + public CrossAxisAlignment getCrossAxisAlignment() { + return crossAxisAlignment; + } + + public MainAxisSize getMainAxisSize() { + return mainAxisSize; + } + + /** + * True for Column (vertical main axis), false for Row. + */ + public abstract boolean isVertical(); + + @Override + public Element createElement() { + return new FlexRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java new file mode 100644 index 00000000000..c15b01a948d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlexRenderElement.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.MainAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Flutter's Flex layout algorithm (RenderFlex, M1 subset): + *
    + *
  1. Non-flex children get an unbounded main axis and the incoming cross + * axis (tight when stretch, loose otherwise).
  2. + *
  3. Remaining main-axis space is distributed to flex children + * (Expanded), each laid out with a tight main extent of + * {@code freeSpace * flex / totalFlex}.
  4. + *
  5. The main size is the max constraint when {@code MainAxisSize.max} + * and bounded, otherwise the sum of the children.
  6. + *
  7. Leading/between spacing per {@link MainAxisAlignment}, cross-axis + * placement per {@link CrossAxisAlignment}.
  8. + *
+ * Owns no CN1 component — pure positioning math over the flattened leaves. + */ +public class FlexRenderElement extends RenderElement { + + private List children = new ArrayList(); + + public FlexRenderElement(Flex widget) { + super(widget); + } + + private Flex flex() { + return (Flex) widget(); + } + + @Override + protected void syncChildren() { + List newWidgets = new ArrayList(); + if (flex().getChildren() != null) { + for (Widget w : flex().getChildren()) { + if (w != null) { + newWidgets.add(w); + } + } + } + children = updateChildren(children, newWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : children) { + if (c != null) { + visitor.call(c); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + boolean vertical = flex().isVertical(); + MainAxisAlignment mainAlign = flex().getMainAxisAlignment(); + CrossAxisAlignment crossAlign = flex().getCrossAxisAlignment(); + MainAxisSize mainSizeMode = flex().getMainAxisSize(); + + double maxMain = vertical ? constraints.maxHeight() : constraints.maxWidth(); + double maxCross = vertical ? constraints.maxWidth() : constraints.maxHeight(); + boolean boundedMain = maxMain != Double.POSITIVE_INFINITY; + boolean boundedCross = maxCross != Double.POSITIVE_INFINITY; + + List renderChildren = renderChildren(); + int n = renderChildren.size(); + + // Cross-axis constraints shared by all children. + double minCrossChild = (crossAlign == CrossAxisAlignment.stretch && boundedCross) ? maxCross : 0; + double maxCrossChild = maxCross; + + // Pass 1: layout inflexible children, tally flex factors. + long totalFlex = 0; + double allocatedMain = 0; + double maxChildCross = 0; + for (RenderElement child : renderChildren) { + long f = flexOf(child); + if (f > 0) { + totalFlex += f; + } else { + BoxConstraints childConstraints = vertical + ? new BoxConstraints(minCrossChild, maxCrossChild, 0, Double.POSITIVE_INFINITY) + : new BoxConstraints(0, Double.POSITIVE_INFINITY, minCrossChild, maxCrossChild); + Size cs = child.layout(childConstraints); + allocatedMain += mainOf(cs, vertical); + maxChildCross = Math.max(maxChildCross, crossOf(cs, vertical)); + } + } + + // Pass 2: layout flexible children in the remaining space. + double freeSpace = boundedMain ? Math.max(0, maxMain - allocatedMain) : 0; + if (totalFlex > 0) { + double allocatedFlex = 0; + int flexSeen = 0; + int flexCount = 0; + for (RenderElement child : renderChildren) { + if (flexOf(child) > 0) { + flexCount++; + } + } + for (RenderElement child : renderChildren) { + long f = flexOf(child); + if (f <= 0) { + continue; + } + flexSeen++; + double extent; + if (boundedMain) { + // last flex child absorbs rounding remainder + extent = (flexSeen == flexCount) + ? freeSpace - allocatedFlex + : freeSpace * f / totalFlex; + allocatedFlex += extent; + } else { + extent = 0; + } + BoxConstraints childConstraints; + if (boundedMain) { + // Expanded is a TIGHT fit — it must fill its share. Flexible + // defaults to LOOSE: it may take less, and whatever it leaves + // is free space for mainAxisAlignment to distribute. Treating + // both as tight is why a `Row(spaceBetween, [Text, + // Flexible(Text)])` put its second child straight after the + // first instead of at the far end — the colors demo's shade + // name and hex value ran together as "50#FFFFEBEE". + double minExtent = looseFit(child) ? 0 : extent; + childConstraints = vertical + ? new BoxConstraints(minCrossChild, maxCrossChild, minExtent, extent) + : new BoxConstraints(minExtent, extent, minCrossChild, maxCrossChild); + } else { + // Degenerate case (flex inside unbounded main axis is an + // error in Flutter); fall back to intrinsic sizing. + childConstraints = vertical + ? new BoxConstraints(minCrossChild, maxCrossChild, 0, Double.POSITIVE_INFINITY) + : new BoxConstraints(0, Double.POSITIVE_INFINITY, minCrossChild, maxCrossChild); + } + Size cs = child.layout(childConstraints); + allocatedMain += mainOf(cs, vertical); + maxChildCross = Math.max(maxChildCross, crossOf(cs, vertical)); + } + } + + // Own size. + double mainSize = (mainSizeMode == MainAxisSize.max && boundedMain) ? maxMain : allocatedMain; + mainSize = vertical ? constraints.constrainHeight(mainSize) : constraints.constrainWidth(mainSize); + double crossSize = vertical ? constraints.constrainWidth(maxChildCross) : constraints.constrainHeight(maxChildCross); + + // Spacing per main-axis alignment. + double remaining = Math.max(0, mainSize - allocatedMain); + double leading = 0; + double between = 0; + switch (mainAlign) { + case start: + break; + case end: + leading = remaining; + break; + case center: + leading = remaining / 2; + break; + case spaceBetween: + between = n > 1 ? remaining / (n - 1) : 0; + break; + case spaceAround: + between = n > 0 ? remaining / n : 0; + leading = between / 2; + break; + case spaceEvenly: + between = remaining / (n + 1); + leading = between; + break; + } + + // Position children. + double mainPos = leading; + for (RenderElement child : renderChildren) { + Size cs = child.size(); + double childCross = crossOf(cs, vertical); + double crossPos; + switch (crossAlign) { + case start: + case stretch: + crossPos = 0; + break; + case end: + crossPos = crossSize - childCross; + break; + case center: + default: + crossPos = (crossSize - childCross) / 2; + break; + } + if (vertical) { + setChildOffset(child, crossPos, mainPos); + } else { + setChildOffset(child, mainPos, crossPos); + } + mainPos += mainOf(cs, vertical) + between; + } + + return vertical ? new Size(crossSize, mainSize) : new Size(mainSize, crossSize); + } + + /** Whether this flexible child may take less than its share ({@code FlexFit.loose}). */ + private static boolean looseFit(RenderElement child) { + if (!(child instanceof ExpandedRenderElement)) { + return false; + } + com.codename1.flutter.Widget w = child.widget(); + return w instanceof Flexible + && ((Flexible) w).getFit() == com.codename1.flutter.FlexFit.loose; + } + + private static long flexOf(RenderElement child) { + if (child instanceof ExpandedRenderElement) { + return ((ExpandedRenderElement) child).flex(); + } + return 0; + } + + private static double mainOf(Size s, boolean vertical) { + return vertical ? s.height() : s.width(); + } + + private static double crossOf(Size s, boolean vertical) { + return vertical ? s.width() : s.height(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flexible.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flexible.java new file mode 100644 index 00000000000..365c6f679bf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Flexible.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.FlexFit; + +/** + * Marks a child of Row/Column as flexible — Flutter's {@code Flexible}. It + * receives a share of the free main-axis space proportional to its flex factor + * (default 1). {@code Expanded} is {@code Flexible} with {@code fit: tight}; + * this class reuses that flex machinery ({@link ExpandedRenderElement} reads + * the flex factor), with {@code fit} retained but not yet distinguished from + * tight in the layout pass. + */ +public class Flexible extends Expanded { + + private FlexFit fit = FlexFit.loose; + + public void fit(FlexFit v) { + this.fit = v == null ? FlexFit.loose : v; + } + + public FlexFit getFit() { + return fit; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterBoxStyle.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterBoxStyle.java new file mode 100644 index 00000000000..362f90b3108 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterBoxStyle.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BoxDecoration; +import com.codename1.flutter.BoxShape; +import com.codename1.flutter.Color; +import com.codename1.ui.Component; +import com.codename1.ui.plaf.RoundBorder; + +/** + * Applies a background color and (best-effort) shape from a Flutter + * {@link BoxDecoration} or plain {@link Color} onto a CN1 component's style. + * Shared by {@link ColoredBoxRenderElement}, {@link DecoratedBoxRenderElement} + * and {@link ContainerRenderElement}. + * + *

Only color and circle shape are honored for this milestone; gradients, + * borders, border radii, shadows and decoration images are not yet painted.

+ */ +final class FlutterBoxStyle { + + private FlutterBoxStyle() { + } + + /** + * Styles {@code face} from an explicit {@code color} and/or a + * {@code decoration} (expected to be a {@link BoxDecoration}). The explicit + * color wins when both are present, matching Flutter (which forbids both). + */ + static void apply(Component face, Color color, Object decoration) { + try { + Color bg = color; + BoxShape shape = BoxShape.rectangle; + if (decoration instanceof BoxDecoration) { + BoxDecoration d = (BoxDecoration) decoration; + if (bg == null) { + bg = d.getColor(); + } + shape = d.getShape(); + } + if (shape == BoxShape.circle && bg != null) { + // RoundBorder with no shadow, which Codename One now draws + // straight onto the Graphics rather than through a + // component-sized image -- the thing that made an image-backed + // circle unusable for a coach mark, whose radius animates. + face.getAllStyles().setBorder(RoundBorder.create() + .directPaint(true) + .color(bg.rgb()) + .opacity(bg.alpha())); + face.getAllStyles().setBgTransparency(0); + return; + } + double radiusLp = cornerRadiusLp(decoration); + boolean shadowed = decoration instanceof BoxDecoration + && ((BoxDecoration) decoration).getBoxShadow() != null; + if (radiusLp > 0 || shadowed) { + // Rounded corners and elevation are what make Material look like + // Material; a flat bgColor drops both. + // The cache is switched on exactly when there is a shadow, and + // that is a correctness requirement rather than a tuning knob: + // with it off, RoundRectBorder renders through + // createTargetComponentImage, which translates the LIVE Graphics + // by the shadow's shape offset and never undoes it, so every + // sibling painted afterwards is displaced -- and displaced + // again by the next shadowed box, accumulating down the page. + // With no shadow that offset is zero and the cheaper uncached + // path is exact. See CardRenderElement, where this cost the + // cards demo 9 device pixels per card. + com.codename1.ui.plaf.RoundRectBorder border = + com.codename1.ui.plaf.RoundRectBorder.create() + .useCache(shadowed) + .cornerRadius(com.codename1.flutter.rendering.Dp.mm(radiusLp)); + if (shadowed) { + border = border.shadowOpacity(40).shadowSpread(0.5f).shadowY(1); + } + border = withOutline(border, decoration); + face.getAllStyles().setBorder(border); + if (bg != null) { + face.getAllStyles().setBgColor(bg.rgb()); + face.getAllStyles().setBgTransparency(bg.alpha()); + } else { + face.getAllStyles().setBgTransparency(0); + } + applyDecorationImage(face, decoration); + return; + } + if (bg != null) { + face.getAllStyles().setBgColor(bg.rgb()); + face.getAllStyles().setBgTransparency(bg.alpha()); + } else { + face.getAllStyles().setBgTransparency(0); + } + applyDecorationImage(face, decoration); + } catch (Exception err) { + // styling is best-effort; layout must survive regardless + } + } + + /** + * The decoration's corner radius in logical pixels, or 0 when it is square. + * CN1 draws one radius for all four corners, so a decoration with mixed corners + * takes its top-left — closer than dropping the rounding altogether. + */ + private static double cornerRadiusLp(Object decoration) { + if (!(decoration instanceof BoxDecoration)) { + return 0; + } + Object br = ((BoxDecoration) decoration).getBorderRadius(); + if (br instanceof com.codename1.flutter.BorderRadius) { + com.codename1.flutter.Radius r = ((com.codename1.flutter.BorderRadius) br).topLeft(); + return r == null ? 0 : r.x(); + } + return 0; + } + + /** + * True when the given color/decoration would paint anything — used to + * decide whether a face component is worth creating. + */ + static boolean paints(Color color, Object decoration) { + if (color != null) { + return true; + } + if (decoration instanceof BoxDecoration) { + BoxDecoration d = (BoxDecoration) decoration; + // getImage() belongs here: a decoration whose ONLY content is an + // image paints something, and leaving it out of this test meant no + // component was created for it at all -- so the code that paints + // the image was never reached and the fix for it looked inert. + return d.getColor() != null || d.getGradient() != null + || d.getBorder() != null || d.getBoxShadow() != null + || d.getImage() != null; + } + return decoration != null; + } + + /** + * Paints a {@code BoxDecoration}'s {@link com.codename1.flutter.DecorationImage} + * as the component's background. + * + *

The decoration image was stored and never read, so every + * {@code BoxDecoration(image:)} in the app painted nothing at all. The cards + * demo is where it shows worst: {@code Ink.image} is how a Material card puts + * a photo behind its ink splash, so the cards kept their layout and their + * captions and lost every photograph.

+ * + *

Codename One paints a background image from the style, so the mapping is + * {@code BoxFit} onto a background type. Only the fits a background can + * express are mapped; anything else takes {@code cover}, which is both + * Flutter's common case here and the one that never leaves the box + * part-empty.

+ */ + private static void applyDecorationImage(Component face, Object decoration) { + if (!(decoration instanceof BoxDecoration)) { + return; + } + Object raw = ((BoxDecoration) decoration).getImage(); + if (!(raw instanceof com.codename1.flutter.DecorationImage)) { + return; + } + com.codename1.flutter.DecorationImage di = (com.codename1.flutter.DecorationImage) raw; + com.codename1.ui.Image img = load(di.getImage()); + if (img == null) { + return; + } + face.getAllStyles().setBgImage(img); + face.getAllStyles().setBackgroundType(backgroundType(di.getFit())); + } + + /** The CN1 background type for a {@code BoxFit}. */ + private static byte backgroundType(com.codename1.flutter.BoxFit fit) { + if (fit == com.codename1.flutter.BoxFit.contain + || fit == com.codename1.flutter.BoxFit.scaleDown) { + return com.codename1.ui.plaf.Style.BACKGROUND_IMAGE_SCALED_FIT; + } + if (fit == com.codename1.flutter.BoxFit.fill) { + return com.codename1.ui.plaf.Style.BACKGROUND_IMAGE_SCALED; + } + return com.codename1.ui.plaf.Style.BACKGROUND_IMAGE_SCALED_FILL; + } + + /** + * Decodes an {@link com.codename1.flutter.ImageProvider} that names a bundled + * asset. A network provider is not resolved here: a background image has no + * placeholder to show while it arrives. + */ + private static com.codename1.ui.Image load(com.codename1.flutter.ImageProvider provider) { + if (!(provider instanceof com.codename1.flutter.AssetImage)) { + return null; + } + String name = ((com.codename1.flutter.AssetImage) provider).resolvedName(); + try { + com.codename1.flutter.FlutterAssets.Resolved res = + com.codename1.flutter.FlutterAssets.open(FlutterBoxStyle.class, name); + if (res == null) { + com.codename1.io.Log.p("Flutter runtime: decoration image not found: " + name); + return null; + } + try { + return com.codename1.ui.EncodedImage.create(res.stream()); + } finally { + com.codename1.io.Util.cleanup(res.stream()); // the chosen stream is ours to close + } + } catch (Exception cannotDecode) { + com.codename1.io.Log.p("Flutter runtime: could not decode decoration image " + name); + return null; + } + } + + /** + * Adds a {@code BoxDecoration.border}'s stroke to a rounded border. + * + *

The border was consulted only to decide whether the decoration paints + * anything, never drawn. An outlined chip is the case that shows it: the + * action chip's whole appearance IS its outline, so without this it read as + * an icon and some text loose on the page.

+ */ + private static com.codename1.ui.plaf.RoundRectBorder withOutline( + com.codename1.ui.plaf.RoundRectBorder border, Object decoration) { + if (!(decoration instanceof BoxDecoration)) { + return border; + } + Object raw = ((BoxDecoration) decoration).getBorder(); + if (!(raw instanceof com.codename1.flutter.Border)) { + return border; + } + com.codename1.flutter.BorderSide side = ((com.codename1.flutter.Border) raw).top(); + if (side == null || side.color() == null) { + return border; + } + double w = side.width() <= 0 ? 1 : side.width(); + return border + .strokeColor(side.color().rgb()) + .strokeOpacity(side.color().alpha()) + .stroke((float) com.codename1.flutter.rendering.Dp.px(w), false); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterLogo.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterLogo.java new file mode 100644 index 00000000000..6a02d55ecbd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FlutterLogo.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Canvas; +import com.codename1.flutter.Color; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Paint; +import com.codename1.flutter.PaintingStyle; +import com.codename1.flutter.Path; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.Curve; +import com.codename1.flutter.rendering.CustomPainter; +import com.codename1.flutter.rendering.Size; + +import dart.core.Duration; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Flutter logo as a widget — Flutter's {@code FlutterLogo}. + * + *

Drawn rather than declared: it rendered nothing at all before, which left a hole in + * every screen that used it as sample content. The mark is four flat polygons, so it is + * exact at any size with no asset to ship.

+ */ +public class FlutterLogo extends StatelessWidget { + + /** Flutter's default logo size in logical pixels. */ + private static final double DEFAULT_SIZE_LP = 24; + + private double size = DEFAULT_SIZE_LP; + private Color textColor; + private Object style; + private Duration duration; + private Curve curve; + + public void size(double v) { + this.size = v; + } + + public void textColor(Color v) { + this.textColor = v; + } + + public void style(Object v) { + this.style = v; + } + + public void duration(Duration v) { + this.duration = v; + } + + public void curve(Curve v) { + this.curve = v; + } + + @Override + public Widget build(BuildContext context) { + CustomPaint paint = new CustomPaint(); + paint.painter(new LogoPainter()); + paint.size(new Size(size, size)); + return paint; + } + + /** + * Paints the mark from its polygons, in a square box. + * + *

Coordinates are the official artwork's, normalised out of its 256x317 frame, so + * the proportions hold at any size. The logo is not square, so it is centred in the box + * the way Flutter's own painter does.

+ */ + static final class LogoPainter extends CustomPainter { + + /** Light beam, dark fold, and the mid-blue shadow between them. */ + private static final int LIGHT = 0x47C5FB; + private static final int DARK = 0x00569E; + private static final int MID = 0x00B5F8; + + private static final double ART_W = 256; + private static final double ART_H = 317; + + @Override + public void paint(Canvas canvas, Size box) { + double scale = Math.min(box.width() / ART_W, box.height() / ART_H); + double dx = (box.width() - ART_W * scale) / 2; + double dy = (box.height() - ART_H * scale) / 2; + + // Upper beam. + fill(canvas, scale, dx, dy, LIGHT, + 157.7, 0, 0, 157.7, 48.8, 206.5, 255.3, 0); + // Lower beam. + fill(canvas, scale, dx, dy, LIGHT, + 156.6, 145.2, 73.0, 228.8, 121.9, 278.7, 170.6, 230.0, 256.3, 145.2); + // The fold, in the darker blue. + fill(canvas, scale, dx, dy, DARK, + 121.9, 278.7, 159.0, 315.8, 255.3, 315.8, 170.7, 230.1); + // The small shadow where the beams meet. + fill(canvas, scale, dx, dy, MID, + 72.4, 229.3, 121.2, 180.5, 170.6, 230.0, 121.9, 278.7); + } + + private static void fill(Canvas canvas, double scale, double dx, double dy, + int rgb, double... xy) { + List points = new ArrayList(); + for (int i = 0; i + 1 < xy.length; i += 2) { + points.add(new Offset(dx + xy[i] * scale, dy + xy[i + 1] * scale)); + } + Path p = new Path(); + p.addPolygon(points, true); + Paint paint = new Paint(); + paint.color(new Color(0xFF000000 | rgb)); + paint.style(PaintingStyle.fill); + canvas.drawPath(p, paint); + } + + @Override + public boolean shouldRepaint(CustomPainter oldDelegate) { + return !(oldDelegate instanceof LogoPainter); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Focus.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Focus.java new file mode 100644 index 00000000000..cd476f56077 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Focus.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.FocusNode; +import com.codename1.flutter.Widget; +import com.codename1.flutter.services.KeyEvent; +import com.codename1.flutter.services.KeyEventResult; + +import dart.runtime.Funcs; + +/** + * Manages a {@link FocusNode} for its subtree — Flutter's {@code Focus}. + * Structural pass-through for this milestone: the {@code child} renders + * unchanged and the focus/key callbacks are captured. {@code onKeyEvent} is + * typed as the real Flutter {@code FocusOnKeyEventCallback} so key handlers can + * inspect the {@link KeyEvent} and return a {@link KeyEventResult}. + */ +public class Focus extends Widget implements HasChild { + + private FocusNode focusNode; + private Boolean autofocus; + private Funcs.VoidFunc1 onFocusChange; + private Object onKey; + private Funcs.Func2 onKeyEvent; + private Boolean canRequestFocus; + private Boolean skipTraversal; + private Boolean descendantsAreFocusable; + private Boolean includeSemantics; + private String debugLabel; + private Widget child; + + public void focusNode(FocusNode v) { this.focusNode = v; } + public void autofocus(Boolean v) { this.autofocus = v; } + public void onFocusChange(Funcs.VoidFunc1 v) { this.onFocusChange = v; } + public void onKey(Object v) { this.onKey = v; } + public void onKeyEvent(Funcs.Func2 v) { this.onKeyEvent = v; } + public void canRequestFocus(Boolean v) { this.canRequestFocus = v; } + public void skipTraversal(Boolean v) { this.skipTraversal = v; } + public void descendantsAreFocusable(Boolean v) { this.descendantsAreFocusable = v; } + public void includeSemantics(Boolean v) { this.includeSemantics = v; } + public void debugLabel(String v) { this.debugLabel = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** Flutter's {@code Focus.of} — the enclosing node (none tracked at this pass). */ + public static FocusNode of(BuildContext context, boolean scopeOk) { + return null; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusOrder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusOrder.java new file mode 100644 index 00000000000..106033a15a8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusOrder.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Base type for an explicit focus-traversal ordering value passed to a + * {@link FocusTraversalOrder} — Flutter's {@code FocusOrder}. Concrete + * ordering: {@link NumericFocusOrder}. + */ +public abstract class FocusOrder { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScope.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScope.java new file mode 100644 index 00000000000..27ea7873c4d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScope.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A focus container that groups its subtree into a focus scope — Flutter's + * {@code FocusScope}. This milestone renders the {@code child} through + * unchanged; scope-based focus traversal is deferred, so the node and focus + * flags are captured only for API shape. + */ +public class FocusScope extends StatelessWidget { + + private FocusScopeNode node; + private Widget child; + + public void node(FocusScopeNode v) { + this.node = v; + } + + public void autofocus(boolean v) { + } + + public void onFocusChange(Object v) { + } + + public void canRequestFocus(boolean v) { + } + + public void skipTraversal(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + /** Dart's {@code FocusScope.of(context)} — the nearest enclosing scope node. */ + public static FocusScopeNode of(BuildContext context) { + return new FocusScopeNode(); + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScopeNode.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScopeNode.java new file mode 100644 index 00000000000..998042d6076 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusScopeNode.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * A node in the focus tree that establishes a focus scope — Flutter's + * {@code FocusScopeNode}. There is one scope, the whole app, so it forwards to + * the node holding the primary focus. + */ +public class FocusScopeNode { + + private String debugLabel; + + public FocusScopeNode() { + } + + public void debugLabel(String v) { + this.debugLabel = v; + } + + /** + * Whether a node holds the focus. The whole app is one scope here, so that is + * whether any node does; these methods were no-ops, so the common + * {@code FocusScope.of(context).unfocus()} left the keyboard up. + */ + public boolean hasFocus() { + return com.codename1.flutter.FocusNode.primaryFocus() != null; + } + + public void requestFocus() { + } + + public void requestFocus(Object node) { + if (node instanceof com.codename1.flutter.FocusNode) { + ((com.codename1.flutter.FocusNode) node).requestFocus(); + } + } + + public void unfocus() { + com.codename1.flutter.FocusNode f = com.codename1.flutter.FocusNode.primaryFocus(); + if (f != null) { + f.unfocus(); + } + } + + public void unfocus(Object disposition) { + unfocus(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalGroup.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalGroup.java new file mode 100644 index 00000000000..d704a03a266 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalGroup.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Groups its descendants into a single focus-traversal scope with an optional + * {@code policy} — Flutter's {@code FocusTraversalGroup}. This pass renders the + * {@code child} through unchanged; directional/reading-order traversal is + * deferred, so the policy is captured only for API shape. + */ +public class FocusTraversalGroup extends StatelessWidget { + + private Object policy; + private Widget child; + + public void policy(Object v) { + this.policy = v; + } + + public void descendantsAreFocusable(boolean v) { + } + + public void descendantsAreTraversable(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalOrder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalOrder.java new file mode 100644 index 00000000000..bb13926cf6e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FocusTraversalOrder.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Assigns an explicit traversal {@code order} (a {@link FocusOrder}, e.g. + * {@link NumericFocusOrder}) to its {@code child} within the enclosing + * {@code FocusTraversalGroup} — Flutter's {@code FocusTraversalOrder}. This + * pass hosts the child; ordered traversal is deferred, so the order is captured + * for API shape only. + */ +public class FocusTraversalOrder extends StatelessWidget { + + private FocusOrder order; + private Widget child; + + public void order(FocusOrder v) { + this.order = v; + } + + public void child(Widget v) { + this.child = v; + } + + public FocusOrder getOrder() { + return order; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Form.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Form.java new file mode 100644 index 00000000000..6283a3a0895 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Form.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Groups form fields that validate/save together — Flutter's {@code Form}. A + * {@link com.codename1.flutter.GlobalKey}{@code } attached to the + * form gives access to the {@link FormState} that drives + * validate/save/reset across the fields. Structural pass-through for this + * milestone: the {@code child} renders unchanged. + */ +public class Form extends Widget implements HasChild { + + private Widget child; + private Object onChanged; + private Object onWillPop; + private Object canPop; + private Object onPopInvoked; + private Object autovalidateMode; + + public void child(Widget v) { + this.child = v; + } + + public void onChanged(Object v) { + this.onChanged = v; + } + + public void onWillPop(Object v) { + this.onWillPop = v; + } + + public void canPop(Object v) { + this.canPop = v; + } + + public void onPopInvoked(Object v) { + this.onPopInvoked = v; + } + + public void autovalidateMode(Object v) { + this.autovalidateMode = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** Flutter's {@code Form.of} — the nearest enclosing {@link FormState}. */ + public static FormState of(BuildContext context) { + return null; + } + + /** Flutter's {@code Form.maybeOf}. */ + public static FormState maybeOf(BuildContext context) { + return null; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormField.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormField.java new file mode 100644 index 00000000000..9215fcfd9ed --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormField.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A single form field wired to {@link Form} validation/save — Flutter's + * {@code FormField}. Application fields supply a {@code builder} that renders + * the input from the current {@link FormFieldState}. This pass builds the field + * from a fresh state; registration with the enclosing form lands with the form + * renderer. + * + * @param the field's value type + */ +public class FormField extends StatelessWidget { + + private Funcs.Func1, Widget> builder; + private FormFieldValidator validator; + private FormFieldSetter onSaved; + private T initialValue; + private Boolean enabled; + private Object autovalidateMode; + private String restorationId; + + public void builder(Funcs.Func1, Widget> v) { this.builder = v; } + public void validator(FormFieldValidator v) { this.validator = v; } + public void onSaved(FormFieldSetter v) { this.onSaved = v; } + public void initialValue(T v) { this.initialValue = v; } + public void enabled(Boolean v) { this.enabled = v; } + public void autovalidateMode(Object v) { this.autovalidateMode = v; } + public void restorationId(String v) { this.restorationId = v; } + + @Override + public Widget build(BuildContext context) { + if (builder == null) { + return null; + } + FormFieldState state = new FormFieldState(); + if (initialValue != null) { + state.didChange(initialValue); + } + return builder.call(state); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldSetter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldSetter.java new file mode 100644 index 00000000000..39cf22c5934 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldSetter.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Persists a form field's value when the form is saved — Flutter's + * {@code FormFieldSetter} typedef ({@code void Function(T? newValue)}). A + * single-abstract-method interface so transpiled Dart closures and method + * references bind as Java lambdas. + * + * @param the field's value type + */ +public interface FormFieldSetter { + void call(T value); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldState.java new file mode 100644 index 00000000000..fd889568cce --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldState.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * The state of a single {@link FormField} — Flutter's {@code FormFieldState}. + * Reached through a {@code GlobalKey>().currentState}; the + * text-field demo reads/writes {@link #value()} and drives + * didChange/validate/save/reset. + * + * @param the field's value type + */ +public class FormFieldState { + + private T value; + private String errorText; + + public T value() { + return value; + } + + public boolean hasError() { + return errorText != null; + } + + public boolean isValid() { + return errorText == null; + } + + public String errorText() { + return errorText; + } + + public void didChange(T value) { + this.value = value; + } + + public boolean validate() { + return errorText == null; + } + + public void save() { + } + + public void reset() { + this.value = null; + this.errorText = null; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldValidator.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldValidator.java new file mode 100644 index 00000000000..18e60107ecd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormFieldValidator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Validates a form field's value, returning an error message or {@code null} + * when valid — Flutter's {@code FormFieldValidator} typedef + * ({@code String? Function(T? value)}). A single-abstract-method interface so + * transpiled Dart closures and method references bind as Java lambdas. + * + * @param the field's value type + */ +public interface FormFieldValidator { + String call(T value); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormState.java new file mode 100644 index 00000000000..ac09a7cd220 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FormState.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * The state of a {@link Form}, driving validation/save/reset across its fields — + * Flutter's {@code FormState}. Reached through a + * {@code GlobalKey().currentState}. This pass exposes the control + * surface; the field registry that makes validate/save fan out lands with the + * form renderer. + */ +public class FormState { + + /** Validates every field; returns {@code true} when all are valid. */ + public boolean validate() { + return true; + } + + /** Saves every field (invokes their {@code onSaved}). */ + public void save() { + } + + /** Resets every field to its initial value. */ + public void reset() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslation.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslation.java new file mode 100644 index 00000000000..60df6595e33 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslation.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Widget; + +/** + * Translates its {@code child} by an {@link Offset} expressed as a fraction of the child's + * own size before painting — Flutter's {@code FractionalTranslation}. + */ +public class FractionalTranslation extends Widget + implements FractionalTranslationRenderElement.FractionSource { + + private Offset translation; + private boolean transformHitTests = true; + private Widget child; + + public void translation(Offset v) { + this.translation = v; + } + + public void transformHitTests(boolean v) { + this.transformHitTests = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Offset getTranslation() { + return translation; + } + + public Widget getChild() { + return child; + } + + @Override + public Offset fraction() { + return translation; + } + + @Override + public Widget child() { + return child; + } + + @Override + public com.codename1.flutter.foundation.Listenable driver() { + return null; // a static translation: nothing to follow + } + + @Override + public Element createElement() { + return new FractionalTranslationRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslationRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslationRenderElement.java new file mode 100644 index 00000000000..54eab9e980e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionalTranslationRenderElement.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Offset; +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Paints a subtree shifted by an offset expressed as a FRACTION OF ITS OWN SIZE — Flutter's + * {@code FractionalTranslation}, and the mechanism behind {@code SlideTransition}. + * + *

Like Flutter this is a paint effect: the child is laid out where it belongs and only + * the painting moves, so a sliding page does not disturb the layout around it. The fraction + * is resolved against the laid-out size, which is why it must happen here rather than in the + * widget — the size is not known until layout has run.

+ * + *

Both widgets previously reported the translation as unimplemented and drew the child in + * place, which turned every slide in the app into a jump.

+ */ +public class FractionalTranslationRenderElement extends EffectRenderElement { + + /** Resolves the current fractional offset — the widget's, or an animation's. */ + public interface FractionSource { + Offset fraction(); + + Widget child(); + + /** + * The animation to repaint with, or null for a static translation. Repainting is + * enough: the fraction is read at paint time and the child's layout never moves, + * so a tick must not cost a layout pass. + */ + com.codename1.flutter.foundation.Listenable driver(); + } + + private com.codename1.flutter.foundation.Listenable listened; + private final dart.runtime.Funcs.VoidFunc0 repaint = new dart.runtime.Funcs.VoidFunc0() { + @Override + public void call() { + // The pane's box depends on the fraction, so a tick has to re-derive it + // before the repaint -- otherwise the frame paints translated content + // against the previous frame's clip. It is four setters on one component, + // not a layout pass, which is why an animated slide stays cheap. + refreshPaneBounds(); + markNeedsPaint(); + } + }; + + // The box Flutter laid this effect out at, BEFORE the pane was grown to make room + // for the translation. position() is the only writer; refreshPaneBounds re-derives + // the pane from it on every tick. + private int laidX; + private int laidY; + private int laidW; + private int laidH; + + public FractionalTranslationRenderElement(Widget widget) { + super(widget); + } + + /** + * The CURRENT configuration. Read through {@code widget()} rather than captured at + * construction: a rebuild swaps the widget, and a captured one would keep reporting the + * offset and the animation of a configuration that is no longer on screen. + */ + private FractionSource source() { + Widget w = widget(); + return w instanceof FractionSource ? (FractionSource) w : null; + } + + @Override + public void mount(com.codename1.flutter.Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + } + + @Override + public void update(Widget newWidget) { + unsubscribe(); + super.update(newWidget); + subscribe(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + private void subscribe() { + FractionSource src = source(); + listened = src == null ? null : src.driver(); + if (listened != null) { + listened.addListener(repaint); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(repaint); + listened = null; + } + } + + @Override + protected Widget effectChild() { + FractionSource src = source(); + return src == null ? null : src.child(); + } + + /** + * Grows the pane to cover BOTH the laid-out box and the translated one, and moves its + * origin to the union's top-left. + * + *

This is what makes a negative translation visible at all. Codename One clips every + * component to its own rectangle in {@code Component.paintInternalImpl} before calling + * its {@code paint}, so painting the subtree shifted by {@code -w/2} inside a pane whose + * box is still the laid-out one drew three quarters of it into the discarded region: the + * gallery's feature-discovery circle, positioned at its centre and pulled back by half + * its size, rendered as the bottom-right quadrant of a circle with two hard straight + * edges meeting exactly at the centre point. No form of transform escapes that clip -- + * it is already installed in device space by the time this runs -- so the box has to + * cover the pixels the effect intends to touch.

+ * + *

The subtree sits at the pane's origin, so moving that origin moves the subtree with + * it; {@link #paintOffsetX} compensates, leaving the net shift exactly the fraction.

+ */ + private void refreshPaneBounds() { + Container pane = pane(); + if (pane == null) { + return; + } + int[] box = translatedBox(laidX, laidY, laidW, laidH, deltaX(), deltaY()); + pane.setX(box[0]); + pane.setY(box[1]); + pane.setWidth(box[2]); + pane.setHeight(box[3]); + } + + /** + * The union of a box and the same box shifted by {@code (dx, dy)}, plus the + * translation still owed at paint time: {@code {x, y, w, h, paintDx, paintDy}}. + * + *

Separated out and package visible so the arithmetic can be asserted without a + * display: the whole defect this fixes was a box that did not cover the pixels the + * paint was going to touch.

+ */ + static int[] translatedBox(int x, int y, int w, int h, int dx, int dy) { + return new int[] { + x + Math.min(dx, 0), + y + Math.min(dy, 0), + w + Math.abs(dx), + h + Math.abs(dy), + Math.max(dx, 0), + Math.max(dy, 0), + }; + } + + private Container pane() { + com.codename1.ui.Component c = component(); + return c instanceof Container ? (Container) c : null; + } + + private Offset fraction() { + FractionSource src = source(); + return src == null ? null : src.fraction(); + } + + private int deltaX() { + Offset f = fraction(); + return f == null ? 0 : (int) Math.round(f.dx() * laidW); + } + + private int deltaY() { + Offset f = fraction(); + return f == null ? 0 : (int) Math.round(f.dy() * laidH); + } + + /** + * The translation to apply while painting: the fraction minus the part already + * absorbed by moving the pane's origin. {@code min(d,0) + max(d,0) == d} for either + * sign, so the subtree lands exactly {@code d} from where it was laid out. + */ + private int paintOffsetX() { + return Math.max(deltaX(), 0); + } + + private int paintOffsetY() { + return Math.max(deltaY(), 0); + } + + @Override + public void position(int x, int y) { + super.position(x, y); + Container pane = pane(); + if (pane == null) { + return; + } + laidX = x; + laidY = y; + laidW = pane.getWidth(); + laidH = pane.getHeight(); + refreshPaneBounds(); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + int dx = paintOffsetX(); + int dy = paintOffsetY(); + if (dx == 0 && dy == 0) { + paintChildren.paint(g); + return; + } + // A plain integer translate, deliberately: this shifts a SUBTREE, and + // Container.paint translates by its own x/y on top of it. The core's child + // painting is written against the Graphics translation, not against an affine + // transform, so installing a transform here would leave that bookkeeping + // disagreeing with the clip. An affine belongs where a shape is rendered + // directly (see FlutterShapeBorderPainter), not around a subtree walk. + g.translate(dx, dy); + try { + paintChildren.paint(g); + } finally { + g.translate(-dx, -dy); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBox.java new file mode 100644 index 00000000000..989276b79b3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBox.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Sizes its child to a fraction of the available space — Flutter's + * {@code FractionallySizedBox}. {@code widthFactor}/{@code heightFactor} are + * multiples of the incoming max extent on each axis (null leaves that axis + * loose); the child is positioned by {@code alignment} (default center). + */ +public class FractionallySizedBox extends Widget { + + private Object alignment; + private Double widthFactor; + private Double heightFactor; + private Widget child; + + public void alignment(Object v) { + this.alignment = v; + } + + public void widthFactor(double v) { + this.widthFactor = v; + } + + public void heightFactor(double v) { + this.heightFactor = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getAlignment() { + return alignment; + } + + public Double getWidthFactor() { + return widthFactor; + } + + public Double getHeightFactor() { + return heightFactor; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new FractionallySizedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBoxRenderElement.java new file mode 100644 index 00000000000..a0d7e9c6798 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FractionallySizedBoxRenderElement.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.AlignmentDirectional; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Flutter's RenderFractionallySizedOverflowBox (bounded subset): fills the + * incoming constraints and lays the child out tight to a fraction of each + * bounded axis, positioning it by the alignment. Owns no CN1 component. + */ +public class FractionallySizedBoxRenderElement extends SingleChildRenderElement { + + public FractionallySizedBoxRenderElement(FractionallySizedBox widget) { + super(widget); + } + + private FractionallySizedBox box() { + return (FractionallySizedBox) widget(); + } + + private Alignment alignment() { + Object a = box().getAlignment(); + if (a instanceof Alignment) { + return (Alignment) a; + } + if (a instanceof AlignmentDirectional) { + return ((AlignmentDirectional) a).resolve(); + } + return Alignment.center; + } + + @Override + protected Widget childWidget() { + return box().getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double selfW = constraints.hasBoundedWidth() ? constraints.maxWidth() : 0; + double selfH = constraints.hasBoundedHeight() ? constraints.maxHeight() : 0; + Size self = constraints.constrain(new Size(selfW, selfH)); + + RenderElement child = renderChild(); + if (child == null) { + return self; + } + + Double wf = box().getWidthFactor(); + Double hf = box().getHeightFactor(); + double minW = 0; + double maxW = constraints.maxWidth(); + double minH = 0; + double maxH = constraints.maxHeight(); + if (wf != null && constraints.hasBoundedWidth()) { + minW = maxW = constraints.maxWidth() * wf; + } + if (hf != null && constraints.hasBoundedHeight()) { + minH = maxH = constraints.maxHeight() * hf; + } + Size cs = child.layout(new BoxConstraints(minW, maxW, minH, maxH)); + Alignment a = alignment(); + setChildOffset(child, + Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(a.y(), self.height(), cs.height())); + return self; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilder.java new file mode 100644 index 00000000000..6d80b02b792 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilder.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Builds itself from the latest snapshot of a {@code Future} -- Flutter's + * {@code FutureBuilder}. The snapshot and the subscription live in + * {@link FutureBuilderElement}, as a State would hold them in Flutter. + * + * @param the future's value type + */ +public class FutureBuilder extends StatelessWidget { + + private Object future; + private T initialData; + private Funcs.Func2 builder; + + public void future(Object v) { + this.future = v; + } + + public void initialData(T v) { + this.initialData = v; + } + + public void builder(Funcs.Func2 v) { + this.builder = v; + } + + Object getFuture() { + return future; + } + + T getInitialData() { + return initialData; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new FutureBuilderElement(this); + } + + @Override + public Widget build(BuildContext context) { + return buildWith(context, initialData != null + ? new AsyncSnapshot(ConnectionState.none, initialData, null, null) : new AsyncSnapshot()); + } + + Widget buildWith(BuildContext context, AsyncSnapshot snapshot) { + return builder == null ? null : builder.call(context, snapshot); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilderElement.java new file mode 100644 index 00000000000..c53c15944cb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/FutureBuilderElement.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.ComposedElement; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.async.Future; +import dart.runtime.Funcs; + +/** + * Element for {@link FutureBuilder}: holds the snapshot and subscribes to the future, + * mirroring Flutter's {@code _FutureBuilderState}. + * + *

FutureBuilder used to be a plain stateless widget that never read its future, so it + * built its initial snapshot once and stayed there: every result and every error was + * dropped, and a screen fed by a future sat on its loading state for good.

+ * + *

As in Flutter: the snapshot starts from {@code initialData} (state none), turns + * {@code waiting} while a future is pending and {@code done} with its value or error; a + * NEW future (by identity) resubscribes from {@code none}, and a callback from a future + * this element no longer tracks -- replaced, or unmounted -- is ignored.

+ */ +public class FutureBuilderElement extends ComposedElement { + + private AsyncSnapshot snapshot; + private Object subscribedTo; + /** Identifies the live subscription; a callback carrying another one is stale. */ + private Object activeCallback; + private boolean subscribing; + + public FutureBuilderElement(FutureBuilder widget) { + super(widget); + } + + @Override + public void mount(Element parent, int slot) { + FutureBuilder w = (FutureBuilder) widget(); + snapshot = w.getInitialData() != null + ? new AsyncSnapshot(ConnectionState.none, w.getInitialData(), null, null) + : new AsyncSnapshot(); + subscribe(w.getFuture()); + super.mount(parent, slot); + } + + @Override + public void update(Widget newWidget) { + Object next = ((FutureBuilder) newWidget).getFuture(); + if (next != subscribedTo) { + activeCallback = null; + snapshot = new AsyncSnapshot(ConnectionState.none, snapshot.data(), snapshot.error(), + snapshot.stackTrace()); + subscribe(next); + } + super.update(newWidget); + } + + @Override + public void unmount() { + activeCallback = null; + subscribedTo = null; + super.unmount(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void subscribe(Object future) { + subscribedTo = future; + if (!(future instanceof Future)) { + return; + } + final Object token = new Object(); + activeCallback = token; + subscribing = true; + try { + ((Future) future).then(new Funcs.VoidFunc1() { + @Override + public void call(Object value) { + settle(token, new AsyncSnapshot(ConnectionState.done, value, null, null)); + } + }).catchError(new Funcs.VoidFunc1() { + @Override + public void call(Object error) { + settle(token, new AsyncSnapshot(ConnectionState.done, null, error, null)); + } + }); + } finally { + subscribing = false; + } + if (activeCallback == token && snapshot.connectionState() != ConnectionState.done) { + snapshot = new AsyncSnapshot(ConnectionState.waiting, snapshot.data(), snapshot.error(), + snapshot.stackTrace()); + } + } + + private void settle(final Object token, final AsyncSnapshot result) { + if (subscribing) { + // An already-completed future answers during subscribe(), before this + // element has built: take the result as the snapshot to build with. + if (activeCallback == token) { + snapshot = result; + } + return; + } + Runnable apply = new Runnable() { + @Override + public void run() { + if (activeCallback != token) { + return; // a replaced future, or an unmounted element + } + snapshot = result; + markNeedsBuild(); + } + }; + // Completion can arrive off the EDT (a headless timer, a network callback); + // element state is only ever touched on it. + if (com.codename1.ui.Display.isInitialized() && !com.codename1.ui.CN.isEdt()) { + com.codename1.ui.CN.callSerially(apply); + } else { + apply.run(); + } + } + + @Override + protected Widget build() { + return ((FutureBuilder) widget()).buildWith(this, snapshot); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureDetector.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureDetector.java new file mode 100644 index 00000000000..45882f84743 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureDetector.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.gestures.GestureDragEndCallback; +import com.codename1.flutter.gestures.GestureDragStartCallback; +import com.codename1.flutter.gestures.GestureDragUpdateCallback; +import com.codename1.flutter.gestures.GestureTapDownCallback; +import com.codename1.flutter.gestures.GestureTapUpCallback; +import com.codename1.flutter.rendering.HitTestBehavior; + +import dart.runtime.Funcs; + +/** + * Detects taps and long presses on its child. There is no CN1 component for + * the child itself; a transparent overlay component (UIID "FlutterGesture") + * is positioned exactly over the child's bounds in the flat container and + * receives the pointer events. + * + *

Known limitation: interactive widgets INSIDE a GestureDetector (a + * button in the detected subtree) are shadowed by the overlay, which sits on + * top of them — rare in practice.

+ */ +public class GestureDetector extends Widget { + + private Funcs.VoidFunc0 onTap; + private Funcs.VoidFunc0 onLongPress; + private Funcs.VoidFunc0 onDoubleTap; + private Widget child; + private HitTestBehavior behavior; + private GestureTapDownCallback onTapDown; + private GestureTapUpCallback onTapUp; + private GestureDragStartCallback onVerticalDragStart; + private GestureDragUpdateCallback onVerticalDragUpdate; + private GestureDragEndCallback onVerticalDragEnd; + private GestureDragStartCallback onHorizontalDragStart; + private GestureDragUpdateCallback onHorizontalDragUpdate; + private GestureDragEndCallback onHorizontalDragEnd; + private GestureDragStartCallback onPanStart; + private GestureDragUpdateCallback onPanUpdate; + private GestureDragEndCallback onPanEnd; + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public void onLongPress(Funcs.VoidFunc0 v) { + this.onLongPress = v; + } + + public void onDoubleTap(Funcs.VoidFunc0 v) { + this.onDoubleTap = v; + } + + public void behavior(HitTestBehavior v) { + this.behavior = v; + } + + public void excludeFromSemantics(boolean v) { + } + + public void dragStartBehavior(Object v) { + } + + public void onTapDown(GestureTapDownCallback v) { + this.onTapDown = v; + } + + public void onTapUp(GestureTapUpCallback v) { + this.onTapUp = v; + } + + public void onVerticalDragStart(GestureDragStartCallback v) { + this.onVerticalDragStart = v; + } + + public void onVerticalDragUpdate(GestureDragUpdateCallback v) { + this.onVerticalDragUpdate = v; + } + + public void onVerticalDragEnd(GestureDragEndCallback v) { + this.onVerticalDragEnd = v; + } + + public void onHorizontalDragStart(GestureDragStartCallback v) { + this.onHorizontalDragStart = v; + } + + public void onHorizontalDragUpdate(GestureDragUpdateCallback v) { + this.onHorizontalDragUpdate = v; + } + + public void onHorizontalDragEnd(GestureDragEndCallback v) { + this.onHorizontalDragEnd = v; + } + + public void onPanStart(GestureDragStartCallback v) { + this.onPanStart = v; + } + + public void onPanUpdate(GestureDragUpdateCallback v) { + this.onPanUpdate = v; + } + + public void onPanEnd(GestureDragEndCallback v) { + this.onPanEnd = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Funcs.VoidFunc0 getOnTap() { + return onTap; + } + + /// The vertical drag-end callback, or null. + /// + /// #### Returns + /// + /// the callback given to {@code onVerticalDragEnd} + public com.codename1.flutter.gestures.GestureDragEndCallback getOnVerticalDragEnd() { + return onVerticalDragEnd; + } + + public com.codename1.flutter.gestures.GestureDragStartCallback getOnVerticalDragStart() { + return onVerticalDragStart; + } + + public com.codename1.flutter.gestures.GestureDragUpdateCallback getOnVerticalDragUpdate() { + return onVerticalDragUpdate; + } + + public com.codename1.flutter.gestures.GestureDragStartCallback getOnHorizontalDragStart() { + return onHorizontalDragStart; + } + + public com.codename1.flutter.gestures.GestureDragUpdateCallback getOnHorizontalDragUpdate() { + return onHorizontalDragUpdate; + } + + public com.codename1.flutter.gestures.GestureDragEndCallback getOnHorizontalDragEnd() { + return onHorizontalDragEnd; + } + + public com.codename1.flutter.gestures.GestureDragStartCallback getOnPanStart() { + return onPanStart; + } + + public com.codename1.flutter.gestures.GestureDragUpdateCallback getOnPanUpdate() { + return onPanUpdate; + } + + public com.codename1.flutter.gestures.GestureDragEndCallback getOnPanEnd() { + return onPanEnd; + } + + /** Whether any vertical-drag callback is set. */ + boolean handlesVerticalDrag() { + return onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null; + } + + /** Whether any horizontal-drag callback is set. */ + boolean handlesHorizontalDrag() { + return onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null; + } + + /** Whether any pan callback is set. */ + boolean handlesPan() { + return onPanStart != null || onPanUpdate != null || onPanEnd != null; + } + + public Funcs.VoidFunc0 getOnLongPress() { + return onLongPress; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new GestureRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlay.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlay.java new file mode 100644 index 00000000000..3cc7518975b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlay.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Internal widget of {@link GestureDetector}: the transparent pointer + * overlay mounted after the detected child so its component sits on top of + * the child subtree in the flat container. Not part of the Dart-facing API. + */ +class GestureOverlay extends Widget { + + @Override + public Element createElement() { + return new GestureOverlayRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlayRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlayRenderElement.java new file mode 100644 index 00000000000..3d7dc919de1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureOverlayRenderElement.java @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Graphics; +import com.codename1.ui.accessibility.AccessibilityAction; +import com.codename1.ui.accessibility.AccessibilityRole; + +import dart.runtime.Funcs; + +/** + * Leaf render box for {@link GestureOverlay}: a transparent CN1 component + * (UIID "FlutterGesture", paints nothing, grabs pointer events) sized by the + * parent {@link GestureRenderElement} to exactly the child's bounds. A tap + * is a pointer release inside the bounds that neither activated a drag nor + * followed a long press; long presses ride CN1's built-in long-press + * dispatch. + */ +public class GestureOverlayRenderElement extends RenderElement { + + public GestureOverlayRenderElement(GestureOverlay widget) { + super(widget); + } + + private GestureDetector gesture() { + Element p = parent(); + if (p instanceof GestureRenderElement) { + return ((GestureRenderElement) p).gesture(); + } + return null; + } + + /** + * Runs a gesture callback, and REPORTS anything it throws. + * + *

An exception here travelled up into Codename One's pointer dispatch, which + * catches it, so the gesture did nothing and said nothing. A control that is visibly + * pressed and then simply does not act is the hardest kind of defect to find from a + * screenshot, and the sweep photographs screens at rest so it never sees one at all. + * Whatever the handler does wrong, the error channel should carry it.

+ */ + private void fire(Funcs.VoidFunc0 f) { + if (f == null) { + return; + } + try { + f.call(); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.unimplemented("Gesture", + "a tap handler threw " + t.getClass().getName() + + (t.getMessage() == null ? "" : ": " + t.getMessage())); + } + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Component c = new OverlayComponent(); + publishSemantics(c); + return c; + } + + @Override + protected void updateComponent(Component c) { + publishSemantics(c); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + // The parent hands us tight constraints matching the child's bounds. + return constraints.smallest(); + } + + /** + * Publishes the tap target to the accessibility tree. + * + *

The overlay is the only component that knows a subtree is tappable — + * the child it covers is ordinary content — so without this a + * GestureDetector/InkWell is invisible to screen readers and to anything + * driving the UI through semantics. Mirrors Flutter, which gives a + * GestureDetector with an onTap the button role and a tap action.

+ */ + private void publishSemantics(Component c) { + GestureDetector g = gesture(); + if (g == null || g.getOnTap() == null) { + return; + } + try { + c.getSemantics() + .setRole(AccessibilityRole.BUTTON) + .addAction(new AccessibilityAction(AccessibilityAction.ACTIVATE, null, + new AccessibilityAction.Handler() { + @Override + public boolean perform(Component component, Object argument) { + GestureDetector target = gesture(); + if (target == null || target.getOnTap() == null) { + return false; + } + fire(target.getOnTap()); + return true; + } + })); + } catch (Throwable t) { + // semantics are best-effort; never fail a build over them + } + } + + /** + * The interactive component of THIS gesture's own subtree under {@code (x, y)}, or null. + * + *

The overlay covers its whole subtree and grabs pointer events, which is what lets a + * GestureDetector wrap ordinary content — a label, a card — and still be tappable. But + * it also means a wrapper sitting above a BUTTON swallows that button's presses, and + * Flutter's hit test does the opposite: the innermost target wins. The gallery hits this + * wherever a scrollable or a tappable region contains controls, which is how the app + * bar's overflow button came to render perfectly and do nothing at all.

+ * + *

Walking our own subtree — rather than everything under the point — keeps a + * neighbouring widget's components from being handed events that were never theirs.

+ */ + private Component interactiveTargetAt(int x, int y) { + Element p = parent(); + if (!(p instanceof GestureRenderElement)) { + return null; + } + Element content = ((GestureRenderElement) p).contentElement(); + if (content == null) { + return null; + } + java.util.List hits = new java.util.ArrayList(); + collectInteractive(content, x, y, hits); + // Last in tree order is the topmost, and the deepest — Flutter's winner. + return hits.isEmpty() ? null : hits.get(hits.size() - 1); + } + + private static void collectInteractive(Element e, final int x, final int y, + final java.util.List out) { + if (e == null) { + return; + } + if (e instanceof RenderElement) { + Component c = ((RenderElement) e).component(); + if (c != null && c.isEnabled() && c.contains(x, y) && isInteractive(c)) { + out.add(c); + } + } + e.visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element child) { + collectInteractive(child, x, y, out); + } + }); + } + + /** Components that act on a press of their own — the ones a wrapper must not shadow. */ + private static boolean isInteractive(Component c) { + return c instanceof com.codename1.ui.Button + || c instanceof com.codename1.ui.TextArea + || c instanceof OverlayComponent + || isScrollPane(c); + } + + /** + * A pane that scrolls on its own axis. + * + *

These matter because a DRAG over them is theirs, not ours. CN1 routes a drag from + * whichever component took the press to the nearest scrollable ancestor, so an overlay + * stretched across the page handed every horizontal drag to the page's VERTICAL scroll + * and the inner pane never moved — a data table wide enough to need scrolling simply + * would not.

+ */ + private static boolean isScrollPane(Component c) { + if (!(c instanceof com.codename1.ui.Container)) { + return false; + } + com.codename1.ui.Container container = (com.codename1.ui.Container) c; + return container.isScrollableX() || container.isScrollableY(); + } + + class OverlayComponent extends Component implements InkFeedback.DragAware { + + @Override + public boolean gestureBecameDrag() { + return isDragActivated(); + } + + + private boolean suppressTap; + /** The inner component this press was handed to, if any. */ + private Component forwardTo; + /** Where the press landed, for the slop test in {@link #pointerReleased}. */ + private int pressX; + private int pressY; + /// When the press landed, so a drag can report how fast it was going. + private long pressAt; + + OverlayComponent() { + setUIID("FlutterGesture"); + setGrabsPointerEvents(true); + setFocusable(false); + stylesToSet(this).setBgTransparency(0); + stylesToSet(this).setPadding(0, 0, 0, 0); + stylesToSet(this).setMargin(0, 0, 0, 0); + } + + @Override + public void paint(Graphics g) { + ink.paint(g, this); + } + + @Override + public void pointerPressed(int x, int y) { + suppressTap = false; + pressX = x; + pressY = y; + pressAt = com.codename1.ui.animations.AnimationTime.now(); + forwardTo = interactiveTargetAt(x, y); + if (forwardTo != null) { + // The press belongs to something inside us. We stay CN1's event target, so + // an ancestor scroll still sees the drag, but the tap itself is not ours. + forwardTo.pointerPressed(x, y); + super.pointerPressed(x, y); + return; + } + ink.press(this, x - getAbsoluteX(), y - getAbsoluteY(), inkResponse()); + super.pointerPressed(x, y); + } + + /** The drag this detector claimed for the current press: one of the DRAG_ constants. */ + private int dragAxis = DRAG_NONE; + private int lastX; + private int lastY; + + @Override + public void pointerDragged(int x, int y) { + // A drag this detector has callbacks for is ITS drag, as in Flutter's gesture + // arena where the innermost recognizer wins: once claimed, every move goes to + // those callbacks and no enclosing scrollable moves. An inner scrollable still + // takes precedence. These callbacks used to be stored and never read, so a + // carousel, a slider or a panning surface rendered and never moved. + GestureDetector g = gesture(); + if (dragAxis == DRAG_NONE && g != null && movedBeyondSlop(x, y) + && !(forwardTo != null && isScrollPane(forwardTo))) { + dragAxis = claimAxis(x - pressX, y - pressY, g.handlesVerticalDrag(), + g.handlesHorizontalDrag(), g.handlesPan()); + if (dragAxis != DRAG_NONE) { + // Whatever was pressed inside us is not getting a tap now. + dragInitiated(); + fireDragStart(g); + lastX = pressX; + lastY = pressY; + } + } + if (dragAxis != DRAG_NONE) { + fireDragUpdate(g, x, y); + lastX = x; + lastY = y; + return; + } + // Past the slop this is a scroll, not a tap, and Flutter drops the splash -- + // whether or not Codename One has decided to call it a drag yet. Waiting for + // its verdict leaves a highlight standing on a row the finger has left. + if (movedBeyondSlop(x, y)) { + ink.cancel(this); + if (forwardTo instanceof OverlayComponent) { + ((OverlayComponent) forwardTo).ownInk().cancel(forwardTo); + } + } + // Only a scrollable target gets the drag: handing one to a button would start a + // press it never finishes, and CN1 already treats our own drag as a scroll. + if (forwardTo != null && isScrollPane(forwardTo)) { + forwardTo.pointerDragged(x, y); + return; + } + super.pointerDragged(x, y); + } + + /** This overlay's ink, so a forwarding neighbour can cancel it. */ + InkFeedback ownInk() { + return ink; + } + + /// Whether the pointer travelled far enough for this to be a scroll rather than + /// a tap. See {@link #TOUCH_SLOP_LP}. + private boolean movedBeyondSlop(int x, int y) { + double dx = x - pressX; + double dy = y - pressY; + double slop = com.codename1.flutter.rendering.Dp.px(TOUCH_SLOP_LP); + return dx * dx + dy * dy > slop * slop; + } + + @Override + public void dragInitiated() { + // A drag means the press was a scroll, not a tap: Flutter cancels the splash. + super.dragInitiated(); + ink.cancel(this); + // And whatever we handed the press to. It is not Codename One's event target, + // so nothing else will ever tell it the gesture ended -- its ink would stay + // HELD, and a held press deliberately keeps its highlight standing. That is + // why a mail row in the study went grey when touched and never came back: + // two nested InkWells, the outer forwarding to the inner, and the inner never + // hearing that the finger had moved away. + Component target = forwardTo; + forwardTo = null; + if (target instanceof OverlayComponent) { + ((OverlayComponent) target).dragInitiated(); + } + } + + @Override + public void longPointerPress(int x, int y) { + super.longPointerPress(x, y); + GestureDetector g = gesture(); + if (g != null && g.getOnLongPress() != null) { + suppressTap = true; + fire(g.getOnLongPress()); + } + } + + @Override + public void pointerReleased(int x, int y) { + boolean wasDrag = movedBeyondSlop(x, y); + if (forwardTo != null) { + Component target = forwardTo; + forwardTo = null; + super.pointerReleased(x, y); + // ALWAYS, even when the gesture turned out to be a drag. The target was + // told the pointer went down; a press with no matching release leaves it + // held -- a mail row in the study stayed grey after being touched and + // never came back. A target that is itself one of these works out that it + // was a drag from its own press point and cancels its ink instead of + // firing, which is what Flutter does with a splash a scroll interrupted. + target.pointerReleased(x, y); + suppressTap = false; + return; + } + super.pointerReleased(x, y); + if (wasDrag) { + ink.cancel(this); + } else { + ink.release(this); + } + if (!wasDrag && !suppressTap && contains(x, y)) { + GestureDetector g = gesture(); + if (g != null) { + fire(g.getOnTap()); + } + } else if (dragAxis != DRAG_NONE) { + fireDragEnd(x, y); + } + dragAxis = DRAG_NONE; + suppressTap = false; + } + + private double lp(double px) { + double scale = com.codename1.flutter.rendering.Dp.scale(); + return scale > 0 ? px / scale : px; + } + + private com.codename1.flutter.Offset global(int x, int y) { + return new com.codename1.flutter.Offset(lp(x), lp(y)); + } + + private com.codename1.flutter.Offset local(int x, int y) { + return new com.codename1.flutter.Offset(lp(x - getAbsoluteX()), lp(y - getAbsoluteY())); + } + + private void fireDragStart(GestureDetector g) { + com.codename1.flutter.gestures.GestureDragStartCallback cb = + dragAxis == DRAG_VERTICAL ? g.getOnVerticalDragStart() + : dragAxis == DRAG_HORIZONTAL ? g.getOnHorizontalDragStart() : g.getOnPanStart(); + if (cb == null) { + return; + } + try { + cb.call(new com.codename1.flutter.gestures.DragStartDetails(global(pressX, pressY), + local(pressX, pressY))); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + + private void fireDragUpdate(GestureDetector g, int x, int y) { + if (g == null) { + return; + } + com.codename1.flutter.gestures.GestureDragUpdateCallback cb = + dragAxis == DRAG_VERTICAL ? g.getOnVerticalDragUpdate() + : dragAxis == DRAG_HORIZONTAL ? g.getOnHorizontalDragUpdate() : g.getOnPanUpdate(); + if (cb == null) { + return; + } + double dx = lp(x - lastX); + double dy = lp(y - lastY); + com.codename1.flutter.Offset delta = dragAxis == DRAG_VERTICAL ? new com.codename1.flutter.Offset(0.0, dy) + : dragAxis == DRAG_HORIZONTAL ? new com.codename1.flutter.Offset(dx, 0.0) + : new com.codename1.flutter.Offset(dx, dy); + Double primary = dragAxis == DRAG_VERTICAL ? Double.valueOf(dy) + : dragAxis == DRAG_HORIZONTAL ? Double.valueOf(dx) : null; + try { + cb.call(new com.codename1.flutter.gestures.DragUpdateDetails(global(x, y), local(x, y), + delta, primary)); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + + /** + * Reports the end of the claimed drag, with the speed it finished at. + * + *

Velocity is measured over the WHOLE press rather than the last few moves. + * That understates a flick that began slowly, which is the safe direction to be + * wrong in: it misses a gesture rather than inventing one, and every caller here + * compares against a threshold. (The gallery's splash screen is entered with a + * downward flick and left with an upward one.)

+ */ + private void fireDragEnd(int x, int y) { + GestureDetector g = gesture(); + if (g == null) { + return; + } + com.codename1.flutter.gestures.GestureDragEndCallback cb = + dragAxis == DRAG_VERTICAL ? g.getOnVerticalDragEnd() + : dragAxis == DRAG_HORIZONTAL ? g.getOnHorizontalDragEnd() : g.getOnPanEnd(); + if (cb == null) { + return; + } + long ms = com.codename1.ui.animations.AnimationTime.now() - pressAt; + if (ms <= 0) { + ms = 1; + } + double vx = dragAxis == DRAG_VERTICAL ? 0 : lp(x - pressX) * 1000.0 / ms; + double vy = dragAxis == DRAG_HORIZONTAL ? 0 : lp(y - pressY) * 1000.0 / ms; + Double primary = dragAxis == DRAG_VERTICAL ? Double.valueOf(vy) + : dragAxis == DRAG_HORIZONTAL ? Double.valueOf(vx) : null; + try { + cb.call(new com.codename1.flutter.gestures.DragEndDetails( + new com.codename1.flutter.gestures.Velocity(new com.codename1.flutter.Offset(vx, vy)), + primary)); + } catch (Throwable t) { + com.codename1.flutter.FlutterErrorReport.record(t); + } + } + } + + static final int DRAG_NONE = 0; + static final int DRAG_VERTICAL = 1; + static final int DRAG_HORIZONTAL = 2; + static final int DRAG_PAN = 3; + + /** + * Which drag a detector claims once the pointer has passed the slop: vertical when + * it has vertical callbacks and the motion is mostly vertical, horizontal likewise, + * otherwise a pan if it has pan callbacks -- and none, leaving the motion to an + * enclosing scrollable, when it has no callback for this direction. + */ + static int claimAxis(double dx, double dy, boolean vertical, boolean horizontal, boolean pan) { + double ax = Math.abs(dx); + double ay = Math.abs(dy); + if (vertical && ay >= ax) { + return DRAG_VERTICAL; + } + if (horizontal && ax > ay) { + return DRAG_HORIZONTAL; + } + return pan ? DRAG_PAN : DRAG_NONE; + } + + /// Flutter's {@code kTouchSlop}: how far a pointer may travel and still be a tap. + /// + /// Codename One reports a drag as soon as it sees pointer movement, and taking that + /// as "not a tap" cancels a tap on any tremor -- a finger on glass always moves a + /// pixel or two, so inside a scrollable a control could be pressed, show its ink, and + /// then do nothing. Flutter measures the DISTANCE instead and only stops calling it a + /// tap past 18 logical pixels, which is far short of any real scroll. + private static final double TOUCH_SLOP_LP = 18; + + /** The InkWell/InkResponse configuration for this tap area, or null for a plain gesture. */ + private com.codename1.flutter.material.InkResponse inkResponse() { + GestureDetector g = gesture(); + return g instanceof com.codename1.flutter.material.InkResponse + ? (com.codename1.flutter.material.InkResponse) g : null; + } + + /** + * The Material ink for this tap area: a splash expanding from the touch point plus the + * press highlight underneath it. + * + *

Kept on the ELEMENT rather than the component so it survives the component being + * re-styled or re-configured, and so a subtree rebuild mid-press cannot strand a + * running animation.

+ */ + private final InkFeedback ink = new InkFeedback(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureRenderElement.java new file mode 100644 index 00000000000..29f1197452d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GestureRenderElement.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +import dart.runtime.Funcs; + +/** + * Render element for {@link GestureDetector} (and material InkWell). Owns no + * component itself: it mounts the child at slot 0 and a synthesized + * {@link GestureOverlay} at slot 1 whose transparent component covers the + * child's bounds. Slot order puts the overlay's component AFTER the child + * subtree in the flat container, so it sits on top for pointer dispatch. + */ +public class GestureRenderElement extends RenderElement { + + private Element childElement; + private Element overlayElement; + + public GestureRenderElement(GestureDetector widget) { + super(widget); + } + + GestureDetector gesture() { + return (GestureDetector) widget(); + } + + /** The wrapped content (slot 0) — the subtree the overlay must not shadow. */ + Element contentElement() { + return childElement; + } + + @Override + protected void syncChildren() { + childElement = updateChild(childElement, gesture().getChild(), 0); + overlayElement = updateChild(overlayElement, new GestureOverlay(), 1); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (childElement != null) { + visitor.call(childElement); + } + if (overlayElement != null) { + visitor.call(overlayElement); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = findRenderElement(childElement); + Size cs; + if (child != null) { + cs = child.layout(constraints); + setChildOffset(child, 0, 0); + } else { + cs = constraints.smallest(); + } + RenderElement overlay = findRenderElement(overlayElement); + if (overlay != null) { + overlay.layout(BoxConstraints.tight(cs.width(), cs.height())); + setChildOffset(overlay, 0, 0); + } + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContent.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContent.java new file mode 100644 index 00000000000..a4e52f6a53a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Internal content widget of {@link GridView} — the non-scrolling grid body + * placed inside the scroll boundary. Not part of the Dart-facing API. + */ +class GridContent extends Widget { + + private final long crossAxisCount; + private final Double childAspectRatio; + private final Double mainAxisSpacing; + private final Double crossAxisSpacing; + private final DartList children; + + GridContent(long crossAxisCount, Double childAspectRatio, Double mainAxisSpacing, + Double crossAxisSpacing, DartList children) { + this.crossAxisCount = crossAxisCount; + this.childAspectRatio = childAspectRatio; + this.mainAxisSpacing = mainAxisSpacing; + this.crossAxisSpacing = crossAxisSpacing; + this.children = children; + } + + long getCrossAxisCount() { + return crossAxisCount; + } + + Double getChildAspectRatio() { + return childAspectRatio; + } + + Double getMainAxisSpacing() { + return mainAxisSpacing; + } + + Double getCrossAxisSpacing() { + return crossAxisSpacing; + } + + DartList getChildren() { + return children; + } + + @Override + public Element createElement() { + return new GridContentRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContentRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContentRenderElement.java new file mode 100644 index 00000000000..9d01ccd81ec --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridContentRenderElement.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Grid layout math for {@link GridView}: rows of {@code crossAxisCount} + * tight cells; cell width is the available width divided by the count (minus + * cross-axis spacing), cell height is {@code cellWidth / childAspectRatio} + * (default ratio 1.0). Owns no CN1 component. An unbounded width (a vertical + * grid needs a bounded cross axis) falls back to 100lp cells. + */ +public class GridContentRenderElement extends RenderElement { + + private static final double FALLBACK_CELL_LP = 100; + + private List children = new ArrayList(); + + public GridContentRenderElement(GridContent widget) { + super(widget); + } + + private GridContent grid() { + return (GridContent) widget(); + } + + @Override + protected void syncChildren() { + List newWidgets = new ArrayList(); + if (grid().getChildren() != null) { + for (Widget w : grid().getChildren()) { + if (w != null) { + newWidgets.add(w); + } + } + } + children = updateChildren(children, newWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : children) { + if (c != null) { + visitor.call(c); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + long n = Math.max(1, grid().getCrossAxisCount()); + double ratio = grid().getChildAspectRatio() == null ? 1.0 : grid().getChildAspectRatio(); + if (ratio <= 0) { + ratio = 1.0; + } + double mainSp = grid().getMainAxisSpacing() == null ? 0 : Dp.px(grid().getMainAxisSpacing()); + double crossSp = grid().getCrossAxisSpacing() == null ? 0 : Dp.px(grid().getCrossAxisSpacing()); + + double width = constraints.hasBoundedWidth() + ? constraints.maxWidth() + : n * Dp.px(FALLBACK_CELL_LP) + (n - 1) * crossSp; + double cellW = Math.max(0, (width - (n - 1) * crossSp) / n); + double cellH = cellW / ratio; + + List kids = renderChildren(); + int count = kids.size(); + for (int i = 0; i < count; i++) { + RenderElement kid = kids.get(i); + long row = i / n; + long col = i % n; + kid.layout(BoxConstraints.tight(cellW, cellH)); + setChildOffset(kid, col * (cellW + crossSp), row * (cellH + mainSp)); + } + long rows = (count + n - 1) / n; + double height = rows == 0 ? 0 : rows * cellH + (rows - 1) * mainSp; + return constraints.constrain(new Size(width, height)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTile.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTile.java new file mode 100644 index 00000000000..58054a71ebf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTile.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A single tile of a Material grid — Flutter's {@code GridTile}. An optional + * {@code header}/{@code footer} band (typically a {@link GridTileBar}) overlays the main + * {@code child}, pinned to the top and bottom edges. + * + *

Both bands were dropped before, so every tile in the grid demo lost its caption. + */ +public class GridTile extends StatelessWidget { + + private Widget header; + private Widget footer; + private Widget child; + + public void header(Widget v) { + this.header = v; + } + + public void footer(Widget v) { + this.footer = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getHeader() { + return header; + } + + public Widget getFooter() { + return footer; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + if (header == null && footer == null) { + return child; + } + dart.core.DartList layers = new dart.core.DartList(); + if (child != null) { + layers.add(Positioned.fill(null, null, null, null, null, child)); + } + if (header != null) { + Positioned p = new Positioned(); + p.top(0); + p.left(0); + p.right(0); + p.child(header); + layers.add(p); + } + if (footer != null) { + Positioned p = new Positioned(); + p.bottom(0); + p.left(0); + p.right(0); + p.child(footer); + layers.add(p); + } + Stack stack = new Stack(); + stack.children(layers); + return stack; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTileBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTileBar.java new file mode 100644 index 00000000000..4723e1171f7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridTileBar.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * The header/footer band shown inside a {@link GridTile} — Flutter's + * {@code GridTileBar}: an optional {@code leading} widget, a {@code title} and + * {@code subtitle}, and a {@code trailing} widget over a translucent + * {@code backgroundColor}. This pass lays the pieces out as a horizontal + * {@link Row}; precise Material spacing is deferred. + */ +public class GridTileBar extends StatelessWidget { + + private Color backgroundColor; + private Widget leading; + private Widget title; + private Widget subtitle; + private Widget trailing; + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void leading(Widget v) { + this.leading = v; + } + + public void title(Widget v) { + this.title = v; + } + + public void subtitle(Widget v) { + this.subtitle = v; + } + + public void trailing(Widget v) { + this.trailing = v; + } + + public Widget getTitle() { + return title; + } + + @Override + public Widget build(BuildContext context) { + Column texts = new Column(); + dart.core.DartList lines = new dart.core.DartList(); + if (title != null) { + lines.add(title); + } + if (subtitle != null) { + lines.add(subtitle); + } + texts.children(lines); + + Row row = new Row(); + dart.core.DartList kids = new dart.core.DartList(); + if (leading != null) { + kids.add(leading); + } + kids.add(texts); + if (trailing != null) { + kids.add(trailing); + } + row.children(kids); + return row; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridView.java new file mode 100644 index 00000000000..917a9991593 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridView.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A scrollable grid with a fixed number of cross-axis cells, created via + * Dart's {@code GridView.count} named constructor. Cell width is the + * viewport width divided by {@code crossAxisCount} (minus spacing), cell + * height is {@code cellWidth / childAspectRatio}. + */ +public class GridView extends Widget { + + private long crossAxisCount = 1; + private Double childAspectRatio; + private Double mainAxisSpacing; + private Double crossAxisSpacing; + private EdgeInsets padding; + private DartList children; + private Long itemCount; + private Funcs.Func2 itemBuilder; + + private GridView() { + } + + /** + * Dart's {@code GridView.builder} named constructor. The cross-axis count + * carried by {@code gridDelegate} is not decoded at this milestone (held + * opaquely); items build lazily like {@link com.codename1.flutter.widgets.ListView#builder}. + */ + public static GridView builder(Key key, Long itemCount, + Funcs.Func2 itemBuilder, + Object gridDelegate, EdgeInsets padding, Boolean shrinkWrap, + Object physics) { + GridView g = new GridView(); + g.key(key); + g.itemCount = itemCount; + g.itemBuilder = itemBuilder; + g.padding = padding; + return g; + } + + /** + * Dart's {@code GridView.count} named constructor in canonical positional + * form. + */ + public static GridView count(Key key, String restorationId, Object physics, Boolean primary, + long crossAxisCount, Double childAspectRatio, + Double mainAxisSpacing, Double crossAxisSpacing, + EdgeInsets padding, DartList children) { + GridView g = new GridView(); + g.key(key); + g.crossAxisCount = Math.max(1, crossAxisCount); + g.childAspectRatio = childAspectRatio; + g.mainAxisSpacing = mainAxisSpacing; + g.crossAxisSpacing = crossAxisSpacing; + g.padding = padding; + g.children = children; + return g; + } + + public long getCrossAxisCount() { + return crossAxisCount; + } + + public Double getChildAspectRatio() { + return childAspectRatio; + } + + public Double getMainAxisSpacing() { + return mainAxisSpacing; + } + + public Double getCrossAxisSpacing() { + return crossAxisSpacing; + } + + public EdgeInsets getPadding() { + return padding; + } + + public DartList getChildren() { + return children; + } + + public Long getItemCount() { + return itemCount; + } + + public Funcs.Func2 getItemBuilder() { + return itemBuilder; + } + + public boolean isBuilderMode() { + return itemBuilder != null; + } + + @Override + public Element createElement() { + return new GridViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridViewRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridViewRenderElement.java new file mode 100644 index 00000000000..0f858261217 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/GridViewRenderElement.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; + +/** + * Scroll boundary for {@link GridView}: the content is a {@link GridContent} + * carrying the grid configuration, optionally inset by the padding. + */ +public class GridViewRenderElement extends ScrollRenderElement { + + public GridViewRenderElement(GridView widget) { + super(widget); + } + + @Override + protected Widget buildContent() { + GridView w = (GridView) widget(); + GridContent gc = new GridContent(w.getCrossAxisCount(), w.getChildAspectRatio(), + w.getMainAxisSpacing(), w.getCrossAxisSpacing(), w.getChildren()); + if (w.getPadding() == null) { + return gc; + } + Padding p = new Padding(); + p.padding(w.getPadding()); + p.child(gc); + return p; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasChild.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasChild.java new file mode 100644 index 00000000000..7f0438be0e4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasChild.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; + +/** + * Implemented by single-child wrapper widgets that render their child + * unchanged (accessibility, clipping, hover, tooltip, ...). Lets a single + * {@link PassThroughRenderElement} serve every such widget. + */ +public interface HasChild { + + /** + * The wrapped child widget (may be null). + */ + Widget getChild(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasIcon.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasIcon.java new file mode 100644 index 00000000000..989a6ef93af --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/HasIcon.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.IconData; + +/** + * Implemented by widgets that ARE a glyph without being an {@link Icon} — the + * platform-adaptive icons Flutter ships as small StatelessWidgets. + * + *

The runtime often needs the glyph rather than the widget: a Codename One + * button carries a font icon, not a child component, so a FAB or an IconButton + * asks "what glyph is in here?" and walks down the wrapper chain looking for an + * Icon. A widget that only produces one from its {@code build} is invisible to + * that walk — and, having found nothing, the button drew a default glyph, which + * reads as the wrong button rather than as a missing one.

+ */ +public interface HasIcon { + + /** The glyph this widget stands for, or null when it has none. */ + IconData iconData(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Hero.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Hero.java new file mode 100644 index 00000000000..fac9b5eb5b5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Hero.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Marks a subtree as a shared-element that flies between routes during a + * navigation transition — Flutter's {@code Hero}. This milestone renders the + * {@code child} in place; the cross-route flight animation is deferred. + */ +public class Hero extends StatelessWidget { + + private Object tag; + private Widget child; + private boolean transitionOnUserGestures; + + public void tag(Object v) { + this.tag = v; + } + + public void child(Widget v) { + this.child = v; + } + + public void createRectTween(Object v) { + } + + public void flightShuttleBuilder(Object v) { + } + + public void placeholderBuilder(Object v) { + } + + public void transitionOnUserGestures(boolean v) { + this.transitionOnUserGestures = v; + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Icon.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Icon.java new file mode 100644 index 00000000000..f7067f35ec1 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Icon.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Element; +import com.codename1.flutter.IconData; +import com.codename1.flutter.Widget; + +/** + * A material icon glyph, backed by a CN1 Label with a FontImage + * (UIID "FlutterIcon"). Default size 24 logical pixels. + */ +public class Icon extends Widget { + + private final IconData icon; + private Double size; + private Color color; + private String semanticLabel; + + public Icon(IconData icon) { + this.icon = icon; + } + + public void semanticLabel(String v) { + this.semanticLabel = v; + } + + public String getSemanticLabel() { + return semanticLabel; + } + + public void size(double v) { + this.size = v; + } + + public void color(Color v) { + this.color = v; + } + + public IconData getIcon() { + return icon; + } + + public Double getSize() { + return size; + } + + public Color getColor() { + return color; + } + + @Override + public Element createElement() { + return new IconRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java new file mode 100644 index 00000000000..22416332ad3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IconRenderElement.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.FontImage; +import com.codename1.ui.Label; +import com.codename1.ui.plaf.Style; + +/** + * Leaf render box for {@link Icon}: a CN1 Label carrying a material + * FontImage sized in millimeters equivalent to the requested logical pixels. + */ +public class IconRenderElement extends RenderElement { + + /** Flutter's default icon size in logical pixels. */ + public static final double DEFAULT_SIZE_LP = 24; + + /// Glyph rasterisation cost, and how much of it is REPEATED work: a + /// material icon is drawn into an image per element, so the same glyph at + /// the same size and colour is rasterised once per place it appears. + private static long glyphMs; + private static int glyphCount; + private static final java.util.Set GLYPHS = new java.util.HashSet(); + + /** Icon rasterisations, distinct glyphs among them, and the cost. */ + public static String glyphCost() { + return glyphCount + " icon(s) (" + GLYPHS.size() + " distinct) in " + glyphMs + "ms"; + } + + public IconRenderElement(Icon widget) { + super(widget); + } + + private Icon icon() { + return (Icon) widget(); + } + + /** + * The ambient icon theme, or an empty one. + * + *

Resolved per paint rather than cached: the theme an icon sits under can + * change when an ancestor rebuilds, and an icon that sampled its colour once + * would keep the first one forever.

+ */ + private com.codename1.flutter.material.IconThemeData ambient() { + try { + return com.codename1.flutter.material.IconTheme.of(this); + } catch (Throwable t) { + return new com.codename1.flutter.material.IconThemeData(); + } + } + + private double sizeLp() { + return sizeLp(icon().getSize() != null ? null : ambient()); + } + + private double sizeLp(com.codename1.flutter.material.IconThemeData themed) { + if (icon().getSize() != null) { + return icon().getSize(); + } + Double size = themed == null ? null : themed.size(); + return size != null ? size.doubleValue() : DEFAULT_SIZE_LP; + } + + /** {@code Icon.color}, else the ambient {@code IconTheme}'s, else the default ink. */ + private com.codename1.flutter.Color effectiveColor( + com.codename1.flutter.material.IconThemeData themed) { + if (icon().getColor() != null) { + return icon().getColor(); + } + return themed == null ? null : themed.color(); + } + + @Override + protected Component createComponent() { + if (!com.codename1.ui.Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Label l = new Label("", "FlutterIcon"); + stylesToSet(l).setPadding(0, 0, 0, 0); + stylesToSet(l).setMargin(0, 0, 0, 0); + applyIcon(l); + return l; + } + + @Override + protected void updateComponent(Component c) { + applyIcon((Label) c); + } + + private void applyIcon(Label l) { + if (icon().getIcon() == null) { + l.setIcon(null); + return; + } + // One lookup for both the colour and the size: each is a walk to the + // root of the element tree, and this runs for every icon on screen. + com.codename1.flutter.material.IconThemeData themed = + (icon().getColor() == null || icon().getSize() == null) ? ambient() : null; + Style s = new Style(l.getUnselectedStyle()); + com.codename1.flutter.Color fg = effectiveColor(themed); + if (fg != null) { + s.setFgColor(fg.rgb()); + stylesToSet(l).setFgColor(fg.rgb()); + } + s.setBgTransparency(0); + try { + long g0 = System.currentTimeMillis(); + l.setIcon(FontImage.createMaterial(icon().getIcon().codePoint(), s, + Dp.mm(sizeLp(themed)))); + glyphMs += System.currentTimeMillis() - g0; + glyphCount++; + GLYPHS.add(icon().getIcon().codePoint() + "/" + (int) sizeLp(themed) + + "/" + (fg == null ? -1 : fg.rgb())); + } catch (Exception err) { + // headless or missing icon font: layout still reserves the box + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double px = Dp.px(sizeLp()); + return constraints.constrain(new Size(px, px)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointer.java new file mode 100644 index 00000000000..40a7e4fdda5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointer.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Prevents its subtree from receiving pointer events — Flutter's {@code IgnorePointer}. + * + *

The child renders unchanged and its subtree is made deaf to touch, so a handler + * ABOVE this widget receives a press that lands on top of the child -- which is the whole + * point of it. See {@link IgnorePointerRenderElement}.

+ */ +public class IgnorePointer extends Widget implements HasChild { + + private Boolean ignoring; + private Boolean ignoringSemantics; + private Widget child; + + public void ignoring(Boolean v) { this.ignoring = v; } + + /// Whether the subtree is deaf to touch. Null means the default, which is true. + /// + /// #### Returns + /// + /// the ignoring flag as given, or null when it was never set + public Boolean getIgnoring() { + return ignoring; + } + public void ignoringSemantics(Boolean v) { this.ignoringSemantics = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new IgnorePointerRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointerRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointerRenderElement.java new file mode 100644 index 00000000000..3fdc71ccd79 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IgnorePointerRenderElement.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; +import com.codename1.ui.Component; +import com.codename1.ui.Container; + +/** + * Renders {@link IgnorePointer}'s child and makes that subtree deaf to touch. + * + *

The widget used to be a pure structural pass-through, which renders the right thing + * and is not what the widget is FOR: it exists so that something above it can receive a + * touch that lands on top of its child. With the child still live, whatever it contains + * answers first and the handler above never runs.

+ * + *

That is how the gallery's splash screen became a trap. The splash slides the home + * page down and leaves a strip of it showing; the strip is wrapped in an IgnorePointer + * inside a gesture detector whose tap dismisses the splash, so the ONLY way back is a + * touch that passes through the strip. Ignoring nothing meant the strip's own rows + * answered the tap, the dismiss never fired, and the screen could be entered and not + * left.

+ * + *

Codename One's hit test walks UP from the component it lands on for as long as each + * one ignores pointer events, so the flag has to be set across the whole subtree rather + * than on its root: a touch landing on a deep child would otherwise stop there.

+ */ +public class IgnorePointerRenderElement extends PassThroughRenderElement { + + public IgnorePointerRenderElement(Widget widget) { + super(widget); + } + + /** {@code IgnorePointer.ignoring} defaults to true, as Flutter's does. */ + private boolean ignoring() { + Boolean v = ((IgnorePointer) widget()).getIgnoring(); + return v == null || v.booleanValue(); + } + + @Override + protected void positionChildren(int x, int y) { + super.positionChildren(x, y); + // After the child is placed, so the components it owns exist. A wrapper owns no + // component of its own, so the search descends until it finds the ones that do. + applyToRenderSubtree(this, ignoring()); + } + + private static void applyToRenderSubtree(com.codename1.flutter.RenderElement e, + boolean deaf) { + for (com.codename1.flutter.RenderElement child : e.renderChildren()) { + Component c = child.component(); + if (c != null) { + apply(c, deaf); + } else { + applyToRenderSubtree(child, deaf); + } + } + } + + private static void apply(Component c, boolean deaf) { + c.setIgnorePointerEvents(deaf); + if (c instanceof Container) { + Container g = (Container) c; + int n = g.getComponentCount(); + for (int i = 0; i < n; i++) { + apply(g.getComponentAt(i), deaf); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java new file mode 100644 index 00000000000..0854415b663 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Image.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BoxFit; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.ImageProvider; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * An image, created via Dart's {@code Image.asset} (bundled under the app's + * {@code /assets} resources) or {@code Image.network} named constructors. + * Backed by a CN1 Label (UIID "FlutterImage") carrying an EncodedImage or a + * URLImage. + */ +public class Image extends Widget { + + private String assetName; + private String url; + /// Request headers from a NetworkImage provider; null when there are none. + private java.util.Map headers; + /// The encoded picture a MemoryImage provider carries; null otherwise. + private byte[] memoryBytes; + /// The provider's own key for a memory image, so a new buffer reloads. + private String memoryKey; + /// Encoded pixels per logical pixel for a memory image: MemoryImage's scale. + private double memoryScale = 1; + private Double width; + /// Flutter's decode-size hints. cacheWidth/cacheHeight decode the asset at + /// that many DEVICE pixels, so they bound the picture's intrinsic size -- + /// a 200px decode is 200 device pixels wide however large the file is. + /// + /// They were accepted and dropped, so an image asking to be decoded small + /// took its full intrinsic size instead and laid out several times too + /// large wherever the box did not pin it. + Long cacheWidth; + Long cacheHeight; + + private Double height; + private BoxFit fit; + private ImageProvider imageProvider; + private Funcs.Func4 frameBuilder; + + private Image(String assetName, String url) { + this.assetName = assetName; + this.url = url; + } + + /** Dart's unnamed {@code Image(image: ...)} constructor, in allocate-then-setters form. */ + public Image() { + } + + /** + * {@code Image(image: provider)}: resolves the provider's source into this widget's + * asset/url identity so the render path is unchanged. + */ + public void image(ImageProvider v) { + this.imageProvider = v; + // Providers WRAP: ResizeImage(AssetImage(...)) is the ordinary way to + // ask for a thumbnail, and its own source key reads + // "resize:80x80:asset:...". Matching the prefix on the outermost key + // therefore found no asset at all, so Crane's destination photographs + // never loaded -- and, because nothing had failed, nothing was + // reported either. + ImageProvider inner = v; + for (int depth = 0; inner instanceof com.codename1.flutter.ResizeImage && depth < 8; depth++) { + com.codename1.flutter.ResizeImage rz = (com.codename1.flutter.ResizeImage) inner; + // A ResizeImage is a DECODE instruction, in raw pixels: "give me + // this picture at 80x80 and never hold it larger". Discarding it + // meant a list of thumbnails each kept a full-resolution decode -- + // for the gallery's 3.0x artwork that is roughly a thousand times + // the pixels the screen ever shows. The innermost wrapper wins, + // as it does in Flutter. + if (rz.getWidth() != null) { + resizeWidthPx = rz.getWidth(); + } + if (rz.getHeight() != null) { + resizeHeightPx = rz.getHeight(); + } + inner = rz.getImageProvider(); + } + if (inner != null) { + String key = inner.sourceKey(); + if (key != null && key.startsWith("asset:")) { + this.assetName = key.substring("asset:".length()); + } else if (key != null && key.startsWith("url:")) { + this.url = key.substring("url:".length()); + // The headers go with the URL. Keeping only the URL sent every + // request bare, so an image behind an Authorization header was + // refused on every load and never drawn. + if (inner instanceof com.codename1.flutter.NetworkImage) { + Object h = ((com.codename1.flutter.NetworkImage) inner).getHeaders(); + this.headers = h instanceof java.util.Map && !((java.util.Map) h).isEmpty() + ? (java.util.Map) h : null; + // NetworkImage(url, scale: 2.0) says two encoded pixels make one + // logical one, as MemoryImage's scale does below. It was dropped, so + // such an image came out twice the size it asked for. + double scale = ((com.codename1.flutter.NetworkImage) inner).getScale(); + this.memoryScale = scale > 0 ? scale : 1; + } + } else if (inner instanceof com.codename1.flutter.MemoryImage) { + // The bytes are already here, so there is nothing to resolve: the + // render element decodes them directly. This branch used to report + // the provider as unimplemented and draw nothing. + dart.typed_data.Uint8List data = ((com.codename1.flutter.MemoryImage) inner).getBytes(); + this.memoryBytes = data == null ? null : data.toBytes(); + this.memoryKey = key; + // Kept, not dropped: MemoryImage(bytes, scale: 2.0) says two encoded + // pixels make one logical one. Without it the render element sized + // the picture as if each were one logical pixel -- twice as large + // as asked, with every fit computed against the wrong size. + double scale = ((com.codename1.flutter.MemoryImage) inner).getScale(); + this.memoryScale = scale > 0 ? scale : 1; + } else { + com.codename1.flutter.FlutterErrorReport.unimplemented("ImageProvider", + inner.getClass().getName() + " resolves to no asset or URL (" + + key + "), so nothing will be drawn"); + } + } + } + + public void width(Double v) { + this.width = v; + } + + public void height(Double v) { + this.height = v; + } + + public void fit(Object v) { + this.fit = (v instanceof BoxFit) ? (BoxFit) v : null; + } + + public void excludeFromSemantics(boolean v) { + } + + public void frameBuilder(Funcs.Func4 v) { + this.frameBuilder = v; + } + + /** + * Dart's {@code Image.asset} named constructor in canonical positional + * form. + */ + public static Image asset(String name, Key key, Double width, Double height, BoxFit fit, + String packageName, Boolean excludeFromSemantics, + Boolean gaplessPlayback, Long cacheWidth, Long cacheHeight, + com.codename1.flutter.Color color, Object colorBlendMode, + Object alignment, String semanticLabel) { + // The package qualifier is part of the PATH, exactly as in AssetImage: an asset + // shipped by a package lives at packages//. Dropping it - which is + // what happened while this factory had no such parameter - leaves the image looking + // for a file that is not there, and the widget renders nothing with no error. + Image i = new Image(com.codename1.flutter.AssetImage.qualify(name, packageName), null); + i.key(key); + i.width = width; + i.height = height; + i.fit = fit; + i.cacheWidth = cacheWidth; + i.cacheHeight = cacheHeight; + return i; + } + + /** + * Dart's {@code Image.network} named constructor in canonical positional + * form. + */ + public static Image network(String src, Key key, Double width, Double height, BoxFit fit) { + Image i = new Image(null, src); + i.key(key); + i.width = width; + i.height = height; + i.fit = fit; + return i; + } + + /// The decode size a ResizeImage asked for, in raw pixels, or null. + private Long resizeWidthPx; + private Long resizeHeightPx; + + /** The width a {@code ResizeImage} asked this picture to be decoded at, or null. */ + public Long getResizeWidthPx() { + return resizeWidthPx; + } + + /** The height a {@code ResizeImage} asked this picture to be decoded at, or null. */ + public Long getResizeHeightPx() { + return resizeHeightPx; + } + + public String getAssetName() { + return assetName; + } + + public String getUrl() { + return url; + } + + public Double getWidth() { + return width; + } + + public Double getHeight() { + return height; + } + + public BoxFit getFit() { + return fit; + } + + /** + * A stable identity for the image source, used to detect source changes + * across in-place widget updates. + */ + public String sourceKey() { + if (assetName != null) { + return "asset:" + assetName; + } + if (memoryBytes != null) { + return memoryKey; + } + // The headers are part of what was asked for: a refreshed token must + // fetch again rather than be answered by the load made with the old one. + // Content-derived, the same SHA-256 the storage cache keys by: the header map's + // hashCode is identity-based for a Dart map and 32 bits for any other, so a + // mutated map or a collision kept the key and the old credentials' bytes. + return headers == null ? "url:" + url : "url:" + url + "#" + ImageRenderElement.storageKey(url, headers); + } + + /// Request headers for a network image, or null. + public java.util.Map getHeaders() { + return headers; + } + + /// The encoded bytes of a memory image, or null. + public byte[] getMemoryBytes() { + return memoryBytes; + } + + /// Encoded pixels per logical pixel for a memory or network image -- the provider's + /// {@code scale} (1 unless given). + public double getMemoryScale() { + return memoryScale; + } + + @Override + public Element createElement() { + return new ImageRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageIcon.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageIcon.java new file mode 100644 index 00000000000..77d18b441bd --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageIcon.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BoxFit; +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.ImageProvider; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * An icon rendered from an {@link ImageProvider} rather than an icon font — + * Flutter's {@code ImageIcon}. Captures the image, size and tint; this pass + * reserves the icon's box via a {@link SizedBox}, with the actual image decode + * and tinting deferred to the image layer. + */ +public class ImageIcon extends StatelessWidget { + + private final ImageProvider image; + private Double size; + private Color color; + + public ImageIcon(ImageProvider image) { + this.image = image; + } + + public void size(double v) { + this.size = v; + } + + public void color(Color v) { + this.color = v; + } + + public void semanticLabel(String v) { + } + + public ImageProvider getImage() { + return image; + } + + @Override + public Widget build(BuildContext context) { + double side = size != null ? size.doubleValue() : iconThemeSize(context); + if (image == null) { + SizedBox box = new SizedBox(); + box.width(side); + box.height(side); + return box; + } + // This used to return the empty SizedBox above and nothing else, so an ImageIcon + // reserved its space and drew nothing -- the mail study's logo is one, and its + // bottom bar simply had a gap where the mark belongs. + Image img = new Image(); + img.image(image); + img.width(Double.valueOf(side)); + img.height(Double.valueOf(side)); + // An icon is CONTAINED in its box: Flutter sizes an ImageIcon by the icon theme + // and expects the artwork to fit inside that square whatever its aspect. + img.fit(BoxFit.contain); + return img; + } + + /// {@code IconTheme.of(context).size}, or Material's 24 when there is none -- the + /// same fallback an Icon uses. + private static double iconThemeSize(BuildContext context) { + try { + com.codename1.flutter.material.IconThemeData theme = + com.codename1.flutter.material.IconTheme.of(context); + if (theme != null && theme.size() != null) { + return theme.size().doubleValue(); + } + } catch (Throwable noTheme) { + // fall through to Material's default + } + return 24.0; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageRenderElement.java new file mode 100644 index 00000000000..ece67c06e19 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ImageRenderElement.java @@ -0,0 +1,1069 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BoxFit; +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.io.Log; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.EncodedImage; +import com.codename1.ui.Label; +import com.codename1.ui.URLImage; + +import java.io.InputStream; + +/** + * Leaf render box for {@link Image} (UIID "FlutterImage"). + * + *
    + *
  • asset: loaded from {@code /assets/<name>} on the + * classpath (the app build copies {@code src/main/flutter/assets} + * there) as an EncodedImage.
  • + *
  • network: downloaded (or read from the storage cache) and + * decoded at its natural size, like an asset, so BoxFit sees the real + * aspect ratio; nothing is shown until the bytes arrive.
  • + *
+ * + *

Sizing: explicit width/height tighten the incoming constraints; the + * natural image size fills in unconstrained axes. BoxFit maps onto CN1 image + * scaling at position time: {@code fill} stretches, {@code contain} (the + * default) scales the larger dimension to fit, {@code cover} uses + * {@code Image.fill} (scale smaller dimension, center-crop approximation), + * {@code fitWidth}/{@code fitHeight} scale one axis, {@code none} keeps the + * natural size.

+ */ +public class ImageRenderElement extends RenderElement { + + private com.codename1.ui.Image img; + private String loadedSource; + /** Device pixels per logical pixel in the loaded asset file (1, 2, 3...). */ + private double assetRatio = 1; + + public ImageRenderElement(Image widget) { + super(widget); + } + + private Image image() { + return (Image) widget(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + FittedImage l = new FittedImage(); + stylesToSet(l).setPadding(0, 0, 0, 0); + stylesToSet(l).setMargin(0, 0, 0, 0); + loadImage(l); + return l; + } + + @Override + protected void updateComponent(Component c) { + loadImage((Label) c); + } + + private void loadImage(Label l) { + String source = image().sourceKey(); + if (source.equals(loadedSource)) { + return; + } + loadedSource = source; + img = null; + assetRatio = 1; + try { + if (image().getAssetName() != null) { + FlutterAssets.Resolved res = FlutterAssets.open(getClass(), image().getAssetName()); + if (res == null) { + Log.p("Flutter runtime: asset image not found: " + image().getAssetName() + + " (resource " + FlutterAssets.resourceName(image().getAssetName()) + ")"); + } else { + // Closed here: FlutterAssets.open closes the candidates it rejects + // and hands over the chosen one, and on a port whose resources are + // files or native handles an unclosed stream per image leaks until + // no further resource can be opened. + try { + img = downsample(encodedWithKnownSize(res.stream())); + } finally { + com.codename1.io.Util.cleanup(res.stream()); + } + assetRatio = res.ratio(); + } + } else if (image().getMemoryBytes() != null) { + // MemoryImage: already encoded bytes in hand, decoded the way an + // asset's are -- same size bookkeeping, same downsampling. + img = downsample(encodedWithKnownSize( + new java.io.ByteArrayInputStream(image().getMemoryBytes()))); + // The provider's scale is exactly what an asset variant's ratio is: + // encoded pixels per logical pixel. + assetRatio = image().getMemoryScale(); + } else if (image().getUrl() != null) { + // Fetched and decoded at its NATURAL size, then applied like an asset. + // URLImage with RESIZE_SCALE resampled the download to the placeholder's + // exact box -- a square 100lp one when no size was given -- and this + // element read the placeholder's size as the image's own, so BoxFit + // contain/cover could never recover the real aspect ratio. Until the + // bytes arrive there is simply no image, as in Flutter. + assetRatio = image().getMemoryScale(); // NetworkImage's scale + fetchNetworkImage(l, source, image().getUrl(), image().getHeaders()); + } + } catch (Exception err) { + Log.p("Flutter runtime: could not load image " + source); + Log.e(err); + } + // The NATURAL size is remembered here, at full resolution, because the + // picture may later be re-decoded smaller to fit its box (see + // shrinkToBox) and the size this box reports must not change when that + // happens -- a natural size that shrank would shrink the box, which + // would shrink the picture again. + applyLoaded(l); + } + + /** Takes the current {@code img} as the natural size and hands it to the component. */ + private void applyLoaded(Label l) { + naturalW = img == null ? 0 : img.getWidth(); + naturalH = img == null ? 0 : img.getHeight(); + if (l instanceof FittedImage) { + // The SOURCE, not a scaled copy: FittedImage draws it into its box + // at paint time. See fitNow. + ((FittedImage) l).setSource(img); + l.setIcon(null); + } else { + l.setIcon(img); + } + } + + /// The decoded size of the artwork as it was loaded; see loadImage. + private int naturalW; + private int naturalH; + + /** + * Honours a {@code ResizeImage}: keep the picture at the size it asked to + * be decoded at, and let the full-resolution decode go. + * + *

The scaled copy is the ONLY thing retained — the EncodedImage it came + * from, its bytes and its full-size bitmap all become garbage as this + * method returns. That is the point: the gallery's flight thumbnails ask + * for 80x80 out of artwork that is a thousand times the area, and holding + * the big one to draw the small one is the single largest piece of resident + * memory an image-heavy screen carries.

+ */ + /// Builds an EncodedImage that already knows its own size. + /// + /// EncodedImage.create(stream) leaves width and height unknown, and + /// getWidth() then answers by DECODING the picture -- see + /// EncodedImage.getWidth. loadImage asks for the natural size of every image + /// it mounts, so every asset was fully decoded during the first build merely + /// to be measured. Profiled interpreted on the desktop port, that single + /// getWidth() chain was 39.7% of start-up. + /// + /// The size is in the file's header, so read it from there -- see pngSize + /// for the PNG side -- and hand it to the four-argument create() -- whose own + /// documentation exists for exactly this ("doesn't need to actually traverse + /// the pixels of an image to find out details about it"). The decode then + /// happens when something actually paints the image, and an image that never + /// becomes visible is never decoded at all. + /// + /// Anything whose header is not recognised falls back to the old behaviour, + /// so an unsupported format is slower but never wrong. + /** + * The storage key for a network image: a SHA-256 over the URL and the sorted request + * headers. It was the URL's String.hashCode (plus the header map's), and URLImage + * serves whatever is already stored or pending under a key -- two URLs with the same + * 32-bit hash, which is easy to construct, got each other's picture, including one + * fetched with another user's credentials. + */ + static String storageKey(String url, java.util.Map headers) { + StringBuilder sb = new StringBuilder(url); + if (headers != null && !headers.isEmpty()) { + java.util.List lines = new java.util.ArrayList(); + for (java.util.Map.Entry e : headers.entrySet()) { + lines.add(String.valueOf(e.getKey()) + ": " + String.valueOf(e.getValue())); + } + java.util.Collections.sort(lines); + for (String line : lines) { + sb.append('\n').append(line); + } + } + byte[] digest; + try { + digest = com.codename1.security.Hash.sha256(sb.toString().getBytes("UTF-8")); + } catch (java.io.UnsupportedEncodingException e) { + digest = com.codename1.security.Hash.sha256(sb.toString().getBytes()); + } + // A table rather than Character.forDigit, which ParparVM's JavaAPI lacks. + String digits = "0123456789abcdef"; + StringBuilder hex = new StringBuilder("flutter-img-"); + for (int i = 0; i < digest.length; i++) { + int b = digest[i] & 0xff; + hex.append(digits.charAt(b >> 4)).append(digits.charAt(b & 0xf)); + } + return hex.toString(); + } + + /** Loads a network image from the storage cache, or downloads and caches it. */ + private void fetchNetworkImage(final Label l, final String source, String url, + final java.util.Map headers) { + final String key = storageKey(url, headers); + final com.codename1.io.Storage storage = com.codename1.io.Storage.getInstance(); + if (storage.exists(key)) { + java.io.InputStream in = null; + try { + in = storage.createInputStream(key); + img = downsample(encodedWithKnownSize(in)); + return; + } catch (java.io.IOException e) { + storage.deleteStorageFile(key); // a damaged cache entry: fetch again + } finally { + com.codename1.io.Util.cleanup(in); + } + } + final com.codename1.io.ConnectionRequest req = new com.codename1.io.ConnectionRequest(url, false); + req.setDuplicateSupported(true); + if (headers != null) { + for (java.util.Map.Entry e : headers.entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + req.addRequestHeader(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + } + } + } + req.addResponseListener(new com.codename1.ui.events.ActionListener() { + @Override + public void actionPerformed(com.codename1.io.NetworkEvent evt) { + final byte[] data = req.getResponseData(); + if (data == null || data.length == 0) { + return; + } + com.codename1.ui.CN.callSerially(new Runnable() { + @Override + public void run() { + if (!source.equals(loadedSource) || !isMounted()) { + return; // the widget moved on to another image + } + try { + img = downsample(encodedWithKnownSize(new java.io.ByteArrayInputStream(data))); + } catch (java.io.IOException e) { + Log.p("Flutter runtime: could not decode image " + source); + return; + } + java.io.OutputStream out = null; + try { + out = storage.createOutputStream(key); + out.write(data); + } catch (java.io.IOException e) { + // not cached; it will simply be fetched again next time + } finally { + com.codename1.io.Util.cleanup(out); + } + applyLoaded(l); + markNeedsLayout(); + } + }); + } + }); + com.codename1.io.NetworkManager.getInstance().addToQueue(req); + } + + private static com.codename1.ui.Image encodedWithKnownSize(java.io.InputStream in) + throws java.io.IOException { + byte[] data = com.codename1.io.Util.readInputStream(in); + int w = -1, h = -1; + boolean opaque = false; + if (data.length > 24 && (data[0] & 0xff) == 0x89 && data[1] == 'P' + && data[2] == 'N' && data[3] == 'G') { + int[] size = pngSize(data); + w = size[0]; + h = size[1]; + } else if (data.length > 10 && (data[0] & 0xff) == 0xFF && (data[1] & 0xff) == 0xD8) { + // JPEG: walk the marker segments to the frame header, which carries + // the dimensions. JPEG has no alpha channel, hence opaque. + opaque = true; + int i = 2; + while (i + 9 < data.length) { + if ((data[i] & 0xff) != 0xFF) { + i++; + continue; + } + int marker = data[i + 1] & 0xff; + int len = ((data[i + 2] & 0xff) << 8) | (data[i + 3] & 0xff); + // SOF0-SOF15, excluding the four that are not frame headers. + if (marker >= 0xC0 && marker <= 0xCF + && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) { + h = ((data[i + 5] & 0xff) << 8) | (data[i + 6] & 0xff); + w = ((data[i + 7] & 0xff) << 8) | (data[i + 8] & 0xff); + break; + } + if (len <= 0) { + break; + } + i += 2 + len; + } + } else if (data.length > 10 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F') { + // GIF: logical screen width/height, little-endian, straight after + // the six-byte signature. + w = (data[6] & 0xff) | ((data[7] & 0xff) << 8); + h = (data[8] & 0xff) | ((data[9] & 0xff) << 8); + } + if (w > 0 && h > 0) { + return EncodedImage.create(data, w, h, opaque); + } + return EncodedImage.create(data); + } + + /** + * A PNG's pixel size, read out of its IHDR chunk. + * + *

IHDR is the first chunk in a standards-conforming PNG, and reading its + * payload at a fixed offset is what this used to do. It is NOT first in the + * PNGs an iOS app actually ships: Xcode rewrites every bundled PNG into + * Apple's CgBI form, which puts a four-byte {@code CgBI} chunk in front of + * it. A fixed offset then reads that chunk's payload as the width and its + * checksum as the height, and the picture is drawn to a garbage aspect -- + * the gallery's category icons came out squashed to half their height on + * device while being pixel-exact in every desktop sweep, because the desktop + * copy of the same asset is an ordinary PNG.

+ * + * @return {@code {width, height}}, or {@code {-1, -1}} when no IHDR is found + */ + static int[] pngSize(byte[] data) { + int off = 8; + while (off + 12 <= data.length) { + int len = be32(data, off); + if (len < 0) { + break; + } + if (data[off + 4] == 'I' && data[off + 5] == 'H' + && data[off + 6] == 'D' && data[off + 7] == 'R') { + if (off + 16 > data.length) { + break; + } + return new int[] {be32(data, off + 8), be32(data, off + 12)}; + } + // length + the 4-byte type + the 4-byte CRC + long next = (long) off + 12L + (long) len; + if (next <= off || next > data.length) { + break; + } + off = (int) next; + } + return new int[] {-1, -1}; + } + + private static int be32(byte[] d, int off) { + return ((d[off] & 0xff) << 24) | ((d[off + 1] & 0xff) << 16) + | ((d[off + 2] & 0xff) << 8) | (d[off + 3] & 0xff); + } + + private com.codename1.ui.Image downsample(com.codename1.ui.Image full) { + if (full == null) { + return null; + } + Long tw = image().getResizeWidthPx(); + Long th = image().getResizeHeightPx(); + if (tw == null && th == null) { + return full; + } + int iw = full.getWidth(); + int ih = full.getHeight(); + if (iw <= 0 || ih <= 0) { + return full; + } + // A missing axis keeps the aspect ratio, as ResizeImage does. + int w = tw != null ? (int) tw.longValue() + : Math.max(1, (int) Math.round(iw * (th.doubleValue() / ih))); + int h = th != null ? (int) th.longValue() + : Math.max(1, (int) Math.round(ih * (tw.doubleValue() / iw))); + if (w >= iw && h >= ih) { + return full; // never upscale; that is ResizeImage's rule too + } + try { + return full.scaled(Math.max(1, w), Math.max(1, h)); + } catch (Throwable t) { + return full; + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Double wPx = image().getWidth() == null ? null : Double.valueOf(Dp.px(image().getWidth())); + Double hPx = image().getHeight() == null ? null : Double.valueOf(Dp.px(image().getHeight())); + BoxConstraints inner = constraints.tighten(wPx, hPx); + // A density variant carries `assetRatio` device pixels per logical + // pixel, so its natural size on screen is the decoded size rescaled + // from that density to the screen's — a 3.0x file on a 3x screen is + // 1:1, the same file on a 2x screen is two thirds the size. + double naturalScale = assetRatio > 0 ? Dp.scale() / assetRatio : 1; + // cacheWidth/cacheHeight bound the DECODED bitmap, so they apply in + // decoded pixels -- before the density rescale above, not after it. + double[] decoded = decodeHinted(naturalW, naturalH, + image().cacheWidth, image().cacheHeight); + Size natural = img == null + ? new Size(wPx == null ? 0 : wPx, hPx == null ? 0 : hPx) + : new Size(decoded[0] * naturalScale, decoded[1] * naturalScale); + // Aspect-preserving, which is what RenderImage does. Clamping the axes + // independently gives a box with the constraint's width and the + // picture's own height: an asset laid out under a width-driven fit then + // sat in a box taller than its content, the artwork centred in the + // slack, and everything below it pushed down by half the difference. + // The gallery's lead photo was 46px low that way, and every element + // under it with it. + return inner.constrainSizeAndAttemptToPreserveAspectRatio(natural); + } + + /** + * Applies {@code cacheWidth}/{@code cacheHeight}, Flutter's decode-size + * hints, to the picture's intrinsic size. + * + *

They decode the asset at that many DEVICE pixels, so they bound what + * the image is intrinsically: an attachment asking for a 200px decode is + * 200 device pixels wide however large the file on disk is, and a box + * roomier than that leaves it at its own size rather than blowing it up. + * Ignoring them made every such image lay out at the full asset's size -- + * the mail study's attachment strip drew three photographs across the + * screen where the design has small thumbnails.

+ * + *

One hint given alone carries the other axis with it, which is what + * decoding does: the ratio is a property of the picture, not of the box.

+ */ + /** + * The size a decoded picture presents once {@code cacheWidth} and + * {@code cacheHeight} are taken into account, in the same units as the + * size passed in. + * + *

{@code Image.asset} wraps its provider in a {@code ResizeImage}, so the + * hints bound the decoded bitmap and therefore everything downstream of it: + * the intrinsic size layout constrains, and the size the picture is painted + * at. They never enlarge -- {@code ResizeImage} passes + * {@code allowUpscaling: false} -- and when BOTH are given dart:ui decodes to + * exactly those dimensions, so the aspect ratio is preserved only while one + * of them is left open.

+ */ + static double[] decodeHinted(double w, double h, Long cacheWidth, Long cacheHeight) { + if (w <= 0 || h <= 0) { + return new double[] {w, h}; + } + double tw = cacheWidth != null && cacheWidth.longValue() > 0 + ? Math.min(cacheWidth.doubleValue(), w) : -1; + double th = cacheHeight != null && cacheHeight.longValue() > 0 + ? Math.min(cacheHeight.doubleValue(), h) : -1; + if (tw > 0 && th > 0) { + return new double[] {tw, th}; + } + if (tw > 0) { + return new double[] {tw, h * (tw / w)}; + } + if (th > 0) { + return new double[] {w * (th / h), th}; + } + return new double[] {w, h}; + } + + /** + * The picture's on-screen size in device pixels: the decoded bitmap after + * its decode hints and its density variant. This is what Flutter measures + * against the box, and so the largest size {@code scaleDown} draws it at. + */ + private double[] sourceSizeForFit(com.codename1.ui.Image src) { + double scale = assetRatio > 0 ? Dp.scale() / assetRatio : 1; + double[] d = decodeHinted(src.getWidth(), src.getHeight(), + image().cacheWidth, image().cacheHeight); + return new double[] {d[0] * scale, d[1] * scale}; + } + + @Override + public void position(int x, int y) { + super.position(x, y); + applyFit(); + } + + /** + * Scales the icon into the laid-out box per the BoxFit (best-effort CN1 + * approximation; URLImage instances are left to their adapter). + */ + /// The source image, box and fit the current icon was scaled for. Rescaling resamples + /// the whole bitmap, and position() runs on every layout pass - so without this a + /// carousel that rebuilds its cards per scroll frame re-scaled every card's full-size + /// artwork every frame. Measured on the iOS simulator that was 11ms of paint per pass + /// with 150ms spikes as new cards came into view, i.e. the whole frame budget. + private com.codename1.ui.Image fittedFrom; + private int fittedW = -1; + private int fittedH = -1; + private BoxFit fittedFit; + private int fittedRadius; + + /** + * Whether a deferred fit is already queued for this element. + * + *

Scaling an image DECODES it, and this runs inside layout — so the very + * first frame used to wait for every visible image to decode. Measured on + * the gallery's home screen that was 636ms of a 1187ms first frame, for 15 + * images. Flutter does not do this: {@code Image.asset} resolves + * asynchronously and the first frame paints without the artwork, which then + * appears a frame or two later. Matching that is worth more than any + * micro-optimisation of the decode itself. + * + *

Deferred with {@code callSerially} rather than a background thread on + * purpose: image creation is not safe off the event thread on every port, + * and the win here comes from not blocking the FIRST frame, not from using + * another core.

+ */ + private boolean fitScheduled; + + /** + * Whether artwork may be resolved after the frame that asked for it. + * + *

True only until the first frame is on screen. Deferring FOREVER is + * what Flutter does and is the better model, but it needs the box and the + * bitmap to converge over several frames, and on an image-heavy grid ours + * settles on the wrong crop. Bounding it to start-up takes the whole win + * that matters for cold start — the first frame no longer waits for every + * visible image to decode — without changing steady-state rendering.

+ */ + private static boolean deferFits = true; + + /** Called once the first frame is up; see {@link #deferFits}. */ + public static void firstFrameShown() { + deferFits = false; + } + + private void applyFit() { + Label l = (Label) component(); + if (l == null || img == null || img instanceof URLImage) { + return; + } + if (!com.codename1.ui.Display.isInitialized() || !deferFits) { + fitNow(); + return; + } + if (needsFit() && !fitScheduled) { + fitScheduled = true; + com.codename1.ui.Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + fitScheduled = false; + fitNow(); + } + }); + } + } + + /** Whether the icon in place is not the one this box now wants. */ + private boolean needsFit() { + int bw = (int) Math.round(size().width()); + int bh = (int) Math.round(size().height()); + if (bw <= 0 || bh <= 0) { + return false; + } + BoxFit fit = image().getFit() == null ? BoxFit.scaleDown : image().getFit(); + return !(img == fittedFrom && bw == fittedW && bh == fittedH && fit == fittedFit + && enclosingCornerRadius(bw, bh) == fittedRadius); + } + + private void fitNow() { + Label l = (Label) component(); + if (l == null || img == null || img instanceof URLImage) { + return; + } + int bw = (int) Math.round(size().width()); + int bh = (int) Math.round(size().height()); + double[] src = sourceSizeForFit(img); + int iw = (int) Math.round(src[0]); + int ih = (int) Math.round(src[1]); + if (bw <= 0 || bh <= 0 || iw <= 0 || ih <= 0) { + return; + } + BoxFit fit = image().getFit() == null ? BoxFit.scaleDown : image().getFit(); + int radius = enclosingCornerRadius(bw, bh); + if (img == fittedFrom && bw == fittedW && bh == fittedH && fit == fittedFit + && radius == fittedRadius) { + return; + } + fittedFrom = img; + fittedW = bw; + fittedH = bh; + fittedFit = fit; + fittedRadius = radius; + // A fit that OVERFLOWS the box cannot be rounded by rounding the whole + // picture: `cover` paints a rectangle larger than the component, so the + // picture's rounded corners fall outside the clip and what shows is + // square -- the gallery's carousel cards were once square on device and + // round in every sweep for exactly this reason. FittedImage.paint rounds + // at the BOX instead, drawing only the part of the picture inside it + // (Graphics.drawImageRegionRounded), so where the platform rounds, an + // overflowing fit takes the no-copy path too. + // + // It used to go through the copy below, and the copy held every card + // twice more -- a full ARGB bitmap on the CPU and a second texture on the + // GPU, beside the decoded original: about 2MB a card on a Retina Mac, and + // the largest single item in the gallery's memory at rest. The copy + // remains the path where the platform cannot round. + if ((radius <= 0 || FittedImage.roundsInHardware()) && l instanceof FittedImage) { + // NO COPY. The component draws the decoded source into its own box + // under the fit rule, the way Flutter draws one texture through a + // transform. Materialising a scaled bitmap per image and handing it + // to a Label held the artwork twice -- the decoded original (behind + // its soft reference) and the copy (hard, from the live component) + // -- and it did the scaling on the layout pass that produced the + // first frame. + // + // Rounded corners come through here too WHEN THE PLATFORM CAN DRAW + // THEM. Where it can, the corners are a property of the draw and cost + // nothing: no getRGB, no second bitmap, no second texture, and the + // edge is anti-aliased because the platform computes coverage rather + // than testing a stencil. + // + // Where it cannot, the copy below is still the right answer, and it + // is worth knowing why: painting the source through a shaped CLIP was + // tried and measured worse -- a grid of clipped thumbnails came out + // 12.90% wrong against the reference where the rounded bitmap + // measures 8.03% (`/demo/grid-lists`) -- because Codename One's + // shaped clip has a hard edge and the reference anti-aliases its + // corners. A hardware rounded draw does not have that problem; a + // clip does. + FittedImage f = (FittedImage) l; + f.setSource(img); + f.srcW = iw; + f.srcH = ih; + f.fit = fit; + f.radius = radius; + l.repaint(); + return; + } + long fitStart = System.currentTimeMillis(); + com.codename1.ui.Image scaled; + // Scale to a bitmap, not through the image codec. EncodedImage.scaled() + // defaults to "scaled encoded": it decodes, scales, RE-ENCODES the result + // and keeps the compressed bytes -- and it picks JPEG at quality 0.9 for + // any opaque picture, so the artwork this runtime displays had been + // through a lossy round trip it never needed. The result is decoded again + // on the next line anyway (roundCorners reads getRGB()), so the encode + // bought nothing and cost a codec pass per image inside the layout that + // produces the first frame: 36 of them in the gallery, and the retained + // rasters behind them. + Object prevScaling = restoreScalingTo(); + try { + scaled = scaleUnencoded(img, fit, bw, bh, iw, ih); + } finally { + restoreScaling(prevScaling); + } + // The icon is only looked at when there is no source: FittedImage paints + // its source directly and ignores the icon entirely. Handing it a rounded + // copy while a source was still set meant the copy was built, retained, + // and never drawn -- the picture rendered with square corners and paid + // for round ones. Clear the source so the copy is what shows. + if (l instanceof FittedImage) { + ((FittedImage) l).setSource(null); + } + l.setIcon(roundCorners(scaled, radius)); + scaleMs += System.currentTimeMillis() - fitStart; + scaleCount++; + // The box was laid out before the artwork existed, so the frame that + // showed it empty has to be replaced. + l.repaint(); + } + + /// Turns off {@code encodedImageScaling} for the duration of one scale and + /// returns the previous setting for {@link #restoreScaling}. The property is + /// Codename One's own switch for this: with it off, {@code EncodedImage + /// .scaled} scales the decoded bitmap instead of re-encoding. + private static Object restoreScalingTo() { + try { + com.codename1.ui.Display d = com.codename1.ui.Display.getInstance(); + String prev = d.getProperty("encodedImageScaling", "true"); + d.setProperty("encodedImageScaling", "false"); + return prev; + } catch (Throwable t) { + return null; + } + } + + private static void restoreScaling(Object prev) { + if (prev == null) { + return; + } + try { + com.codename1.ui.Display.getInstance().setProperty("encodedImageScaling", (String) prev); + } catch (Throwable t) { + // leaving it off is safe; it only ever means "scale pixels, not bytes" + } + } + + private static com.codename1.ui.Image scaleUnencoded(com.codename1.ui.Image img, + BoxFit fit, int bw, int bh, int iw, int ih) { + com.codename1.ui.Image scaled; + switch (fit) { + case fill: + scaled = img.scaled(bw, bh); + break; + case cover: + scaled = img.fill(bw, bh); + break; + case fitWidth: + scaled = img.scaled(bw, Math.max(1, ih * bw / iw)); + break; + case fitHeight: + scaled = img.scaled(Math.max(1, iw * bh / ih), bh); + break; + case none: + scaled = img; + break; + case scaleDown: { + double sr = Math.min(Math.min((double) bw / iw, (double) bh / ih), 1.0); + scaled = img.scaled(Math.max(1, (int) Math.round(iw * sr)), + Math.max(1, (int) Math.round(ih * sr))); + break; + } + case contain: + default: + double r = Math.min((double) bw / iw, (double) bh / ih); + scaled = img.scaled(Math.max(1, (int) Math.round(iw * r)), + Math.max(1, (int) Math.round(ih * r))); + break; + } + return scaled; + } + + /** + * A Label that paints its SOURCE image into its own box under a + * {@link BoxFit} rule, instead of holding a scaled copy of it. + * + *

Codename One clips a component's paint to its bounds before calling + * {@code paint}, so {@code cover} — which draws a rectangle larger than the + * box — crops for free, and more faithfully than the whole-image + * {@code Image.fill} approximation it replaces.

+ */ + /** + * Holds at most one image lock, on whatever picture is currently on screen. + * + *

Separated from {@link FittedImage} because this is where the bugs live — + * a lock left on a replaced picture never gets released, and a second lock on + * the same picture never gets balanced — and because it can then be tested at + * all: constructing any {@link com.codename1.ui.Image} needs an initialised + * {@code Display}, which a headless test does not have. The two hooks are + * overridable so a test can drive the bookkeeping with plain objects.

+ */ + static class ImageLock { + + private Object held; + + /// Makes {@code img} the locked image, releasing whatever was locked + /// before. Null means "nothing should be locked". + final void want(Object img) { + if (held == img) { + return; + } + if (held != null) { + unlock(held); + } + held = img; + if (held != null) { + lock(held); + } + } + + final Object held() { + return held; + } + + void lock(Object img) { + ((com.codename1.ui.Image) img).lock(); + } + + void unlock(Object img) { + ((com.codename1.ui.Image) img).unlock(); + } + } + + static final class FittedImage extends Label { + + private com.codename1.ui.Image source; + /// Corner radius in device pixels, drawn by the platform. Only ever set + /// when {@link #roundsInHardware()} is true; otherwise the caller builds + /// a rounded bitmap instead and this stays zero. + int radius; + private final ImageLock lock = new ImageLock(); + BoxFit fit = BoxFit.scaleDown; + /// The source's on-screen size in device pixels once its decode hints and + /// density variant are applied; 0 means "ask the bitmap". + int srcW; + int srcH; + FittedImage() { + super("", "FlutterImage"); + } + + /// Sets the picture to draw, keeping the decode lock on whatever is + /// actually being shown. + /// + /// An {@link com.codename1.ui.EncodedImage} keeps its decoded bitmap behind + /// a soft reference and re-decodes on demand, so a picture that is drawn + /// every frame and collected between them is decoded every frame. Label + /// avoids that by locking its ICON while it is on screen; this component + /// paints its source directly instead of holding a scaled copy as an icon, + /// which is what keeps one bitmap in memory instead of two -- and which + /// also stepped outside the locking Label does for free. Lock and unlock + /// on the same boundary Label uses, so the picture is pinned exactly while + /// it is on screen and collectable the moment it is not. + void setSource(com.codename1.ui.Image img) { + if (source == img) { + return; + } + source = img; + syncLock(); + } + + com.codename1.ui.Image getSource() { + return source; + } + + /// Whether this component is on screen, tracked rather than read back from + /// {@code isInitialized()} so the two transitions drive the lock directly: + /// the framework clears the initialised flag before it calls + /// {@code deinitialize()}, and a lock that depends on the ORDER of those + /// two is a lock that leaks the day the order changes. + private boolean onScreen; + + private void syncLock() { + lock.want(onScreen ? source : null); + } + + @Override + protected void initComponent() { + super.initComponent(); + onScreen = true; + syncLock(); + } + + @Override + protected void deinitialize() { + onScreen = false; + syncLock(); + super.deinitialize(); + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + com.codename1.ui.Image s = source; + if (s == null) { + super.paint(g); + return; + } + int bw = getWidth(); + int bh = getHeight(); + int iw = srcW > 0 ? srcW : s.getWidth(); + int ih = srcH > 0 ? srcH : s.getHeight(); + if (bw <= 0 || bh <= 0 || iw <= 0 || ih <= 0) { + return; + } + double[] r = fittedSize(fit, bw, bh, iw, ih); + double dw = r[0]; + double dh = r[1]; + // Centred in the box, which is what every BoxFit but `fill` means. + int dx = getX() + (int) Math.round((bw - dw) / 2); + int dy = getY() + (int) Math.round((bh - dh) / 2); + // CLIPPED TO THE BOX, because several of the fits deliberately overflow it: + // `cover` scales until the box is covered and spills on the other axis, + // `none` draws the picture at its own size however big that is. Flutter's + // paintImage clips; this did not, so a picture drew outside the component + // that owns it -- and nothing else on the screen expects that. It showed up + // as smearing in the status bar while scrolling the mail list: the avatars + // were painting above the top of the list, over a band that is not part of + // any scrolling content and so is not repainted as the list moves. + int[] clip = g.getClip(); + g.clipRect(getX(), getY(), bw, bh); + boolean overflows = dw > bw + 0.5 || dh > bh + 0.5; + if (radius > 0 && overflows) { + // Rounded AT THE BOX, not at the picture: the picture overflows, + // so its own corners are outside the box and would be clipped + // away square. Draw just the part inside the box, with the box's + // corners rounded -- the platform samples that region of the one + // texture it already has, so no cropped or rounded copy exists. + int vx = Math.max(dx, getX()); + int vy = Math.max(dy, getY()); + int vw = Math.min(dx + (int) Math.round(dw), getX() + bw) - vx; + int vh = Math.min(dy + (int) Math.round(dh), getY() + bh) - vy; + if (vw > 0 && vh > 0) { + int sw = s.getWidth(); + int sh = s.getHeight(); + int sx = (int) Math.round((vx - dx) * sw / dw); + int sy = (int) Math.round((vy - dy) * sh / dh); + int srw = Math.max(1, Math.min(sw - sx, (int) Math.round(vw * sw / dw))); + int srh = Math.max(1, Math.min(sh - sy, (int) Math.round(vh * sh / dh))); + g.drawImageRegionRounded(s, sx, sy, srw, srh, vx, vy, vw, vh, radius); + } + } else if (radius > 0) { + g.drawImageRounded(s, dx, dy, (int) Math.round(dw), (int) Math.round(dh), radius); + } else { + g.drawImage(s, dx, dy, (int) Math.round(dw), (int) Math.round(dh)); + } + g.setClip(clip[0], clip[1], clip[2], clip[3]); + } + + /// Whether the platform rounds a picture's corners as it draws it, in + /// which case no rounded copy has to be built. Resolved once: it is a + /// property of the renderer, not of any one image. + static boolean roundsInHardware() { + if (roundsInHardware == null) { + try { + roundsInHardware = com.codename1.ui.Display.isInitialized() + && com.codename1.ui.Display.getInstance().isRoundedImageSupported(); + } catch (Throwable t) { + roundsInHardware = Boolean.FALSE; + } + } + return roundsInHardware.booleanValue(); + } + + private static Boolean roundsInHardware; + } + + /** + * The size {@code iw x ih} is drawn at inside a box {@code bw x bh} under + * {@code fit} — the whole of BoxFit's arithmetic, free of any Graphics so + * it can be pinned by a test. + * + * @return {@code {width, height}} in device pixels; {@code cover} returns a + * size LARGER than the box, which the component's own clip crops. + */ + static double[] fittedSize(BoxFit fit, double bw, double bh, double iw, double ih) { + switch (fit == null ? BoxFit.scaleDown : fit) { + case fill: + return new double[] {bw, bh}; + case cover: { + double r = Math.max(bw / iw, bh / ih); + return new double[] {iw * r, ih * r}; + } + case fitWidth: + return new double[] {bw, ih * (bw / iw)}; + case fitHeight: + return new double[] {iw * (bh / ih), bh}; + case none: + return new double[] {iw, ih}; + case scaleDown: { + // Flutter's paintImage defaults to scaleDown when the widget + // names no fit, and scaleDown is `contain` that never ENLARGES: + // a picture smaller than its box is drawn at its own size, + // centred in the slack. Treating the default as `contain` blew + // every under-sized picture up to fill its box -- the reply + // study's attachment strip drew its 200px thumbnails at 432px. + double r = Math.min(Math.min(bw / iw, bh / ih), 1.0); + return new double[] {iw * r, ih * r}; + } + case contain: + default: { + double r = Math.min(bw / iw, bh / ih); + return new double[] {iw * r, ih * r}; + } + } + } + + /// Cumulative cost of decoding and rescaling artwork, which happens inside + /// layout and therefore inside the first frame. Read through + /// {@link #scalingCost()}; see the startup trace in FlutterUI. + private static long scaleMs; + private static int scaleCount; + + /** How much of the frame went into decoding and rescaling images. */ + public static String scalingCost() { + return scaleCount + " image(s) in " + scaleMs + "ms"; + } + + /** + * The corner radius this image has to round into its own bitmap, or 0. + * + *

Non-zero only when the image exactly fills a clipping {@link + * com.codename1.flutter.material.Material} with rounded corners — i.e. when the image + * IS the card's surface and its own square corners are what you would see.

+ * + *

Flutter expresses this as a clip and so does this runtime, but a clip is only as + * good as the port underneath: on Codename One's iOS Metal backend a polygon clip masks + * geometry (a {@code fillRect} through it comes out round) and does NOT mask a textured + * quad, so the card's artwork paints straight over the rounded corners. Rounding the + * bitmap once, when it is scaled, does not depend on the clip at all — and costs + * nothing per frame, which a clip does.

+ */ + private int enclosingCornerRadius(int bw, int bh) { + for (com.codename1.flutter.Element e = parent(); e != null; e = e.parent()) { + if (!(e instanceof com.codename1.flutter.material.MaterialRenderElement)) { + continue; + } + com.codename1.flutter.material.MaterialRenderElement m = + (com.codename1.flutter.material.MaterialRenderElement) e; + int r = m.clipRadiusPx(); + if (r <= 0) { + return 0; + } + // Only when this image really is the surface. An image inset inside a card has + // square corners in Flutter too, and rounding it would be wrong. + com.codename1.flutter.rendering.Size ms = m.size(); + if (ms == null || Math.abs(ms.width() - bw) > 1 || Math.abs(ms.height() - bh) > 1) { + return 0; + } + return Math.min(r, Math.min(bw, bh) / 2); + } + return 0; + } + + /** + * Returns {@code src} with its corners cut to {@code radius}, transparent outside the + * curve. Touches only the four corner squares, so the cost is the pixel copy rather + * than the rounding. + */ + private static com.codename1.ui.Image roundCorners(com.codename1.ui.Image src, int radius) { + if (radius <= 0) { + return src; + } + int w = src.getWidth(); + int h = src.getHeight(); + int[] argb = src.getRGB(); + roundCornersInPlace(argb, w, h, radius); + return com.codename1.ui.Image.createImage(argb, w, h); + } + + /// Clears the alpha of every pixel lying outside the four corner arcs. Package-private + /// and free of any Image so the geometry can be asserted headlessly. + static void roundCornersInPlace(int[] argb, int w, int h, int radius) { + int r = Math.min(radius, Math.min(w, h) / 2); + for (int cy = 0; cy < r; cy++) { + for (int cx = 0; cx < r; cx++) { + // Distance from the centre of THIS corner's arc; outside it, clear alpha. + double dx = r - 0.5 - cx; + double dy = r - 0.5 - cy; + if (dx * dx + dy * dy <= (double) r * r) { + continue; + } + argb[cy * w + cx] = 0; // top left + argb[cy * w + (w - 1 - cx)] = 0; // top right + argb[(h - 1 - cy) * w + cx] = 0; // bottom left + argb[(h - 1 - cy) * w + (w - 1 - cx)] = 0; // bottom right + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStack.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStack.java new file mode 100644 index 00000000000..537cf1d1825 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStack.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Shows a single child of a stack by {@code index}, keeping the others in the + * tree — Flutter's {@code IndexedStack}. See {@link IndexedStackRenderElement} + * for how the unselected children are kept alive without being drawn. + */ +public class IndexedStack extends Widget { + + private Object alignment; + private Object textDirection; + private Object sizing; + private long index; + private DartList children; + + public void alignment(Object v) { this.alignment = v; } + public void textDirection(Object v) { this.textDirection = v; } + public void sizing(Object v) { this.sizing = v; } + public void index(long v) { this.index = v; } + + public void children(DartList v) { + this.children = v; + } + + public long getIndex() { + return index; + } + + public DartList getChildren() { + return children; + } + + @Override + public Element createElement() { + return new IndexedStackRenderElement(this, new SimpleChildrenRenderElement.Children() { + @Override + public DartList get() { + return children; + } + }); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStackRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStackRenderElement.java new file mode 100644 index 00000000000..32b6370c9bf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IndexedStackRenderElement.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +import java.util.List; + +/** + * Lays out every child but shows only the one at {@code index} — Flutter's + * {@code IndexedStack}. + * + *

All of them used to be laid out AND painted, at the same origin. Crane's + * back layer is an {@code IndexedStack} over the Fly / Sleep / Eat forms, so + * the study came up with all three printed on top of each other: "Travelers" + * over "Diners", "Choose Origin" over "Select Location".

+ * + *

The unselected children stay in the tree — that is the whole point of an + * IndexedStack over a conditional child, and what keeps a hidden form's state + * alive — but they are laid out against a zero-size constraint, which is how a + * subtree renders nothing here without being torn down.

+ */ +public class IndexedStackRenderElement extends SimpleChildrenRenderElement { + + public IndexedStackRenderElement(Widget widget, Children provider) { + super(widget, provider); + } + + /** The child to show; out-of-range indices show nothing, as in Flutter. */ + private int selected() { + Widget w = widget(); + return w instanceof IndexedStack ? (int) ((IndexedStack) w).getIndex() : 0; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + List rc = renderChildren(); + int idx = selected(); + BoxConstraints loose = constraints.loosen(); + BoxConstraints none = BoxConstraints.tight(0, 0); + Size shown = new Size(0, 0); + for (int i = 0; i < rc.size(); i++) { + RenderElement k = rc.get(i); + Size cs = k.layout(i == idx ? loose : none); + if (i == idx) { + shown = cs; + } + setChildOffset(k, 0, 0); + } + return constraints.constrain(shown); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedElement.java new file mode 100644 index 00000000000..41a8385f5e4 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedElement.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.StatelessElement; +import com.codename1.flutter.Widget; + +import java.util.ArrayList; +import java.util.List; + +/** + * Element for an {@link InheritedWidget}: remembers who read it, and rebuilds them when it + * changes — Flutter's {@code InheritedElement}. + * + *

Without this, {@code dependOnInheritedWidgetOfExactType} is a plain ancestor search: a + * consumer gets the value that was current the first time it built and never hears about + * another. Everything context-delivered depends on it — the gallery's options model, the + * theme, provider — so a setting could be changed and nothing that read it would notice.

+ * + *

Order matters. Dependents are notified AFTER {@code super.update} has swapped + * the widget and rebuilt this element's own subtree, which is where Flutter puts it + * ({@code ProxyElement.update} runs {@code updated()} then rebuilds; the notification takes + * effect on the dependents' own rebuild). Notifying first — before this element's child + * tree has been rebuilt — marks dependents dirty against a tree that is about to be + * replaced underneath them, and the elements they rebuilt into are unmounted moments later. + * That renders as whole pages going blank, with no error anywhere, and it passes every + * headless test.

+ */ +public class InheritedElement extends StatelessElement { + + private final List dependents = new ArrayList(); + + public InheritedElement(InheritedWidget widget) { + super(widget); + } + + /** + * Adds itself to what its subtree can see (see Element.publishAsInherited) BEFORE the + * first build mounts that subtree. It used to happen after mount returned, by which + * time the initial descendants had already copied the parent's map without this + * widget in it. Under a root with no map the lookups fell back to walking ancestors + * and found it anyway, which is why a headless test never noticed; under a real app, + * whose map is never empty, a nested inherited widget was invisible to its children. + */ + @Override + protected void firstBuild() { + publishAsInherited(); + super.firstBuild(); + } + + /** Registers {@code e} as reading this widget; idempotent, since a rebuild re-reads. */ + public void addDependent(Element e) { + if (e != null && !dependents.contains(e)) { + dependents.add(e); + } + } + + public void removeDependent(Element e) { + dependents.remove(e); + } + + /** How many elements currently depend on this one; for tests. */ + int dependentCount() { + return dependents.size(); + } + + @Override + public void update(Widget newWidget) { + Widget old = widget(); + super.update(newWidget); + if (old == newWidget || !(old instanceof InheritedWidget) + || !(newWidget instanceof InheritedWidget)) { + return; + } + if (((InheritedWidget) newWidget).updateShouldNotify((InheritedWidget) old)) { + notifyDependents(); + } + } + + /** + * Marks the readers dirty — but not before the build that changed this widget has + * finished. + * + *

This runs from inside {@code update()}, which is itself inside a build flush that + * is part-way through rebuilding this subtree. Marking a dependent dirty at that moment + * puts it back in the queue while its ancestors are still being replaced around it, and + * the flush then rebuilds it against a tree that is torn down underneath it: the page + * renders blank, with no error, and every headless test still passes. Deferring to a + * serial call means the whole tree is consistent before any reader is asked to rebuild, + * which is the same guarantee {@code setState} already relies on.

+ */ + /// Whether {@code e}'s widget is named in a comma-separated allow-list. Used to notify + /// readers a few at a time while working out which rebuild is destructive - the census + /// says WHO reads the value, this says which of them get told. + private static boolean matches(Element e, String csv) { + Widget w = e.widget(); + if (w == null) { + return false; + } + // Scanned by hand rather than with String.split: ParparVM's Java API does not + // carry the regex-based String methods, so split compiles on the desktop and fails + // the iOS translation with an undeclared-function error. + String n = w.getClass().getSimpleName(); + int from = 0; + while (from <= csv.length()) { + int comma = csv.indexOf(',', from); + int end = comma < 0 ? csv.length() : comma; + if (n.equals(csv.substring(from, end).trim())) { + return true; + } + if (comma < 0) { + break; + } + from = comma + 1; + } + return false; + } + + private void notifyDependents() { + if (dependents.isEmpty()) { + return; + } + // Snapshot: a dependent's rebuild re-runs its lookups and re-registers, mutating + // this list. + final List snapshot = new ArrayList(dependents); + Runnable mark = new Runnable() { + @Override + public void run() { + String only = com.codename1.ui.Display.isInitialized() + ? com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.inheritedOnly", "") + : ""; + for (int i = 0; i < snapshot.size(); i++) { + Element e = snapshot.get(i); + // Re-checked here, not at snapshot time: an element that left the tree + // in the meantime must never be scheduled. + if (!e.isMounted()) { + dependents.remove(e); + continue; + } + if (only != null && only.length() > 0 && !matches(e, only)) { + continue; + } + e.didChangeDependencies(); + } + } + }; + if (!com.codename1.ui.Display.isInitialized()) { + mark.run(); // headless tests drive the flush themselves + return; + } + if ("true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.inheritedCensus", "false"))) { + StringBuilder who = new StringBuilder(); + for (int i = 0; i < snapshot.size() && i < 12; i++) { + if (who.length() > 0) { + who.append(", "); + } + Widget w = snapshot.get(i).widget(); + who.append(w == null ? "?" : w.getClass().getSimpleName()); + } + com.codename1.flutter.FlutterErrorReport.unimplemented("InheritedNotify", + snapshot.size() + " dependents of " + + (widget() == null ? "?" : widget().getClass().getSimpleName()) + + ": " + who); + } + // Deferred, not inline: this runs from update(), part-way through a build flush + // that is still replacing this subtree, and a reader marked dirty at that moment + // rebuilds against a tree being torn down around it. + // + // This was gated off for a while because turning it on emptied whole pages. That + // turned out not to be a fault of the notification at all: a rebuilt subtree can + // contain scroll and effect panes carrying RenderHosts of their own, and the build + // flush only revalidated the host the rebuilt element was mounted into - so a + // nested host kept children it never laid out. Fixed in BuildOwner, and readers are + // notified normally now. + com.codename1.ui.CN.callSerially(mark); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedWidget.java new file mode 100644 index 00000000000..306794b77ba --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InheritedWidget.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Flutter's InheritedWidget: a widget that exposes itself to descendants via + * {@link BuildContext#dependOnInheritedWidgetOfExactType(Class)} and otherwise + * renders its single {@code child}. Application subclasses (PageStatus, + * LayoutCache, CodeStyle, ...) extend this and add their own fields; the lookup + * is by runtime type, so no per-type wiring is required. + * + *

Rendered as a {@link StatelessWidget} whose {@code build} returns the child; the + * {@link InheritedElement} it produces sits in the tree as the discoverable ancestor, and + * is what remembers the descendants that read it so {@link #updateShouldNotify} can rebuild + * them.

+ */ +/* + * Implements HasChild so that the helpers which walk down a wrapper chain -- + * "what glyph is inside this button?", "what text is inside this label?" -- + * can see THROUGH an inherited widget. IconTheme and DefaultTextStyle are + * inherited widgets, and they are exactly what a theme puts between a button + * and its icon: the gallery wraps every study's back arrow in an IconTheme, and + * the FAB's glyph search stopped there and fell back to a plus sign. + */ +public class InheritedWidget extends StatelessWidget implements HasChild { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** + * Whether descendants that read this widget should rebuild — Flutter's + * {@code updateShouldNotify}. Defaults to true: a rebuilt inherited widget usually + * carries a new value, and a false negative is invisible (stale UI) where a false + * positive only costs a rebuild. + */ + public boolean updateShouldNotify(InheritedWidget oldWidget) { + return true; + } + + @Override + public Element createElement() { + return new InheritedElement(this); + } + + @Override + public Widget build(BuildContext context) { + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InkFeedback.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InkFeedback.java new file mode 100644 index 00000000000..a8a2d2f5fab --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InkFeedback.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.material.InkResponse; +import com.codename1.flutter.rendering.Dp; +import com.codename1.ui.Component; +import com.codename1.ui.Graphics; +import com.codename1.ui.animations.Animation; + +/** + * Material touch feedback for one tap area: the splash that expands from the touch point + * and the press highlight underneath it — Flutter's {@code InkResponse} ink. + * + *

Two overlapping effects, because Material uses both and they read very differently + * with only one of them: the HIGHLIGHT is a flat wash over the whole target that fades in + * while the finger is down and says "this is pressed"; the SPLASH is a circle growing from + * the exact touch point that says "this is where you touched". Neither alone feels right.

+ * + *

Timings follow Flutter's InkRipple: the splash keeps growing while the finger is held + * and only fades once the press is confirmed, so a long press does not leave a + * half-finished circle sitting on screen.

+ * + *

It rides Codename One's animation loop rather than an + * {@code AnimationController}: this is component-level feedback with no widget of its own, + * and it must keep running after the press that started it — including through a rebuild + * of the subtree it belongs to.

+ */ +final class InkFeedback { + + /// Growth of the ripple while the finger is still down. + /// + /// A full second, and that is the point: unconfirmed growth is slow enough to read as + /// a circle travelling outward. Growing it in a third of a second instead -- which + /// this did -- means the disc has already covered the target before the eye finds it, + /// so the whole effect registers as the row flashing a flat grey rather than as ink. + private static final long UNCONFIRMED_MS = 1000; + /// Growth once the press is confirmed: the SAME progress, finished at this rate. + private static final long RADIUS_MS = 225; + /// Fade of the ink in. + private static final long FADE_IN_MS = 75; + /// Fade of the ink out, once confirmed. + private static final long FADE_OUT_MS = 375; + /// ... of which the ink holds full opacity for this fraction before it starts to go. + private static final double FADE_OUT_HOLD = 225.0 / 375.0; + /// Fade of the flat press highlight, both directions. + private static final long HIGHLIGHT_MS = 200; + + /// The ripple begins as a disc this fraction of its target, never at nothing, and + /// finishes slightly past the target so no seam shows at the edge. + private static final double START_RADIUS_FRACTION = 0.30; + private static final double RADIUS_OVERSHOOT = 5; + + /// Default ink, as ARGB. The alpha travels WITH the colour because a caller's + /// explicit splash colour carries its own, and masking it off left every custom ink + /// at the default weight. + /// + /// Deliberately NOT ThemeData's splashColor/highlightColor defaults (40% of a light + /// grey). Those are the Material 2 fallbacks; what a Material 3 ink actually paints + /// is a state layer at a fraction of onSurface, and it is far lighter. Measured + /// against the reference on a pressed mail card: 100ms into the press its card is + /// still clean white, while the 40% wash covered the whole card -- text, sender and + /// icon all visibly greyed -- which reads as the row having changed colour rather + /// than as ink. + private static final int LIGHT_SPLASH_ARGB = 0x12000000; + private static final int LIGHT_HIGHLIGHT_ARGB = 0x0F000000; + private static final int DARK_INK_ARGB = 0x12FFFFFF; + + private double originX; + private double originY; + private double targetRadius; + private int splashArgb = LIGHT_SPLASH_ARGB; + private int highlightArgb = LIGHT_HIGHLIGHT_ARGB; + + /// Where the radius and the highlight had got to when the finger lifted. + /// + /// The confirmed growth does not restart: it carries the same normalized progress on + /// at the faster rate, so a ripple released early finishes from where it stood rather + /// than jumping back to the start. + private double progressAtRelease; + private double highlightAtRelease; + + /// Start of the growth phase, and of the fade once released, on the ANIMATION clock. + /// + /// Not the wall clock. Ink is an animation like any other, and reading the wall clock + /// made this one the only thing on screen that a frozen clock could not hold still: + /// asked for the frame at 100ms the ripple showed however far real time had carried + /// it, so it saturated within two frames and then sat there. On a device that reads + /// as ink that arrives too fast; in a test it is simply not reproducible. + private long startedAt; + private long releasedAt; + private boolean held; + private boolean active; + + private Animation clock; + + /** + * A target that can say whether Codename One has turned the gesture into a drag. + * + *

{@code Component.isDragActivated()} is protected, so only the component itself + * can answer. The clock needs the answer because once a drag starts, Codename One + * delivers the rest of the gesture -- the release included -- to whatever is + * scrolling, and nothing will ever release this ink.

+ */ + interface DragAware { + boolean gestureBecameDrag(); + } + + // ------------------------------------------------------------------ + + void press(Component c, int x, int y, InkResponse config) { + if (config == null) { + return; + } + originX = x; + originY = y; + boolean light = isLight(backgroundUnder(c)); + splashArgb = resolveArgb(config.getSplashColor(), config.getHighlightColor(), + light ? LIGHT_SPLASH_ARGB : DARK_INK_ARGB); + highlightArgb = resolveArgb(config.getHighlightColor(), config.getSplashColor(), + light ? LIGHT_HIGHLIGHT_ARGB : DARK_INK_ARGB); + targetRadius = radiusFor(c, config, x, y); + startedAt = com.codename1.flutter.animation.MotionClock.now(); + releasedAt = 0; + progressAtRelease = 0; + highlightAtRelease = 0; + held = true; + active = true; + attach(c); + c.repaint(); + } + + /** The press became a tap: finish growing, then fade out. */ + void release(Component c) { + if (!active || !held) { + return; + } + held = false; + long now = com.codename1.flutter.animation.MotionClock.now(); + progressAtRelease = clamp01((now - startedAt) / (double) UNCONFIRMED_MS); + highlightAtRelease = clamp01((now - startedAt) / (double) HIGHLIGHT_MS); + releasedAt = now; + // The clock may have stopped itself while the press was held (see animate()); the + // fade still needs frames, so make sure it is running again. + attach(c); + c.repaint(); + } + + /** + * The press turned into a scroll. Flutter drops the ink immediately in that case — + * leaving it to fade would paint a splash on a list that is already moving under the + * finger, which reads as a mis-tap. + */ + void cancel(Component c) { + if (!active) { + return; + } + active = false; + held = false; + detach(c); + c.repaint(); + } + + // ------------------------------------------------------------------ + + void paint(Graphics g, Component c) { + if (!active) { + return; + } + long now = com.codename1.flutter.animation.MotionClock.now(); + // One normalized progress for the radius, advanced at whichever rate applies: + // slowly while the finger is down, then finished quickly once the tap is + // confirmed. It never restarts, so the circle does not jump on release. + double progress = held + ? clamp01((now - startedAt) / (double) UNCONFIRMED_MS) + : clamp01(progressAtRelease + (now - releasedAt) / (double) RADIUS_MS); + double fadeIn = clamp01((now - startedAt) / (double) FADE_IN_MS); + // The ink holds full opacity for the first stretch of the fade and only then + // starts to go, so a quick tap still shows a complete ripple. + double fadeOut = held ? 0 + : clamp01((clamp01((now - releasedAt) / (double) FADE_OUT_MS) + - FADE_OUT_HOLD) / (1 - FADE_OUT_HOLD)); + double ink = fadeIn * (1 - fadeOut); + double highlight = held + ? clamp01((now - startedAt) / (double) HIGHLIGHT_MS) + : clamp01(highlightAtRelease - (now - releasedAt) / (double) HIGHLIGHT_MS); + + int oldColor = g.getColor(); + int oldAlpha = g.getAlpha(); + // PARENT-relative, not absolute: inside Component.paint the Graphics has already + // accumulated every ancestor's translation, so absolute coordinates land at roughly + // twice the offset and the clip below then intersects to nothing. + int cx = c.getX(); + int cy = c.getY(); + int w = c.getWidth(); + int h = c.getHeight(); + + int clipX = g.getClipX(); + int clipY = g.getClipY(); + int clipW = g.getClipWidth(); + int clipH = g.getClipHeight(); + // Ink is CONTAINED: it must not bleed past the tap target, and a splash from a + // corner is wider than the box by construction. + g.clipRect(cx, cy, w, h); + try { + if (highlight > 0) { + g.setColor(highlightArgb & 0xFFFFFF); + g.setAlpha((int) Math.round(((highlightArgb >>> 24) & 0xFF) * highlight)); + g.fillRect(cx, cy, w, h); + } + double start = targetRadius * START_RADIUS_FRACTION; + double r = start + (targetRadius + RADIUS_OVERSHOOT - start) + * com.codename1.flutter.animation.Curves.ease.transform(progress); + if (r > 0 && ink > 0) { + g.setColor(splashArgb & 0xFFFFFF); + g.setAlpha((int) Math.round(((splashArgb >>> 24) & 0xFF) * ink)); + int d = (int) Math.round(r * 2); + g.fillArc((int) Math.round(cx + originX - r), + (int) Math.round(cy + originY - r), d, d, 0, 360); + } + } finally { + g.setAlpha(oldAlpha); + g.setColor(oldColor); + g.setClip(clipX, clipY, clipW, clipH); + } + + if (!held && fadeOut >= 1) { + active = false; + detach(c); + } + } + + // ------------------------------------------------------------------ + + private void attach(Component c) { + com.codename1.ui.Form f = c.getComponentForm(); + if (f == null || clock != null) { + return; + } + final Component target = c; + clock = new Animation() { + @Override + public boolean animate() { + // Expire on the CLOCK, not in paint(). paint() only runs while the + // component is actually being painted, so ink on a component that scrolls + // away or stops repainting would stay "active" forever and keep this + // clock - and its repaint - running for the life of the form. + long now = com.codename1.flutter.animation.MotionClock.now(); + if (active && !held && now - releasedAt >= FADE_OUT_MS) { + active = false; + } + // The gesture turned into a drag. Codename One then delivers the rest of + // it -- including the release -- to whatever is scrolling, so nothing will + // ever release this ink and a HELD press keeps its highlight standing + // (deliberately, see below). A mail row in the study went grey when + // touched and stayed grey for exactly this: pressed inside a scrollable, + // the finger moved a pixel, and the release went to the list. + if (active && held && target instanceof DragAware + && ((DragAware) target).gestureBecameDrag()) { + active = false; + held = false; + target.repaint(); + detach(target); + return false; + } + // A held press that never releases would otherwise animate for the life of + // the form: the expiry above only fires once the finger is up. A real + // finger always lifts, but a press whose release is swallowed - the + // component removed by a rebuild mid-press, a cancelled gesture - would + // leave this clock repainting forever. Once the splash has fully covered + // the target there is nothing left to animate anyway, so stop asking for + // frames and let the static ink stand until release. + if (active && held && now - startedAt >= UNCONFIRMED_MS) { + detach(target); + return false; + } + if (active) { + // Repaint the COMPONENT, and always return false. + // + // Returning true from a registered Animation that is not a Component + // makes paintDirty set the flush region to the whole screen, call + // paint() on the animation - which paints nothing here, the ink is + // drawn by the component - and then flush the entire screen. That + // pushes a buffer this frame never painted into, which is visible as + // a full-screen flicker for as long as any ink is running. + // + // Repainting the component instead queues it with a real dirty + // region, so only the tap target is flushed. + target.repaint(); + } else { + detach(target); + } + return false; + } + + @Override + public void paint(Graphics g) { + } + }; + f.registerAnimated(clock); + } + + private void detach(Component c) { + if (clock == null) { + return; + } + com.codename1.ui.Form f = c == null ? null : c.getComponentForm(); + if (f != null) { + f.deregisterAnimated(clock); + } + clock = null; + } + + /** + * Flutter's target radius: far enough to cover the whole box from wherever the finger + * landed, so the splash never stops short of a corner. + */ + private static double radiusFor(Component c, InkResponse config, double x, double y) { + if (config.getRadius() != null) { + return Dp.px(config.getRadius()); + } + double w = c.getWidth(); + double h = c.getHeight(); + double dx = Math.max(x, w - x); + double dy = Math.max(y, h - y); + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * The ink colour: the widget's own splashColor when it names one, else a neutral + * derived from the surface it sits on — dark ink on light surfaces and light ink on + * dark ones, which is what Material's onSurface state layer amounts to. + */ + /// The first colour that was actually given, ALPHA INCLUDED, else the default. + /// + /// A caller states one of the two far more often than both, and the reference falls + /// back to the other one rather than to its theme default in that case -- an ink that + /// names only a splash colour should not highlight in an unrelated grey. + private static int resolveArgb(com.codename1.flutter.Color preferred, + com.codename1.flutter.Color fallback, int defaultArgb) { + com.codename1.flutter.Color explicit = preferred != null ? preferred : fallback; + if (explicit == null) { + return defaultArgb; + } + int argb = (int) explicit.value(); + // A colour given with no alpha at all is opaque by construction, and painting ink + // at full opacity hides the row under it. Treat it as the default weight. + return (argb >>> 24) == 0 ? (defaultArgb & 0xFF000000) | (argb & 0xFFFFFF) : argb; + } + + /// The colour actually behind this tap area: the overlay itself is transparent, so ask + /// the ancestors until one of them paints. + private static int backgroundUnder(Component c) { + for (Component a = c; a != null; a = a.getParent()) { + com.codename1.ui.plaf.Style s = a.getStyle(); + if (s != null && s.getBgTransparency() != 0) { + return s.getBgColor(); + } + } + return 0xFFFFFF; + } + + private static boolean isLight(int rgb) { + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + return (r * 299 + g * 587 + b * 114) / 1000 >= 128; + } + + + private static double clamp01(double v) { + return v < 0 ? 0 : (v > 1 ? 1 : v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InlineSpan.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InlineSpan.java new file mode 100644 index 00000000000..6a26be87bce --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InlineSpan.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * The base of the styled-text tree — Flutter's {@code InlineSpan}, the common + * supertype of {@link TextSpan} (and, later, {@code WidgetSpan}). Held as + * configuration consumed by {@link RichText}; not a Widget. + */ +public abstract class InlineSpan { + + /** + * {@code InlineSpan.toPlainText}: the concatenated raw text of this span and + * all descendants, in depth-first order. + */ + public abstract String toPlainText(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InteractiveViewer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InteractiveViewer.java new file mode 100644 index 00000000000..6511bd5bc76 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/InteractiveViewer.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A pan/zoom viewport for its {@code child} — Flutter's {@code InteractiveViewer}. + * + *

Applies the controller's matrix; the interactive half (dragging and + * pinching to change it) is not wired yet, so a viewer whose matrix never + * changes renders correctly and one the user expects to pan does not move. + * + *

It was a pass-through, which meant the matrix an app sets up front was + * dropped too: the 2D-transformations demo centres its board by handing its + * controller a translation, and the board rendered in the corner instead.

+ */ +public class InteractiveViewer extends StatelessWidget implements HasChild { + + private TransformationController transformationController; + private EdgeInsets boundaryMargin; + private double minScale; + private double maxScale; + private Boolean constrained; + private Boolean panEnabled; + private Boolean scaleEnabled; + private double scaleFactor; + private Object onInteractionStart; + private Object onInteractionUpdate; + private Object onInteractionEnd; + private Object clipBehavior; + private Boolean alignPanAxis; + private Widget child; + + public void transformationController(TransformationController v) { this.transformationController = v; } + public void boundaryMargin(EdgeInsets v) { this.boundaryMargin = v; } + public void minScale(double v) { this.minScale = v; } + public void maxScale(double v) { this.maxScale = v; } + public void constrained(Boolean v) { this.constrained = v; } + public void panEnabled(Boolean v) { this.panEnabled = v; } + public void scaleEnabled(Boolean v) { this.scaleEnabled = v; } + public void scaleFactor(double v) { this.scaleFactor = v; } + public void onInteractionStart(dart.runtime.Funcs.VoidFunc1 v) { this.onInteractionStart = v; } + public void onInteractionUpdate(dart.runtime.Funcs.VoidFunc1 v) { this.onInteractionUpdate = v; } + public void onInteractionEnd(dart.runtime.Funcs.VoidFunc1 v) { this.onInteractionEnd = v; } + public void clipBehavior(Object v) { this.clipBehavior = v; } + public void alignPanAxis(Boolean v) { this.alignPanAxis = v; } + + public void child(Widget v) { + this.child = v; + } + + public TransformationController getTransformationController() { + return transformationController; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + if (child == null) { + return null; + } + if (transformationController == null + || transformationController.value() == null) { + return child; + } + Transform t = new Transform(); + t.transform(transformationController.value()); + t.child(child); + // The viewport clips what the matrix pushes outside it. + ClipRect clip = new ClipRect(); + clip.child(t); + return clip; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicHeight.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicHeight.java new file mode 100644 index 00000000000..4d03990a71c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicHeight.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Sizes its {@code child} to the child's intrinsic height — Flutter's {@code IntrinsicHeight}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class IntrinsicHeight extends Widget implements HasChild { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicWidth.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicWidth.java new file mode 100644 index 00000000000..cc7ad354563 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/IntrinsicWidth.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Sizes its {@code child} to the child's intrinsic width — Flutter's {@code IntrinsicWidth}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class IntrinsicWidth extends Widget implements HasChild { + + private double stepWidth; + private double stepHeight; + private Widget child; + + public void stepWidth(double v) { this.stepWidth = v; } + public void stepHeight(double v) { this.stepHeight = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/KeyboardListener.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/KeyboardListener.java new file mode 100644 index 00000000000..d5fe420c1ce --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/KeyboardListener.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.FocusNode; +import com.codename1.flutter.Widget; +import com.codename1.flutter.services.KeyEvent; + +import dart.runtime.Funcs; + +/** + * A raw keyboard listener — Flutter's {@code KeyboardListener}. Structural + * pass-through: the single {@code child} renders unchanged; the focus node and + * key-event callback are held for a later input pass. + */ +public class KeyboardListener extends Widget implements HasChild { + + private FocusNode focusNode; + private Boolean autofocus; + private Boolean includeSemantics; + private Funcs.VoidFunc1 onKeyEvent; + private Widget child; + + public void focusNode(FocusNode v) { this.focusNode = v; } + public void autofocus(boolean v) { this.autofocus = v; } + public void includeSemantics(boolean v) { this.includeSemantics = v; } + public void onKeyEvent(Funcs.VoidFunc1 v) { this.onKeyEvent = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilder.java new file mode 100644 index 00000000000..3b1ecd3f9b5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; + +import dart.runtime.Funcs; + +/** + * Builds a widget tree that depends on the parent's size — Flutter's + * {@code LayoutBuilder}. The builder receives {@link BoxConstraints} (logical + * pixels). + * + *

The builder runs during layout, against the constraints the parent + * actually handed down — see {@link LayoutBuilderElement}.

+ */ +public class LayoutBuilder extends Widget { + + private Funcs.Func2 builder; + + public void builder(Funcs.Func2 v) { + this.builder = v; + } + + public Funcs.Func2 getBuilder() { + return builder; + } + + @Override + public Element createElement() { + return new LayoutBuilderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilderElement.java new file mode 100644 index 00000000000..b6e743aef86 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/LayoutBuilderElement.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +/** + * Element for {@link LayoutBuilder}: runs the builder DURING layout, with the + * constraints the parent actually handed down. + * + *

It used to run once at build time against the whole display, which is a + * plausible-looking approximation and wrong wherever the widget is not the + * whole screen. The 2D-transformations demo centres its board on the viewport + * the builder reports, and was handed the screen instead — so the board was + * offset by exactly the app bar plus the footer and hung off the bottom. + * + *

Two details make a layout-time build safe here. The builder's result is + * cached against the constraints that produced it, so a second layout pass with + * the same box does not rebuild — without that, building inside layout is an + * easy way to loop. And the constraints are converted to LOGICAL pixels first: + * layout runs in device pixels, and a builder written against Flutter's + * coordinate system would otherwise see numbers a factor of the device pixel + * ratio too large. + */ +public class LayoutBuilderElement extends SingleChildRenderElement { + + private BoxConstraints builtFor; + private Widget built; + + /// Whether one unbounded pass has already been sat out; see performLayout. + private boolean skippedUnbounded; + + /** + * Whether these constraints are a speculative MEASUREMENT rather than a box + * the child will occupy. + * + *

Unbounded on its own is not the signal, and reading it that way is a + * bug: a viewport's child is legitimately unbounded along the scroll axis + * and Flutter hands it infinity too. What separates the two is the CROSS + * axis. A viewport gives its child a tight cross-axis extent -- a vertical + * list hands down a tight width -- whereas "how big would you like to be" + * is loose in both directions. Sitting out a real viewport pass returns a + * zero size that the list then keeps, which silently emptied the reply + * study's entire mail list while the diff score went DOWN, because blank + * background differs from the reference less than mis-rendered cards do.

+ */ + private static boolean isSpeculativeMeasurement(BoxConstraints c) { + boolean unbounded = Double.isInfinite(c.maxWidth()) || Double.isInfinite(c.maxHeight()); + return unbounded && !c.hasTightWidth() && !c.hasTightHeight(); + } + + private static long builderMs; + private static long syncMs; + private static long layoutMs; + + /** Where the time inside layout-time building actually goes. */ + public static String cost() { + return "builder=" + builderMs + "ms mount=" + syncMs + "ms childLayout=" + layoutMs + "ms"; + } + + public LayoutBuilderElement(LayoutBuilder widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return built; + } + + @Override + public void update(Widget newWidget) { + // A new configuration means a new builder; the cached result is stale. + builtFor = null; + super.update(newWidget); + } + + @Override + protected void performRebuild() { + // A REBUILD re-runs the builder, even at the same constraints. + // + // Caching against the constraints alone is right for repeated layout + // passes and wrong for setState: Flutter's LayoutBuilder rebuilds its + // builder whenever the element is dirty, because the builder closes + // over state that the constraints know nothing about. Keeping the + // cached result meant a LayoutBuilder whose state changed kept showing + // what it built the first time -- the gallery's feature discovery is + // exactly that shape, and the coach mark stayed at the zero radius it + // was born with however far its animation ran. + builtFor = null; + invalidateLayoutCache(); + super.performRebuild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + BoxConstraints logical = toLogical(constraints); + // A DRY pass must never run the builder. Codename One measures a + // container far more often than it lays one out, and it measures with + // loose, unbounded constraints -- so the builder would be handed a + // viewport of infinity. Flutter does not support dry layout for + // LayoutBuilder at all, for exactly this reason. It matters beyond the + // measurement itself because a builder is allowed to latch: the + // 2D-transformations demo computes its home matrix from the FIRST + // viewport it is shown and keeps it forever, so one speculative call + // with the wrong box mis-centres the board for the life of the screen. + if (isDryPass() && builtFor == null) { + return constraints.smallest(); + } + // Same hazard, reached by a pass that is not dry. Codename One asks a box + // how wide it would like to be -- a real layout call, with the width + // unbounded -- before it asks again with the box the child will actually + // occupy. A builder that latches keeps whatever it was shown FIRST, so + // that speculative call decides the screen: the 2D-transformations demo + // centres its board against `constraints.maxWidth` and never recomputes, + // and an infinite width left the board drawn in the corner for the life + // of the route. Sit out one unbounded pass. If the next one is unbounded + // too then this really is an unbounded layout -- a viewport's child, say + // -- and the builder runs against it as Flutter would. + if (builtFor == null && !skippedUnbounded && isSpeculativeMeasurement(logical)) { + skippedUnbounded = true; + // The next pass can carry these same constraints, and the layout + // cache would hand it this placeholder instead of running the + // builder at all -- which is how sitting out once turned into never + // building for a viewport's child. + invalidateLayoutCache(); + return constraints.smallest(); + } + if (!isDryPass() && (builtFor == null || !same(builtFor, logical))) { + LayoutBuilder w = (LayoutBuilder) widget(); + long t0 = System.currentTimeMillis(); + // The builder runs Dart code, but not from performRebuild -- so + // without this a `!` failure inside it named no location at all, + // which is exactly how a startup crash here read as an anonymous + // TypeError seven frames deep in the layout recursion. + Object previous = dart.runtime.DartRuntime.diagnosticContextValue(); + dart.runtime.DartRuntime.diagnosticContext( + "running the LayoutBuilder in " + describeParentWidget()); + try { + built = w.getBuilder() == null ? null : w.getBuilder().call(this, logical); + } finally { + dart.runtime.DartRuntime.diagnosticContext(previous); + } + long t1 = System.currentTimeMillis(); + builtFor = logical; + syncChildren(); + long t2 = System.currentTimeMillis(); + // Split so the cost can be attributed instead of described: calling + // the transpiled builder is Dart-side widget construction, while + // syncChildren is element mounting plus Codename One component + // creation. They are different problems with different fixes. + builderMs += t1 - t0; + syncMs += t2 - t1; + } + RenderElement child = renderChild(); + if (child == null) { + return constraints.smallest(); + } + long lt = System.currentTimeMillis(); + Size cs = child.layout(constraints); + layoutMs += System.currentTimeMillis() - lt; + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } + + /** The nearest enclosing application widget, for the diagnostic above. */ + private String describeParentWidget() { + com.codename1.flutter.Element e = this; + for (int depth = 0; e != null && depth < 12; depth++) { + Widget w = e.widget(); + if (w != null && !w.getClass().getName().startsWith("com.codename1.flutter.")) { + return w.getClass().getName(); + } + e = e.parent(); + } + return "an unnamed subtree"; + } + + private static BoxConstraints toLogical(BoxConstraints c) { + double scale = Dp.scale(); + if (scale <= 0) { + scale = 1; + } + return new BoxConstraints(div(c.minWidth(), scale), div(c.maxWidth(), scale), + div(c.minHeight(), scale), div(c.maxHeight(), scale)); + } + + private static double div(double v, double scale) { + return Double.isInfinite(v) ? v : v / scale; + } + + private static boolean same(BoxConstraints a, BoxConstraints b) { + return a.minWidth() == b.minWidth() && a.maxWidth() == b.maxWidth() + && a.minHeight() == b.minHeight() && a.maxHeight() == b.maxHeight(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListView.java new file mode 100644 index 00000000000..90cf3570e49 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListView.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.core.UnsupportedError; +import dart.runtime.Funcs; + +/** + * A scrollable vertical list. Two modes: + *
    + *
  • Children mode ({@code new ListView()} + setters): the given + * children stacked in a scrollable column.
  • + *
  • Builder mode ({@link #builder}): M2 materializes + * {@code itemBuilder(context, index)} EAGERLY for every index in + * {@code 0..itemCount-1}; windowed/lazy building is an M3 milestone, + * which is also why a null (infinite) {@code itemCount} is rejected + * with an {@link UnsupportedError}.
  • + *
+ */ +public class ListView extends Widget { + + private DartList children; + private EdgeInsets padding; + private boolean shrinkWrap; + private Long itemCount; + private Funcs.Func2 itemBuilder; + private String restorationId; + private ScrollPhysics physics; + private boolean reverse; + private com.codename1.flutter.Axis scrollDirection = com.codename1.flutter.Axis.vertical; + private ScrollController controller; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void physics(ScrollPhysics v) { + this.physics = v; + } + + public void reverse(boolean v) { + this.reverse = v; + } + + public void scrollDirection(com.codename1.flutter.Axis v) { + this.scrollDirection = v; + } + + public void controller(ScrollController v) { + this.controller = v; + } + + /** The controller driving this list, or null. */ + public ScrollController getController() { + return controller; + } + + public ScrollPhysics getPhysics() { + return physics; + } + + public boolean getReverse() { + return reverse; + } + + public com.codename1.flutter.Axis getScrollDirection() { + return scrollDirection; + } + + public ListView() { + } + + /** + * Dart's {@code ListView.builder} named constructor, in canonical positional form. + * + *

The trailing parameters are not decoration. A named argument this factory does not + * declare is dropped by the transpiler without a word, so {@code shrinkWrap: true} — + * which is how a list inside a Column says "size to your content" — used to be + * discarded, and the list took the whole height it was offered instead. The settings + * page's expanding options list is exactly that shape.

+ */ + public static ListView builder(Key key, Long itemCount, + Funcs.Func2 itemBuilder, + EdgeInsets padding, Boolean shrinkWrap, Object physics, + com.codename1.flutter.Axis scrollDirection, Object controller, + String restorationId, Boolean primary, Double itemExtent, + Boolean reverse) { + if (itemCount == null) { + throw new UnsupportedError( + "ListView.builder without itemCount (an infinite list) is not supported in M2; " + + "items are materialized eagerly and windowed building lands in M3"); + } + ListView l = new ListView(); + l.key(key); + l.itemCount = itemCount; + l.itemBuilder = itemBuilder; + l.padding = padding; + l.shrinkWrap = shrinkWrap != null && shrinkWrap.booleanValue(); + if (scrollDirection != null) { + l.scrollDirection(scrollDirection); + } + l.restorationId(restorationId); + if (itemExtent != null) { + l.itemExtent(itemExtent.doubleValue()); + } + if (reverse != null) { + l.reverse(reverse.booleanValue()); + } + if (primary != null) { + l.primary(primary.booleanValue()); + } + // Declared by the builder and dropped: a builder list's controller never + // reached the list. + if (controller instanceof ScrollController) { + l.controller((ScrollController) controller); + } + return l; + } + + public void children(DartList v) { + this.children = v; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void shrinkWrap(boolean v) { + this.shrinkWrap = v; + } + + /** + * A fixed per-item extent along the scroll axis — Flutter's + * {@code ListView.itemExtent}. Held for a later layout pass. + */ + public void itemExtent(double v) { + } + + /** + * Whether this is the primary scroll view associated with the parent + * {@code PrimaryScrollController} ({@code ListView.primary}). Accepted for + * API compatibility; scroll-controller association is not modelled here. + */ + public void primary(boolean v) { + } + + public DartList getChildren() { + return children; + } + + public EdgeInsets getPadding() { + return padding; + } + + public boolean getShrinkWrap() { + return shrinkWrap; + } + + public Long getItemCount() { + return itemCount; + } + + public Funcs.Func2 getItemBuilder() { + return itemBuilder; + } + + public boolean isBuilderMode() { + return itemBuilder != null; + } + + /** + * Dart's {@code ListView.separated} named constructor. + * + *

The separators are real: a separated list of n items is a builder list + * of 2n-1 children where the odd ones come from + * {@code separatorBuilder(context, i)}. Dropping them (what this used to do) + * is not a cosmetic omission — the mail study's inbox separates its cards + * with a 4dp gap through which the page's background shows, so without the + * separators the whole list rendered as one continuous white slab.

+ * + *

An {@code itemCount} of null means an unbounded list, where "2n-1" has + * no meaning; such a list keeps building items alone.

+ */ + public static ListView separated(Key key, Boolean primary, Long itemCount, + final Funcs.Func2 itemBuilder, + final Funcs.Func2 separatorBuilder, + EdgeInsets padding, Boolean shrinkWrap) { + Long count = itemCount; + Funcs.Func2 children = itemBuilder; + if (itemCount != null && separatorBuilder != null && itemCount.longValue() > 0) { + count = Long.valueOf(itemCount.longValue() * 2 - 1); + children = new Funcs.Func2() { + @Override + public Widget call(BuildContext context, Long index) { + long i = index == null ? 0 : index.longValue(); + if ((i & 1L) == 0L) { + return itemBuilder == null ? null + : itemBuilder.call(context, Long.valueOf(i / 2)); + } + return separatorBuilder.call(context, Long.valueOf(i / 2)); + } + }; + } + ListView l = builder(key, count, children, padding, shrinkWrap, + null, null, null, null, null, null, null); + if (shrinkWrap != null) { + l.shrinkWrap(shrinkWrap); + } + return l; + } + + @Override + public Element createElement() { + return new ListViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListViewRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListViewRenderElement.java new file mode 100644 index 00000000000..0c2ad051286 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ListViewRenderElement.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Dp; +import com.codename1.ui.Component; +import com.codename1.ui.events.ScrollListener; + +import dart.core.DartList; + +/** + * Scroll boundary for {@link ListView}. In builder mode it WINDOWS the list: only the items in (and a + * little around) the viewport are materialized, with empty {@link SizedBox} spacers standing in for + * the off-screen items so the scroll geometry is preserved. As the user scrolls, the visible window is + * recomputed and this element is rebuilt, so the number of live components stays roughly constant + * regardless of {@code itemCount}. Children mode (a fixed list of children) still builds all. + * + *

Row heights are tracked per index. A row is measured once it has been built and laid out; + * rows not yet built are estimated from the average of the measured ones. The window's first + * index and both spacers come from prefix sums over those heights. It used to take ONE average + * from the first window and apply it to every index, so a list whose later rows were taller or + * shorter than the first few drifted: the window skipped real rows, showed the wrong ones for + * the scroll position, and reported a wrong total extent.

+ * + *

The list's {@link ScrollController}, if any, is attached for the element's lifetime: user + * scrolls update its offset and notify its listeners, and its jumpTo/animateTo move the list.

+ */ +public class ListViewRenderElement extends ScrollRenderElement { + + private static final int INITIAL = 24; + private static final int BUFFER = 8; + + private Component pane; + private int winStart; + private int winCount = INITIAL; + + /** Measured row heights in physical px, by index; 0 means not measured yet. */ + private double[] heights = new double[0]; + private double measuredSum; + private int measuredCount; + /** What the last build materialized: rows [builtStart, builtEnd), after a top spacer or not. */ + private int builtStart; + private int builtEnd; + private boolean builtTopSpacer; + private Column lastColumn; + + private ScrollController controller; + private final ScrollController.Client client = new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + if (pane instanceof ScrollPane) { + ((ScrollPane) pane).scrollToPosition((int) Math.round(Dp.px(offset)), horizontal()); + } + } + }; + + public ListViewRenderElement(ListView widget) { + super(widget); + } + + private ListView listView() { + return (ListView) widget(); + } + + @Override + protected boolean shrinkWrap() { + return listView().getShrinkWrap(); + } + + @Override + protected boolean horizontal() { + return listView().getScrollDirection() == com.codename1.flutter.Axis.horizontal; + } + + /** + * Windowing is vertical-only: the scroll math and the spacers are written + * against item HEIGHTS. Horizontal lists in practice hold a handful of + * items (a row of cards), so they are built eagerly rather than windowed. + */ + private boolean windowed() { + return listView().isBuilderMode() && !horizontal(); + } + + @Override + public void mount(com.codename1.flutter.Element parent, int slot) { + super.mount(parent, slot); + attachController(listView().getController()); + } + + @Override + public void update(Widget newWidget) { + ScrollController next = ((ListView) newWidget).getController(); + if (next != controller) { + attachController(next); + } + super.update(newWidget); + } + + @Override + public void unmount() { + attachController(null); + super.unmount(); + } + + private void attachController(ScrollController next) { + if (controller != null) { + controller.detach(client); + } + controller = next; + if (controller != null) { + controller.attach(client); + } + } + + @Override + protected Component createComponent() { + Component c = super.createComponent(); + pane = c; + if (c != null) { + c.addScrollListener(new ScrollListener() { + @Override + public void scrollChanged(int scrollX, int scrollY, int oldX, int oldY) { + reportScroll(horizontal() ? scrollX : scrollY); + onScroll(scrollY); + } + }); + } + return c; + } + + /** Tells the attached controller where the user scrolled to. */ + private void reportScroll(int scrollPx) { + if (controller == null || pane == null) { + return; + } + boolean h = horizontal(); + double scale = Dp.scale(); + double s = scale > 0 ? scale : 1; + double content = h ? pane.getScrollDimension().getWidth() : pane.getScrollDimension().getHeight(); + double viewport = h ? pane.getWidth() : pane.getHeight(); + controller.userScrolled(scrollPx / s, Math.max(0, content - viewport) / s, viewport / s); + } + + /** Recomputes the visible window on scroll and rebuilds when it changed. */ + private void onScroll(int scrollY) { + if (pane == null || !windowed()) { + return; + } + recordHeights(); + long count = listView().getItemCount(); + ensureHeights(count); + int viewport = pane.getHeight(); + // The first row at or below the scroll position, by prefix sum. + double y = 0; + int first = 0; + while (first < count && y + heightOf(first) <= scrollY) { + y += heightOf(first); + first++; + } + int last = first; + double bottom = y; + while (last < count && bottom < scrollY + viewport) { + bottom += heightOf(last); + last++; + } + int start = Math.max(0, first - BUFFER); + int cnt = (int) Math.min(count - start, (long) (last - start) + BUFFER); + // Throttle: the BUFFER of extra items above/below already covers small scrolls, so only + // rebuild once the window has drifted by half the buffer. This keeps the viewport always + // populated while avoiding a rebuild on every scroll frame (which would itself cause jank). + boolean drifted = Math.abs(start - winStart) >= BUFFER / 2; + boolean grew = cnt > winCount; + if (drifted || grew) { + winStart = start; + winCount = Math.max(cnt, winCount); + markNeedsBuild(); + } + } + + private void ensureHeights(long count) { + if (heights.length != count) { + double[] next = new double[(int) count]; + System.arraycopy(heights, 0, next, 0, (int) Math.min(heights.length, count)); + heights = next; + measuredSum = 0; + measuredCount = 0; + for (double h : heights) { + if (h > 0) { + measuredSum += h; + measuredCount++; + } + } + } + } + + /** A row's height: measured if it has been, else the average of those that have. */ + private double heightOf(int i) { + if (i < heights.length && heights[i] > 0) { + return heights[i]; + } + return measuredCount > 0 ? measuredSum / measuredCount : Dp.px(64); + } + + /** Measures every row the last build materialized, from its laid-out component. */ + private void recordHeights() { + com.codename1.ui.Container rows = rowsContainer(); + if (rows == null) { + return; + } + ensureHeights(listView().getItemCount()); + int offset = builtTopSpacer ? 1 : 0; + for (int i = builtStart; i < builtEnd && i < heights.length; i++) { + int ci = offset + (i - builtStart); + if (ci >= rows.getComponentCount()) { + break; + } + Component c = rows.getComponentAt(ci); + double h = c.getHeight() + c.getStyle().getVerticalMargins(); + if (h <= 0) { + continue; + } + if (heights[i] > 0) { + measuredSum += h - heights[i]; + } else { + measuredSum += h; + measuredCount++; + } + heights[i] = h; + } + } + + /** The component laying out the rows: the one rendered for the last built Column. */ + private com.codename1.ui.Container rowsContainer() { + final com.codename1.ui.Container[] found = {null}; + final Column target = lastColumn; + if (target == null) { + return null; + } + visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(com.codename1.flutter.Element e) { + if (found[0] != null) { + return; + } + if (e.widget() == target && e instanceof com.codename1.flutter.RenderElement) { + Component c = ((com.codename1.flutter.RenderElement) e).component(); + if (c instanceof com.codename1.ui.Container) { + found[0] = (com.codename1.ui.Container) c; + } + return; + } + e.visitChildren(this); + } + }); + return found[0]; + } + + @Override + protected Widget buildContent() { + ListView w = listView(); + DartList items = new DartList(); + if (!w.isBuilderMode()) { + items = w.getChildren() == null ? new DartList() : w.getChildren(); + return wrap(w, items); + } + if (!windowed()) { + long n = w.getItemCount(); + for (long i = 0; i < n; i++) { + items.add(w.getItemBuilder().call(this, i)); + } + return wrap(w, items); + } + // Heights of rows built last time are known now; record them before rebuilding. + recordHeights(); + long count = w.getItemCount(); + ensureHeights(count); + int start = winStart; + if (start >= count) { + start = (int) Math.max(0, count - 1); + } + int end = (int) Math.min(count, start + (long) winCount); + boolean estimable = measuredCount > 0; + // top spacer for the items scrolled off above (only once some row height is known) + builtTopSpacer = estimable && start > 0; + if (builtTopSpacer) { + double above = 0; + for (int i = 0; i < start; i++) { + above += heightOf(i); + } + items.add(spacer(above)); + } + for (long i = start; i < end; i++) { + items.add(w.getItemBuilder().call(this, i)); + } + builtStart = start; + builtEnd = end; + // bottom spacer for the items below the window + if (estimable && end < count) { + double below = 0; + for (int i = end; i < count; i++) { + below += heightOf(i); + } + items.add(spacer(below)); + } + return wrap(w, items); + } + + private Widget spacer(double physicalHeight) { + SizedBox s = new SizedBox(); + double scale = Dp.scale(); + s.height(scale > 0 ? physicalHeight / scale : physicalHeight); + return s; + } + + private Widget wrap(ListView w, DartList items) { + Widget line; + if (horizontal()) { + Row row = new Row(); + row.crossAxisAlignment(CrossAxisAlignment.stretch); + row.mainAxisSize(MainAxisSize.min); + row.children(items); + line = row; + } else { + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(items); + lastColumn = col; + line = col; + } + return padForScrollAxis(line, w.getPadding()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Listener.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Listener.java new file mode 100644 index 00000000000..bcaebd0c45c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Listener.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A low-level pointer-event listener — Flutter's {@code Listener}. Structural + * pass-through: the single {@code child} renders unchanged; the pointer + * callbacks are held for a later input pass. + */ +public class Listener extends Widget implements HasChild { + + private Funcs.VoidFunc1 onPointerDown; + private Funcs.VoidFunc1 onPointerMove; + private Funcs.VoidFunc1 onPointerUp; + private Funcs.VoidFunc1 onPointerCancel; + private Funcs.VoidFunc1 onPointerHover; + private Funcs.VoidFunc1 onPointerSignal; + private Object behavior; + private Widget child; + + public void onPointerDown(Funcs.VoidFunc1 v) { this.onPointerDown = v; } + public void onPointerMove(Funcs.VoidFunc1 v) { this.onPointerMove = v; } + public void onPointerUp(Funcs.VoidFunc1 v) { this.onPointerUp = v; } + public void onPointerCancel(Funcs.VoidFunc1 v) { this.onPointerCancel = v; } + public void onPointerHover(Funcs.VoidFunc1 v) { this.onPointerHover = v; } + public void onPointerSignal(Funcs.VoidFunc1 v) { this.onPointerSignal = v; } + public void behavior(Object v) { this.behavior = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Localizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Localizations.java new file mode 100644 index 00000000000..6ee1a6da09c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Localizations.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Locale; + +/** + * Flutter's {@code Localizations} inherited-widget lookup helpers. + * + *

The app uses only the static lookups: {@code Localizations.of(context, + * type)} to reach a localizations object published up the tree, and + * {@code Localizations.localeOf(context)} for the ambient {@link Locale}. The + * transpiler threads the requested {@code T} as a trailing {@code Class} + * witness for {@code of}.

+ */ +public final class Localizations { + + private Localizations() { + } + + /** + * {@code Localizations.of(context, type)}. Returns the nearest inherited + * localizations object of the requested type, or {@code null} when absent. + */ + public static T of(BuildContext context, Object type, Class witness) { + if (context == null || witness == null) { + return null; + } + T value; + try { + value = context.read(witness); + } catch (Throwable t) { + report("Localizations.of(" + witness.getName() + ") threw: " + t); + return null; + } + if (value == null) { + report("Localizations.of(" + witness.getName() + ") found nothing; the app's " + + "localizationsDelegates produced no matching object"); + } + return value; + } + + private static int reports; + + /** + * Dart writes {@code Foo.of(context)!}, so a null here becomes a null-check + * TypeError elsewhere with no hint of which lookup failed. Capped, because + * a missing localization is missing on every build. + */ + private static void report(String message) { + if (reports >= 5) { + return; + } + reports++; + try { + com.codename1.io.Log.p("Flutter runtime: " + message); + } catch (Throwable ignore) { + // headless: Log has no storage backend + } + } + + /** {@code Localizations.localeOf(context)} — the ambient locale. */ + public static Locale localeOf(BuildContext context) { + if (context != null) { + Object l = context.providerValueOfType(Locale.class); + if (l instanceof Locale) { + return (Locale) l; + } + } + return new Locale("en", "US"); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridView.java new file mode 100644 index 00000000000..af98c840186 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridView.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A staggered, Pinterest-style grid from the {@code flutter_staggered_grid_view} + * package — {@code MasonryGridView}. Crane's backdrop builds one via the + * {@code .count} constructor to lay out its destination cards. + */ +public class MasonryGridView extends Widget { + + private String restorationId; + private long crossAxisCount = 1; + private Double mainAxisSpacing; + private Double crossAxisSpacing; + private Long itemCount; + private Funcs.Func2 itemBuilder; + + public MasonryGridView() { + } + + /** Dart's {@code MasonryGridView.count} named constructor in positional form. */ + public static MasonryGridView count(Key key, String restorationId, long crossAxisCount, + Double mainAxisSpacing, Double crossAxisSpacing, + Long itemCount, Funcs.Func2 itemBuilder, Object scrollDirection, + Boolean shrinkWrap, Object physics, Object padding, + Object controller) { + MasonryGridView g = new MasonryGridView(); + g.key(key); + g.restorationId = restorationId; + g.crossAxisCount = Math.max(1, crossAxisCount); + g.mainAxisSpacing = mainAxisSpacing; + g.crossAxisSpacing = crossAxisSpacing; + g.itemCount = itemCount; + g.itemBuilder = itemBuilder; + return g; + } + + public long getCrossAxisCount() { + return crossAxisCount; + } + + public Long getItemCount() { + return itemCount; + } + + public Funcs.Func2 getItemBuilder() { + return itemBuilder; + } + + public String getRestorationId() { + return restorationId; + } + + /** The gap between items down a column, in logical pixels. */ + public double getMainAxisSpacing() { + return mainAxisSpacing == null ? 0 : mainAxisSpacing.doubleValue(); + } + + /** The gap between columns, in logical pixels. */ + public double getCrossAxisSpacing() { + return crossAxisSpacing == null ? 0 : crossAxisSpacing.doubleValue(); + } + + @Override + public Element createElement() { + return new MasonryGridViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridViewRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridViewRenderElement.java new file mode 100644 index 00000000000..fd99b568f94 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MasonryGridViewRenderElement.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Scroll boundary and content for {@link MasonryGridView}. + * + *

Built as a row of columns: item i goes to column i % n, with + * the requested gaps between columns and between items. That is not Flutter's + * shortest-column rule, so at more than one column the vertical offsets can + * differ where items have very unequal heights; at one column — the mobile + * layout, and the only one the gallery uses on a phone — it is exactly right. + * + *

It previously returned no content at all, so the scrollable rendered + * EMPTY. Nothing reported it: Crane's destination list, the whole point of the + * screen, was simply absent, and the sweep stayed quiet because an empty + * scrollable throws nothing.

+ */ +public class MasonryGridViewRenderElement extends ScrollRenderElement { + + public MasonryGridViewRenderElement(MasonryGridView widget) { + super(widget); + } + + private MasonryGridView grid() { + return (MasonryGridView) widget(); + } + + @Override + protected Widget buildContent() { + MasonryGridView g = grid(); + if (g.getItemBuilder() == null || g.getItemCount() == null) { + return null; + } + int count = (int) Math.max(0, g.getItemCount().longValue()); + int columns = (int) Math.max(1, g.getCrossAxisCount()); + if (count == 0) { + return null; + } + + DartList> byColumn = new DartList>(); + for (int c = 0; c < columns; c++) { + byColumn.add(new DartList()); + } + for (int i = 0; i < count; i++) { + Widget item = g.getItemBuilder().call(this, Long.valueOf(i)); + if (item == null) { + continue; + } + DartList column = byColumn.get(i % columns); + if (!column.isEmpty() && g.getMainAxisSpacing() > 0) { + column.add(gap(0, g.getMainAxisSpacing())); + } + column.add(item); + } + + if (columns == 1) { + return column(byColumn.get(0)); + } + + DartList row = new DartList(); + for (int c = 0; c < columns; c++) { + if (c > 0 && g.getCrossAxisSpacing() > 0) { + row.add(gap(g.getCrossAxisSpacing(), 0)); + } + Expanded e = new Expanded(); + e.child(column(byColumn.get(c))); + row.add(e); + } + Row r = new Row(); + r.children(row); + r.crossAxisAlignment(CrossAxisAlignment.start); + r.mainAxisSize(MainAxisSize.max); + return r; + } + + private static Widget column(DartList children) { + Column c = new Column(); + c.children(children); + c.crossAxisAlignment(CrossAxisAlignment.stretch); + c.mainAxisSize(MainAxisSize.min); + return c; + } + + private static Widget gap(double width, double height) { + SizedBox b = new SizedBox(); + if (width > 0) { + b.width(width); + } + if (height > 0) { + b.height(height); + } + return b; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MergeSemantics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MergeSemantics.java new file mode 100644 index 00000000000..f57b7bbadd0 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MergeSemantics.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Merges the semantics of its child subtree into one node. Renders the child + * unchanged for this milestone. See {@link PassThroughRenderElement}. + */ +public class MergeSemantics extends Widget implements HasChild { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ModalBarrier.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ModalBarrier.java new file mode 100644 index 00000000000..2f636663473 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ModalBarrier.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A full-screen barrier that optionally dismisses a route when tapped — + * Flutter's {@code ModalBarrier}. new_gallery's backdrop wraps one in a + * {@code Listener} to intercept taps while the settings page is open. This + * milestone renders an inert filled box; dismissal is wired by the enclosing + * gesture handler. + */ +public class ModalBarrier extends StatelessWidget { + + private Color color; + private boolean dismissible = true; + + public void color(Color v) { + this.color = v; + } + + public void dismissible(boolean v) { + this.dismissible = v; + } + + public void semanticsLabel(String v) { + } + + public void barrierSemanticsDismissible(boolean v) { + } + + public void onDismiss(Object v) { + } + + public Color getColor() { + return color; + } + + public boolean isDismissible() { + return dismissible; + } + + @Override + public Widget build(BuildContext context) { + return new Container(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MouseRegion.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MouseRegion.java new file mode 100644 index 00000000000..df7b9513dad --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/MouseRegion.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Tracks the pointer as it enters/exits/moves over its child. Pointer hover is + * a no-op on touch targets; the child renders unchanged. See + * {@link PassThroughRenderElement}. + */ +public class MouseRegion extends Widget implements HasChild { + + private Object cursor; + private boolean opaque = true; + private Object onEnter; + private Object onExit; + private Object onHover; + private Object hitTestBehavior; + private Widget child; + + public void cursor(Object v) { + this.cursor = v; + } + + public void opaque(boolean v) { + this.opaque = v; + } + + public void onEnter(Object v) { + this.onEnter = v; + } + + public void onExit(Object v) { + this.onExit = v; + } + + public void onHover(Object v) { + this.onHover = v; + } + + public void hitTestBehavior(Object v) { + this.hitTestBehavior = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NestedScrollView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NestedScrollView.java new file mode 100644 index 00000000000..5e412859d89 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NestedScrollView.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A scroll view whose header slivers scroll with an inner scrollable — + * Flutter's {@code NestedScrollView}. This milestone renders the {@code body}; + * the {@code headerSliverBuilder} slivers and the coordinated + * outer/inner scroll linkage are deferred. + */ +public class NestedScrollView extends StatelessWidget { + + private Widget body; + private Funcs.Func2> headerSliverBuilder; + + public void body(Widget v) { + this.body = v; + } + + public void headerSliverBuilder(Funcs.Func2> v) { + this.headerSliverBuilder = v; + } + + public void controller(Object v) { + } + + public void scrollDirection(Object v) { + } + + public void reverse(boolean v) { + } + + public void physics(Object v) { + } + + public void floatHeaderSlivers(boolean v) { + } + + @Override + public Widget build(BuildContext context) { + return body != null ? body : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NeverScrollableScrollPhysics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NeverScrollableScrollPhysics.java new file mode 100644 index 00000000000..e0f6711c682 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NeverScrollableScrollPhysics.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Scroll physics that does not allow the user to scroll — Flutter's {@code + * NeverScrollableScrollPhysics}. + */ +public class NeverScrollableScrollPhysics extends ScrollPhysics { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Notification.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Notification.java new file mode 100644 index 00000000000..a39f07cbc0c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Notification.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; + +/** + * The base of notifications that bubble up the widget tree — Flutter's + * {@code Notification}. A subclass is dispatched with {@link #dispatch} from a + * build context; an enclosing {@code NotificationListener} of a matching type + * receives it. This pass captures the dispatch API shape. + */ +public class Notification { + + public Notification() { + } + + /** + * Sends this notification up the tree from {@code target}. Returns whether + * it was consumed (always {@code false} until listener wiring lands). + */ + public boolean dispatch(BuildContext target) { + return false; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NotificationListener.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NotificationListener.java new file mode 100644 index 00000000000..938f0681985 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NotificationListener.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Listens for a {@link Notification} bubbling up from its subtree — Flutter's + * {@code NotificationListener}. new_gallery only ever listens for scroll + * notifications, so {@code onNotification} is typed against + * {@link ScrollNotification}; the return value ({@code true} to stop the + * notification bubbling) is captured. Structural pass-through for this + * milestone: the {@code child} renders unchanged. + * + * @param the notification type (erased at this pass) + */ +public class NotificationListener extends Widget implements HasChild { + + private Funcs.Func1 onNotification; + private Widget child; + + public void onNotification(Funcs.Func1 v) { + this.onNotification = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Funcs.Func1 getOnNotification() { + return onNotification; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NumericFocusOrder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NumericFocusOrder.java new file mode 100644 index 00000000000..141cc2fdcd7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/NumericFocusOrder.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Orders a focusable subtree by an ascending numeric value — Flutter's + * {@code NumericFocusOrder}. Lower orders are traversed first. + */ +public class NumericFocusOrder extends FocusOrder { + + private final double order; + + public NumericFocusOrder(double order) { + this.order = order; + } + + public double getOrder() { + return order; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Opacity.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Opacity.java new file mode 100644 index 00000000000..cc3d013f82d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Opacity.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Makes its {@code child} partially transparent — Flutter's {@code Opacity}. + * The opacity value (0.0 fully transparent .. 1.0 fully opaque) is captured; + * this pass renders the child at full opacity, with alpha compositing deferred + * to the paint layer. + */ +public class Opacity extends Widget { + + private double opacity = 1.0; + private Widget child; + + public void opacity(double v) { + this.opacity = v; + } + + public void alwaysIncludeSemantics(boolean v) { + } + + public void child(Widget v) { + this.child = v; + } + + public double getOpacity() { + return opacity; + } + + public Widget getChild() { + return child; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new OpacityRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OpacityRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OpacityRenderElement.java new file mode 100644 index 00000000000..2a8c2175f22 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OpacityRenderElement.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Paints {@link Opacity}'s subtree at its opacity, by compositing the whole nested pane + * through the Graphics alpha rather than tinting components individually — so overlapping + * children fade as one layer, the way Flutter's Opacity behaves. + */ +public class OpacityRenderElement extends EffectRenderElement { + + public OpacityRenderElement(Opacity widget) { + super(widget); + } + + private Opacity opacity() { + return (Opacity) widget(); + } + + @Override + protected Widget effectChild() { + return opacity().getChild(); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + double o = opacity().getOpacity(); + if (o >= 1.0) { + paintChildren.paint(g); + return; + } + if (o <= 0.0) { + return; // fully transparent: painting anything would be wrong + } + int previous = g.getAlpha(); + // Compose with the alpha already in effect, so nested Opacity multiplies. + g.setAlpha((int) Math.round(previous * o)); + try { + paintChildren.paint(g); + } finally { + g.setAlpha(previous); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OrderedTraversalPolicy.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OrderedTraversalPolicy.java new file mode 100644 index 00000000000..c3c9054bd61 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OrderedTraversalPolicy.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Traverses focus in explicit FocusTraversalOrder — Flutter's {@code OrderedTraversalPolicy}. Captured for API shape by + * {@link FocusTraversalGroup}; live focus traversal is deferred to a later pass. + */ +public class OrderedTraversalPolicy { + + private Object secondary; + + public OrderedTraversalPolicy() { + } + + public void secondary(Object v) { + this.secondary = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBar.java new file mode 100644 index 00000000000..c6ca57d860a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBar.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.MainAxisAlignment; + +import dart.core.DartList; + +/** + * Lays its {@code children} out horizontally, falling back to a vertical column + * when they do not fit — Flutter's {@code OverflowBar} (the modern + * {@code ButtonBar}). This pass always renders the horizontal {@link Row} form; + * the overflow-to-column behavior is deferred. + */ +public class OverflowBar extends StatelessWidget { + + private double spacing; + private Object alignment; + private double overflowSpacing; + private Object overflowAlignment; + private Object overflowDirection; + private Object textDirection; + private DartList children; + + public void spacing(double v) { + this.spacing = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public void overflowSpacing(double v) { + this.overflowSpacing = v; + } + + public void overflowAlignment(Object v) { + this.overflowAlignment = v; + } + + public void overflowDirection(Object v) { + this.overflowDirection = v; + } + + public void textDirection(Object v) { + this.textDirection = v; + } + + public void children(DartList v) { + this.children = v; + } + + public double getSpacing() { + return spacing; + } + + public DartList getChildren() { + return children; + } + + @Override + public Widget build(BuildContext context) { + Row row = new Row(); + // Both of these were dropped, and both are visible. Shrine's login states an END + // alignment and 8 logical pixels of spacing: its CANCEL and NEXT sat hard against + // the left edge with no gap, where the reference has them apart and to the right. + if (alignment instanceof MainAxisAlignment) { + row.mainAxisAlignment((MainAxisAlignment) alignment); + } + row.children(spacing > 0 ? spaced(children) : children); + return row; + } + + /// The children with a gap of {@code spacing} between each pair. + private DartList spaced(DartList kids) { + if (kids == null || kids.size() < 2) { + return kids; + } + DartList out = new DartList(); + for (int i = 0; i < kids.size(); i++) { + if (i > 0) { + SizedBox gap = new SizedBox(); + gap.width(spacing); + out.add(gap); + } + out.add(kids.get(i)); + } + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBox.java new file mode 100644 index 00000000000..8c2ae02d265 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBox.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Lets its child overflow its own constraints — Flutter's {@code OverflowBox}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged; the imposed min/max constraints and alignment are held + * for a later render pass.

+ */ +public class OverflowBox extends Widget implements HasChild { + + private Object alignment; + private Double minWidth; + private Double maxWidth; + private Double minHeight; + private Double maxHeight; + private Widget child; + + public void alignment(Object v) { this.alignment = v; } + public void minWidth(double v) { this.minWidth = v; } + public void maxWidth(double v) { this.maxWidth = v; } + public void minHeight(double v) { this.minHeight = v; } + public void maxHeight(double v) { this.maxHeight = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + public Object getAlignment() { + return alignment; + } + + public Double getMinWidth() { + return minWidth; + } + + public Double getMaxWidth() { + return maxWidth; + } + + public Double getMinHeight() { + return minHeight; + } + + public Double getMaxHeight() { + return maxHeight; + } + + @Override + public Element createElement() { + return new OverflowBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBoxRenderElement.java new file mode 100644 index 00000000000..e6974738ebf --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverflowBoxRenderElement.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; + +/** + * Lays its child out against DIFFERENT constraints from the ones it was given, and lets + * the result overflow -- Flutter's {@code OverflowBox}. + * + *

The box itself still takes the size its own constraints allow; only the child is + * freed. Overriding just {@code maxHeight} and leaving the child centred is how a layout + * lifts something above its slot: the child grows past the box at both ends and the + * alignment splits the difference.

+ * + *

This was a pass-through, so the overrides did nothing and the child was laid out in + * the box. Crane's three front layers are positioned exactly that way -- the middle one + * rides 60dp higher than its slot because the TabBarView is allowed to overflow by 120 + * and centred, and the outer two pad themselves back down by 60. With the overflow + * ignored the padding had nothing to cancel, so every layer sat 60dp too low and took + * the whole page below it along.

+ */ +public class OverflowBoxRenderElement extends SingleChildRenderElement { + + public OverflowBoxRenderElement(Widget widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return box().getChild(); + } + + private OverflowBox box() { + return (OverflowBox) widget(); + } + + /** The child's constraints: each edge overridden where given, inherited where not. */ + static BoxConstraints innerConstraints(BoxConstraints outer, Double minW, Double maxW, + Double minH, Double maxH) { + return new BoxConstraints( + minW != null ? minW.doubleValue() : outer.minWidth(), + maxW != null ? maxW.doubleValue() : outer.maxWidth(), + minH != null ? minH.doubleValue() : outer.minHeight(), + maxH != null ? maxH.doubleValue() : outer.maxHeight()); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + Size self = constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + RenderElement child = renderChild(); + if (child == null) { + return self; + } + OverflowBox w = box(); + Size cs = child.layout(innerConstraints(constraints, + px(w.getMinWidth()), px(w.getMaxWidth()), + px(w.getMinHeight()), px(w.getMaxHeight()))); + Alignment a = alignment(); + setChildOffset(child, + Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(a.y(), self.height(), cs.height())); + return self; + } + + /** A logical-pixel override in device pixels, or null when it was not given. */ + private static Double px(Double lp) { + return lp == null ? null + : Double.valueOf(com.codename1.flutter.rendering.Dp.px(lp.doubleValue())); + } + + /** Flutter's default is {@code Alignment.center}. */ + private Alignment alignment() { + Object a = box().getAlignment(); + return a instanceof Alignment ? (Alignment) a : Alignment.center; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Overlay.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Overlay.java new file mode 100644 index 00000000000..2cbe4ff7888 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Overlay.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.ComposedElement; +import com.codename1.flutter.Element; +import com.codename1.flutter.StackFit; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * The stack of {@link OverlayEntry} objects floating above a route — Flutter's + * {@code Overlay}. + * + *

An overlay is what lets something be drawn over the whole screen without + * the widget that asked for it having to sit at the top of the tree: dialogs, + * modal sheets, drag feedback, and the gallery's feature-discovery coach marks + * all reach the nearest overlay and insert an entry. + * + *

The whole subsystem used to be inert — {@code of()} answered one shared + * state whose {@code insert} was an empty method body, so every entry ever + * created went into a void. Nothing threw, and the only sign was a screen that + * quietly lacked whatever should have floated above it. + * + *

Every route mounts inside one (see {@code Navigator}), so {@code of()} + * finds the overlay belonging to the route the caller is on rather than a + * process-wide singleton. + */ +public class Overlay extends Widget { + + private DartList initialEntries; + private Object clipBehavior; + /** The widget the entries float above — supplied by the runtime, not by the app. */ + private Widget base; + + public void initialEntries(DartList v) { + this.initialEntries = v; + } + + public DartList getInitialEntries() { + return initialEntries; + } + + public void clipBehavior(Object v) { + this.clipBehavior = v; + } + + public Widget getBase() { + return base; + } + + /** An overlay hosting {@code base}, which every route is wrapped in. */ + public static Overlay hosting(Widget base) { + Overlay o = new Overlay(); + o.base = base; + return o; + } + + /** Flutter's {@code Overlay.of} — the nearest ancestor overlay's state. */ + public static OverlayState of(BuildContext context, boolean rootOverlay, Object debugRequiredFor) { + OverlayState s = maybeOf(context, rootOverlay); + return s == null ? new OverlayState() : s; + } + + /** Flutter's {@code Overlay.maybeOf}. */ + public static OverlayState maybeOf(BuildContext context, boolean rootOverlay) { + Element e = context instanceof Element ? (Element) context : null; + OverlayElement found = null; + while (e != null) { + if (e instanceof OverlayElement) { + found = (OverlayElement) e; + if (!rootOverlay) { + return found.state(); + } + } + e = e.ancestor(); + } + return found == null ? null : found.state(); + } + + @Override + public Element createElement() { + return new OverlayElement(this); + } + + /** + * Builds the overlay's content: the base with every live entry stacked on + * top, in insertion order. + */ + static final class OverlayElement extends ComposedElement { + + private final OverlayState state = new OverlayState(); + + OverlayElement(Overlay widget) { + super(widget); + state.attach(this); + Overlay o = widget; + if (o.initialEntries != null) { + // Attached as OverlayState.insert attaches, minus the rebuild -- + // the first build is about to include them. Copied in bare, they + // had no owner: markNeedsBuild() did nothing and remove() marked + // the entry unmounted without the rebuild that takes it off + // screen, so an initial entry could never be dismissed. + for (OverlayEntry entry : o.initialEntries) { + if (entry != null && !state.entries().contains(entry)) { + entry.attach(state); + state.entries().add(entry); + } + } + } + } + + OverlayState state() { + return state; + } + + @Override + protected Widget build() { + Overlay o = (Overlay) widget(); + DartList children = new DartList(); + if (o.getBase() != null) { + children.add(o.getBase()); + } + for (OverlayEntry entry : state.entries()) { + if (!entry.mounted() || entry.getBuilder() == null) { + continue; + } + Widget w = entry.getBuilder().call(this); + if (w != null) { + children.add(w); + } + } + if (children.isEmpty()) { + return null; + } + // ALWAYS a Stack, even for a lone base. + // + // Returning the base directly when there are no entries and a Stack + // once there is one changes the widget TYPE of the overlay's child, + // and a type change is exactly what Widget.canUpdate refuses -- so + // inserting the first entry replaced the entire route subtree + // instead of updating it. That is self-defeating for anything that + // inserts an overlay from inside the tree: the feature-discovery + // coach mark inserted its entry, the insert tore down the widget + // that had just inserted it, its animation controllers were + // disposed mid-run, and the replacement -- built fresh -- saw the + // app's one-shot "already shown" flag and never inserted anything + // again. The entry stayed on screen at radius zero. + Stack stack = new Stack(); + stack.children(children); + // The entries cover the route, so the stack takes the whole box + // rather than shrink-wrapping its largest child. + stack.fit(StackFit.expand); + return stack; + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayEntry.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayEntry.java new file mode 100644 index 00000000000..f51175bc8ed --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayEntry.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * One entry painted into an {@link Overlay} — Flutter's {@code OverlayEntry}. + * Feature-discovery builds it from a {@code builder}, rebuilds it via + * {@link #markNeedsBuild()} and tears it down with {@link #remove()}. + */ +public class OverlayEntry { + + private Funcs.Func1 builder; + private Boolean opaque; + private Boolean maintainState; + private boolean mounted = true; + + public OverlayEntry() { + } + + public void builder(Funcs.Func1 v) { + this.builder = v; + } + + public void opaque(Boolean v) { + this.opaque = v; + } + + public void maintainState(Boolean v) { + this.maintainState = v; + } + + public Funcs.Func1 getBuilder() { + return builder; + } + + private OverlayState owner; + + void attach(OverlayState state) { + this.owner = state; + } + + /** Marks the entry as needing to rebuild its content on the next frame. */ + public void markNeedsBuild() { + if (owner != null) { + owner.rebuild(); + } + } + + /** Removes this entry from its overlay. */ + public void remove() { + mounted = false; + if (owner != null) { + owner.forget(this); + owner = null; + } + } + + public boolean mounted() { + return mounted; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayRoute.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayRoute.java new file mode 100644 index 00000000000..ceb2687641c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayRoute.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.navigation.Route; + +import dart.core.DartIterable; +import dart.core.DartList; + +/** + * A {@link Route} that inserts one or more {@link OverlayEntry} objects into the + * navigator's {@link Overlay} — Flutter's {@code OverlayRoute}. Subclasses + * override {@link #createOverlayEntries()} to supply their entries (e.g. a page + * plus its modal barrier). This pass captures the entries; wiring them into the + * live overlay lands with the navigation renderer. + * + * @param the value the route completes with when popped + */ +public class OverlayRoute extends Route { + + /** The overlay entries this route paints. Subclasses override. */ + public DartIterable createOverlayEntries() { + return DartIterable.wrap(new DartList()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayState.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayState.java new file mode 100644 index 00000000000..f1f2550ff7d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/OverlayState.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; + +import dart.core.DartList; + +import java.util.ArrayList; +import java.util.List; + +/** + * The mutable state of an {@link Overlay} — Flutter's {@code OverlayState}. + * + *

Holds the live entry list and rebuilds the overlay whenever it changes. + * {@code insert} and {@code insertAll} used to be empty method bodies, so an + * entry could be built, inserted and removed without anything ever appearing. + */ +public class OverlayState { + + private final List entries = new ArrayList(); + private Element element; + + void attach(Element e) { + this.element = e; + } + + List entries() { + return entries; + } + + public void insert(OverlayEntry entry, OverlayEntry below, OverlayEntry above) { + if (entry == null || entries.contains(entry)) { + return; + } + entry.attach(this); + int at = entries.size(); + if (below != null && entries.contains(below)) { + at = entries.indexOf(below); + } else if (above != null && entries.contains(above)) { + at = entries.indexOf(above) + 1; + } + entries.add(at, entry); + rebuild(); + } + + public void insertAll(DartList newEntries, OverlayEntry below, OverlayEntry above) { + if (newEntries == null) { + return; + } + for (OverlayEntry e : newEntries) { + insert(e, below, above); + } + } + + /** Drops an entry that has been removed, and rebuilds without it. */ + void forget(OverlayEntry entry) { + if (entries.remove(entry)) { + rebuild(); + } + } + + void rebuild() { + if (element != null) { + element.markNeedsBuild(); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Padding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Padding.java new file mode 100644 index 00000000000..040273174f3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Padding.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.EdgeInsetsGeometry; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Insets its child by the given edge padding (logical pixels). + */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class Padding extends Widget implements HasChild { + + private EdgeInsets padding; + private Widget child; + + /** + * Flutter's {@code Padding.padding} is an {@code EdgeInsetsGeometry}; the render pass needs + * the resolved {@link EdgeInsets}, so a direction-relative inset (never used by these + * layouts) is dropped rather than resolved here. + */ + public void padding(EdgeInsetsGeometry v) { + this.padding = (v instanceof EdgeInsets) ? (EdgeInsets) v : null; + } + + public void child(Widget v) { + this.child = v; + } + + public EdgeInsets getPadding() { + return padding; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PaddingRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PaddingRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PaddingRenderElement.java new file mode 100644 index 00000000000..f6f167348c5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PaddingRenderElement.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +/** + * Deflates the incoming constraints by the padding (converted from logical + * pixels to device pixels), lays out the child, and reports the child size + * plus the insets. Owns no CN1 component. + */ +public class PaddingRenderElement extends SingleChildRenderElement { + + public PaddingRenderElement(Padding widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((Padding) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + EdgeInsets lp = ((Padding) widget()).getPadding(); + EdgeInsets px = lp == null + ? EdgeInsets.all(0) + : EdgeInsets.only(Dp.px(lp.left()), Dp.px(lp.top()), Dp.px(lp.right()), Dp.px(lp.bottom())); + RenderElement child = renderChild(); + if (child == null) { + return constraints.constrain(new Size(px.horizontal(), px.vertical())); + } + Size cs = child.layout(constraints.deflate(px)); + setChildOffset(child, px.left(), px.top()); + return constraints.constrain(new Size(cs.width() + px.horizontal(), cs.height() + px.vertical())); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageController.java new file mode 100644 index 00000000000..fa35ad849ca --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageController.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.foundation.Listenable; + +import java.util.ArrayList; +import java.util.List; + +import dart.runtime.Funcs; + +/** + * Controls the visible page of a {@link PageView} — Flutter's + * {@code PageController}. + * + *

This is a LIVE scroll model, not a stored page number. Flutter's carousels + * are built on the controller being a {@code Listenable} whose {@link #page()} + * is fractional while a drag is in flight: the gallery's home carousel wraps + * every card in an {@code AnimatedBuilder(animation: controller)} and scales it + * by {@code controller.page - index}, so a controller that never notifies and + * always reports its initial page freezes every card at the scale it happened + * to have on the first frame. The attached {@link PageViewRenderElement} feeds + * {@link #applyMetrics} from the pane's scroll offset, which is what makes + * {@code position.haveDimensions} answer true and drives the rebuilds.

+ */ +public class PageController implements Listenable { + + private long initialPage; + private boolean keepPage = true; + private double viewportFraction = 1.0; + private final ScrollPosition position = new ScrollPosition(); + private final List listeners = new ArrayList(); + + /** The view currently driving this controller, or null when unattached. */ + private PageViewRenderElement view; + + /** The live fractional page; only meaningful once {@link #livePage} is set. */ + private double pageValue; + private boolean livePage; + + public PageController() { + } + + public void initialPage(long v) { + this.initialPage = v; + } + + public void keepPage(boolean v) { + this.keepPage = v; + } + + public void viewportFraction(double v) { + this.viewportFraction = v; + } + + /** + * The current page, fractional while scrolling. Falls back to the initial + * page until a view has reported its metrics, matching Flutter, where + * reading {@code page} before attachment yields the initial page. + */ + public Double page() { + return livePage ? pageValue : (double) initialPage; + } + + public long initialPage() { + return initialPage; + } + + public boolean keepPage() { + return keepPage; + } + + public double viewportFraction() { + return viewportFraction; + } + + public ScrollPosition position() { + return position; + } + + public boolean hasClients() { + return view != null; + } + + /** + * Animates to {@code page} over {@code duration} following {@code curve}, and returns a + * future that completes when the page is reached (or the motion is interrupted). + */ + public Object animateToPage(long page, Object duration, Object curve) { + dart.async.Completer done = new dart.async.Completer(); + if (view == null) { + done.complete(null); + return done.future(); + } + int ms = duration instanceof dart.core.Duration + ? (int) Math.max(0L, Math.min(Integer.MAX_VALUE, ((dart.core.Duration) duration).inMilliseconds())) + : 0; + com.codename1.flutter.animation.Curve c = curve instanceof com.codename1.flutter.animation.Curve + ? (com.codename1.flutter.animation.Curve) curve : null; + view.animateToPage(page, ms, c, done); + return done.future(); + } + + public void jumpToPage(long page) { + if (view != null) { + view.scrollToPage(page, false); + } + } + + public Object nextPage(Object duration, Object curve) { + return animateToPage(Math.round(page().doubleValue()) + 1, duration, curve); + } + + public Object previousPage(Object duration, Object curve) { + return animateToPage(Math.round(page().doubleValue()) - 1, duration, curve); + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + listeners.remove(listener); + } + + public void dispose() { + listeners.clear(); + view = null; + livePage = false; + } + + // ------------------------------------------------------------------ + // Framework plumbing — driven by the attached PageViewRenderElement + // ------------------------------------------------------------------ + + void attach(PageViewRenderElement v) { + this.view = v; + } + + void detach(PageViewRenderElement v) { + if (this.view == v) { + this.view = null; + this.livePage = false; + } + } + + /** + * Publishes the pane's geometry and scroll offset, notifying listeners only + * when the fractional page actually moved. + * + *

The guard matters: this runs from both layout and every scroll event, + * and an unconditional notify would mark the carousel's builders dirty on + * frames where nothing moved.

+ */ + void applyMetrics(double pixels, double pageExtent, double viewportDimension, + double minExtent, double maxExtent) { + position.applyViewportDimension(viewportDimension); + position.applyContentDimensions(minExtent, maxExtent); + position.setPixels(pixels); + double p = pageExtent > 0 ? pixels / pageExtent : 0; + if (livePage && p == pageValue) { + return; + } + livePage = true; + pageValue = p; + notifyListeners(); + } + + private void notifyListeners() { + com.codename1.flutter.foundation.Listeners.notify(listeners); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageView.java new file mode 100644 index 00000000000..99c41b7b23e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageView.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; +import dart.runtime.RefLong; + +/** + * A scrollable list showing one page at a time — Flutter's {@code PageView} (and + * its {@code .builder} named constructor). Two modes mirror {@link ListView}: + *
    + *
  • Children mode — a fixed list of page widgets.
  • + *
  • Builder mode ({@link #builder}) — pages materialized on demand + * from {@code itemBuilder(context, index)} for {@code 0..itemCount-1}.
  • + *
+ * This pass lays the pages out in a scroll boundary; true one-page snapping and + * the {@link PageController} coupling land with the paging renderer, so + * {@code controller} / {@code onPageChanged} are captured. + */ +public class PageView extends Widget { + + private PageController controller; + private Object scrollDirection; + private Boolean reverse; + private Object physics; + private Boolean pageSnapping; + private Funcs.VoidFunc1 onPageChanged; + private DartList children; + private Boolean allowImplicitScrolling; + private String restorationId; + private Object clipBehavior; + + private Funcs.Func2 itemBuilder; + private Long itemCount; + + public PageView() { + } + + public void controller(PageController v) { this.controller = v; } + public void scrollDirection(Object v) { this.scrollDirection = v; } + public void reverse(Boolean v) { this.reverse = v; } + public void physics(Object v) { this.physics = v; } + public void pageSnapping(Boolean v) { this.pageSnapping = v; } + public void onPageChanged(Funcs.VoidFunc1 v) { this.onPageChanged = v; } + public void children(DartList v) { this.children = v; } + public void allowImplicitScrolling(Boolean v) { this.allowImplicitScrolling = v; } + public void restorationId(String v) { this.restorationId = v; } + public void clipBehavior(Object v) { this.clipBehavior = v; } + + /** Dart's {@code PageView.builder} named constructor in positional form. */ + public static PageView builder(Key key, PageController controller, Object scrollDirection, + Boolean reverse, Object physics, Boolean pageSnapping, + Funcs.VoidFunc1 onPageChanged, + Funcs.Func2 itemBuilder, + Long itemCount, Boolean allowImplicitScrolling, + String restorationId, Object clipBehavior) { + PageView p = new PageView(); + p.key(key); + p.controller = controller; + p.scrollDirection = scrollDirection; + p.reverse = reverse; + p.physics = physics; + p.pageSnapping = pageSnapping; + p.onPageChanged = onPageChanged; + p.itemBuilder = itemBuilder; + p.itemCount = itemCount; + p.allowImplicitScrolling = allowImplicitScrolling; + p.restorationId = restorationId; + p.clipBehavior = clipBehavior; + return p; + } + + public PageController getController() { + return controller; + } + + /** The scroll axis; null means Flutter's default, {@code Axis.horizontal}. */ + public Object getScrollDirection() { + return scrollDirection; + } + + public DartList getChildren() { + return children; + } + + public Funcs.Func2 getItemBuilder() { + return itemBuilder; + } + + /** + * Whether a released drag settles on a page. Flutter's default is true, but + * it is genuinely opt-out — the gallery's home carousel passes false and + * scrolls freely, so snapping it would be a fidelity bug, not a nicety. + */ + public boolean isPageSnapping() { + return pageSnapping == null || pageSnapping.booleanValue(); + } + + public Funcs.VoidFunc1 getOnPageChanged() { + return onPageChanged; + } + + public Long getItemCount() { + return itemCount; + } + + public boolean isBuilderMode() { + return itemBuilder != null; + } + + @Override + public Element createElement() { + return new PageViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageViewRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageViewRenderElement.java new file mode 100644 index 00000000000..eddcab37989 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PageViewRenderElement.java @@ -0,0 +1,649 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Axis; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +import dart.core.DartList; + +/* + * NOTE on the gallery: its home carousel passes pageSnapping: false, so it scrolls freely + * and NONE of the settle code below runs for it. Worth knowing before reaching for this + * class to explain something the carousel does - the answer is in the scroll physics. + */ +/** + * Scroll boundary for {@link PageView}: pages sit side by side along the scroll + * axis inside a real CN1 scroll pane, each sized to the controller's + * {@code viewportFraction} of the viewport. + * + *

That fraction is the whole point of the widget's look — a value below 1 is + * what makes the neighbouring pages peek in at the edges, which is how the + * gallery's home carousel is built — so it has to reach the pages as a real + * constraint rather than being ignored.

+ * + *

Pages are materialized eagerly: page lists are short (the carousel holds + * six study cards) and every one of them animates against the controller.

+ * + *

The element also DRIVES the {@link PageController}: the pane's scroll offset + * is published as a fractional page on every scroll event, which is what lets an + * {@code AnimatedBuilder(animation: controller)} rebuild as the finger moves. + * Momentum comes from CN1's pane; a released drag settles onto a page only when + * {@code pageSnapping} is on.

+ */ +public class PageViewRenderElement extends ScrollRenderElement { + + public PageViewRenderElement(PageView widget) { + super(widget); + } + + private PageView pageView() { + return (PageView) widget(); + } + + @Override + protected boolean horizontal() { + return pageView().getScrollDirection() != Axis.vertical; + } + + @Override + protected boolean hideScrollbar() { + return true; + } + + private double viewportW; + private double viewportH; + private PageController attached; + private long reportedPage; + private boolean initialScrollApplied; + + @Override + protected void viewport(double width, double height) { + viewportW = width; + viewportH = height; + } + + // ------------------------------------------------------------------ + // Controller coupling + // ------------------------------------------------------------------ + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + attachController(); + } + + @Override + protected void syncChildren() { + super.syncChildren(); + // the configuration may have swapped the controller out from under us + attachController(); + } + + @Override + public void unmount() { + if (attached != null) { + attached.detach(this); + attached = null; + } + super.unmount(); + } + + private void attachController() { + PageController c = pageView().getController(); + if (c == attached) { + return; + } + if (attached != null) { + attached.detach(this); + } + attached = c; + if (attached != null) { + attached.attach(this); + // Seed the reported page so settling on the page we STARTED on is not + // announced as a change - Flutter fires onPageChanged on transitions, + // never for the initial page. + reportedPage = attached.initialPage(); + publishMetrics(); + } + } + + /** + * Feeds the controller the pane's geometry and offset. This is what makes + * {@code controller.position.haveDimensions} true and {@code controller.page} + * fractional, which is the whole basis of the carousel's per-card scaling. + */ + private void publishMetrics() { + double extent = pageExtent(); + if (attached == null || extent <= 0) { + // Before the first layout there is no viewport, and publishing zeroes + // would claim haveDimensions with a bogus page-0 offset. Flutter's + // contract until then is exactly the initial page, which is what an + // unattached controller already reports. + return; + } + com.codename1.ui.Container pane = pane(); + if (!initialScrollApplied) { + initialScrollApplied = true; + // A PageView opens ON its initialPage; the pane starts at zero, so the + // first layout that knows the page extent is where that is realized. + if (attached.initialPage() != 0) { + scrollToPage(attached.initialPage(), false); + } + } + // Headless there is no pane and therefore no scrolling: the view simply + // sits on its initial page. + double pixels = pane == null + ? attached.initialPage() * extent + : (horizontal() ? pane.getScrollX() : pane.getScrollY()); + double viewportDim = horizontal() ? viewportW : viewportH; + attached.applyMetrics(pixels, extent, viewportDim, 0, Math.max(0, maxScroll())); + firePageChanged(Math.round(pixels / extent)); + } + + /** The largest legal scroll offset: the last page's resting position. */ + private double maxScroll() { + return (pageCount() - 1) * pageExtent(); + } + + private int pageCount() { + PageView w = pageView(); + if (w.isBuilderMode()) { + return w.getItemCount() == null ? 0 : (int) w.getItemCount().longValue(); + } + return w.getChildren() == null ? 0 : w.getChildren().size(); + } + + /** Flutter reports onPageChanged on the SETTLED page, so only on a whole-page change. */ + private void firePageChanged(long page) { + if (page == reportedPage) { + return; + } + reportedPage = page; + dart.runtime.Funcs.VoidFunc1 cb = pageView().getOnPageChanged(); + if (cb != null) { + cb.call(new dart.runtime.RefLong(page)); + } + } + + private com.codename1.ui.Container pane() { + com.codename1.ui.Component c = component(); + return c instanceof com.codename1.ui.Container ? (com.codename1.ui.Container) c : null; + } + + /** Moves the pane onto {@code page}, animated or immediately. */ + void scrollToPage(long page, boolean animate) { + com.codename1.ui.Container pane = pane(); + if (!(pane instanceof SnappingPane)) { + return; + } + int target = (int) Math.round(Math.max(0, Math.min(maxScroll(), page * pageExtent()))); + ((SnappingPane) pane).moveTo(target, animate); + } + + /** + * Animates to {@code page} over {@code durationMs} following {@code curve} (null for + * linear), completing {@code done} when it arrives or is superseded. The duration and + * curve an app passed to animateToPage used to be dropped for the fixed 240ms settle + * spring, so a one-second linear page turn ran fast and eased. + */ + void animateToPage(long page, int durationMs, com.codename1.flutter.animation.Curve curve, + dart.async.Completer done) { + com.codename1.ui.Container pane = pane(); + if (!(pane instanceof SnappingPane)) { + done.complete(null); + return; + } + int target = (int) Math.round(Math.max(0, Math.min(maxScroll(), page * pageExtent()))); + ((SnappingPane) pane).animateTo(target, durationMs, curve, done); + } + + /** The fraction of the viewport one page occupies (Flutter's default is 1). */ + private double viewportFraction() { + PageController c = pageView().getController(); + double f = c == null ? 1.0 : c.viewportFraction(); + return f > 0 && f <= 1 ? f : 1.0; + } + + // ------------------------------------------------------------------ + // Page snapping + // ------------------------------------------------------------------ + + /** How long the settle animation runs, matching Flutter's page settle feel. */ + private static final int SNAP_MS = 240; + + /** + * The scroll offset a release settles to — Flutter's + * {@code PageScrollPhysics._getTargetPixels}. + * + *

Package-private and static so the rule can be asserted directly: it is the whole + * difference between a carousel that pages the way Flutter's does and one that drifts + * to the nearest card.

+ * + * @param from the offset at the moment the finger lifted + * @param extent one page's worth of scroll + * @param velocity the release velocity, device pixels per millisecond, positive + * towards later pages + * @param max the last page's resting offset + */ + static int settleTarget(int from, double extent, float velocity, int max) { + double page = from / extent; + if (velocity < -VELOCITY_TOLERANCE_PX_PER_MS) { + page -= 0.5; + } else if (velocity > VELOCITY_TOLERANCE_PX_PER_MS) { + page += 0.5; + } + int target = (int) Math.round(Math.round(page) * extent); + return Math.max(0, Math.min(target, max)); + } + + /// Below this a release counts as a stop rather than a flick, and the carousel falls + /// back to the nearest page instead of advancing. + /// + /// Flutter's `Tolerance.defaultTolerance.velocity` is 1/(0.05*3) logical pixels per + /// SECOND; this is the same figure in the device pixels per MILLISECOND that Codename + /// One's drag speed is measured in. It is deliberately tiny - in Flutter any purposeful + /// flick clears it, and only a release that is really a stop does not. + private static final float VELOCITY_TOLERANCE_PX_PER_MS = + (float) (com.codename1.flutter.rendering.Dp.px(1.0 / (0.05 * 3.0)) / 1000.0); + + @Override + protected com.codename1.ui.Container createPane(com.codename1.ui.layouts.Layout layout) { + return new SnappingPane(layout); + } + + /** + * A scroll pane that comes to rest ON a page. + * + *

Codename One scrolls freely and keeps its own momentum after the finger lifts, so + * a released drag otherwise stops wherever the momentum ran out - which is why the + * carousel used to sit between two cards. We cannot snap in {@code pointerReleased} + * because the momentum has not run yet; instead we wait for the scroll position to stop + * changing and settle from there.

+ */ + private final class SnappingPane extends com.codename1.ui.Container { + + private boolean settling; + + /// The settle currently running, so it can be called off. + /// + /// Without this a settle is unstoppable once started: a finger landing mid-settle, + /// or a second flick arriving before the first finished, leaves the old animation + /// running alongside the new one. Both write the scroll offset every frame, the + /// loser gets overwritten, and whichever finishes last drags the carousel to ITS + /// target - which is how it ends up resting between two cards. + private com.codename1.ui.animations.Animation settleAnim; + + SnappingPane(com.codename1.ui.layouts.Layout layout) { + super(layout); + // CN1 writes the scroll offset directly while a finger drags it, so the + // setter is not an observation point - the scroll listener is the only + // hook that sees drag, momentum and programmatic scrolling alike. + addScrollListener(new com.codename1.ui.events.ScrollListener() { + @Override + public void scrollChanged(int scrollX, int scrollY, int oldscrollX, + int oldscrollY) { + publishMetrics(); + } + }); + } + + @Override + public void pointerPressed(int x, int y) { + // The finger owns the carousel now; a settle still running would fight it. + cancelSettle(); + super.pointerPressed(x, y); + } + + /** Stops any settle in flight, leaving the scroll exactly where it got to. */ + private void cancelSettle() { + settling = false; + if (pendingDone != null) { + // Arrived, or superseded by another settle or a finger: either way the + // animateToPage future completes, as Flutter's does. + dart.async.Completer d = pendingDone; + pendingDone = null; + d.complete(null); + } + if (settleAnim == null) { + return; + } + com.codename1.ui.Form f = getComponentForm(); + if (f != null) { + f.deregisterAnimated(settleAnim); + } + settleAnim = null; + } + + @Override + public void pointerReleased(int x, int y) { + if (!pageView().isPageSnapping()) { + super.pointerReleased(x, y); + return; + } + // Read the fling BEFORE super consumes the drag state. + float velocity = getDragSpeed(!horizontal()); + super.pointerReleased(x, y); + // Codename One has just started its own decay. Flutter runs exactly ONE + // simulation from the moment of release, aimed at a page; letting the decay + // play out first and settling afterwards is two motions, and it looks like it - + // the carousel coasts to a stop and then visibly shifts again. + stopScrollMomentum(); + settle(velocity); + } + + /** + * Settles onto a page the way Flutter's {@code PageScrollPhysics} does: the target + * is chosen from the position AND the release velocity, then a single spring runs + * to it. + * + *

The half-page bias is what makes a flick feel like a flick. Past the velocity + * tolerance you go to the NEXT page even from a barely-moved carousel; below it you + * fall back to whichever page you are nearest.

+ */ + private void settle(float velocityPxPerMs) { + double extent = pageExtent(); + int from = horizontal() ? getScrollX() : getScrollY(); + int max = Math.max(0, (horizontal() + ? getScrollDimension().getWidth() - getWidth() + : getScrollDimension().getHeight() - getHeight())); + int target = extent > 0 ? settleTarget(from, extent, velocityPxPerMs, max) : from; + if ("true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.debugSettle", "false"))) { + com.codename1.flutter.FlutterErrorReport.unimplemented("PageSettle", + "from=" + from + " extent=" + (int) extent + " v=" + velocityPxPerMs + + " tol=" + VELOCITY_TOLERANCE_PX_PER_MS + " max=" + max + + " target=" + target); + } + if (extent <= 0) { + return; + } + if (target == from) { + settling = false; + return; + } + animateScroll(from, target); + } + + /** The pending completion of a curved animateTo; completed however the motion ends. */ + private dart.async.Completer pendingDone; + + /** Programmatic paging with the caller's own duration and curve. */ + void animateTo(final int target, int durationMs, final com.codename1.flutter.animation.Curve curve, + final dart.async.Completer done) { + int from = horizontal() ? getScrollX() : getScrollY(); + final com.codename1.ui.Form form = getComponentForm(); + if (from == target || durationMs <= 0 || form == null) { + cancelSettle(); + setScroll(target); + done.complete(null); + return; + } + cancelSettle(); + settling = true; + pendingDone = done; + final int start = from; + final com.codename1.ui.animations.Motion progress = + com.codename1.ui.animations.Motion.createLinearMotion(0, 10000, durationMs); + progress.start(); + settleAnim = new com.codename1.ui.animations.Animation() { + @Override + public boolean animate() { + if (settleAnim != this) { + return false; + } + double t = progress.getValue() / 10000.0; + double eased = curve == null ? t : curve.transform(t); + setScroll((int) Math.round(start + (target - start) * eased)); + if (progress.isFinished()) { + setScroll(target); + cancelSettle(); + } + return false; // see animateScroll: setScroll repaints the pane itself + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + } + }; + form.registerAnimated(settleAnim); + } + + /** Programmatic paging from the controller. */ + void moveTo(int target, boolean animate) { + int from = horizontal() ? getScrollX() : getScrollY(); + if (from == target) { + return; + } + if (!animate) { + setScroll(target); + return; + } + animateScroll(from, target); + } + + private void animateScroll(int from, final int target) { + // Exactly one settle may be in flight; starting a second without stopping the + // first leaves two animations writing the scroll offset every frame. + cancelSettle(); + final com.codename1.ui.Form form = getComponentForm(); + if (form == null) { + setScroll(target); + settling = false; + return; + } + settling = true; + final com.codename1.ui.animations.Motion motion = + // Critically damped, like Flutter's page spring - it eases out of the + // release without the symmetric slow start of an ease-in-out, which on + // a carousel that is ALREADY moving reads as a hitch before it goes. + com.codename1.ui.animations.Motion.createCriticalDampedSpringMotion( + from, target, SNAP_MS); + motion.start(); + settleAnim = new com.codename1.ui.animations.Animation() { + @Override + public boolean animate() { + if (settleAnim != this) { + // Superseded by a newer settle, or called off by a finger landing. + return false; + } + setScroll(motion.getValue()); + if (motion.isFinished()) { + setScroll(target); + cancelSettle(); + } + // False, even though this animation changes the screen every frame: + // setScroll already repaints the pane, and returning true from a + // registered Animation that is NOT a Component makes paintDirty flush + // the WHOLE screen after calling paint() on it - which paints nothing + // here. That flushes a buffer the frame never drew into, i.e. a + // full-screen flicker for the length of the settle. + return false; + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + } + }; + form.registerAnimated(settleAnim); + } + + private void setScroll(int v) { + if (horizontal()) { + setScrollX(v); + } else { + setScrollY(v); + } + repaint(); + } + } + + /** + * Publishes metrics once the viewport is known. Layout is the only point at + * which a PageView that has never been scrolled can tell its controller the + * page geometry, and the carousel's very first frame depends on it. + */ + @Override + protected Size performLayout(BoxConstraints constraints) { + Size s = super.performLayout(constraints); + publishMetrics(); + return s; + } + + /** One page's extent along the scroll axis, in device pixels. */ + double pageExtent() { + return (horizontal() ? viewportW : viewportH) * viewportFraction(); + } + + @Override + protected Widget buildContent() { + PageView w = pageView(); + DartList items = new DartList(); + if (w.isBuilderMode()) { + long count = w.getItemCount() == null ? 0 : w.getItemCount(); + for (long i = 0; i < count; i++) { + items.add(new PageSlot(w.getItemBuilder().call(this, i))); + } + } else if (w.getChildren() != null) { + for (Widget child : w.getChildren()) { + items.add(new PageSlot(child)); + } + } + if (horizontal()) { + // A viewportFraction below 1 CENTRES the current page: Flutter rests the + // scroll at -(1-f)*viewport/2, so page 0 sits inset with its neighbour + // peeking. Without this leading gap the first page is flush against the + // leading edge and all the slack piles up on the trailing side. + if (viewportFraction() < 1) { + items.insert(0, new PageGap()); + items.add(new PageGap()); + } + Row row = new Row(); + row.crossAxisAlignment(CrossAxisAlignment.stretch); + row.mainAxisSize(MainAxisSize.min); + row.children(items); + return row; + } + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(items); + return col; + } + + /** + * The leading/trailing slack that centres the resting page. + * + *

Measured at LAYOUT time, not build time: {@code buildContent} runs before the + * viewport is known, so a gap sized during the build would always compute to zero. + * Same reason the pages themselves size from {@link #viewport}.

+ */ + private final class PageGap extends Widget { + @Override + public Element createElement() { + return new PageGapElement(this); + } + } + + private final class PageGapElement extends RenderElement { + PageGapElement(PageGap widget) { + super(widget); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + double slack = viewportW * (1 - viewportFraction()) / 2; + return new Size(Math.max(0, slack), + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0); + } + } + + /** + * One page: {@code viewportFraction} of the viewport along the scroll axis, + * the full extent across it. + */ + private final class PageSlot extends Widget { + + private final Widget child; + + PageSlot(Widget child) { + this.child = child; + } + + @Override + public Element createElement() { + return new PageSlotElement(this); + } + + Widget child() { + return child; + } + } + + private final class PageSlotElement extends SingleChildRenderElement { + + PageSlotElement(PageSlot widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((PageSlot) widget()).child(); + } + + /** + * Sizes against the VIEWPORT, not the incoming constraints: inside a + * scroll boundary the main axis is unbounded by construction, so a page + * has to know the pane's extent to take a fraction of it. The extent + * comes from {@link #viewport}, reported before this layout runs — + * {@code size()} is not assigned yet at this point. + */ + @Override + protected Size performLayout(BoxConstraints constraints) { + double fraction = viewportFraction(); + double w; + double h; + if (horizontal()) { + w = viewportW * fraction; + h = constraints.hasBoundedHeight() ? constraints.maxHeight() : viewportH; + } else { + h = viewportH * fraction; + w = constraints.hasBoundedWidth() ? constraints.maxWidth() : viewportW; + } + RenderElement c = renderChild(); + if (c != null) { + c.layout(BoxConstraints.tight(w, h)); + setChildOffset(c, 0, 0); + } + return new Size(w, h); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PassThroughRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PassThroughRenderElement.java new file mode 100644 index 00000000000..28a1e5657a8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PassThroughRenderElement.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * A render element that lays out its single child with the incoming + * constraints and reports the child's size — the structural behavior shared + * by the wrapper widgets ({@link com.codename1.flutter.widgets.SafeArea}, + * Semantics, Clip*, MouseRegion, Scrollbar, Tooltip, ...). Semantic and visual + * effects those widgets carry (a11y annotations, clipping, hover) are not yet + * applied; the child renders unchanged. Owns no CN1 component. + */ +public class PassThroughRenderElement extends SingleChildRenderElement { + + public PassThroughRenderElement(Widget widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((HasChild) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.smallest(); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PhysicalShape.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PhysicalShape.java new file mode 100644 index 00000000000..a1795623f34 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PhysicalShape.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.Material; + +/** + * Fills, clips and elevates its {@code child} to an arbitrary shape — Flutter's + * {@code PhysicalShape}. + * + *

Built on {@link Material}, which already paints a coloured surface, clips + * its subtree to a rounded shape and draws an elevation shadow. The two widgets + * describe the same thing; the only difference is that PhysicalShape names its + * shape through a clipper.

+ * + *

It was a pass-through, which is a quiet way to lose a whole surface: Crane + * builds its front layer — the white rounded card the destination list sits on + * — as a PhysicalShape, so the card simply did not exist and its contents + * floated on the backdrop.

+ */ +public class PhysicalShape extends StatelessWidget implements HasChild { + + private Object clipper; + private Object clipBehavior; + private double elevation; + private Color color; + private Color shadowColor; + private Widget child; + + public void clipper(Object v) { this.clipper = v; } + public void clipBehavior(Object v) { this.clipBehavior = v; } + public void elevation(double v) { this.elevation = v; } + public void color(Color v) { this.color = v; } + public void shadowColor(Color v) { this.shadowColor = v; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + if (child == null) { + return null; + } + Material m = new Material(); + if (color != null) { + m.color(color); + } + if (shadowColor != null) { + m.shadowColor(shadowColor); + } + m.elevation(elevation); + Object shape = clipper instanceof ShapeBorderClipper + ? ((ShapeBorderClipper) clipper).getShape() : null; + if (shape != null) { + m.shape(shape); + // A shape is only a shape if the subtree is held to it; Flutter's + // PhysicalShape always clips. + m.clipBehavior(Clip.antiAlias); + } + m.child(child); + return m; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Positioned.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Positioned.java new file mode 100644 index 00000000000..e448e64f772 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Positioned.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; + +/** + * Positions a child of a {@link Stack} by insets from the stack's edges + * and/or an explicit extent (all in logical pixels). Only has an effect when + * its render element sits directly below a Stack. + */ +public class Positioned extends Widget { + + private Double left; + private Double top; + private Double right; + private Double bottom; + private Double width; + private Double height; + private Widget child; + + public void left(double v) { + this.left = v; + } + + public void top(double v) { + this.top = v; + } + + public void right(double v) { + this.right = v; + } + + public void bottom(double v) { + this.bottom = v; + } + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Double getLeft() { + return left; + } + + public Double getTop() { + return top; + } + + public Double getRight() { + return right; + } + + public Double getBottom() { + return bottom; + } + + public Double getWidth() { + return width; + } + + public Double getHeight() { + return height; + } + + public Widget getChild() { + return child; + } + + /** + * {@code Positioned.fill}: pins the child to all four edges of the stack + * (each unspecified inset defaults to 0), so it fills the stack. + */ + public static Positioned fill(Key key, Double left, Double top, Double right, Double bottom, + Widget child) { + Positioned p = new Positioned(); + p.key(key); + p.left(left == null ? 0 : left); + p.top(top == null ? 0 : top); + p.right(right == null ? 0 : right); + p.bottom(bottom == null ? 0 : bottom); + if (child != null) { + p.child(child); + } + return p; + } + + @Override + public Element createElement() { + return new PositionedRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedDirectional.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedDirectional.java new file mode 100644 index 00000000000..0568c0bcfd8 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedDirectional.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * The text-direction-aware form of {@link Positioned} used inside a {@code Stack}: + * {@code start}/{@code end} resolve to left/right against the ambient text direction — + * Flutter's {@code PositionedDirectional}. + * + *

It hosted the child and dropped every inset, so anything positioned this way landed + * wherever the Stack happened to put it. Resolving to a real {@link Positioned} is all it + * needs: the ambient direction decides which edge {@code start} means.

+ */ +public class PositionedDirectional extends StatelessWidget { + + private Double start; + private Double top; + private Double end; + private Double bottom; + private Double width; + private Double height; + private Widget child; + + public void start(double v) { + this.start = v; + } + + public void top(double v) { + this.top = v; + } + + public void end(double v) { + this.end = v; + } + + public void bottom(double v) { + this.bottom = v; + } + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Double getStart() { + return start; + } + + public Double getEnd() { + return end; + } + + public Widget getChild() { + return child; + } + + @Override + public Widget build(BuildContext context) { + boolean rtl = Directionality.of(context) == com.codename1.flutter.TextDirection.rtl; + Positioned p = new Positioned(); + if (start != null) { + if (rtl) { + p.right(start.doubleValue()); + } else { + p.left(start.doubleValue()); + } + } + if (end != null) { + if (rtl) { + p.left(end.doubleValue()); + } else { + p.right(end.doubleValue()); + } + } + if (top != null) { + p.top(top.doubleValue()); + } + if (bottom != null) { + p.bottom(bottom.doubleValue()); + } + if (width != null) { + p.width(width.doubleValue()); + } + if (height != null) { + p.height(height.doubleValue()); + } + p.child(child); + return p; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedRenderElement.java new file mode 100644 index 00000000000..66c8c47674b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PositionedRenderElement.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * Pass-through box carrying the inset configuration read by + * {@link StackRenderElement}. The Stack parent hands it the constraints it + * resolved from the insets; it forwards them to its child unchanged. + */ +public class PositionedRenderElement extends SingleChildRenderElement { + + public PositionedRenderElement(Positioned widget) { + super(widget); + } + + public Positioned positioned() { + return (Positioned) widget(); + } + + @Override + protected Widget childWidget() { + return positioned().getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + RenderElement child = renderChild(); + if (child == null) { + return constraints.smallest(); + } + Size cs = child.layout(constraints); + setChildOffset(child, 0, 0); + return constraints.constrain(cs); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSize.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSize.java new file mode 100644 index 00000000000..3731ae9659b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSize.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; + +/** + * Adapts an arbitrary {@code child} into a {@link PreferredSizeWidget} of a + * given size — Flutter's {@code PreferredSize}. Used as the {@code bottom} of an + * app bar so the bar reserves {@code preferredSize.height}. Structural + * pass-through for this milestone: the {@code child} renders unchanged. + */ +public class PreferredSize extends Widget implements HasChild, PreferredSizeWidget { + + private Size preferredSize; + private Widget child; + + public void preferredSize(Size v) { + this.preferredSize = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Size preferredSize() { + return preferredSize; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSizeWidget.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSizeWidget.java new file mode 100644 index 00000000000..75e8c3ad573 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/PreferredSizeWidget.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.rendering.Size; + +/** + * A widget that reports the {@link Size} it prefers to occupy — Flutter's + * {@code PreferredSizeWidget}. App bars and the {@link PreferredSize} adapter + * implement it so a {@code Scaffold}/{@code AppBar} can reserve the right height + * for a bottom widget. + */ +public interface PreferredSizeWidget { + Size preferredSize(); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RawScrollbar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RawScrollbar.java new file mode 100644 index 00000000000..a78ed5d6748 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RawScrollbar.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A scrollbar with no theme defaults wrapping a scrollable — Flutter's + * {@code RawScrollbar}. This milestone renders the {@code child}; the scrollbar + * track/thumb overlay is deferred (CN1 scrollables draw their own indicator). + */ +public class RawScrollbar extends StatelessWidget { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + public void controller(Object v) { + } + + private boolean thumbVisibility; + + public void thumbVisibility(boolean v) { + this.thumbVisibility = v; + } + + /** Whether the thumb stays on screen when nothing is scrolling. */ + public boolean isThumbVisible() { + return thumbVisibility; + } + + public void thumbColor(Object v) { + } + + public void radius(Object v) { + } + + public void thickness(double v) { + } + + public void interactive(boolean v) { + } + + public void notificationPredicate(Object v) { + } + + public void scrollbarOrientation(Object v) { + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReadingOrderTraversalPolicy.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReadingOrderTraversalPolicy.java new file mode 100644 index 00000000000..73c6c4f6bc2 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReadingOrderTraversalPolicy.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Traverses focus in reading order for the ambient text direction — Flutter's {@code ReadingOrderTraversalPolicy}. Captured for API shape by + * {@link FocusTraversalGroup}; live focus traversal is deferred to a later pass. + */ +public class ReadingOrderTraversalPolicy { + + private Object secondary; + + public ReadingOrderTraversalPolicy() { + } + + public void secondary(Object v) { + this.secondary = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReorderableListView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReorderableListView.java new file mode 100644 index 00000000000..7137db71227 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ReorderableListView.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Key; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A list whose items can be reordered by dragging — Flutter's + * {@code ReorderableListView}. This milestone renders the items as a scrollable + * {@link ListView} (children mode or {@code .builder} mode); the drag-handle + * reordering that fires {@code onReorder(oldIndex, newIndex)} is deferred. + */ +public class ReorderableListView extends StatelessWidget { + + private DartList children; + private Widget header; + private Funcs.VoidFunc2 onReorder; + private Funcs.Func2 itemBuilder; + private Long itemCount; + private boolean shrinkWrap; + + public ReorderableListView() { + } + + /** Dart's {@code ReorderableListView.builder} named constructor. */ + public static ReorderableListView builder(Key key, + Funcs.Func2 itemBuilder, + long itemCount, + Funcs.VoidFunc2 onReorder, + Object padding, + Object scrollDirection, + Boolean shrinkWrap) { + ReorderableListView r = new ReorderableListView(); + r.key(key); + r.itemBuilder = itemBuilder; + r.itemCount = itemCount; + r.onReorder = onReorder; + r.shrinkWrap = shrinkWrap != null && shrinkWrap; + return r; + } + + public void children(DartList v) { + this.children = v; + } + + public void header(Widget v) { + this.header = v; + } + + public void onReorder(Funcs.VoidFunc2 v) { + this.onReorder = v; + } + + public void padding(Object v) { + } + + public void scrollDirection(Object v) { + } + + public void shrinkWrap(boolean v) { + this.shrinkWrap = v; + } + + public void physics(Object v) { + } + + public void buildDefaultDragHandles(boolean v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = new DartList(); + if (header != null) { + kids.add(header); + } + if (itemBuilder != null && itemCount != null) { + for (long i = 0; i < itemCount; i++) { + Widget w = itemBuilder.call(context, i); + if (w != null) { + kids.add(w); + } + } + } else if (children != null) { + for (int i = 0; i < children.size(); i++) { + kids.add(children.get(i)); + } + } + ListView list = new ListView(); + list.children(kids); + if (shrinkWrap) { + list.shrinkWrap(true); + } + return list; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RepaintBoundary.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RepaintBoundary.java new file mode 100644 index 00000000000..510fc426719 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RepaintBoundary.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Isolates its subtree onto its own layer for cheaper repaints — Flutter's {@code RepaintBoundary}. + * + *

Structural pass-through for this milestone: the single {@code child} + * renders unchanged (see {@link PassThroughRenderElement}); the captured + * parameters are held for a later render pass.

+ */ +public class RepaintBoundary extends Widget implements HasChild { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RestorationScope.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RestorationScope.java new file mode 100644 index 00000000000..75dcdb44df5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RestorationScope.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Establishes a restoration namespace for its subtree — Flutter's + * {@code RestorationScope}. Structural pass-through for this milestone: the + * {@code child} renders unchanged and the {@code restorationId} is captured. + */ +public class RestorationScope extends Widget implements HasChild { + + private String restorationId; + private Widget child; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + /** Flutter's {@code RestorationScope.of} — no ambient bucket at this pass. */ + public static Object of(BuildContext context) { + return null; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichText.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichText.java new file mode 100644 index 00000000000..b87ff069bd6 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichText.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.Widget; + +/** + * A paragraph of mixed-style text described by a {@link TextSpan} tree + * (Flutter's RichText). The span tree is flattened into styled runs, wrapped + * across lines with per-run fonts, and custom-painted honoring + * {@link TextAlign}. + */ +public class RichText extends Widget { + + private TextSpan text; + private TextAlign textAlign; + + public void text(TextSpan v) { + this.text = v; + } + + public void textAlign(TextAlign v) { + this.textAlign = v; + } + + public TextSpan getText() { + return text; + } + + public TextAlign getTextAlign() { + return textAlign; + } + + @Override + public Element createElement() { + return new RichTextRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java new file mode 100644 index 00000000000..e34e145a14f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RichTextRenderElement.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.RichTextComponent; +import com.codename1.ui.geom.Dimension; + +import java.util.ArrayList; +import java.util.List; + +/** + * Render box for {@link RichText}: it {@link #flatten flattens} the {@link TextSpan} tree into + * resolved {@link Run}s and renders them with a shared {@link RichTextComponent} (UIID + * "FlutterRichText"), which owns word wrapping, per-run styling and multi-size line layout. The + * same component backs the general-purpose Codename One rich text API, so RichText inherits its + * rendering rather than duplicating a wrapping engine. + * + *

Span flattening and style resolution live here (they are pure and headless-testable); wrapping + * and painting are delegated to {@link RichTextComponent}.

+ */ +public class RichTextRenderElement extends RenderElement { + + public RichTextRenderElement(RichText widget) { + super(widget); + } + + private RichText richText() { + return (RichText) widget(); + } + + // ------------------------------------------------------------------ + // Component + // ------------------------------------------------------------------ + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + RichTextComponent c = new RichTextComponent(); + c.setUIID("FlutterRichText"); + stylesToSet(c).setPadding(0, 0, 0, 0); + stylesToSet(c).setMargin(0, 0, 0, 0); + stylesToSet(c).setBgTransparency(0); + applyContent(c); + return c; + } + + @Override + protected void updateComponent(Component comp) { + applyContent((RichTextComponent) comp); + } + + /** Rebuilds the component's content from the current span tree and alignment. */ + private void applyContent(RichTextComponent c) { + c.clear(); + for (Run run : flatten(richText().getText())) { + c.append(run.text, toEditorStyle(run.style)); + } + c.setTextAlign(cn1Align()); + } + + private int cn1Align() { + TextAlign a = richText().getTextAlign(); + if (a == null) { + return Component.LEFT; + } + switch (a) { + case right: + case end: + return Component.RIGHT; + case center: + return Component.CENTER; + default: + return Component.LEFT; + } + } + + /** + * Maps a resolved Flutter {@link TextStyle} to the editor {@link com.codename1.ui.editor.TextStyle} + * the rich text component consumes: font size becomes an absolute pixel size, bold weight and + * color carry over. (This minimal Flutter TextStyle exposes no italic/decoration.) + */ + static com.codename1.ui.editor.TextStyle toEditorStyle(TextStyle style) { + com.codename1.ui.editor.TextStyle s = com.codename1.ui.editor.TextStyle.DEFAULT; + if (style == null) { + return s; + } + if (style.getFontSize() != null) { + s = s.withFontSizePx((int) Math.round(Dp.px(style.getFontSize()))); + } + if (style.getFontWeight() != null && style.getFontWeight().isBold()) { + s = s.withBold(true); + } + if (style.getColor() != null) { + s = s.withForeColor(style.getColor().rgb()); + } + return s; + } + + // ------------------------------------------------------------------ + // Layout (delegated to the rich text component's height-for-width sizing) + // ------------------------------------------------------------------ + + @Override + protected Size performLayout(BoxConstraints constraints) { + RichTextComponent c = (RichTextComponent) component(); + if (c == null) { + return constraints.smallest(); + } + double maxWidth = constraints.maxWidth(); + int w = maxWidth == Double.POSITIVE_INFINITY || maxWidth > Integer.MAX_VALUE / 4 + ? Integer.MAX_VALUE / 4 + : (int) Math.ceil(maxWidth); + Dimension d = c.preferredSizeForWidth(w); + return constraints.constrain(new Size(d.getWidth(), d.getHeight())); + } + + // ------------------------------------------------------------------ + // Span flattening (pure — headless-testable) + // ------------------------------------------------------------------ + + /** + * A flattened text run with its fully RESOLVED style. + */ + public static class Run { + public final String text; + public final TextStyle style; + + public Run(String text, TextStyle style) { + this.text = text; + this.style = style; + } + } + + /** + * Depth-first flattening of a span tree: a span's own text precedes its + * children; each node's effective style is its own style with null + * properties inherited from the parent chain. Null/empty texts + * contribute no run (their children still do). + */ + public static List flatten(TextSpan root) { + List out = new ArrayList(); + collect(root, null, out); + return out; + } + + private static void collect(TextSpan span, TextStyle inherited, List out) { + if (span == null) { + return; + } + TextStyle eff = resolve(inherited, span.getStyle()); + if (span.getText() != null && span.getText().length() > 0) { + out.add(new Run(span.getText(), eff)); + } + if (span.getChildren() != null) { + for (TextSpan c : span.getChildren()) { + collect(c, eff, out); + } + } + } + + /** + * Style inheritance: the child's non-null properties win, everything + * else comes from the parent. Identity is preserved when one side is + * null (so downstream style handling can key resolved styles by identity). + */ + public static TextStyle resolve(TextStyle parent, TextStyle child) { + if (child == null) { + return parent; + } + if (parent == null) { + return child; + } + TextStyle out = new TextStyle(); + if (child.getFontSize() != null) { + out.fontSize(child.getFontSize()); + } else if (parent.getFontSize() != null) { + out.fontSize(parent.getFontSize()); + } + if (child.getFontWeight() != null) { + out.fontWeight(child.getFontWeight()); + } else if (parent.getFontWeight() != null) { + out.fontWeight(parent.getFontWeight()); + } + if (child.getColor() != null) { + out.color(child.getColor()); + } else if (parent.getColor() != null) { + out.color(parent.getColor()); + } + if (child.getFontFamily() != null) { + out.fontFamily(child.getFontFamily()); + } else if (parent.getFontFamily() != null) { + out.fontFamily(parent.getFontFamily()); + } + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBox.java new file mode 100644 index 00000000000..13cb33356e9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBox.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Rotates its {@code child} by an integral number of quarter turns — Flutter's + * {@code RotatedBox}. Unlike {@code Transform.rotate}, the rotation also affects layout: a + * 1- or 3-turn box swaps width and height. + */ +public class RotatedBox extends Widget { + + private long quarterTurns; + private Widget child; + + public void quarterTurns(long v) { + this.quarterTurns = v; + } + + public void child(Widget v) { + this.child = v; + } + + public long getQuarterTurns() { + return quarterTurns; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new RotatedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBoxRenderElement.java new file mode 100644 index 00000000000..66cf0784cff --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/RotatedBoxRenderElement.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Lays out and paints a {@link RotatedBox}: quarter turns that affect LAYOUT, not just + * painting. + * + *

That is the whole difference from {@code Transform.rotate} — an odd number of quarter + * turns swaps the box's width and height, so the parent reserves the rotated footprint. The + * child is measured against constraints with the axes swapped, and the painting is rotated + * about the centre to match.

+ */ +public class RotatedBoxRenderElement extends EffectRenderElement { + + public RotatedBoxRenderElement(RotatedBox widget) { + super(widget); + } + + private RotatedBox box() { + return (RotatedBox) widget(); + } + + /** Quarter turns normalised to 0..3; only the parity affects the axes. */ + private int turns() { + long q = box().getQuarterTurns() % 4; + return (int) (q < 0 ? q + 4 : q); + } + + private boolean swapsAxes() { + return (turns() & 1) == 1; + } + + @Override + protected Widget effectChild() { + return box().getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + if (!swapsAxes()) { + return super.performLayout(constraints); + } + // Measure the child in the ROTATED frame, then report its footprint swapped back. + BoxConstraints swapped = new BoxConstraints(constraints.minHeight(), + constraints.maxHeight(), constraints.minWidth(), constraints.maxWidth()); + Size child = super.performLayout(swapped); + return constraints.constrain(new Size(child.height(), child.width())); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + int t = turns(); + if (t == 0) { + paintChildren.paint(g); + return; + } + if (!g.isTransformSupported()) { + com.codename1.flutter.FlutterErrorReport.unimplemented("RotatedBox", + "this platform has no transform support; the rotation is not painted"); + paintChildren.paint(g); + return; + } + // The subtree is rendered to a layer and the LAYER is turned, rather than the + // subtree being walked through a rotated Graphics. Under a matrix, Codename + // One's paint-time cull compares device-pixel component bounds against a clip + // reported in the matrix's own coordinates, and drops children that are in + // fact on screen -- see the note on EffectRenderElement.layer. + // + // The layer takes the CHILD's box, not the pane's: performLayout above reports + // the child's footprint with its axes swapped, so for a quarter turn the two + // differ and a pane-sized layer would clip the child before turning it. + int lw = swapsAxes() ? pane.getHeight() : pane.getWidth(); + int lh = swapsAxes() ? pane.getWidth() : pane.getHeight(); + com.codename1.ui.Image rendered = layer(pane, paintChildren, lw, lh); + if (rendered == null) { + return; + } + com.codename1.ui.Transform saved = g.getTransform(); + com.codename1.ui.Transform r = saved.copy(); + // The pivot is in the pane's own parent coordinates -- the space this Graphics + // is in. Absolute screen coordinates are a different space once any ancestor + // has translated, which is every ancestor. + float cx = pane.getX() + pane.getWidth() / 2f; + float cy = pane.getY() + pane.getHeight() / 2f; + r.translate(cx, cy); + r.rotate((float) (t * Math.PI / 2), 0, 0); + r.translate(-cx, -cy); + g.setTransform(r); + try { + g.drawImage(rendered, Math.round(cx - lw / 2f), Math.round(cy - lh / 2f)); + } finally { + g.setTransform(saved); + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Row.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Row.java new file mode 100644 index 00000000000..4944df8b35f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Row.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * A horizontal Flex. + */ +public class Row extends Flex { + + @Override + public boolean isVertical() { + return false; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SafeArea.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SafeArea.java new file mode 100644 index 00000000000..2bedd76c506 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SafeArea.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Insets its child to avoid system intrusions — the status bar, the notch and the home + * indicator. Flutter's {@code SafeArea}. + * + *

It used to render the child unchanged, on the theory that Codename One's Form already + * keeps content clear. That is not true of a Flutter subtree laid out inside a raw + * container: content ran under the notch and the home indicator, and controls near the + * bottom edge could not be reached at all. Every SafeArea in the app was a no-op.

+ * + *

It resolves to a {@link Padding} over the ambient {@code MediaQuery.padding}, taking + * only the edges whose flags are set and never less than {@code minimum} — which is what + * Flutter's own implementation does.

+ */ +public class SafeArea extends com.codename1.flutter.StatelessWidget implements HasChild { + + private boolean left = true; + private boolean top = true; + private boolean right = true; + private boolean bottom = true; + private EdgeInsets minimum; + private boolean maintainBottomViewPadding; + private Widget child; + + public void left(boolean v) { + this.left = v; + } + + public void top(boolean v) { + this.top = v; + } + + public void right(boolean v) { + this.right = v; + } + + public void bottom(boolean v) { + this.bottom = v; + } + + public void minimum(EdgeInsets v) { + this.minimum = v; + } + + public void maintainBottomViewPadding(boolean v) { + this.maintainBottomViewPadding = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Widget build(com.codename1.flutter.BuildContext context) { + EdgeInsets safe = com.codename1.flutter.MediaQuery.of(context).padding(); + double l = left ? safe.left() : 0; + double t = top ? safe.top() : 0; + double r = right ? safe.right() : 0; + double b = bottom ? safe.bottom() : 0; + if (minimum != null) { + l = Math.max(l, minimum.left()); + t = Math.max(t, minimum.top()); + r = Math.max(r, minimum.right()); + b = Math.max(b, minimum.bottom()); + } + if (l == 0 && t == 0 && r == 0 && b == 0) { + return child; + } + Padding pad = new Padding(); + pad.padding(EdgeInsets.fromLTRB(l, t, r, b)); + pad.child(child); + // The inset is spent here, so the subtree must not see it again. Flutter + // does the same, and without it two nested safe areas inset twice for + // one notch. + return com.codename1.flutter.MediaQuery.removePadding(context, + Boolean.valueOf(left), Boolean.valueOf(top), + Boolean.valueOf(right), Boolean.valueOf(bottom), pad); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollBehavior.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollBehavior.java new file mode 100644 index 00000000000..ff12c94a464 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollBehavior.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Describes how scrollables should behave app-wide — Flutter's + * {@code ScrollBehavior}: which input devices drag, whether scrollbars and + * overscroll indicators appear, and the default physics. {@link #copyWith} + * produces a derived behaviour with selected properties overridden. + */ +public class ScrollBehavior { + + private Boolean scrollbars; + private Boolean overscroll; + + public ScrollBehavior() { + } + + /** + * {@code ScrollBehavior.copyWith}: a copy of this behaviour with the given + * properties overridden. Unmodelled properties are accepted and ignored. + */ + public ScrollBehavior copyWith(Boolean scrollbars, Boolean overscroll, + Object physics, Object platform, Object dragDevices) { + ScrollBehavior b = newInstance(); + b.scrollbars = scrollbars != null ? scrollbars : this.scrollbars; + b.overscroll = overscroll != null ? overscroll : this.overscroll; + return b; + } + + /** Allows subclasses (e.g. MaterialScrollBehavior) to preserve their type. */ + protected ScrollBehavior newInstance() { + return new ScrollBehavior(); + } + + public Boolean getScrollbars() { + return scrollbars; + } + + public Boolean getOverscroll() { + return overscroll; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollController.java new file mode 100644 index 00000000000..59bff168d9d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollController.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.animation.Curve; + +import dart.async.Future; +import dart.core.Duration; +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Controls the offset of a scrollable ({@code ScrollController} in Flutter). The + * new_gallery desktop carousel reads {@link #offset()} / {@link #position()}, + * calls {@link #animateTo}, and adds a listener to toggle its paging buttons. + * + *

The controller owns a {@link ScrollPosition}; the mounted scroll render + * element keeps that position's extents in sync and forwards user scrolls, which + * notify listeners.

+ */ +public class ScrollController { + + private final ScrollPosition scrollPosition = new ScrollPosition(); + private final List listeners = new ArrayList(); + private double initialScrollOffset; + private boolean keepScrollOffset = true; + private String debugLabel; + private boolean attached; + + public ScrollController() { + } + + // Named-parameter setters. + public void initialScrollOffset(double v) { + this.initialScrollOffset = v; + this.scrollPosition.setPixels(v); + } + + public void keepScrollOffset(boolean v) { + this.keepScrollOffset = v; + } + + public void debugLabel(String v) { + this.debugLabel = v; + } + + public double offset() { + return scrollPosition.pixels(); + } + + public ScrollPosition position() { + return scrollPosition; + } + + public boolean hasClients() { + return attached; + } + + /** The animateTo in progress, or null. */ + private com.codename1.flutter.animation.AnimationController motion; + private dart.async.Completer motionDone; + + /** + * Scrolls the attached list to {@code offset} over {@code duration}, following + * {@code curve}, and completes when it arrives -- or when the motion is interrupted + * by a jump, another animateTo or dispose, as Flutter's does. It used to assign the + * target at once and return a completed future, so the list jumped and a caller + * awaiting the scroll resumed before any motion could have run. + */ + public Future animateTo(final double offset, Duration duration, final Curve curve) { + interruptMotion(); + if (client == null || duration == null || duration.inMilliseconds() <= 0) { + moveTo(offset); + return Future.value(null); + } + final double from = scrollPosition.pixels(); + final com.codename1.flutter.animation.AnimationController run = + new com.codename1.flutter.animation.AnimationController(); + run.duration(duration); + run.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + if (motion != run) { + return; + } + double v = run.value(); + double t = curve == null ? v : curve.transform(v); + moveTo(from + (offset - from) * t); + } + }); + final dart.async.Completer done = new dart.async.Completer(); + motion = run; + motionDone = done; + run.forward().then(new Funcs.Func1() { + @Override + public Object call(Object ignored) { + if (motion == run) { + moveTo(offset); + motion = null; + motionDone = null; + run.dispose(); + done.complete(null); + } + return null; + } + }); + return done.future(); + } + + /** Stops an animateTo in progress where it is, completing its future. */ + private void interruptMotion() { + com.codename1.flutter.animation.AnimationController run = motion; + dart.async.Completer done = motionDone; + motion = null; + motionDone = null; + if (run != null) { + run.dispose(); + } + if (done != null) { + done.complete(null); + } + } + + private void moveTo(double px) { + scrollPosition.jumpTo(px); + if (client != null) { + client.scrollToOffset(px); + } + notifyListeners(); + } + + public void jumpTo(double value) { + interruptMotion(); + scrollPosition.jumpTo(value); + if (client != null) { + client.scrollToOffset(value); + } + notifyListeners(); + } + + public void addListener(Funcs.VoidFunc0 listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(Funcs.VoidFunc0 listener) { + listeners.remove(listener); + } + + public void dispose() { + interruptMotion(); + listeners.clear(); + attached = false; + } + + // ------------------------------------------------------------------ + // Framework plumbing + // ------------------------------------------------------------------ + + /** + * The scrollable a controller drives: the mounted list moves itself to an offset. + * + *

A ListView used to store its controller and never attach it, so hasClients + * stayed false, user scrolling never reached offset or the listeners, and + * jumpTo/animateTo moved only this detached model while the list sat still.

+ */ + interface Client { + /** Scrolls to {@code offset} logical pixels. */ + void scrollToOffset(double offset); + } + + private Client client; + + void attach() { + this.attached = true; + } + + /** Attaches the scrollable this controller drives. */ + void attach(Client c) { + this.client = c; + this.attached = true; + // The position this controller already holds -- its initialScrollOffset, or + // where an earlier list left it -- is where the newly attached one starts. + // Recording it only here left the list at zero while offset reported it. + if (c != null && scrollPosition.pixels() != 0) { + c.scrollToOffset(scrollPosition.pixels()); + } + } + + void detach() { + this.attached = false; + this.client = null; + } + + /** Detaches {@code c}, if it is still the one attached. */ + void detach(Client c) { + if (client == c) { + detach(); + } + } + + /** The user scrolled the attached list to {@code offset} logical pixels. */ + void userScrolled(double offset, double maxExtent, double viewport) { + scrollPosition.applyViewportDimension(viewport); + scrollPosition.applyContentDimensions(0, maxExtent); + if (scrollPosition.pixels() != offset) { + scrollPosition.setPixels(offset); + notifyListeners(); + } + } + + void notifyListeners() { + com.codename1.flutter.foundation.Listeners.notify(listeners); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollMetrics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollMetrics.java new file mode 100644 index 00000000000..f9402fa456c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollMetrics.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * A read-only description of a scrollable's content and viewport extents + * ({@code ScrollMetrics} in Flutter). {@link ScrollPosition} and scroll + * notifications expose it; new_gallery's carousel physics read {@link #pixels()} + * and {@link #maxScrollExtent()} to decide button visibility and target offsets. + * + *

This base holds the extents in fields (defaulting to zero) so subtypes and + * the render layer can update them as the scrollable lays out.

+ */ +public class ScrollMetrics { + + protected double pixels; + protected double minScrollExtent; + protected double maxScrollExtent; + protected double viewportDimension; + protected boolean hasContentDimensions; + protected boolean hasPixels; + protected boolean hasViewportDimension; + + public double pixels() { + return pixels; + } + + public double minScrollExtent() { + return minScrollExtent; + } + + public double maxScrollExtent() { + return maxScrollExtent; + } + + public double viewportDimension() { + return viewportDimension; + } + + /** Amount of content scrolled off the leading edge. */ + public double extentBefore() { + return Math.max(0.0, pixels - minScrollExtent); + } + + /** Amount of content still below the trailing edge. */ + public double extentAfter() { + return Math.max(0.0, maxScrollExtent - pixels); + } + + /** Amount of content currently visible in the viewport. */ + public double extentInside() { + return viewportDimension; + } + + /** Whether the scrollable is at its minimum or maximum extent. */ + public boolean atEdge() { + return pixels <= minScrollExtent || pixels >= maxScrollExtent; + } + + /** + * Whether the scroll offset has been dragged PAST an extent — Flutter's + * {@code outOfRange}. Distinct from {@link #atEdge()}: sitting exactly on the edge is + * in range, and the physics leave a drag untouched until it actually overshoots. + */ + public boolean outOfRange() { + return pixels < minScrollExtent || pixels > maxScrollExtent; + } + + public boolean hasContentDimensions() { + return hasContentDimensions; + } + + public boolean hasPixels() { + return hasPixels; + } + + public boolean hasViewportDimension() { + return hasViewportDimension; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollNotification.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollNotification.java new file mode 100644 index 00000000000..1867e547f5c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollNotification.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.rendering.ScrollDirection; + +/** + * A notification that bubbles up the widget tree as a scrollable scrolls — + * Flutter's {@code ScrollNotification}. The reply adaptive-nav reads + * {@link #direction()} and the nesting {@code depth} to drive the bottom app + * bar. Instances are produced by the scroll machinery; this pass captures the + * inspected shape. + */ +public class ScrollNotification { + + private ScrollMetrics metrics; + private long depth; + private BuildContext context; + private ScrollDirection direction; + + public ScrollMetrics metrics() { + return metrics; + } + + public void metrics(ScrollMetrics v) { + this.metrics = v; + } + + /** The number of scrollables this notification has bubbled through. */ + public long get$depth() { + return depth; + } + + public void depth(long v) { + this.depth = v; + } + + public BuildContext context() { + return context; + } + + public void context(BuildContext v) { + this.context = v; + } + + public ScrollDirection direction() { + return direction; + } + + public void direction(ScrollDirection v) { + this.direction = v; + } + + /** + * Delivers this notification to every {@link NotificationListener} above {@code target}, + * innermost first, stopping at the first one that returns true -- Flutter's + * {@code Notification.dispatch}. + * + *

{@link #get$depth()} counts the scroll views the notification has bubbled THROUGH, + * not the listeners. That is the distinction the gallery relies on: Reply hides its + * bottom bar on {@code UserScrollNotification(depth: 0)}, so that dragging the image + * carousel inside a mail card -- a scroll view of its own, whose notifications reach + * the same listener at depth 1 -- does not move the bar.

+ */ + public boolean dispatch(BuildContext target) { + if (!(target instanceof com.codename1.flutter.Element)) { + return false; + } + context(target); + long depth = 0; + com.codename1.flutter.Element from = (com.codename1.flutter.Element) target; + for (com.codename1.flutter.Element a = from.parent(); a != null; a = a.parent()) { + if (a instanceof ScrollRenderElement) { + depth++; + continue; + } + if (!(a.widget() instanceof NotificationListener)) { + continue; + } + dart.runtime.Funcs.Func1 cb = + ((NotificationListener) a.widget()).getOnNotification(); + if (cb == null) { + continue; + } + depth(depth); + if (Boolean.TRUE.equals(cb.call(this))) { + return true; + } + } + return false; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPhysics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPhysics.java new file mode 100644 index 00000000000..ecb90685c82 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPhysics.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.physics.SpringDescription; + +/** + * Determines how scrollable widgets respond to user input — Flutter's {@code + * ScrollPhysics}. Physics can be composed by chaining a {@code parent}. This + * pass captures the physics selection (and any parent) for API shape; the + * concrete fling/overscroll behaviour is applied by the scrolling layer. + */ +public class ScrollPhysics { + + /** Material's default scroll spring, matching Flutter's ScrollPhysics.spring. */ + private static final SpringDescription DEFAULT_SPRING = + SpringDescription.withDampingRatio(0.5, 100.0, 1.1); + + private ScrollPhysics parent; + + public ScrollPhysics() { + } + + public void parent(ScrollPhysics v) { + this.parent = v; + } + + public ScrollPhysics getParent() { + return parent; + } + + /** + * {@code ScrollPhysics.spring}: the default spring an overriding + * {@code createBallisticSimulation} feeds to a ScrollSpringSimulation. + */ + public SpringDescription spring() { + return DEFAULT_SPRING; + } + + /** + * Returns a copy of this physics with {@code ancestor} as the tail of the parent + * chain — Flutter's {@code ScrollPhysics.applyTo}. Subclasses override to return + * their own type; the base composes a plain ScrollPhysics. + */ + public ScrollPhysics applyTo(ScrollPhysics ancestor) { + ScrollPhysics p = new ScrollPhysics(); + p.parent = ancestor; + return p; + } + + /** + * Helper for {@code applyTo}: applies this physics's parent onto {@code ancestor} + * — Flutter's {@code ScrollPhysics.buildParent}. + */ + public ScrollPhysics buildParent(ScrollPhysics ancestor) { + return parent == null ? ancestor : parent.applyTo(ancestor); + } + + /** + * The ballistic (fling) simulation for a release at {@code velocity} — Flutter's + * {@code ScrollPhysics.createBallisticSimulation}. Delegates to the parent, else none. + */ + public com.codename1.flutter.physics.Simulation createBallisticSimulation( + ScrollMetrics position, double velocity) { + return parent == null ? null : parent.createBallisticSimulation(position, velocity); + } + + /** The tolerance for this physics ({@code ScrollPhysics.toleranceFor}). */ + public com.codename1.flutter.physics.Tolerance toleranceFor(ScrollMetrics metrics) { + return parent == null ? new com.codename1.flutter.physics.Tolerance() + : parent.toleranceFor(metrics); + } + + /** Whether implicit scrolling (e.g. for accessibility) is allowed. */ + public boolean allowImplicitScrolling() { + return true; + } + + /** + * Converts a raw drag delta into the offset actually applied to the scroll position — + * Flutter's {@code ScrollPhysics.applyPhysicsToUserOffset}. + * + *

The base passes the drag straight through: one pixel of finger is one pixel of + * content. {@link BouncingScrollPhysics} is where that stops being true.

+ */ + public double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { + return parent == null ? offset : parent.applyPhysicsToUserOffset(position, offset); + } + + /** Whether the scrollable should respond to a drag at all. */ + public boolean shouldAcceptUserOffset(ScrollMetrics position) { + if (parent != null) { + return parent.shouldAcceptUserOffset(position); + } + return position.pixels() != 0.0 + || position.minScrollExtent() != position.maxScrollExtent(); + } + + // The fling thresholds, in logical pixels (per second for the velocities). Values are + // Flutter's, confirmed against the SDK by the flutter_scroll_physics behavioral case + // rather than transcribed from its constants. + public double minFlingVelocity() { + return parent == null ? 100.0 : parent.minFlingVelocity(); + } + + public double maxFlingVelocity() { + return parent == null ? 8000.0 : parent.maxFlingVelocity(); + } + + public double minFlingDistance() { + return parent == null ? 18.0 : parent.minFlingDistance(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPosition.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPosition.java new file mode 100644 index 00000000000..485b8f122f7 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollPosition.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.animation.Curve; + +import dart.async.Future; +import dart.core.Duration; + +/** + * The live scroll offset of a single scrollable ({@code ScrollPosition} in + * Flutter), extending {@link ScrollMetrics} with mutation. new_gallery reads + * {@code controller.position.maxScrollExtent} / {@code .haveDimensions}. Actual + * animated scrolling is driven by the mounted scroll render element; here the + * pixel offset is updated synchronously. + */ +public class ScrollPosition extends ScrollMetrics { + + /** Whether the viewport and content dimensions are known yet. */ + public boolean haveDimensions() { + return hasContentDimensions && hasViewportDimension; + } + + /** Animate to {@code to}; completes immediately in the M1 model. */ + public Future animateTo(double to, Duration duration, Curve curve) { + jumpTo(to); + return Future.value(null); + } + + public void jumpTo(double value) { + this.pixels = Math.max(minScrollExtent, Math.min(maxScrollExtent, value)); + this.hasPixels = true; + } + + // ------------------------------------------------------------------ + // Framework plumbing + // ------------------------------------------------------------------ + + void applyContentDimensions(double min, double max) { + this.minScrollExtent = min; + this.maxScrollExtent = max; + this.hasContentDimensions = true; + } + + void applyViewportDimension(double dim) { + this.viewportDimension = dim; + this.hasViewportDimension = true; + } + + void setPixels(double p) { + this.pixels = p; + this.hasPixels = true; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollRenderElement.java new file mode 100644 index 00000000000..70ea5f58c32 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollRenderElement.java @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.ScrollRootLayout; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; + +import dart.runtime.Funcs; + +/** + * Base render element for the vertical scrollables + * (SingleChildScrollView/ListView/GridView). The content subtree becomes a + * REAL CN1 scroll boundary: this element owns a nested scrollable-Y + * {@link Container} with its own {@link RenderHost} and + * {@link ScrollRootLayout} scope, so the content's leaf components are flat + * children of the pane (not of the outer host container) and CN1's native + * tensile scrolling drives the scroll. Inside the pane the content is laid + * out with a tight viewport width and an unbounded main axis. + * + *

Headless (no Display) there is no pane; layout runs the same + * content-constraint math directly so scroll layout is unit-testable.

+ */ +public abstract class ScrollRenderElement extends RenderElement { + + private Element content; + private RenderHost innerHost; + + protected ScrollRenderElement(Widget widget) { + super(widget); + } + + /** + * The widget describing the scrolled content (rebuilt on every sync from + * the current configuration), or null for an empty scrollable. + */ + protected abstract Widget buildContent(); + + /** + * The content, padded the way Flutter pads a scroll view that was given no + * padding of its own. + * + *

{@code BoxScrollView} does not simply leave the padding null: it takes + * the ambient MediaQuery padding along its OWN axis, applies that, and + * removes it for everything inside so it is not counted twice. That is how a + * full-screen list keeps its first item out from under the display cutout + * without anyone writing a SafeArea, and the gallery's home list relies on + * it entirely -- it has no padding, no SafeArea and no app bar, and without + * this its title sat 133 device pixels too high, under the island.

+ * + *

A list that DOES name its own padding keeps it, and a list under an app + * bar sees nothing to add, because the Scaffold has already taken the top + * padding off its body.

+ */ + protected final Widget padForScrollAxis(Widget content, + com.codename1.flutter.EdgeInsets explicit) { + if (explicit != null) { + return padded(explicit, content); + } + if (content == null) { + return null; + } + com.codename1.flutter.EdgeInsets media; + try { + media = com.codename1.flutter.MediaQuery.paddingOf(this); + } catch (Throwable noMediaQuery) { + return content; + } + if (media == null) { + return content; + } + boolean across = horizontal(); + double left = across ? media.left() : 0; + double right = across ? media.right() : 0; + double top = across ? 0 : media.top(); + double bottom = across ? 0 : media.bottom(); + if (left == 0 && right == 0 && top == 0 && bottom == 0) { + return content; + } + Widget inner = com.codename1.flutter.MediaQuery.removePadding(this, + across ? Boolean.TRUE : Boolean.FALSE, + across ? Boolean.FALSE : Boolean.TRUE, + across ? Boolean.TRUE : Boolean.FALSE, + across ? Boolean.FALSE : Boolean.TRUE, + content); + return padded(com.codename1.flutter.EdgeInsets.only(left, top, right, bottom), inner); + } + + private static Widget padded(com.codename1.flutter.EdgeInsets insets, Widget child) { + Padding p = new Padding(); + p.padding(insets); + p.child(child); + return p; + } + + /** + * When true the scrollable sizes its main axis to the content instead of + * filling the incoming constraints. + */ + protected boolean shrinkWrap() { + return false; + } + + /** + * The scroll axis. Horizontal scrollables lay their content out with a + * tight viewport height and an unbounded width — the mirror of the + * vertical contract — and hand CN1 an X-scrollable pane. + */ + protected boolean horizontal() { + return false; + } + + /** + * Whether CN1's scroll indicator is suppressed. + * + *

Flutter shows a scrollbar only where the tree asks for one, by wrapping the + * scrollable in a {@link Scrollbar} or {@link RawScrollbar}; a bare ListView, + * SingleChildScrollView or PageView draws none. Codename One draws one by default, + * so without this every scrollable in a transpiled app carried a bar Flutter never + * put there — on the gallery's carousel it painted a black thumb across the bottom + * edge of the study card.

+ * + *

An ancestor walk is the right test because that is exactly the relationship + * Flutter uses: {@code Scrollbar} WRAPS the scrollable it decorates.

+ */ + /** + * Whether this pane draws no scrollbar. + * + *

A Flutter {@code Scrollbar} is INVISIBLE at rest — the thumb fades in + * while the list is moving and fades out again — unless the app asks for + * {@code thumbVisibility: true}. Codename One's is always on, so wrapping a + * list in a Scrollbar used to paint a permanent bar down the edge of a + * screen that should have none.

+ */ + protected boolean hideScrollbar() { + for (Element a = parent(); a != null; a = a.parent()) { + Widget w = a.widget(); + if (w instanceof Scrollbar) { + return !((Scrollbar) w).isThumbVisible(); + } + if (w instanceof RawScrollbar) { + return !((RawScrollbar) w).isThumbVisible(); + } + } + return true; + } + + /// The direction last reported to the widgets above this scroll view. + private com.codename1.flutter.rendering.ScrollDirection reportedDirection = + com.codename1.flutter.rendering.ScrollDirection.idle; + + /** + * Raises a {@code UserScrollNotification} when the drag turns around. + * + *

Flutter raises one when the user starts or stops dragging rather than per pixel, + * so reporting only a CHANGE of direction is both the right shape and the reason this + * is cheap: a listener that moves chrome runs once per turn, not once per frame.

+ * + *

Moving further into the content is {@code reverse} -- the direction the content + * travels, not the finger. That is the sense Reply reads: reverse folds the bar away, + * forward brings it back.

+ */ + private void noteScroll(int now, int before) { + // Only while the finger is actually down. Flutter raises this from the DRAG, so + // momentum, the settle at the end of a fling and the bounce at an edge never + // speak -- and here they were the loudest thing in the room. Reading down the + // list overshot to 666 and settled back to 609, so the drag ENDED by reporting + // the direction opposite to the one the user made, and dragging back up bounced + // past zero and reported the other one. The bar folded away when you scrolled + // back to the top and reappeared as you read on: the right animation, driven + // backwards, which is worse than none. + Component c = component(); + if (c instanceof ScrollPane && !((ScrollPane) c).userDragging()) { + // Between drags there is no user direction. Clearing it means the next drag + // reports its first move rather than being swallowed as "no change". + reportedDirection = com.codename1.flutter.rendering.ScrollDirection.idle; + return; + } + if (now == before) { + return; + } + com.codename1.flutter.rendering.ScrollDirection d = now > before + ? com.codename1.flutter.rendering.ScrollDirection.reverse + : com.codename1.flutter.rendering.ScrollDirection.forward; + if (d == reportedDirection) { + return; + } + reportedDirection = d; + UserScrollNotification n = new UserScrollNotification(); + n.direction(d); + n.dispatch(this); + } + + private RenderHost innerHost() { + if (innerHost == null) { + innerHost = new RenderHost(); + innerHost.rootSupplier(new Funcs.Func0() { + @Override + public Element call() { + return content; + } + }); + } + return innerHost; + } + + @Override + protected RenderHost hostForChild(int slot) { + return innerHost(); + } + + @Override + protected Component createComponent() { + if (!Display.isInitialized()) { + // headless unit tests: no CN1 components can exist + return null; + } + Container pane = createPane(horizontal() + ? new com.codename1.flutter.rendering.HorizontalScrollRootLayout(innerHost()) + : new ScrollRootLayout(innerHost())); + pane.setUIID("FlutterScroll"); + pane.getAllStyles().setPadding(0, 0, 0, 0); + pane.getAllStyles().setMargin(0, 0, 0, 0); + pane.getAllStyles().setBgTransparency(0); + if (horizontal()) { + pane.setScrollableX(true); + pane.setScrollableY(false); + } else { + pane.setScrollableY(true); + } + if (hideScrollbar()) { + pane.setScrollVisible(false); + } + innerHost().container(pane); + // Flutter reports a drag's direction to the widgets above the scroll view, and + // apps steer real chrome with it: Reply folds its bottom bar away while you read + // down a list and brings it back when you turn around. Nothing ever raised one of + // these, so every NotificationListener in the gallery was inert. + pane.addScrollListener(new com.codename1.ui.events.ScrollListener() { + @Override + public void scrollChanged(int scrollX, int scrollY, int oldscrollX, int oldscrollY) { + noteScroll(horizontal() ? scrollX : scrollY, + horizontal() ? oldscrollX : oldscrollY); + } + }); + return pane; + } + + /** The scrolling pane itself, so a subclass can add behaviour such as page snapping. */ + protected Container createPane(com.codename1.ui.layouts.Layout layout) { + return new ScrollPane(layout); + } + + /** + * The scrolling container, with one thing added: whether the user's finger is on it. + * + *

Codename One knows, but keeps {@code isDragActivated} protected, and the answer is + * what separates a drag from everything else the scroll offset does by itself.

+ */ + public static class ScrollPane extends Container { + + public ScrollPane(com.codename1.ui.layouts.Layout layout) { + super(layout); + } + + /** Whether this pane is currently being dragged by the user. */ + public boolean userDragging() { + return isDragActivated(); + } + + /** A position asked for before the pane had a size, applied at its first layout. */ + private int pendingPx = -1; + private boolean pendingHorizontal; + + /** + * Moves the scroll position -- what a ScrollController's jumpTo drives. Before + * the pane has been laid out there is nothing to scroll (and the position may + * be clamped to zero), so it is kept and applied once layout gives the pane a + * size: a controller's initialScrollOffset reaches the list that way. + */ + public void scrollToPosition(int px, boolean horizontal) { + if ((horizontal ? getWidth() : getHeight()) <= 0) { + pendingPx = px; + pendingHorizontal = horizontal; + return; + } + pendingPx = -1; + if (horizontal) { + setScrollX(px); + } else { + setScrollY(px); + } + } + + @Override + public void layoutContainer() { + super.layoutContainer(); + if (pendingPx >= 0 && (pendingHorizontal ? getWidth() : getHeight()) > 0) { + int px = pendingPx; + pendingPx = -1; + if (pendingHorizontal) { + setScrollX(px); + } else { + setScrollY(px); + } + } + } + } + + @Override + protected void syncChildren() { + content = updateChild(content, buildContent(), 0); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + if (content != null) { + visitor.call(content); + } + } + + public Element contentElement() { + return content; + } + + protected RenderElement contentRender() { + return findRenderElement(content); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + return horizontal() ? layoutHorizontal(constraints) : layoutVertical(constraints); + } + + private Size layoutVertical(BoxConstraints constraints) { + RenderElement c = contentRender(); + double width = constraints.hasBoundedWidth() ? constraints.maxWidth() : 0; + viewport(width, constraints.hasBoundedHeight() ? constraints.maxHeight() : 0); + Size cs = Size.ZERO; + if (c != null) { + cs = c.layout(constraints.hasBoundedWidth() + ? ScrollRootLayout.contentConstraints(width) + : BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + if (!constraints.hasBoundedWidth()) { + width = cs.width(); + } + } + double height; + // A SingleChildScrollView takes its CHILD's height, clamped to what it + // was offered -- Flutter constrains the viewport to the child's size + // rather than filling. Filling is right for a ListView and wrong here: + // the progress demo is Center(SingleChildScrollView(...)), and a + // viewport that fills leaves the Center nothing to centre, so the two + // indicators sat at the top of the page against the reference's middle. + if (shrinkWrap() || isSingleChild() || !constraints.hasBoundedHeight()) { + height = cs.height(); + } else { + height = constraints.maxHeight(); + } + return constraints.constrain(new Size(width, height)); + } + + /** + * The viewport this scrollable presents, reported BEFORE the content is + * laid out. Content that needs to size itself against the viewport (a + * PageView's pages take a fraction of it) cannot read {@code size()} — that + * is only assigned after this layout returns, so it would see a stale or + * zero extent. + */ + protected void viewport(double width, double height) { + } + + /** The vertical contract with the axes swapped. */ + private Size layoutHorizontal(BoxConstraints constraints) { + RenderElement c = contentRender(); + double height = constraints.hasBoundedHeight() ? constraints.maxHeight() : 0; + viewport(constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, height); + Size cs = Size.ZERO; + if (c != null) { + cs = c.layout(constraints.hasBoundedHeight() + ? com.codename1.flutter.rendering.HorizontalScrollRootLayout.contentConstraints(height) + : BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + if (!constraints.hasBoundedHeight()) { + height = cs.height(); + } + } + double width; + if (shrinkWrap() || !constraints.hasBoundedWidth()) { + width = cs.width(); + } else { + width = constraints.maxWidth(); + } + return constraints.constrain(new Size(width, height)); + } + + @Override + protected void positionChildren(int x, int y) { + // With a real pane the content lives in the inner host and the pane's + // ScrollRootLayout positions it in pane coordinates. Headless we + // position the content directly so tests observe absolute positions. + if (component() == null) { + RenderElement c = contentRender(); + if (c != null) { + c.position(x, y); + } + } + } + + /** Whether this is a SingleChildScrollView rather than a list viewport. */ + private boolean isSingleChild() { + return widget() instanceof SingleChildScrollView; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollUpdateNotification.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollUpdateNotification.java new file mode 100644 index 00000000000..7ffccbfe25c --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ScrollUpdateNotification.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * A {@link ScrollNotification} fired as the scroll offset changes — Flutter's + * {@code ScrollUpdateNotification}, carrying the {@link #scrollDelta()} since the + * previous update. + */ +public class ScrollUpdateNotification extends ScrollNotification { + + private Double scrollDelta; + + public Double scrollDelta() { + return scrollDelta; + } + + public void scrollDelta(Double v) { + this.scrollDelta = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Scrollbar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Scrollbar.java new file mode 100644 index 00000000000..034e5af6233 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Scrollbar.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Adds a scrollbar to a scrollable child. Codename One draws its own + * scrollbars, so this renders the child unchanged. See + * {@link PassThroughRenderElement}. + */ +public class Scrollbar extends Widget implements HasChild { + + private Object controller; + private boolean thumbVisibility; + private boolean trackVisibility; + private double thickness; + private Object radius; + private boolean interactive; + private Object notificationPredicate; + private Object scrollbarOrientation; + private Widget child; + + public void controller(Object v) { + this.controller = v; + } + + public void thumbVisibility(boolean v) { + this.thumbVisibility = v; + } + + /** Whether the thumb stays on screen when nothing is scrolling. */ + public boolean isThumbVisible() { + return thumbVisibility; + } + + public void trackVisibility(boolean v) { + this.trackVisibility = v; + } + + public void thickness(double v) { + this.thickness = v; + } + + public void radius(Object v) { + this.radius = v; + } + + public void interactive(boolean v) { + this.interactive = v; + } + + public void notificationPredicate(Object v) { + this.notificationPredicate = v; + } + + public void scrollbarOrientation(Object v) { + this.scrollbarOrientation = v; + } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SelectableText.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SelectableText.java new file mode 100644 index 00000000000..4273979704b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SelectableText.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Key; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; + +/** + * Selectable, non-editable text — Flutter's {@code SelectableText}. Text + * selection is not yet wired, so this composes a plain {@link Text} (or + * {@link RichText} for the {@code .rich} constructor), which is faithful to the + * rendered appearance. + */ +public class SelectableText extends StatelessWidget { + + private final String data; + private TextSpan textSpan; + private TextStyle style; + private TextAlign textAlign; + + public SelectableText(String data) { + this.data = data; + } + + /** + * Dart's {@code SelectableText.rich} named constructor in canonical + * positional form. + */ + public static SelectableText rich(TextSpan textSpan, Key key, TextStyle style, + TextAlign textAlign, Long maxLines, + Object textDirection) { + SelectableText t = new SelectableText(null); + t.key(key); + t.textSpan = textSpan; + t.style = style; + t.textAlign = textAlign; + return t; + } + + public void style(TextStyle v) { + this.style = v; + } + + public void textAlign(TextAlign v) { + this.textAlign = v; + } + + public void maxLines(long v) { + } + + public void textScaleFactor(double v) { + } + + public void showCursor(boolean v) { + } + + public void semanticsLabel(String v) { + } + + public void cursorColor(Object v) { + } + + public void onTap(Object v) { + } + + public void focusNode(Object v) { + } + + public void scrollPhysics(Object v) { + } + + @Override + public Widget build(BuildContext context) { + if (textSpan != null) { + RichText r = new RichText(); + r.text(textSpan); + if (textAlign != null) { + r.textAlign(textAlign); + } + return r; + } + Text t = new Text(data); + if (style != null) { + t.style(style); + } + if (textAlign != null) { + t.textAlign(textAlign); + } + return t; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Semantics.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Semantics.java new file mode 100644 index 00000000000..9bf249cf38e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Semantics.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Annotates its child subtree with accessibility semantics. The Codename One + * runtime renders the child unchanged for this milestone (the annotations are + * retained but not yet mapped onto CN1 accessibility). See + * {@link PassThroughRenderElement}. + */ +public class Semantics extends Widget implements HasChild { + + private Widget child; + private String label; + private String value; + private String increasedValue; + private String decreasedValue; + private String hint; + private String tooltip; + private Object sortKey; + private Funcs.VoidFunc0 onTap; + private Funcs.VoidFunc0 onLongPress; + + public void child(Widget v) { + this.child = v; + } + + public void container(boolean v) { + } + + public void explicitChildNodes(boolean v) { + } + + public void excludeSemantics(boolean v) { + } + + public void enabled(boolean v) { + } + + public void checked(boolean v) { + } + + public void selected(boolean v) { + } + + public void toggled(boolean v) { + } + + public void button(boolean v) { + } + + public void link(boolean v) { + } + + public void header(boolean v) { + } + + public void textField(boolean v) { + } + + public void readOnly(boolean v) { + } + + public void focusable(boolean v) { + } + + public void focused(boolean v) { + } + + public void image(boolean v) { + } + + public void liveRegion(boolean v) { + } + + public void hidden(boolean v) { + } + + public void obscured(boolean v) { + } + + public void multiline(boolean v) { + } + + public void label(String v) { + this.label = v; + } + + public void value(String v) { + this.value = v; + } + + public void increasedValue(String v) { + this.increasedValue = v; + } + + public void decreasedValue(String v) { + this.decreasedValue = v; + } + + public void hint(String v) { + this.hint = v; + } + + public void tooltip(String v) { + this.tooltip = v; + } + + public void sortKey(Object v) { + this.sortKey = v; + } + + public void onLongPressHint(String v) { + } + + public void onTapHint(String v) { + } + + public void onTap(Funcs.VoidFunc0 v) { + this.onTap = v; + } + + public void onLongPress(Funcs.VoidFunc0 v) { + this.onLongPress = v; + } + + public void properties(SemanticsProperties v) { + if (v != null) { + this.label = v.getLabel(); + this.value = v.getValue(); + } + } + + /** + * {@code Semantics.fromProperties}: builds a Semantics node from a + * pre-assembled {@link SemanticsProperties} bag. Positional parameters mirror + * the stub's named-argument declaration order. + */ + public static Semantics fromProperties(com.codename1.flutter.Key key, SemanticsProperties properties, + Boolean container, Boolean explicitChildNodes, Boolean excludeSemantics, Widget child) { + Semantics s = new Semantics(); + s.properties(properties); + if (child != null) { + s.child(child); + } + return s; + } + + public String getLabel() { + return label; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SemanticsProperties.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SemanticsProperties.java new file mode 100644 index 00000000000..a168c52e2aa --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SemanticsProperties.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.TextDirection; + +import dart.runtime.Funcs; + +/** + * A bag of semantic annotations passed to {@link Semantics#fromProperties} and + * to {@code CustomPainterSemantics}, mirroring Flutter's + * {@code SemanticsProperties}. The annotations are retained but not yet mapped + * onto Codename One accessibility at this milestone; the named-argument setters + * accept the values the emitter feeds after construction. + */ +public class SemanticsProperties { + + private String label; + private String value; + private TextDirection textDirection; + private Boolean button; + private Boolean enabled; + + public void enabled(boolean v) { this.enabled = v; } + public void checked(boolean v) { } + public void selected(boolean v) { } + public void toggled(boolean v) { } + public void button(boolean v) { this.button = v; } + public void link(boolean v) { } + public void header(boolean v) { } + public void textField(boolean v) { } + public void readOnly(boolean v) { } + public void focusable(boolean v) { } + public void focused(boolean v) { } + public void inMutuallyExclusiveGroup(boolean v) { } + public void hidden(boolean v) { } + public void obscured(boolean v) { } + public void multiline(boolean v) { } + public void scopesRoute(boolean v) { } + public void namesRoute(boolean v) { } + public void image(boolean v) { } + public void liveRegion(boolean v) { } + public void label(String v) { this.label = v; } + public void value(String v) { this.value = v; } + public void increasedValue(String v) { } + public void decreasedValue(String v) { } + public void hint(String v) { } + public void onTapHint(String v) { } + public void onLongPressHint(String v) { } + public void textDirection(TextDirection v) { this.textDirection = v; } + public void sortKey(Object v) { } + public void onTap(Funcs.VoidFunc0 v) { } + public void onLongPress(Funcs.VoidFunc0 v) { } + + public String getLabel() { return label; } + public String getValue() { return value; } + public TextDirection getTextDirection() { return textDirection; } + public Boolean getButton() { return button; } + public Boolean getEnabled() { return enabled; } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ShapeBorderClipper.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ShapeBorderClipper.java new file mode 100644 index 00000000000..d58c8a86bae --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ShapeBorderClipper.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.TextDirection; + +/** + * Adapts a {@code ShapeBorder} to the {@code CustomClipper} protocol consumed + * by {@code PhysicalShape} — Flutter's {@code ShapeBorderClipper}. crane's + * backdrop uses it to round the front layer's top corners. API-shape only for + * this milestone; the shape is captured for the clipper to apply. + */ +public class ShapeBorderClipper { + + private Object shape; + private TextDirection textDirection; + + public void shape(Object v) { + this.shape = v; + } + + public void textDirection(TextDirection v) { + this.textDirection = v; + } + + public Object getShape() { + return shape; + } + + public TextDirection getTextDirection() { + return textDirection; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SimpleChildrenRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SimpleChildrenRenderElement.java new file mode 100644 index 00000000000..af4f966787a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SimpleChildrenRenderElement.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +import dart.core.DartList; +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * A minimal multi-child render element: it lays every (non-null) child out with + * the loosened incoming constraints, stacks them at the origin and sizes itself + * to the biggest child. Shared by the structural multi-child widgets that this + * milestone renders without their full paint/positioning semantics + * ({@link IndexedStack}, {@link Overlay}, ...). The child list is supplied lazily + * through {@link Children} so each widget can compute (or materialize) its + * children at build time. Owns no CN1 component. + */ +public class SimpleChildrenRenderElement extends RenderElement { + + /** Supplies the current child widgets of the owning widget. */ + public interface Children { + DartList get(); + } + + private final Children provider; + private List kids = new ArrayList(); + + public SimpleChildrenRenderElement(Widget widget, Children provider) { + super(widget); + this.provider = provider; + } + + @Override + protected void syncChildren() { + List newWidgets = new ArrayList(); + DartList src = provider == null ? null : provider.get(); + if (src != null) { + for (Widget w : src) { + if (w != null) { + newWidgets.add(w); + } + } + } + kids = updateChildren(kids, newWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : kids) { + if (c != null) { + visitor.call(c); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + List rc = renderChildren(); + BoxConstraints loose = constraints.loosen(); + double maxW = 0; + double maxH = 0; + for (RenderElement k : rc) { + Size cs = k.layout(loose); + maxW = Math.max(maxW, cs.width()); + maxH = Math.max(maxH, cs.height()); + setChildOffset(k, 0, 0); + } + return constraints.constrain(new Size(maxW, maxH)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollView.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollView.java new file mode 100644 index 00000000000..4dd601e5c61 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollView.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Axis; +import com.codename1.flutter.Clip; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Makes its child scrollable: the child subtree becomes a real CN1 scrollable container + * boundary laid out with an unbounded main axis inside. + * + *

{@code scrollDirection} picks the axis. It defaults to vertical, as in Flutter, and + * the horizontal case is what lets wide content — a data table with more columns than fit + * a phone — be reached rather than crushed into the available width.

+ */ +public class SingleChildScrollView extends Widget { + + private EdgeInsets padding; + private Widget child; + private String restorationId; + private Clip clipBehavior; + private Axis scrollDirection = Axis.vertical; + + public void restorationId(String v) { + this.restorationId = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void scrollDirection(Axis v) { + this.scrollDirection = v == null ? Axis.vertical : v; + } + + public Axis getScrollDirection() { + return scrollDirection; + } + + public void padding(EdgeInsets v) { + this.padding = v; + } + + public void child(Widget v) { + this.child = v; + } + + public EdgeInsets getPadding() { + return padding; + } + + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new SingleChildScrollViewRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollViewRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollViewRenderElement.java new file mode 100644 index 00000000000..295b8777136 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SingleChildScrollViewRenderElement.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Axis; +import com.codename1.flutter.Widget; + +/** + * Scroll boundary for {@link SingleChildScrollView}: the content is the + * child, optionally inset by the padding (synthesized {@link Padding}). + */ +public class SingleChildScrollViewRenderElement extends ScrollRenderElement { + + public SingleChildScrollViewRenderElement(SingleChildScrollView widget) { + super(widget); + } + + @Override + protected boolean horizontal() { + return ((SingleChildScrollView) widget()).getScrollDirection() == Axis.horizontal; + } + + @Override + protected Widget buildContent() { + SingleChildScrollView w = (SingleChildScrollView) widget(); + return padForScrollAxis(w.getChild(), w.getPadding()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBox.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBox.java new file mode 100644 index 00000000000..83164831fe3 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBox.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Key; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.Size; + +/** + * A box with a fixed width and/or height (logical pixels). Without a child + * it is a fixed-size spacer; with a child it tightens the child to the given + * dimensions. + */ +/// Implements {@link HasChild} so the widget walkers can see THROUGH it. +/// A button consumes its content rather than mounting it, and the walk that finds that +/// content stops at any wrapper it cannot open: Shrine's login buttons wrap their label +/// in a Padding, and both rendered with no label at all -- the row collapsed to a blob +/// where the reference reads CANCEL and NEXT. +public class SizedBox extends Widget implements HasChild { + + private Double width; + private Double height; + private Widget child; + + public void width(double v) { + this.width = v; + } + + public void height(double v) { + this.height = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Double getWidth() { + return width; + } + + public Double getHeight() { + return height; + } + + public Widget getChild() { + return child; + } + + /** + * {@code SizedBox.shrink}: a zero-size box (a minimal spacer / placeholder). + */ + public static SizedBox shrink(Key key, Widget child) { + SizedBox b = new SizedBox(); + b.key(key); + b.width(0); + b.height(0); + if (child != null) { + b.child(child); + } + return b; + } + + /** + * {@code SizedBox.expand}: a box that expands to fill its parent (infinite + * width and height). + */ + public static SizedBox expand(Key key, Widget child) { + SizedBox b = new SizedBox(); + b.key(key); + b.width(Double.POSITIVE_INFINITY); + b.height(Double.POSITIVE_INFINITY); + if (child != null) { + b.child(child); + } + return b; + } + + /** + * {@code SizedBox.fromSize}: a box tightened to the given {@link Size}. + */ + public static SizedBox fromSize(Key key, Size size, Widget child) { + SizedBox b = new SizedBox(); + b.key(key); + if (size != null) { + b.width(size.width()); + b.height(size.height()); + } + if (child != null) { + b.child(child); + } + return b; + } + + @Override + public Element createElement() { + return new SizedBoxRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBoxRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBoxRenderElement.java new file mode 100644 index 00000000000..22b404c2390 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SizedBoxRenderElement.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.SingleChildRenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +/** + * Flutter's RenderConstrainedBox with tight additional constraints for the + * specified dimensions, merged into the incoming constraints via + * {@link BoxConstraints#tighten}. Owns no CN1 component. + */ +public class SizedBoxRenderElement extends SingleChildRenderElement { + + public SizedBoxRenderElement(SizedBox widget) { + super(widget); + } + + @Override + protected Widget childWidget() { + return ((SizedBox) widget()).getChild(); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + SizedBox w = (SizedBox) widget(); + Double widthPx = w.getWidth() == null ? null : Double.valueOf(Dp.px(w.getWidth())); + Double heightPx = w.getHeight() == null ? null : Double.valueOf(Dp.px(w.getHeight())); + BoxConstraints inner = constraints.tighten(widthPx, heightPx); + RenderElement child = renderChild(); + if (child == null) { + return inner.constrain(new Size( + widthPx == null ? 0 : widthPx, + heightPx == null ? 0 : heightPx)); + } + Size cs = child.layout(inner); + setChildOffset(child, 0, 0); + return cs; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverAppBar.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverAppBar.java new file mode 100644 index 00000000000..725c8bc096e --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverAppBar.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Color; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * An app bar that integrates into a {@link CustomScrollView} and can expand, + * float, pin or snap as the user scrolls — Flutter's {@code SliverAppBar}. This + * milestone renders the {@code title} (with {@code flexibleSpace} preferred when + * present); the scroll-driven collapse/expand behavior is deferred. + */ +public class SliverAppBar extends StatelessWidget { + + private Widget title; + private Widget leading; + private DartList actions; + private Widget flexibleSpace; + private Color backgroundColor; + + public void title(Widget v) { + this.title = v; + } + + public void leading(Widget v) { + this.leading = v; + } + + public void actions(DartList v) { + this.actions = v; + } + + public void flexibleSpace(Widget v) { + this.flexibleSpace = v; + } + + public void backgroundColor(Color v) { + this.backgroundColor = v; + } + + public void pinned(boolean v) { + } + + public void floating(boolean v) { + } + + public void snap(boolean v) { + } + + public void expandedHeight(double v) { + } + + public void automaticallyImplyLeading(boolean v) { + } + + public void bottom(Widget v) { + } + + public void centerTitle(boolean v) { + } + + public void elevation(double v) { + } + + @Override + public Widget build(BuildContext context) { + if (flexibleSpace != null) { + return flexibleSpace; + } + if (title != null) { + return title; + } + return new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildBuilderDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildBuilderDelegate.java new file mode 100644 index 00000000000..5a028c77b54 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildBuilderDelegate.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.core.DartList; +import dart.runtime.Funcs; + +/** + * A lazily-built child delegate — Flutter's {@code SliverChildBuilderDelegate}. + * Materializes {@code builder(context, index)} for {@code 0..childCount-1}, or, + * when {@code childCount} is null (an infinite delegate), until the builder + * returns null (Flutter's end-of-list convention), capped to keep the eager + * materialization bounded. + */ +public class SliverChildBuilderDelegate extends SliverChildDelegate { + + private static final int UNBOUNDED_CAP = 10000; + + private final Funcs.Func2 builder; + private Long childCount; + + public SliverChildBuilderDelegate(Funcs.Func2 builder) { + this.builder = builder; + } + + public void childCount(long v) { + this.childCount = v; + } + + public void addAutomaticKeepAlives(boolean v) { + } + + public void addRepaintBoundaries(boolean v) { + } + + public void addSemanticIndexes(boolean v) { + } + + @Override + public DartList buildChildren(BuildContext context) { + DartList out = new DartList(); + if (builder == null) { + return out; + } + long count = childCount != null ? childCount : UNBOUNDED_CAP; + for (long i = 0; i < count; i++) { + Widget w = builder.call(context, i); + if (w == null) { + break; + } + out.add(w); + } + return out; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildDelegate.java new file mode 100644 index 00000000000..3d22329ea86 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildDelegate.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Supplies children to a sliver ({@link SliverList}, {@link SliverGrid}) — + * Flutter's {@code SliverChildDelegate}. In this runtime a delegate can + * eagerly materialize its children into a list for the composited scrollable. + */ +public abstract class SliverChildDelegate { + + /** Builds all children of this delegate in order. */ + public abstract DartList buildChildren(BuildContext context); +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildListDelegate.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildListDelegate.java new file mode 100644 index 00000000000..808723ce703 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverChildListDelegate.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * A child delegate backed by an explicit list — Flutter's + * {@code SliverChildListDelegate}. + */ +public class SliverChildListDelegate extends SliverChildDelegate { + + private final DartList children; + + public SliverChildListDelegate(DartList children) { + this.children = children; + } + + public void addAutomaticKeepAlives(boolean v) { + } + + public void addRepaintBoundaries(boolean v) { + } + + public void addSemanticIndexes(boolean v) { + } + + @Override + public DartList buildChildren(BuildContext context) { + return children != null ? children : new DartList(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverFillRemaining.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverFillRemaining.java new file mode 100644 index 00000000000..ac20319d72b --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverFillRemaining.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * A sliver that fills the remaining viewport space with its child — Flutter's + * {@code SliverFillRemaining}. Renders its {@code child}. + */ +public class SliverFillRemaining extends StatelessWidget { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + public void hasScrollBody(boolean v) { + } + + public void fillOverscroll(boolean v) { + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverGrid.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverGrid.java new file mode 100644 index 00000000000..d99044dbdeb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverGrid.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * A sliver that lays its delegate's children out in a grid — Flutter's + * {@code SliverGrid}. This milestone models it as a linear {@link Column}; the + * {@code gridDelegate}'s cross-axis tiling is deferred. + */ +public class SliverGrid extends StatelessWidget { + + private SliverChildDelegate delegate; + + public void delegate(SliverChildDelegate v) { + this.delegate = v; + } + + public void gridDelegate(Object v) { + } + + @Override + public Widget build(BuildContext context) { + DartList kids = delegate != null + ? delegate.buildChildren(context) : new DartList(); + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverList.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverList.java new file mode 100644 index 00000000000..f485e6fa06a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverList.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.CrossAxisAlignment; +import com.codename1.flutter.MainAxisSize; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * A sliver that lays its delegate's children out linearly — Flutter's + * {@code SliverList}. Modeled as a non-scrolling {@link Column} (the enclosing + * {@link CustomScrollView} provides the scroll viewport). + */ +public class SliverList extends StatelessWidget { + + private SliverChildDelegate delegate; + + public void delegate(SliverChildDelegate v) { + this.delegate = v; + } + + @Override + public Widget build(BuildContext context) { + DartList kids = delegate != null + ? delegate.buildChildren(context) : new DartList(); + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.mainAxisSize(MainAxisSize.min); + col.children(kids); + return col; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverPadding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverPadding.java new file mode 100644 index 00000000000..41cd3113558 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverPadding.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Insets a sliver — Flutter's {@code SliverPadding}. Renders its {@code sliver} + * wrapped in a {@link Padding} when the padding is an {@link EdgeInsets}. + */ +public class SliverPadding extends StatelessWidget { + + private Object padding; + private Widget sliver; + + public void padding(Object v) { + this.padding = v; + } + + public void sliver(Widget v) { + this.sliver = v; + } + + @Override + public Widget build(BuildContext context) { + Widget inner = sliver != null ? sliver : new SizedBox(); + if (padding instanceof EdgeInsets) { + Padding p = new Padding(); + p.padding((EdgeInsets) padding); + p.child(inner); + return p; + } + return inner; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverToBoxAdapter.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverToBoxAdapter.java new file mode 100644 index 00000000000..61f54163a14 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/SliverToBoxAdapter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Adapts a single box widget so it can sit among slivers — Flutter's + * {@code SliverToBoxAdapter}. Renders its {@code child}. + */ +public class SliverToBoxAdapter extends StatelessWidget { + + private Widget child; + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget build(BuildContext context) { + return child != null ? child : new SizedBox(); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Spacer.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Spacer.java new file mode 100644 index 00000000000..22f60f4e725 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Spacer.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Flexible empty space in a {@link Row}/{@link Column} — Flutter's + * {@code Spacer}. It is an {@link Expanded} wrapping an empty box, so a Flex + * lays it out by consuming a {@code flex}-proportional share of the free + * main-axis space. The default flex factor is 1. + */ +public class Spacer extends Expanded { + + public Spacer() { + child(new SizedBox()); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Stack.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Stack.java new file mode 100644 index 00000000000..18c97bfe1c9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Stack.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.core.DartList; + +/** + * Overlaps its children: non-positioned children are placed by the stack's + * alignment, {@link Positioned} children resolve their insets against the + * stack bounds. Later children paint on top (z-order = child order). + */ +public class Stack extends Widget { + + private Alignment alignment; + private DartList children; + private com.codename1.flutter.StackFit fit; + private com.codename1.flutter.Clip clipBehavior; + + public void fit(com.codename1.flutter.StackFit v) { + this.fit = v; + } + + public void clipBehavior(com.codename1.flutter.Clip v) { + this.clipBehavior = v; + } + + public void alignment(Alignment v) { + this.alignment = v; + } + + public void children(DartList v) { + this.children = v; + } + + public Alignment getAlignment() { + return alignment; + } + + public DartList getChildren() { + return children; + } + + /** How non-positioned children are sized; {@code loose} when unset, as in Flutter. */ + public com.codename1.flutter.StackFit getFit() { + return fit == null ? com.codename1.flutter.StackFit.loose : fit; + } + + @Override + public Element createElement() { + return new StackRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java new file mode 100644 index 00000000000..d63a2418461 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StackRenderElement.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Alignment; +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Flutter's RenderStack: + *
    + *
  1. Non-positioned children are laid out against the constraints the + * stack's {@code StackFit} implies - loosened for {@code loose} (the + * default), tight to the stack for {@code expand}, unchanged for + * {@code passthrough}; the stack sizes to the biggest of them + * (constrained), or expands to the bounded axes when every child is + * positioned. Under tight constraints the stack fills them either way.
  2. + *
  3. Non-positioned children are placed by the stack's alignment + * (default topLeft).
  4. + *
  5. {@link Positioned} children resolve left/top/right/bottom/width/ + * height (logical pixels) against the stack bounds: two opposing + * insets, or an inset plus an extent, tighten that axis; an + * unresolved axis stays loose and falls back to alignment.
  6. + *
+ * Z-order is child order — later children paint on top, which the flat + * container honors because components attach in element-tree order. Owns no + * CN1 component. + */ +public class StackRenderElement extends RenderElement { + + private List children = new ArrayList(); + + public StackRenderElement(Stack widget) { + super(widget); + } + + private Stack stack() { + return (Stack) widget(); + } + + private Alignment alignment() { + Alignment a = stack().getAlignment(); + return a == null ? Alignment.topLeft : a; + } + + @Override + protected void syncChildren() { + List newWidgets = new ArrayList(); + if (stack().getChildren() != null) { + for (Widget w : stack().getChildren()) { + if (w != null) { + newWidgets.add(w); + } + } + } + children = updateChildren(children, newWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : children) { + if (c != null) { + visitor.call(c); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + List kids = renderChildren(); + + // Pass 1: size the stack from the non-positioned children. + BoxConstraints nonPositioned = nonPositionedConstraints(constraints); + double maxW = 0; + double maxH = 0; + boolean hasNonPositioned = false; + for (RenderElement kid : kids) { + if (kid instanceof PositionedRenderElement) { + continue; + } + hasNonPositioned = true; + Size cs = kid.layout(nonPositioned); + maxW = Math.max(maxW, cs.width()); + maxH = Math.max(maxH, cs.height()); + } + Size self; + if (hasNonPositioned) { + self = constraints.constrain(new Size(maxW, maxH)); + } else { + self = constraints.constrain(new Size( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0)); + } + + // Pass 2: place everything. + Alignment a = alignment(); + for (RenderElement kid : kids) { + if (kid instanceof PositionedRenderElement) { + placePositioned((PositionedRenderElement) kid, self, a); + } else { + Size cs = kid.size(); + setChildOffset(kid, + Alignment.along(a.x(), self.width(), cs.width()), + Alignment.along(a.y(), self.height(), cs.height())); + } + } + return self; + } + + /** + * The constraints a non-positioned child is laid out against — Flutter's + * {@code RenderStack.performLayout} switch on {@code StackFit}. + * + *

{@code expand} is not a detail: it is how a Stack tells its children to FILL it, + * and a child that also carries its own size gets that size overridden by the tight + * constraints. The gallery's study card is exactly that shape - a + * {@code Stack(fit: StackFit.expand)} over an image that separately declares + * {@code height: 240} - so loosening unconditionally let the image keep its own height + * and sit inside the card instead of covering it, framed by a band of the Material + * surface behind it.

+ */ + private BoxConstraints nonPositionedConstraints(BoxConstraints constraints) { + com.codename1.flutter.StackFit f = stack().getFit(); + if (f == com.codename1.flutter.StackFit.passthrough) { + return constraints; + } + if (f == com.codename1.flutter.StackFit.expand) { + // Tight to what we have room for. An unbounded axis has no biggest to be tight + // to - Flutter asserts here; we loosen that axis instead so an unbounded parent + // degrades to the loose behaviour rather than propagating an infinite size. + return new BoxConstraints( + constraints.hasBoundedWidth() ? constraints.maxWidth() : 0, + constraints.maxWidth(), + constraints.hasBoundedHeight() ? constraints.maxHeight() : 0, + constraints.maxHeight()); + } + return constraints.loosen(); + } + + private void placePositioned(PositionedRenderElement kid, Size self, Alignment a) { + Positioned p = kid.positioned(); + Double left = px(p.getLeft()); + Double top = px(p.getTop()); + Double right = px(p.getRight()); + Double bottom = px(p.getBottom()); + Double width = px(p.getWidth()); + Double height = px(p.getHeight()); + + BoxConstraints childConstraints = new BoxConstraints( + resolvedExtent(left, right, width, self.width(), 0), + resolvedExtent(left, right, width, self.width(), Double.POSITIVE_INFINITY), + resolvedExtent(top, bottom, height, self.height(), 0), + resolvedExtent(top, bottom, height, self.height(), Double.POSITIVE_INFINITY)); + Size cs = kid.layout(childConstraints); + + double x; + if (left != null) { + x = left; + } else if (right != null) { + x = self.width() - right - cs.width(); + } else { + x = Alignment.along(a.x(), self.width(), cs.width()); + } + double y; + if (top != null) { + y = top; + } else if (bottom != null) { + y = self.height() - bottom - cs.height(); + } else { + y = Alignment.along(a.y(), self.height(), cs.height()); + } + setChildOffset(kid, x, y); + } + + /** + * The tight extent implied by two opposing insets or an explicit extent, + * or {@code fallback} (0 for the min bound, ∞ for the max) when the + * axis is unresolved and stays loose. + */ + private static double resolvedExtent(Double lead, Double trail, Double extent, + double stackExtent, double fallback) { + if (lead != null && trail != null) { + return Math.max(0, stackExtent - lead - trail); + } + if (extent != null) { + return extent; + } + return fallback; + } + + private static Double px(Double lp) { + return lp == null ? null : Double.valueOf(Dp.px(lp)); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StatefulBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StatefulBuilder.java new file mode 100644 index 00000000000..677749c0600 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/StatefulBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * A builder that owns a scrap of local state, rebuilt via a {@code setState} + * handed to its {@code builder} — Flutter's {@code StatefulBuilder}. The + * {@code builder} receives the {@link BuildContext} and a state-setter + * ({@code void Function(VoidCallback)}); calling it runs the mutation. This pass + * builds once (the setter runs the mutation but the localized rebuild is + * deferred to the element machinery). + */ +public class StatefulBuilder extends StatelessWidget { + + private Funcs.Func2, Widget> builder; + + public void builder(Funcs.Func2, Widget> v) { + this.builder = v; + } + + @Override + public Widget build(final BuildContext context) { + if (builder == null) { + return null; + } + Funcs.VoidFunc1 setState = new Funcs.VoidFunc1() { + @Override + public void call(Funcs.VoidFunc0 fn) { + if (fn != null) { + fn.call(); + } + // Then rebuild this builder, as State.setState does. The mutation ran and + // nothing was marked dirty, so the subtree showed the old value until an + // unrelated ancestor rebuilt -- a StatefulBuilder in a dialog never updated. + if (context instanceof com.codename1.flutter.Element) { + com.codename1.flutter.Element e = (com.codename1.flutter.Element) context; + if (e.isMounted()) { + e.markNeedsBuild(); + } + } + } + }; + return builder.call(context, setState); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Text.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Text.java new file mode 100644 index 00000000000..52a296b13fb --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Text.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextOverflow; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; + +/** + * A run of styled text, backed by a CN1 Label (UIID "FlutterText"). + */ +public class Text extends Widget { + + private final String data; + private TextStyle style; + private TextAlign textAlign; + private String semanticsLabel; + private TextOverflow overflow; + private Long maxLines; + + public Text(String data) { + this.data = data; + } + + public void style(TextStyle v) { + this.style = v; + } + + public void semanticsLabel(String v) { + this.semanticsLabel = v; + } + + public void overflow(TextOverflow v) { + this.overflow = v; + } + + public void maxLines(long v) { + this.maxLines = v; + } + + public void softWrap(boolean v) { + } + + public String getSemanticsLabel() { + return semanticsLabel; + } + + public TextOverflow getOverflow() { + return overflow; + } + + public Long getMaxLines() { + return maxLines; + } + + public void textAlign(TextAlign v) { + this.textAlign = v; + } + + public String getData() { + return data; + } + + public TextStyle getStyle() { + return style; + } + + public TextAlign getTextAlign() { + return textAlign; + } + + @Override + public Element createElement() { + return new TextRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java new file mode 100644 index 00000000000..414eeb5663d --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextRenderElement.java @@ -0,0 +1,575 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.TextAlign; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; +import com.codename1.ui.Component; +import com.codename1.ui.Font; +import com.codename1.ui.Graphics; +import com.codename1.ui.Label; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Leaf render box for {@link Text}: owns a CN1 Label (UIID "FlutterText") + * whose intrinsic size is measured with the label's font + * ({@code stringWidth}/{@code getHeight}). The TextStyle is applied + * programmatically to the label's AllStyles (font derived from the current + * style font, fg color from the Color). + * + *

M2 wraps: when the measured single line exceeds the incoming max width + * the text is broken on words (hard character breaks for single words wider + * than the line) via {@link #wrap}, the box reports the wrapped extent, and + * the label paints the lines itself honoring {@link TextAlign}.

+ */ +public class TextRenderElement extends RenderElement { + + public TextRenderElement(Text widget) { + super(widget); + } + + private Text text() { + return (Text) widget(); + } + + @Override + protected Component createComponent() { + WrappedLabel l = new WrappedLabel(data()); + stylesToSet(l).setPadding(0, 0, 0, 0); + stylesToSet(l).setMargin(0, 0, 0, 0); + applyStyle(l); + return l; + } + + @Override + protected void updateComponent(Component c) { + WrappedLabel l = (WrappedLabel) c; + l.setText(data()); + l.lines = null; + applyStyle(l); + } + + private String data() { + return text().getData() == null ? "" : text().getData(); + } + + /** + * The style actually in force: the widget's own, over the ambient + * {@code DefaultTextStyle}, field by field. + * + *

Flutter's rule, and the mechanism a container styles its text with. A + * {@code Text} that sets only a size inside a white-on-purple app bar must + * still come out white; reading only the widget's own style is what left + * every themed bar with default-ink glyphs.

+ */ + private TextStyle effectiveStyle() { + TextStyle own = text().getStyle(); + TextStyle ambient; + try { + ambient = DefaultTextStyle.of(this).getStyle(); + } catch (Throwable t) { + ambient = null; + } + if (ambient == null) { + return own; + } + if (own == null) { + return ambient; + } + // Start from the ambient style and let the widget's own non-null fields + // win: copyWith already ignores nulls, so this is exactly Flutter's + // "the nearer style wins field by field". + return ambient.copyWith(null, own.getColor(), null, own.getFontFamily(), + own.getFontSize(), own.getFontWeight(), null, own.getLetterSpacing(), + null, own.height(), null, null, null); + } + + private void applyStyle(Label l) { + TextStyle ts = effectiveStyle(); + if (l instanceof WrappedLabel) { + double sp = ts == null || ts.getLetterSpacing() == null + ? 0 : Dp.px(ts.getLetterSpacing().doubleValue()); + ((WrappedLabel) l).spacingPx = sp; + // A stated height wins; otherwise the FACE's own line height, read from the + // font rather than taken from Codename One's typographic pair, which lays + // text out shorter than Flutter does for the same file. + ((WrappedLabel) l).lineHeightPx = ts == null || ts.height() == null + || ts.getFontSize() == null ? 0 + : Dp.px(ts.getFontSize().doubleValue() * ts.height().doubleValue()); + if (((WrappedLabel) l).lineHeightPx <= 0 && ts != null + && ts.getFontSize() != null && ts.fontFamily() != null) { + double ratio = com.codename1.flutter.fonts.FontResolver.lineHeightRatio( + ts.fontFamily(), ts.getFontWeight(), false); + if (ratio > 0) { + ((WrappedLabel) l).lineHeightPx = + Dp.px(ts.getFontSize().doubleValue() * ratio); + } + } + // A TRANSLUCENT ink is ordinary in Material: the 2018 type scale + // paints its display roles at black54 and its body roles at + // black87, and Codename One's Style carries only an opaque + // foreground (its fgAlpha reaches the border, never the text). Kept + // here and applied when the label paints. + ((WrappedLabel) l).fgAlpha = + ts == null || ts.getColor() == null ? 255 : ts.getColor().alpha(); + } + if (ts != null) { + Font base = l.getUnselectedStyle().getFont(); + // A named family wins over whatever the theme put on the label: + // the style is asking for a specific typeface, and that is the + // difference between a study that looks like its design and one + // painted entirely in the platform default. + Font named = com.codename1.flutter.fonts.FontResolver.resolve( + ts.fontFamily(), ts.getFontWeight(), false); + // Whether the face we ended up with ALREADY carries the asked-for + // weight. Synthesising bold on top of a bold file doubles it. + boolean weighted = named != null; + if (named != null) { + base = named; + } else if (ts.getFontSize() != null) { + // No bundled face, but a size was asked for -- and a size only + // means something on a face that can derive. See + // FontResolver.platformFace. + Font platform = com.codename1.flutter.fonts.FontResolver.platformFace( + ts.getFontWeight(), false); + if (platform != null) { + base = platform; + weighted = true; + } + } + if (base == null) { + base = Font.getDefaultFont(); + } + if (base != null && (ts.getFontSize() != null || ts.getFontWeight() != null + || named != null)) { + float sizePx = ts.getFontSize() != null + ? (float) Dp.px(ts.getFontSize()) + : (base.getPixelSize() > 0 ? base.getPixelSize() : base.getHeight()); + // A resolved face ALREADY carries its weight (the Bold file was + // picked, not the Regular one), so asking Codename One to bold + // it again synthesises a second helping of weight on top. + int weight = !weighted + && ts.getFontWeight() != null && ts.getFontWeight().isBold() + ? Font.STYLE_BOLD : Font.STYLE_PLAIN; + try { + stylesToSet(l).setFont(base.derive(sizePx, weight)); + } catch (Exception err) { + // fonts that can't derive keep the base font + } + } + if (ts.getColor() != null) { + stylesToSet(l).setFgColor(ts.getColor().rgb()); + } + } + TextAlign a = text().getTextAlign(); + if (a != null) { + int cn1Align; + switch (a) { + case right: + case end: + cn1Align = Component.RIGHT; + break; + case center: + cn1Align = Component.CENTER; + break; + default: + cn1Align = Component.LEFT; + break; + } + stylesToSet(l).setAlignment(cn1Align); + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + WrappedLabel l = (WrappedLabel) component(); + if (l == null) { + return constraints.smallest(); + } + final Font f = font(l); + if (f == null) { + return constraints.smallest(); + } + final double spacing = l.spacingPx; + List lines = wrap(data(), new Funcs.Func1() { + @Override + public Double call(String s) { + return spacedWidth(f, s, spacing); + } + }, constraints.maxWidth()); + lines = clamp(lines, effectiveMaxLines(), ellipsize(), + new Funcs.Func1() { + @Override + public Double call(String s) { + return spacedWidth(f, s, spacing); + } + }, constraints.maxWidth()); + if (!isDryPass()) { + // Only a real pass may hand the painter its lines; see isDryPass(). + l.lines = lines; + } + double w = 0; + for (String line : lines) { + w = Math.max(w, spacedWidth(f, line, spacing)); + } + double h = l.lineHeight(f) * Math.max(1, lines.size()); + return constraints.constrain(new Size(w, h)); + } + + /** + * {@code Text.maxLines}, or the ambient {@code DefaultTextStyle}'s, or none. + */ + private Long effectiveMaxLines() { + if (text().getMaxLines() != null) { + return text().getMaxLines(); + } + try { + return DefaultTextStyle.of(this).getMaxLines() == null ? null + : Long.valueOf(DefaultTextStyle.of(this).getMaxLines().longValue()); + } catch (Throwable t) { + return null; + } + } + + /** Whether an over-long line ends in an ellipsis rather than being cut. */ + private boolean ellipsize() { + return text().getOverflow() == com.codename1.flutter.TextOverflow.ellipsis; + } + + /** + * Cuts a wrapped paragraph down to {@code maxLines}, ending the last line + * with an ellipsis when the text asked for one. + * + *

Both were parsed and dropped. A preview meant to be one line long + * rendered its whole body instead, which does not look like a bug so much + * as a different design: the Reply study's inbox showed every message in + * full and pushed the rest of the list off the screen.

+ */ + static List clamp(List lines, Long maxLines, boolean ellipsis, + Funcs.Func1 measure, double maxWidth) { + if (maxLines == null || maxLines.longValue() <= 0 + || lines.size() <= maxLines.longValue()) { + return lines; + } + int keep = (int) maxLines.longValue(); + List out = new ArrayList(lines.subList(0, keep)); + if (!ellipsis) { + return out; + } + // The last kept line has to make room for the ellipsis, and the text + // that follows it is what the ellipsis stands for. + String last = out.get(keep - 1); + String marker = "\u2026"; + while (last.length() > 0 + && measure.call(last + marker) > maxWidth + && maxWidth != Double.POSITIVE_INFINITY) { + last = last.substring(0, last.length() - 1); + } + out.set(keep - 1, trimEnd(last) + marker); + return out; + } + + private static String trimEnd(String s) { + int end = s.length(); + while (end > 0 && s.charAt(end - 1) == ' ') { + end--; + } + return s.substring(0, end); + } + + private static Font font(Label l) { + Font f = l.getUnselectedStyle().getFont(); + return f != null ? f : Font.getDefaultFont(); + } + + /** + * The width of {@code s} once Flutter's letterSpacing is added BETWEEN its glyphs - + * n-1 gaps for n characters, with no trailing space after the last, which is what + * Flutter does. Codename One draws a whole string in one call and has no tracking of + * its own, so both the measurement and the painting have to account for it here. + */ + static double spacedWidth(Font f, String s, double spacing) { + if (s == null || s.length() == 0) { + return 0; + } + // stringWidth measures the WHOLE run in one go, which is the accurate number and + // the one the port itself would use. It is what this measures with, whether or + // not there is tracking -- see trackingScale for how the paint path is made to + // agree with it. + return spacedWidth(f.stringWidth(s), s.length(), spacing); + } + + /// The sum of the per-glyph advances the paint path would step through. + static double sumCharWidths(Font f, String s) { + double total = 0; + for (int i = 0; i < s.length(); i++) { + total += f.charWidth(s.charAt(i)); + } + return total; + } + + /// What to multiply each glyph's advance by so a run laid out glyph by glyph ends + /// exactly where {@code stringWidth} says it should. + /// + /// Tracking forces the paint path to draw one glyph at a time, because Codename One + /// advances a whole string in a single call and has no tracking of its own. But + /// {@code charWidth} returns an INT, so every glyph's advance is rounded up to a + /// whole pixel and the error accumulates: measured against the reference, the same + /// sentence came out 607px wide where it should be 589 -- 3.1%, or about 0.6px per + /// character. Wide text does not merely look wrong, it ellipsises strings that fit + /// and clips the ones that do not. + /// + /// The whole-run {@code stringWidth} does not have that error, so use it for the + /// total and let the per-glyph widths decide only the PROPORTIONS. Note this is not + /// the same as measuring each glyph with {@code stringWidth}: a standalone space + /// measures ~0 there, which is why the paint path uses charWidth in the first place + /// -- as a proportion a space is correct, as an absolute width it is not. + static double trackingScale(Font f, String s) { + return trackingScale(f.stringWidth(s), sumCharWidths(f, s)); + } + + /// The scale arithmetic on its own, so the invariant it exists to hold -- that a run + /// laid out glyph by glyph ends exactly where {@link #spacedWidth} said it would -- + /// can be pinned without a Font. + static double trackingScale(double runWidth, double sumOfCharWidths) { + if (sumOfCharWidths <= 0) { + return 1; + } + return runWidth / sumOfCharWidths; + } + + /** The tracking arithmetic on its own, so it can be pinned without a Font. */ + public static double spacedWidth(double baseWidth, int charCount, double spacing) { + if (charCount <= 0) { + return 0; + } + return spacing == 0 ? baseWidth : baseWidth + spacing * (charCount - 1); + } + + // ------------------------------------------------------------------ + // Word wrapping (pure — headless-testable with stubbed metrics) + // ------------------------------------------------------------------ + + /** + * Greedy word wrap: fills each line with whitespace-separated words up to + * {@code maxWidth} (per the supplied measure function); a single word + * wider than the line is hard-broken at the character level. Embedded + * {@code \n} always breaks. An unbounded {@code maxWidth} yields the + * paragraphs unwrapped. + */ + public static List wrap(String text, Funcs.Func1 measure, double maxWidth) { + List out = new ArrayList(); + if (text == null) { + text = ""; + } + for (String paragraph : split(text, '\n')) { + if (maxWidth == Double.POSITIVE_INFINITY || measure.call(paragraph) <= maxWidth) { + out.add(paragraph); + continue; + } + StringBuilder line = new StringBuilder(); + for (String word : split(paragraph, ' ')) { + String candidate = line.length() == 0 ? word : line + " " + word; + if (measure.call(candidate) <= maxWidth || line.length() == 0 && word.length() == 0) { + line.setLength(0); + line.append(candidate); + continue; + } + if (line.length() > 0) { + out.add(line.toString()); + line.setLength(0); + } + // the word alone: hard-break it if even alone it overflows + while (measure.call(word) > maxWidth && word.length() > 1) { + int cut = word.length() - 1; + while (cut > 1 && measure.call(word.substring(0, cut)) > maxWidth) { + cut--; + } + out.add(word.substring(0, cut)); + word = word.substring(cut); + } + line.append(word); + } + out.add(line.toString()); + } + return out; + } + + private static List split(String s, char sep) { + List parts = new ArrayList(); + int start = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == sep) { + parts.add(s.substring(start, i)); + start = i + 1; + } + } + parts.add(s.substring(start)); + return parts; + } + + /** + * A Label that paints its wrapped lines itself once layout supplied them + * (falling back to standard single-line Label painting before the first + * layout pass). + */ + static class WrappedLabel extends Label { + + List lines; + /** Flutter's TextStyle.letterSpacing, in device pixels. */ + double spacingPx; + /// Flutter's {@code TextStyle.height} MULTIPLIED BY the font size, in device + /// pixels; 0 when the style sets none and the font's own height should stand. + /// + /// The multiplier was parsed and merged and then never used -- the layout took + /// the font's height and the painter advanced by it. Material specifies a height + /// for most of its text styles (bodyMedium 1.43, titleLarge 1.27), so every block + /// was set at the wrong leading, and down a long list the error accumulates until + /// dividers land on the text they were meant to separate. + double lineHeightPx; + + /** The height of one line: the style's, or the FACE's when it sets none. */ + double lineHeight(Font f) { + if (lineHeightPx > 0) { + return lineHeightPx; + } + if (f == null) { + return 0; + } + // Ascent + descent, NOT getHeight(). The reference's line box for a style + // that states no height is the face's own ascent and descent; getHeight() is + // the platform's recommended line SPACING, which adds external leading on top + // of that and is a different quantity. + // + // Measured on the typography demo, whose type scale states no height for any + // role: two wrapped lines of a 96sp style sat 141 logical pixels apart here + // against the reference's 115 -- a ratio of 1.47 where the face asks for + // 1.198 -- and the error repeats on every line of every such style, so the + // page drifted further out of register the further down it went. + int ascent = f.getAscent(); + int descent = Math.abs(f.getDescent()); + int box = ascent + descent; + // A port that does not answer for the face still has to lay text out. + return box > 0 ? box : f.getHeight(); + } + /** The ink's own alpha; see applyStyle. */ + int fgAlpha = 255; + + WrappedLabel(String text) { + super(text, "FlutterText"); + } + + @Override + public void paint(Graphics g) { + // Paint from the wrapped lines whenever layout produced any — NOT + // only when there is more than one. A single line is the interesting + // case: it is what a clamped `maxLines: 1` produces, and falling + // through to Label.paint here drew the label's raw text instead, so + // every one-line preview in the Reply study rendered its whole + // message and got cut off mid-word with no ellipsis. + if (lines == null && spacingPx == 0 && fgAlpha >= 255) { + super.paint(g); + return; + } + boolean multiLine = lines != null; + com.codename1.ui.plaf.Style s = getStyle(); + Font f = s.getFont(); + if (f == null) { + f = Font.getDefaultFont(); + } + if (f == null) { + return; + } + int prevColor = g.getColor(); + Font prevFont = g.getFont(); + int prevAlpha = fgAlpha >= 255 ? -1 : g.concatenateAlpha(fgAlpha); + g.setColor(s.getFgColor()); + g.setFont(f); + int lh = (int) Math.round(lineHeight(f)); + int y = getY(); + // Flutter centres the glyphs in the line box, so a line taller than the font + // pushes the text down by half the difference. Without this the run sits on + // the box's top edge and every line is a little high. + int glyphOffset = Math.max(0, (lh - f.getHeight()) / 2); + int align = s.getAlignment(); + List toPaint = multiLine ? lines + : java.util.Collections.singletonList(getText() == null ? "" : getText()); + if (toPaint.isEmpty()) { + // The graphics is shared with every other component in the + // frame, so an early exit still has to hand it back as it was. + g.setColor(prevColor); + if (prevAlpha >= 0) { + g.setAlpha(prevAlpha); + } + if (prevFont != null) { + g.setFont(prevFont); + } + return; + } + for (String line : toPaint) { + int lineW = (int) Math.ceil(spacedWidth(f, line, spacingPx)); + int x = getX(); + if (align == Component.CENTER) { + x += (getWidth() - lineW) / 2; + } else if (align == Component.RIGHT) { + x += getWidth() - lineW; + } + if (spacingPx == 0) { + g.drawString(line, x, y + glyphOffset); + } else { + // One glyph at a time: the only way to add tracking, since Codename One + // draws a whole string in a single advance. + // + // Advance by charWidth, NOT by stringWidth of a one-character + // string: Codename One measures a standalone space as ~0 wide, + // so a space advanced by the tracking alone and the words ran + // together — Reply's headlines read "Packageshipped!". + double scale = trackingScale(f, line); + double cursor = x; + for (int i = 0; i < line.length(); i++) { + char ch = line.charAt(i); + g.drawString(line.substring(i, i + 1), (int) Math.round(cursor), y + glyphOffset); + cursor += f.charWidth(ch) * scale + spacingPx; + } + } + y += lh; + } + g.setColor(prevColor); + if (prevAlpha >= 0) { + g.setAlpha(prevAlpha); + } + if (prevFont != null) { + g.setFont(prevFont); + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextSpan.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextSpan.java new file mode 100644 index 00000000000..38363c92529 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TextSpan.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.TextStyle; + +import dart.core.DartList; + +/** + * A node in a styled-text tree (Flutter's TextSpan): an optional text run, + * an optional style, and optional child spans. A child span INHERITS every + * style property its own style leaves null from its parent chain (see + * {@link RichTextRenderElement#flatten}). + * + *

Not a Widget — it is configuration consumed by {@link RichText}.

+ */ +public class TextSpan extends InlineSpan { + + private String text; + private TextStyle style; + private DartList children; + private Object recognizer; + + public void text(String v) { + this.text = v; + } + + public void recognizer(Object v) { + this.recognizer = v; + } + + public Object getRecognizer() { + return recognizer; + } + + public void style(TextStyle v) { + this.style = v; + } + + public void children(DartList v) { + this.children = v; + } + + public String getText() { + return text; + } + + public TextStyle getStyle() { + return style; + } + + public DartList getChildren() { + return children; + } + + /** + * {@code InlineSpan.toPlainText}: the concatenated raw text of this span and + * all descendant spans, in depth-first order. + */ + public String toPlainText() { + StringBuilder sb = new StringBuilder(); + appendPlainText(sb); + return sb.toString(); + } + + private void appendPlainText(StringBuilder sb) { + if (text != null) { + sb.append(text); + } + if (children != null) { + for (int i = 0; i < children.size(); i++) { + TextSpan c = children.get(i); + if (c != null) { + c.appendPlainText(sb); + } + } + } + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Transform.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Transform.java new file mode 100644 index 00000000000..de7aff25a72 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Transform.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.Key; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; + +/** + * Applies a geometric transform to its {@code child} before painting — + * Flutter's {@code Transform}. The default constructor takes a 4x4 matrix; the + * {@code .rotate}, {@code .scale} and {@code .translate} named constructors are + * convenience factories. This pass records the transform parameters and renders + * the child untransformed; applying the matrix at paint time is deferred. + */ +public class Transform extends Widget { + + private Object transform; + private Object origin; + private Object alignment; + private boolean transformHitTests = true; + private Object filterQuality; + private Double angle; + private Double scale; + private Double scaleX; + private Double scaleY; + private Object offset; + private Widget child; + + // --- default constructor: named-param setters ----------------------- + + /** {@code Transform(transform: ...)} — the general 4x4 form. */ + public void transform(Object v) { + this.transform = v; + } + + public void origin(Object v) { + this.origin = v; + } + + public void alignment(Object v) { + this.alignment = v; + } + + public void transformHitTests(boolean v) { + this.transformHitTests = v; + } + + public void filterQuality(Object v) { + this.filterQuality = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Widget getChild() { + return child; + } + + // --- named constructors --------------------------------------------- + + public static Transform rotate(Key key, double angle, Object origin, Object alignment, + Boolean transformHitTests, Object filterQuality, Widget child) { + Transform t = new Transform(); + t.key(key); + t.angle = angle; + t.origin = origin; + t.alignment = alignment; + if (transformHitTests != null) { + t.transformHitTests = transformHitTests; + } + t.filterQuality = filterQuality; + t.child = child; + return t; + } + + public static Transform scale(Key key, Double scale, Double scaleX, Double scaleY, Object origin, + Object alignment, Boolean transformHitTests, Object filterQuality, Widget child) { + Transform t = new Transform(); + t.key(key); + t.scale = scale; + t.scaleX = scaleX; + t.scaleY = scaleY; + t.origin = origin; + t.alignment = alignment; + if (transformHitTests != null) { + t.transformHitTests = transformHitTests; + } + t.filterQuality = filterQuality; + t.child = child; + return t; + } + + public static Transform translate(Key key, Object offset, Boolean transformHitTests, + Object filterQuality, Widget child) { + Transform t = new Transform(); + t.key(key); + t.offset = offset; + if (transformHitTests != null) { + t.transformHitTests = transformHitTests; + } + t.filterQuality = filterQuality; + t.child = child; + return t; + } + + /// The horizontal scale in effect: scaleX when given, else the uniform scale, else 1. + public double effectiveScaleX() { + if (scaleX != null) { + return scaleX.doubleValue(); + } + if (scale != null) { + return scale.doubleValue(); + } + return matrixEntry(0, 0, 1.0); + } + + /// The vertical scale in effect: scaleY when given, else the uniform scale, else 1. + public double effectiveScaleY() { + if (scaleY != null) { + return scaleY.doubleValue(); + } + if (scale != null) { + return scale.doubleValue(); + } + return matrixEntry(1, 1, 1.0); + } + + /// The rotation in radians, or null when this is not a rotation. + public Double effectiveAngle() { + return angle; + } + + /// The translation, or null when this is not a translation. + public com.codename1.flutter.Offset effectiveOffset() { + if (offset instanceof com.codename1.flutter.Offset) { + return (com.codename1.flutter.Offset) offset; + } + double tx = matrixEntry(0, 3, 0); + double ty = matrixEntry(1, 3, 0); + return tx == 0 && ty == 0 ? null : new com.codename1.flutter.Offset(tx, ty); + } + + /** + * One cell of the {@code transform} matrix, when this Transform was given one. + * + *

{@code Transform(transform: matrix)} is the general form — the named + * constructors are conveniences over it — and it was accepted and ignored, + * so anything driving a widget through a matrix rendered untransformed. The + * 2D-transformations demo positions its whole board that way, through an + * {@code InteractiveViewer}'s controller, and drew it in the corner. + * + *

Only the scale and translation cells are read; a matrix carrying a + * rotation or a skew is not decomposed.

+ */ + private double matrixEntry(int row, int col, double fallback) { + if (!(transform instanceof com.codename1.flutter.vectormath.Matrix4)) { + return fallback; + } + dart.core.DartList m = + ((com.codename1.flutter.vectormath.Matrix4) transform).storage(); + // vector_math stores column-major: index = col * 4 + row. + int i = col * 4 + row; + if (m == null || i < 0 || i >= m.size()) { + return fallback; + } + Double v = m.get(i); + return v == null ? fallback : v.doubleValue(); + } + + @Override + public com.codename1.flutter.Element createElement() { + return new TransformRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformRenderElement.java new file mode 100644 index 00000000000..2e8dd96a8ef --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformRenderElement.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.FlutterErrorReport; +import com.codename1.flutter.Offset; +import com.codename1.flutter.Widget; +import com.codename1.ui.Container; +import com.codename1.ui.Graphics; + +/** + * Paints {@link Transform}'s subtree through a scale, rotation and/or translation. + * + *

Like Flutter's Transform this is a PAINT effect: the child is laid out at its + * natural size and only the painting is transformed, so a scaled card still occupies the + * same slot in its parent.

+ * + *

The transform is applied about the element's centre, which is Flutter's default + * (Alignment.center) and what the gallery's carousel expects. An explicit + * {@code origin}/{@code alignment} is not honoured yet and is reported rather than + * silently ignored.

+ */ +public class TransformRenderElement extends EffectRenderElement { + + public TransformRenderElement(Transform widget) { + super(widget); + } + + private Transform transform() { + return (Transform) widget(); + } + + @Override + protected Widget effectChild() { + return transform().getChild(); + } + + @Override + protected void paintWithEffect(Graphics g, Container pane, Subtree paintChildren) { + double sx = transform().effectiveScaleX(); + double sy = transform().effectiveScaleY(); + Double angle = transform().effectiveAngle(); + Offset offset = transform().effectiveOffset(); + + // A/B switch, flipped at runtime with + // Display.setProperty("cn1.flutter.noTransform","true"). Transform.scale is the + // main per-frame difference between the carousel (21-29fps on iOS) and a plain + // list (60fps), and a layer per card per frame is only measurable on a device. + // Same trick as cn1.flutter.noShapeClip. + boolean suppressed = "true".equals(com.codename1.ui.Display.getInstance() + .getProperty("cn1.flutter.noTransform", "false")); + boolean scales = !suppressed && (sx != 1.0 || sy != 1.0); + boolean rotates = !suppressed && angle != null && angle.doubleValue() != 0.0; + boolean translates = offset != null && (offset.dx() != 0 || offset.dy() != 0); + if (scales && !(sx != 0.0 && sy != 0.0 && isFinite(sx) && isFinite(sy))) { + // A collapsed (or non-finite) scale shows nothing, so paint nothing -- + // Flutter's RenderTransform returns before painting its child when the + // matrix's determinant is zero. This used to render the whole subtree into + // a layer and then draw that layer at zero size. The gallery's desktop + // settings panel sits under a ScaleTransition at 0 until it is opened, so + // every cold start rasterised the entire hidden settings page, switch + // shadows and all, inside the first frame. + return; + } + if (!scales && !rotates) { + // A translation needs no matrix and no layer: shifting the origin is exact, + // costs nothing, and works on every port. + if (!translates) { + paintChildren.paint(g); + return; + } + int dx = (int) Math.round(com.codename1.flutter.rendering.Dp.px(offset.dx())); + int dy = (int) Math.round(com.codename1.flutter.rendering.Dp.px(offset.dy())); + g.translate(dx, dy); + try { + paintChildren.paint(g); + } finally { + g.translate(-dx, -dy); + } + return; + } + + com.codename1.ui.Image rendered = layer(pane, paintChildren); + if (rendered == null) { + return; + } + + int dx = 0; + int dy = 0; + if (translates) { + dx = (int) Math.round(com.codename1.flutter.rendering.Dp.px(offset.dx())); + dy = (int) Math.round(com.codename1.flutter.rendering.Dp.px(offset.dy())); + g.translate(dx, dy); + } + try { + if (rotates) { + paintRotated(g, pane, rendered, sx, sy, angle.doubleValue()); + } else { + paintScaled(g, pane, rendered, sx, sy); + } + } finally { + if (translates) { + g.translate(-dx, -dy); + } + } + } + + /// Draws the layer scaled about the pane's centre - Flutter's default anchor, and + /// what the gallery's carousel expects. + /// + /// A pure scale is a destination rectangle, so this needs no transform support and + /// behaves identically on every port. Rounding the destination to whole pixels is + /// what drawImage takes anyway. + private static void paintScaled(Graphics g, Container pane, com.codename1.ui.Image layer, + double sx, double sy) { + int w = pane.getWidth(); + int h = pane.getHeight(); + int dw = (int) Math.round(w * sx); + int dh = (int) Math.round(h * sy); + if (dw <= 0 || dh <= 0) { + // Scaled away to nothing: Flutter paints nothing here either. + return; + } + g.drawImage(layer, pane.getX() + (w - dw) / 2, pane.getY() + (h - dh) / 2, dw, dh); + } + + /// Draws the layer rotated (and possibly scaled) about the pane's centre. + /// + /// Rotation has no destination-rectangle form, so this is the one case that still + /// needs a matrix. It is applied to a SINGLE drawImage rather than to a subtree + /// walk, which is what makes it safe: Codename One's paint-time cull never runs + /// under it, and the pivot is expressed in the pane's own parent coordinates -- the + /// space the Graphics is in -- rather than in absolute screen coordinates. + private static void paintRotated(Graphics g, Container pane, com.codename1.ui.Image layer, + double sx, double sy, double angle) { + if (!g.isTransformSupported()) { + // Report rather than quietly dropping the visual: the layer still lands in + // the right place, it simply is not turned. + FlutterErrorReport.unimplemented("Transform", + "this platform has no transform support; rotation is ignored"); + paintScaled(g, pane, layer, sx, sy); + return; + } + com.codename1.ui.Transform saved = g.getTransform(); + com.codename1.ui.Transform t = saved.copy(); + float cx = pane.getX() + pane.getWidth() / 2f; + float cy = pane.getY() + pane.getHeight() / 2f; + // Move the pivot to the centre, apply, move back - otherwise the subtree turns + // about the screen origin instead of in place. + t.translate(cx, cy); + t.rotate((float) angle, 0, 0); + if (sx != 1.0 || sy != 1.0) { + t.scale((float) sx, (float) sy); + } + t.translate(-cx, -cy); + g.setTransform(t); + try { + g.drawImage(layer, pane.getX(), pane.getY()); + } finally { + g.setTransform(saved); + } + } + + /// True for any value but NaN and the infinities. Arithmetic rather than + /// Double.isFinite, which neither vm/JavaAPI nor Ports/CLDC11 declares. + private static boolean isFinite(double v) { + return v - v == 0.0; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformationController.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformationController.java new file mode 100644 index 00000000000..d022f89942a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/TransformationController.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Offset; +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.flutter.vectormath.Matrix4; + +/** + * The 4x4-matrix controller shared with an {@link InteractiveViewer} — Flutter's + * {@code TransformationController} (a {@code ValueNotifier}). The + * transformations demo animates {@link #value()} and maps viewport points to the + * child's coordinate space with {@link #toScene(Offset)}. + */ +public class TransformationController extends ValueNotifier { + + public TransformationController() { + super(Matrix4.identity()); + } + + public TransformationController(Matrix4 value) { + super(value == null ? Matrix4.identity() : value); + } + + /** + * Maps a point from the viewport (widget) coordinate space to the child's + * coordinate space. This pass returns the point unchanged (identity mapping) + * until the live pan/zoom transform is applied. + */ + public Offset toScene(Offset viewportPoint) { + return viewportPoint; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/UserScrollNotification.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/UserScrollNotification.java new file mode 100644 index 00000000000..678579c1d16 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/UserScrollNotification.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.rendering.ScrollDirection; + +/** + * A {@link ScrollNotification} fired when the user starts or stops dragging, + * carrying the new scroll {@link #direction()} — Flutter's + * {@code UserScrollNotification}. + */ +public class UserScrollNotification extends ScrollNotification { + + public UserScrollNotification() { + } + + public void context(BuildContext v) { + super.context(v); + } + + public void metrics(ScrollMetrics v) { + super.metrics(v); + } + + public void direction(ScrollDirection v) { + super.direction(v); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilder.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilder.java new file mode 100644 index 00000000000..57d15594f39 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilder.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.ValueListenable; + +/** + * Rebuilds part of the tree whenever a {@code ValueListenable} changes — + * Flutter's {@code ValueListenableBuilder}. The {@code builder} is a + * three-argument closure {@code (context, value, child)} that produces the + * subtree, invoked with the listenable's current value and the optional + * pass-through {@code child}. + * + *

{@link ValueListenableBuilderElement} does the listening; this widget is only the + * configuration.

+ * + * @param the value type the listenable exposes + */ +public class ValueListenableBuilder extends StatelessWidget { + + private Object valueListenable; + private Object builder; + private Widget child; + + public void valueListenable(Object v) { + this.valueListenable = v; + } + + public void builder(dart.runtime.Funcs.Func3 v) { + this.builder = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Object getBuilder() { + return builder; + } + + public Object getValueListenable() { + return valueListenable; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new ValueListenableBuilderElement(this); + } + + @Override + public Widget build(BuildContext context) { + return buildWith(context); + } + + /** Invokes the builder with the listenable's current value. */ + @SuppressWarnings("unchecked") + Widget buildWith(BuildContext context) { + if (builder instanceof dart.runtime.Funcs.Func3) { + T value = valueListenable instanceof ValueListenable + ? ((ValueListenable) valueListenable).value() : null; + return ((dart.runtime.Funcs.Func3) builder) + .call(context, value, child); + } + return child; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilderElement.java new file mode 100644 index 00000000000..04a336ed216 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/ValueListenableBuilderElement.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.ComposedElement; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.ValueListenable; + +import dart.runtime.Funcs; + +/** + * Element for {@link ValueListenableBuilder}: subscribes to the listenable on mount and + * rebuilds on every notification, mirroring Flutter's + * {@code _ValueListenableBuilderState}. + * + *

Without the subscription the builder still produced a correct FIRST frame, which is + * why this looked like it worked: the widget rendered, and only stopped tracking after + * that. Everything driven by a ValueNotifier was therefore frozen at its initial value — + * in the gallery, the settings button toggled its notifier and nothing on screen moved, + * so the whole settings panel was unreachable.

+ */ +public class ValueListenableBuilderElement extends ComposedElement { + + private ValueListenable listened; + + private final Funcs.VoidFunc0 handler = new Funcs.VoidFunc0() { + @Override + public void call() { + markNeedsBuild(); + } + }; + + public ValueListenableBuilderElement(ValueListenableBuilder widget) { + super(widget); + } + + @Override + public void mount(Element parent, int slot) { + super.mount(parent, slot); + subscribe(); + } + + @Override + public void update(Widget newWidget) { + // The new configuration may name a DIFFERENT listenable; resubscribing + // unconditionally is simpler than comparing and cannot leave a stale listener + // attached to the old one. + unsubscribe(); + super.update(newWidget); + subscribe(); + } + + @Override + public void unmount() { + unsubscribe(); + super.unmount(); + } + + @SuppressWarnings("unchecked") + private void subscribe() { + Object l = ((ValueListenableBuilder) widget()).getValueListenable(); + if (l instanceof ValueListenable) { + listened = (ValueListenable) l; + listened.addListener(handler); + } + } + + private void unsubscribe() { + if (listened != null) { + listened.removeListener(handler); + listened = null; + } + } + + @Override + protected Widget build() { + return ((ValueListenableBuilder) widget()).buildWith(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Visibility.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Visibility.java new file mode 100644 index 00000000000..05d676af453 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Visibility.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +/** + * Whether (and how) to include its {@code child} in the tree — Flutter's {@code Visibility}. + * + *

{@code visible: false} shows the {@code replacement} instead — nothing, by + * default. It used to draw the child regardless, which is the loudest possible + * reading of "do not show this".

+ * + *

{@code maintainState} is not modelled: a hidden child is rebuilt when it + * comes back rather than kept alive. {@code maintainSize} is honoured only in + * that a replacement can hold space if one is given.

+ */ +public class Visibility extends Widget implements HasChild { + + private boolean visible = true; + private Widget replacement; + private boolean maintainState; + private boolean maintainAnimation; + private boolean maintainSize; + private boolean maintainSemantics; + private boolean maintainInteractivity; + private Widget child; + + public void visible(boolean v) { this.visible = v; } + public void replacement(Widget v) { this.replacement = v; } + public void maintainState(boolean v) { this.maintainState = v; } + public void maintainAnimation(boolean v) { this.maintainAnimation = v; } + public void maintainSize(boolean v) { this.maintainSize = v; } + public void maintainSemantics(boolean v) { this.maintainSemantics = v; } + public void maintainInteractivity(boolean v) { this.maintainInteractivity = v; } + public boolean getVisible() { return visible; } + + public void child(Widget v) { + this.child = v; + } + + @Override + public Widget getChild() { + if (visible) { + return child; + } + if (replacement != null) { + return replacement; + } + // Flutter's default replacement is SizedBox.shrink() — an empty box, + // not the child it was just told to hide. + SizedBox empty = new SizedBox(); + empty.width(0); + empty.height(0); + return empty; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetOrderTraversalPolicy.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetOrderTraversalPolicy.java new file mode 100644 index 00000000000..566630e6f81 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetOrderTraversalPolicy.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +/** + * Traverses focus in widget (tree) order — Flutter's {@code WidgetOrderTraversalPolicy}. Captured for API shape by + * {@link FocusTraversalGroup}; live focus traversal is deferred to a later pass. + */ +public class WidgetOrderTraversalPolicy { + + private Object secondary; + + public WidgetOrderTraversalPolicy() { + } + + public void secondary(Object v) { + this.secondary = v; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsBinding.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsBinding.java new file mode 100644 index 00000000000..e488c6b30c5 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsBinding.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.scheduler.SchedulerBinding; + +/** + * The glue binding the widget layer to the engine — Flutter's + * {@code WidgetsBinding}. new_gallery reaches the ambient brightness through + * {@code WidgetsBinding.instance.platformDispatcher.platformBrightness}. + * + *

Dart's {@code static get instance} is emitted as a Java static field + * reference (accessed as {@code WidgetsBinding.instance}, no call), mirroring + * {@link SchedulerBinding#instance}.

+ */ +public final class WidgetsBinding { + + /** + * The engine's view of the platform, exposed to match the Dart chain + * {@code platformDispatcher.platformBrightness}. {@code platformBrightness} + * is a public field so the (dynamic in Dart) {@code .platformBrightness} + * access resolves as a member read. + */ + public static final class PlatformDispatcher { + + /** The system-wide brightness. Defaults to light in this runtime. */ + public Brightness platformBrightness = Brightness.light; + + /** The primary display's device-pixel ratio. */ + public double devicePixelRatio = 1.0; + } + + /** Dart's {@code WidgetsBinding.instance}. */ + public static final WidgetsBinding instance = new WidgetsBinding(); + + private final PlatformDispatcher platformDispatcher = new PlatformDispatcher(); + + private WidgetsBinding() { + } + + /** Dart's {@code platformDispatcher} getter. */ + public PlatformDispatcher platformDispatcher() { + return platformDispatcher; + } + + /** Dart's {@code window} getter (legacy alias for the dispatcher). */ + public Object window() { + return platformDispatcher; + } + + /** Dart's {@code addPostFrameCallback}: run after the current frame. */ + public void addPostFrameCallback(dart.runtime.Funcs.VoidFunc1 callback) { + SchedulerBinding.instance.addPostFrameCallback(callback); + } + + /** Dart's {@code addObserver}: register a binding observer. A no-op here. */ + public void addObserver(Object observer) { + } + + /** Dart's {@code removeObserver}: unregister a binding observer. A no-op. */ + public void removeObserver(Object observer) { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsLocalizations.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsLocalizations.java new file mode 100644 index 00000000000..2749820458a --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WidgetsLocalizations.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Locale; + +import dart.core.DartIterable; +import dart.core.DartList; + +/** + * The widget-layer localizations surface — Flutter's + * {@code WidgetsLocalizations}. new_gallery only calls the static + * {@link #basicLocaleListResolution} helper as the fallback for its + * {@code localeListResolutionCallback}. + */ +public abstract class WidgetsLocalizations { + + private WidgetsLocalizations() { + } + + /** + * Flutter's top-level {@code basicLocaleListResolution}: pick the best + * supported locale for the user's preferred list. This minimal + * implementation returns the first preferred locale whose language matches a + * supported locale (preferring an exact language+country match), falling + * back to the first supported locale. + */ + public static Locale basicLocaleListResolution(DartList preferredLocales, + DartIterable supportedLocales) { + Locale firstSupported = null; + if (supportedLocales != null) { + for (Locale s : supportedLocales) { + if (firstSupported == null) { + firstSupported = s; + break; + } + } + } + if (preferredLocales == null || preferredLocales.isEmpty()) { + return firstSupported; + } + for (int i = 0; i < preferredLocales.size(); i++) { + Locale preferred = preferredLocales.get(i); + if (preferred == null || supportedLocales == null) { + continue; + } + Locale languageMatch = null; + for (Locale supported : supportedLocales) { + if (supported == null) { + continue; + } + if (eq(preferred.languageCode(), supported.languageCode())) { + if (eq(preferred.countryCode(), supported.countryCode())) { + return supported; + } + if (languageMatch == null) { + languageMatch = supported; + } + } + } + if (languageMatch != null) { + return languageMatch; + } + } + return firstSupported != null ? firstSupported : preferredLocales.get(0); + } + + private static boolean eq(String a, String b) { + return a == null ? b == null : a.equals(b); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WillPopScope.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WillPopScope.java new file mode 100644 index 00000000000..6d7c20e713f --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WillPopScope.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; + +import dart.runtime.Funcs; + +/** + * Intercepts the system back gesture, consulting {@code onWillPop} before the + * route is popped — Flutter's {@code WillPopScope}. Structural pass-through for + * this milestone: the {@code child} renders unchanged and the callback is + * captured (not yet wired to Codename One's back command). + */ +public class WillPopScope extends Widget implements HasChild { + + private Funcs.Func0 onWillPop; + private Widget child; + + public void onWillPop(Funcs.Func0 v) { + this.onWillPop = v; + } + + public void child(Widget v) { + this.child = v; + } + + public Funcs.Func0 getOnWillPop() { + return onWillPop; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public Element createElement() { + return new PassThroughRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Wrap.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Wrap.java new file mode 100644 index 00000000000..238ea700970 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/Wrap.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Axis; +import com.codename1.flutter.Clip; +import com.codename1.flutter.Element; +import com.codename1.flutter.Widget; +import com.codename1.flutter.WrapAlignment; +import com.codename1.flutter.WrapCrossAlignment; + +import dart.core.DartList; + +/** + * Lays its children out in runs along a main axis, wrapping to a new run when + * the current one is full — Flutter's {@code Wrap}. + */ +public class Wrap extends Widget { + + private Axis direction = Axis.horizontal; + private WrapAlignment alignment = WrapAlignment.start; + private double spacing; + private WrapAlignment runAlignment = WrapAlignment.start; + private double runSpacing; + private WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start; + private Object textDirection; + private Object verticalDirection; + private Clip clipBehavior = Clip.none; + private DartList children; + + public void direction(Axis v) { + this.direction = v == null ? Axis.horizontal : v; + } + + public void alignment(WrapAlignment v) { + this.alignment = v == null ? WrapAlignment.start : v; + } + + public void spacing(double v) { + this.spacing = v; + } + + public void runAlignment(WrapAlignment v) { + this.runAlignment = v == null ? WrapAlignment.start : v; + } + + public void runSpacing(double v) { + this.runSpacing = v; + } + + public void crossAxisAlignment(WrapCrossAlignment v) { + this.crossAxisAlignment = v == null ? WrapCrossAlignment.start : v; + } + + public void textDirection(Object v) { + this.textDirection = v; + } + + public void verticalDirection(Object v) { + this.verticalDirection = v; + } + + public void clipBehavior(Clip v) { + this.clipBehavior = v; + } + + public void children(DartList v) { + this.children = v; + } + + public Axis getDirection() { + return direction; + } + + public WrapAlignment getAlignment() { + return alignment; + } + + public double getSpacing() { + return spacing; + } + + public double getRunSpacing() { + return runSpacing; + } + + public WrapCrossAlignment getCrossAxisAlignment() { + return crossAxisAlignment; + } + + public DartList getChildren() { + return children; + } + + @Override + public Element createElement() { + return new WrapRenderElement(this); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java new file mode 100644 index 00000000000..5a87da4dba9 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/flutter/widgets/WrapRenderElement.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Axis; +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.WrapCrossAlignment; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Flutter's RenderWrap: children are laid out with loose constraints and + * greedily packed into runs along the main axis (wrapping when a run would + * overflow the bounded main extent), runs stacked along the cross axis with + * {@code runSpacing}, items separated by {@code spacing}. Main-axis alignment + * supports start/center/end (space* variants fall back to start); + * cross-axis-within-run alignment supports start/center/end. Owns no CN1 + * component. + */ +public class WrapRenderElement extends RenderElement { + + private List children = new ArrayList(); + + public WrapRenderElement(Wrap widget) { + super(widget); + } + + private Wrap wrap() { + return (Wrap) widget(); + } + + @Override + protected void syncChildren() { + List newWidgets = new ArrayList(); + if (wrap().getChildren() != null) { + for (Widget w : wrap().getChildren()) { + if (w != null) { + newWidgets.add(w); + } + } + } + children = updateChildren(children, newWidgets); + } + + @Override + public void visitChildren(Funcs.VoidFunc1 visitor) { + for (Element c : children) { + if (c != null) { + visitor.call(c); + } + } + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + boolean horizontal = wrap().getDirection() == Axis.horizontal; + double spacing = Dp.px(wrap().getSpacing()); + double runSpacing = Dp.px(wrap().getRunSpacing()); + double maxMain = horizontal ? constraints.maxWidth() : constraints.maxHeight(); + + BoxConstraints childC = constraints.loosen(); + List kids = renderChildren(); + + List> runs = new ArrayList>(); + List runCross = new ArrayList(); + List runMain = new ArrayList(); + Map mainStart = new HashMap(); + + List cur = new ArrayList(); + double curMain = 0; + double curCross = 0; + for (RenderElement kid : kids) { + Size cs = kid.layout(childC); + double m = horizontal ? cs.width() : cs.height(); + double c = horizontal ? cs.height() : cs.width(); + double add = (cur.isEmpty() ? 0 : spacing) + m; + if (!cur.isEmpty() && curMain + add > maxMain) { + runs.add(cur); + runCross.add(curCross); + runMain.add(curMain); + cur = new ArrayList(); + curMain = 0; + curCross = 0; + } + if (!cur.isEmpty()) { + curMain += spacing; + } + mainStart.put(kid, curMain); + curMain += m; + curCross = Math.max(curCross, c); + cur.add(kid); + } + if (!cur.isEmpty()) { + runs.add(cur); + runCross.add(curCross); + runMain.add(curMain); + } + + double totalMain = 0; + double totalCross = 0; + for (int i = 0; i < runs.size(); i++) { + totalMain = Math.max(totalMain, runMain.get(i)); + totalCross += runCross.get(i); + if (i > 0) { + totalCross += runSpacing; + } + } + + Size self = horizontal + ? constraints.constrain(new Size(totalMain, totalCross)) + : constraints.constrain(new Size(totalCross, totalMain)); + double boundedMain = horizontal ? self.width() : self.height(); + + double crossPos = 0; + for (int i = 0; i < runs.size(); i++) { + List run = runs.get(i); + double runCr = runCross.get(i); + double leading = mainLeading(boundedMain, runMain.get(i)); + for (RenderElement kid : run) { + Size cs = kid.size(); + double kidCross = horizontal ? cs.height() : cs.width(); + double within = crossWithin(kidCross, runCr); + double mp = leading + mainStart.get(kid); + if (horizontal) { + setChildOffset(kid, mp, crossPos + within); + } else { + setChildOffset(kid, crossPos + within, mp); + } + } + crossPos += runCr + runSpacing; + } + return self; + } + + private double mainLeading(double boundedMain, double runExtent) { + double free = boundedMain - runExtent; + if (free <= 0) { + return 0; + } + switch (wrap().getAlignment()) { + case center: + return free / 2; + case end: + return free; + default: + return 0; + } + } + + private double crossWithin(double kidCross, double runCross) { + WrapCrossAlignment a = wrap().getCrossAxisAlignment(); + if (a == WrapCrossAlignment.center) { + return (runCross - kidCross) / 2; + } + if (a == WrapCrossAlignment.end) { + return runCross - kidCross; + } + return 0; + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/BoxPainter.java b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/BoxPainter.java new file mode 100644 index 00000000000..f544c013f11 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/BoxPainter.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.Canvas; +import com.codename1.flutter.ImageConfiguration; +import com.codename1.flutter.Offset; + +/** + * The object that paints a {@link com.codename1.flutter.Decoration}, created by + * {@code Decoration.createBoxPainter} — Flutter's {@code BoxPainter}. The Rally + * pie-chart outline and the Crane tab indicator subclass it to draw directly on + * the canvas. + * + *

Lives in the transpiler's generated package because new_gallery's custom + * {@code Decoration}s reference it unqualified (the Flutter SDK type carries no + * {@code @JavaName} mapping); the concrete painters emitted next to it extend + * this base.

+ */ +public abstract class BoxPainter { + + /** + * Paints the decoration onto {@code canvas} at {@code offset} for the box + * described by {@code configuration} (notably its size). + */ + public abstract void paint(Canvas canvas, Offset offset, ImageConfiguration configuration); + + /** Releases resources held by this painter (no-op in this milestone). */ + public void dispose() { + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialAccentColor.java b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialAccentColor.java new file mode 100644 index 00000000000..4a3b2f7d032 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialAccentColor.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.Color; + +/** + * An accent color swatch with a primary value plus the four accent shades + * (100, 200, 400, 700) — Flutter's {@code MaterialAccentColor}. The colors demo + * indexes it (Dart's {@code swatch[key]}, transpiled to {@link #idx(long)}) to + * list every accent shade of a palette. + * + *

Lives in the transpiler's generated package for the same reason as + * {@link MaterialColor}.

+ */ +public class MaterialAccentColor extends Color { + + private final long[] keys; + private final long[] values; + + /** A swatch whose shades all resolve to {@code primary}. */ + public MaterialAccentColor(long primary) { + this(primary, null, null); + } + + public MaterialAccentColor(long primary, long[] keys, long[] values) { + super(primary); + this.keys = keys; + this.values = values; + } + + /** + * The shade for {@code key} (Dart's {@code operator []}), or the primary + * value when this swatch does not define that shade. + */ + public Color idx(long key) { + if (keys != null) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] == key) { + return new Color(values[i]); + } + } + } + return this; + } + + public Color shade100() { + return idx(100); + } + + public Color shade200() { + return idx(200); + } + + public Color shade400() { + return idx(400); + } + + public Color shade700() { + return idx(700); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialColor.java b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialColor.java new file mode 100644 index 00000000000..40468933b26 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/MaterialColor.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.Color; + +/** + * A color swatch with a primary value plus its indexed shades (50, 100..900, and + * for grey also 350 and 850) — Flutter's {@code MaterialColor}. The colors demo + * indexes it (Dart's {@code swatch[key]}, transpiled to {@link #idx(long)}) to + * list every shade of a palette. + * + *

Lives in the transpiler's generated package because new_gallery references + * it unqualified (the Flutter SDK type carries no {@code @JavaName} mapping) and + * {@code _Palette} names it in the same package.

+ * + *

The swatch is two parallel {@code long} arrays rather than a map: a swatch + * has at most a dozen entries, so a scan beats a hash, and primitive arrays cost + * no boxing on the ports with the tightest runtime.

+ */ +public class MaterialColor extends Color { + + private final long[] keys; + private final long[] values; + + /** + * A swatch whose shades all resolve to {@code primary}. + * + *

Retained for a palette that has no shade table of its own; prefer the + * three-argument constructor, because a swatch that answers the same color + * for every shade renders a palette as one flat block.

+ */ + public MaterialColor(long primary) { + this(primary, null, null); + } + + public MaterialColor(long primary, long[] keys, long[] values) { + super(primary); + this.keys = keys; + this.values = values; + } + + /** + * The shade for {@code key} (Dart's {@code operator []}), or the primary + * value when this swatch does not define that shade. + */ + public Color idx(long key) { + if (keys != null) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] == key) { + return new Color(values[i]); + } + } + } + return this; + } + + public Color shade50() { + return idx(50); + } + + public Color shade100() { + return idx(100); + } + + public Color shade200() { + return idx(200); + } + + public Color shade300() { + return idx(300); + } + + public Color shade400() { + return idx(400); + } + + public Color shade500() { + return idx(500); + } + + public Color shade600() { + return idx(600); + } + + public Color shade700() { + return idx(700); + } + + public Color shade800() { + return idx(800); + } + + public Color shade900() { + return idx(900); + } +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/RangeSliderThumbShape.java b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/RangeSliderThumbShape.java new file mode 100644 index 00000000000..40d761c4eea --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/RangeSliderThumbShape.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +/** + * Base class for the shape that draws a {@code RangeSlider}'s thumbs — Flutter's + * {@code RangeSliderThumbShape}. The sliders demo subclasses it with a custom + * triangular thumb. + * + *

Lives in the transpiler's generated package because the demo references it + * unqualified (the Flutter SDK type carries no {@code @JavaName} mapping); kept + * as an open base so the generated subclass's {@code getPreferredSize} / + * {@code paint} signatures compile.

+ */ +public abstract class RangeSliderThumbShape { +} diff --git a/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/SliderComponentShape.java b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/SliderComponentShape.java new file mode 100644 index 00000000000..241a2d6da77 --- /dev/null +++ b/maven/flutter-runtime/src/main/java/com/codename1/generated/flutter/SliderComponentShape.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +/** + * Base class for the shapes that draw the pieces of a {@code Slider} (thumb, + * value indicator, overlay, tick marks) — Flutter's {@code SliderComponentShape}. + * The sliders demo subclasses it with a custom thumb and value-indicator shape. + * + *

Lives in the transpiler's generated package because the demo references it + * unqualified (the Flutter SDK type carries no {@code @JavaName} mapping). The + * concrete {@code getPreferredSize} / {@code paint} overrides are supplied by + * the generated subclasses; kept as an open base so their exact (Animation / + * TextPainter / RenderBox / SliderThemeData) signatures compile without the + * base pinning them.

+ */ +public abstract class SliderComponentShape { +} diff --git a/maven/flutter-runtime/src/main/resources/CN1FlutterMaterialTheme.res b/maven/flutter-runtime/src/main/resources/CN1FlutterMaterialTheme.res new file mode 100644 index 00000000000..8ddb327a6b8 Binary files /dev/null and b/maven/flutter-runtime/src/main/resources/CN1FlutterMaterialTheme.res differ diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/flutter_material.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/flutter_material.dart new file mode 100644 index 00000000000..c43579f4248 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/flutter_material.dart @@ -0,0 +1,847 @@ +// Codename One Flutter runtime API stubs (M1). +// +// These signature-only declarations tell the Dart transpiler how the +// hand-written Java runtime (codenameone-flutter-runtime) looks from Dart: +// which classes exist, their Java names, and — crucially — parameter shapes. +// +// Conventions the emitter applies to stub classes: +// - positional constructor parameters -> Java constructor arguments +// - named constructor parameters -> void setter methods of the same name +// - instance getters -> no-arg method calls (name()) +// - static getters -> static field access (Name.field) +// - named parameters of methods -> canonical positional order as declared +// - the VoidCallback type -> dart.runtime.Funcs.VoidFunc0 +// +// M1 scope only. This file is parsed with the transpiler's own Dart parser. + +// --- entry points ----------------------------------------------------- + +@JavaName('com.codename1.flutter.FlutterUI.runApp') +external void runApp(Widget app); + +// --- framework core --------------------------------------------------- + +@JavaName('com.codename1.flutter.Key') +abstract class Key {} + +@JavaName('com.codename1.flutter.ValueKey') +class ValueKey extends Key { + external ValueKey(Object value); +} + +@JavaName('com.codename1.flutter.BuildContext') +abstract class BuildContext { + // Return type `T` is a method type parameter: the transpiler recovers the + // Dart witness (dropped from the call) as a trailing Class token and + // casts the result, so the Java runtime methods take a trailing Class. + external T dependOnInheritedWidgetOfExactType(); + external T watch(); + external T read(); + // The render object of this element — Flutter's `BuildContext.findRenderObject`. + external RenderObject? findRenderObject(); + // The nearest ancestor State of the given type (recovered via the trailing + // Class witness) — Flutter's `BuildContext.findAncestorStateOfType`. + external T findAncestorStateOfType(); + external Size? get size; +} + +@JavaName('com.codename1.flutter.Widget') +abstract class Widget { + external Widget({Key? key}); +} + +@JavaName('com.codename1.flutter.StatelessWidget') +abstract class StatelessWidget extends Widget { + external StatelessWidget({Key? key}); + Widget build(BuildContext context); +} + +@JavaName('com.codename1.flutter.StatefulWidget') +abstract class StatefulWidget extends Widget { + external StatefulWidget({Key? key}); + State createState(); +} + +@JavaName('com.codename1.flutter.State') +abstract class State { + external T get widget; + external BuildContext get context; + external void setState(VoidCallback fn); + external void initState(); + external void dispose(); + Widget build(BuildContext context); +} + +// --- value types ------------------------------------------------------ + +@JavaName('com.codename1.flutter.Color') +class Color { + external Color(int value); + // The 32-bit ARGB integer this color was built from — Flutter's `Color.value`. + external int get value; + // Cascade fixers: member access on a Color must stay statically typed (P2). + external Color withOpacity(double opacity); + external Color withAlpha(int a); + external Color copyWith({int? alpha, int? red, int? green, int? blue}); + external static Color alphaBlend(Color foreground, Color background); + external static Color fromRGBO(int r, int g, int b, double opacity); +} + +@JavaName('com.codename1.flutter.Colors') +abstract class Colors { + external static Color get transparent; + external static Color get red; + external static Color get redAccent; + external static Color get pink; + external static Color get pinkAccent; + external static Color get purple; + external static Color get purpleAccent; + external static Color get deepPurple; + external static Color get deepPurpleAccent; + external static Color get indigo; + external static Color get indigoAccent; + external static Color get blue; + external static Color get blueAccent; + external static Color get lightBlue; + external static Color get lightBlueAccent; + external static Color get cyan; + external static Color get cyanAccent; + external static Color get teal; + external static Color get tealAccent; + external static Color get green; + external static Color get greenAccent; + external static Color get lightGreen; + external static Color get lightGreenAccent; + external static Color get lime; + external static Color get limeAccent; + external static Color get yellow; + external static Color get yellowAccent; + external static Color get amber; + external static Color get amberAccent; + external static Color get orange; + external static Color get orangeAccent; + external static Color get deepOrange; + external static Color get deepOrangeAccent; + external static Color get brown; + external static Color get grey; + external static Color get blueGrey; + external static Color get white; + external static Color get white70; + external static Color get white60; + external static Color get white54; + external static Color get white38; + external static Color get white30; + external static Color get white24; + external static Color get white12; + external static Color get white10; + external static Color get black; + external static Color get black87; + external static Color get black54; + external static Color get black45; + external static Color get black38; + external static Color get black26; + external static Color get black12; +} + +@JavaName('com.codename1.flutter.EdgeInsets') +class EdgeInsets extends EdgeInsetsGeometry { + external static EdgeInsets get zero; + external static EdgeInsets all(double value); + external static EdgeInsets only({double left, double top, double right, double bottom}); + external static EdgeInsets symmetric({double horizontal, double vertical}); + external static EdgeInsets fromLTRB(double left, double top, double right, double bottom); + // The four resolved edge insets — Flutter's `EdgeInsets.left/top/right/bottom`. + external double get left; + external double get top; + external double get right; + external double get bottom; + // Summed horizontal (left+right) and vertical (top+bottom) insets. + external double get horizontal; + external double get vertical; + external EdgeInsets copyWith({double? left, double? top, double? right, double? bottom}); + external EdgeInsets add(EdgeInsetsGeometry other); +} + +@JavaName('com.codename1.flutter.MainAxisAlignment') +enum MainAxisAlignment { start, end, center, spaceBetween, spaceAround, spaceEvenly } + +@JavaName('com.codename1.flutter.CrossAxisAlignment') +enum CrossAxisAlignment { start, end, center, stretch } + +@JavaName('com.codename1.flutter.MainAxisSize') +enum MainAxisSize { min, max } + +@JavaName('com.codename1.flutter.TextAlign') +enum TextAlign { left, right, center, start, end } + +@JavaName('com.codename1.flutter.FontWeight') +abstract class FontWeight { + external static FontWeight get w100; + external static FontWeight get w200; + external static FontWeight get w300; + external static FontWeight get w400; + external static FontWeight get w500; + external static FontWeight get w600; + external static FontWeight get w700; + external static FontWeight get w800; + external static FontWeight get w900; + external static FontWeight get normal; + external static FontWeight get bold; +} + +@JavaName('com.codename1.flutter.TextStyle') +class TextStyle { + external TextStyle({double? fontSize, FontWeight? fontWeight, Color? color, String? fontFamily, double? letterSpacing, double? height}); + external Color? get color; + external double? get fontSize; + external FontWeight? get fontWeight; + external String? get fontFamily; + external double? get letterSpacing; + external double? get height; + external TextStyle copyWith({bool? inherit, Color? color, Color? backgroundColor, String? fontFamily, + double? fontSize, FontWeight? fontWeight, dynamic fontStyle, double? letterSpacing, + double? wordSpacing, double? height, dynamic background, dynamic foreground, dynamic decoration}); + external TextStyle apply({Color? color, Color? backgroundColor, String? fontFamily, + double? fontSizeFactor, double? fontSizeDelta, dynamic decoration}); +} + +@JavaName('com.codename1.flutter.IconData') +class IconData {} + +@JavaName('com.codename1.flutter.Icons') +abstract class Icons { + external static IconData get access_alarm; + external static IconData get access_time; + external static IconData get account_circle; + external static IconData get add; + external static IconData get add_circle; + external static IconData get add_circle_outline; + external static IconData get add_comment; + external static IconData get add_shopping_cart; + external static IconData get airplanemode_active; + external static IconData get alarm_on; + external static IconData get arrow_back; + external static IconData get arrow_back_ios; + external static IconData get arrow_drop_down; + external static IconData get arrow_drop_up; + external static IconData get arrow_forward; + external static IconData get arrow_forward_ios; + external static IconData get arrow_left; + external static IconData get attach_money; + external static IconData get book; + external static IconData get bookmark_border; + external static IconData get brightness_5; + external static IconData get calendar_today; + external static IconData get camera_enhance; + external static IconData get check; + external static IconData get check_circle; + external static IconData get check_circle_outline; + external static IconData get chevron_right; + external static IconData get close; + external static IconData get code; + external static IconData get comment; + external static IconData get create; + external static IconData get credit_card; + external static IconData get date_range; + external static IconData get delete; + external static IconData get directions_bike; + external static IconData get edit; + external static IconData get email; + external static IconData get favorite; + external static IconData get favorite_border; + external static IconData get feedback; + external static IconData get format_bold; + external static IconData get format_italic; + external static IconData get format_underline; + external static IconData get fullscreen; + external static IconData get help; + external static IconData get home; + external static IconData get hotel; + external static IconData get info; + external static IconData get info_outline; + external static IconData get keyboard_arrow_down; + external static IconData get keyboard_arrow_up; + external static IconData get library_books; + external static IconData get link; + external static IconData get lock; + external static IconData get menu; + external static IconData get mic; + external static IconData get money_off; + external static IconData get more_vert; + external static IconData get not_interested; + external static IconData get notifications; + external static IconData get person; + external static IconData get person_add; + external static IconData get phone; + external static IconData get photo; + external static IconData get photo_library; + external static IconData get pie_chart; + external static IconData get place; + external static IconData get remove; + external static IconData get remove_circle_outline; + external static IconData get refresh; + external static IconData get replay; + external static IconData get reply_all; + external static IconData get restaurant_menu; + external static IconData get search; + external static IconData get security; + external static IconData get settings; + external static IconData get share; + external static IconData get shopping_cart; + external static IconData get sort; + external static IconData get star; + external static IconData get star_border; + external static IconData get table_chart; + external static IconData get tune; + external static IconData get vertical_split; + external static IconData get visibility; + external static IconData get visibility_off; + external static IconData get web_asset; +} + +// --- basic widgets ---------------------------------------------------- + +@JavaName('com.codename1.flutter.widgets.Text') +class Text extends Widget { + external Text(String data, {Key? key, TextStyle? style, TextAlign? textAlign}); +} + +@JavaName('com.codename1.flutter.widgets.Icon') +class Icon extends Widget { + external Icon(IconData icon, {Key? key, double? size, Color? color}); +} + +@JavaName('com.codename1.flutter.widgets.Column') +class Column extends Widget { + external Column({Key? key, MainAxisAlignment? mainAxisAlignment, CrossAxisAlignment? crossAxisAlignment, MainAxisSize? mainAxisSize, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Row') +class Row extends Widget { + external Row({Key? key, MainAxisAlignment? mainAxisAlignment, CrossAxisAlignment? crossAxisAlignment, MainAxisSize? mainAxisSize, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Center') +class Center extends Widget { + external Center({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Padding') +class Padding extends Widget { + external Padding({Key? key, EdgeInsets padding, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.SizedBox') +class SizedBox extends Widget { + external SizedBox({Key? key, double? width, double? height, Widget? child}); + external static SizedBox shrink({Key? key, Widget? child}); + external static SizedBox expand({Key? key, Widget? child}); + external static SizedBox fromSize({Key? key, Size? size, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Expanded') +class Expanded extends Widget { + external Expanded({Key? key, int flex, Widget child}); +} + +// --- material --------------------------------------------------------- + +@JavaName('com.codename1.flutter.material.MaterialApp') +class MaterialApp extends Widget { + external MaterialApp({Key? key, String? title, ThemeData? theme, ThemeData? darkTheme, ThemeMode? themeMode, Widget? home, Route Function(RouteSettings)? onGenerateRoute}); +} + +@JavaName('com.codename1.flutter.material.Scaffold') +class Scaffold extends Widget { + external Scaffold({Key? key, Widget? appBar, Widget? body, Widget? floatingActionButton, Widget? drawer, Widget? bottomNavigationBar}); + external static ScaffoldState of(BuildContext context); +} + +@JavaName('com.codename1.flutter.material.AppBar') +class AppBar extends Widget { + external AppBar({Key? key, Widget? title, Color? backgroundColor, bool? centerTitle}); + // The height this app bar prefers to occupy — Flutter's + // `AppBar.preferredSize` (an app bar implements PreferredSizeWidget). + external Size get preferredSize; +} + +@JavaName('com.codename1.flutter.material.FloatingActionButton') +class FloatingActionButton extends Widget { + external FloatingActionButton({Key? key, VoidCallback? onPressed, String? tooltip, Widget? child}); + external static FloatingActionButton extended({Key? key, VoidCallback? onPressed, Widget? label, Widget? icon, String? tooltip, Object? heroTag, Color? backgroundColor}); +} + +@JavaName('com.codename1.flutter.material.ThemeData') +class ThemeData { + external ThemeData({ColorScheme? colorScheme, Color? colorSchemeSeed, bool? useMaterial3, Brightness? brightness, + TextTheme? textTheme, TextTheme? primaryTextTheme, Color? primaryColor, + Color? scaffoldBackgroundColor, Color? canvasColor, Color? cardColor, Color? dividerColor, + Color? focusColor, Color? highlightColor, Color? splashColor, Color? hintColor, + Color? disabledColor, Color? shadowColor, Color? indicatorColor, Color? secondaryHeaderColor, + IconThemeData? iconTheme, IconThemeData? primaryIconTheme, AppBarTheme? appBarTheme, + ChipThemeData? chipTheme, CheckboxThemeData? checkboxTheme, CardTheme? cardTheme, + BottomAppBarThemeData? bottomAppBarTheme, DividerThemeData? dividerTheme, + dynamic snackBarTheme, dynamic inputDecorationTheme, dynamic radioTheme, dynamic switchTheme, + dynamic tooltipTheme, dynamic bottomSheetTheme, dynamic floatingActionButtonTheme, + dynamic elevatedButtonTheme, dynamic textButtonTheme, dynamic outlinedButtonTheme, + dynamic pageTransitionsTheme, dynamic visualDensity, dynamic typography, dynamic platform, + NavigationRailThemeData? navigationRailTheme, + bool? applyElevationOverlayColor, String? fontFamily}); + external static ThemeData dark({bool? useMaterial3}); + external ColorScheme get colorScheme; + external TextTheme get textTheme; + external TextTheme get primaryTextTheme; + external Brightness get brightness; + external IconThemeData? get iconTheme; + external AppBarTheme? get appBarTheme; + external ChipThemeData? get chipTheme; + external CardTheme? get cardTheme; + external DividerThemeData? get dividerTheme; + external Color? get primaryColor; + external Color? get scaffoldBackgroundColor; + external Color? get canvasColor; + external Color? get cardColor; + external Color? get dividerColor; + external Color? get focusColor; + external Color? get highlightColor; + external Color? get splashColor; + external Color? get hintColor; + external Color? get disabledColor; + external dynamic get platform; + // The ambient NavigationRail theme — Flutter's `ThemeData.navigationRailTheme`. + external NavigationRailThemeData get navigationRailTheme; + // The ambient Slider / BottomSheet themes — Flutter's `ThemeData.sliderTheme` + // / `ThemeData.bottomSheetTheme`. + external SliderThemeData get sliderTheme; + external BottomSheetThemeData get bottomSheetTheme; + external ThemeData copyWith({ColorScheme? colorScheme, TextTheme? textTheme, TextTheme? primaryTextTheme, + Brightness? brightness, Color? primaryColor, Color? scaffoldBackgroundColor, Color? canvasColor, + Color? cardColor, Color? dividerColor, Color? focusColor, Color? highlightColor, Color? splashColor, + Color? hintColor, Color? disabledColor, IconThemeData? iconTheme, AppBarTheme? appBarTheme, + ChipThemeData? chipTheme, CardTheme? cardTheme, DividerThemeData? dividerTheme, dynamic platform, + NavigationRailThemeData? navigationRailTheme, + bool? applyElevationOverlayColor, BottomAppBarThemeData? bottomAppBarTheme, BottomSheetThemeData? bottomSheetTheme, Object? inputDecorationTheme, IconThemeData? primaryIconTheme, PageTransitionsTheme? pageTransitionsTheme, Color? indicatorColor, TextSelectionThemeData? textSelectionTheme, Object? tabBarTheme, Object? snackBarTheme, Object? tooltipTheme}); +} + +@JavaName('com.codename1.flutter.material.ColorScheme') +class ColorScheme { + external ColorScheme({Brightness? brightness, Color? primary, Color? onPrimary, + Color? primaryContainer, Color? onPrimaryContainer, Color? inversePrimary, Color? secondary, + Color? onSecondary, Color? secondaryContainer, Color? onSecondaryContainer, Color? tertiary, + Color? onTertiary, Color? tertiaryContainer, Color? onTertiaryContainer, Color? error, + Color? onError, Color? errorContainer, Color? onErrorContainer, Color? surface, Color? onSurface, + Color? surfaceVariant, Color? onSurfaceVariant, Color? background, Color? onBackground, + Color? outline, Color? outlineVariant, Color? shadow, Color? scrim, Color? inverseSurface, + Color? onInverseSurface}); + external static ColorScheme fromSeed({Color seedColor, Brightness? brightness}); + external static ColorScheme light({Color? primary, Color? onPrimary, Color? primaryContainer, Color? onPrimaryContainer, Color? secondary, Color? onSecondary, Color? secondaryContainer, Color? onSecondaryContainer, Color? tertiary, Color? onTertiary, Color? error, Color? onError, Color? errorContainer, Color? onErrorContainer, Color? surface, Color? onSurface, Color? surfaceVariant, Color? onSurfaceVariant, Color? background, Color? onBackground, Color? outline, Color? shadow, Color? inverseSurface, Color? onInverseSurface, Color? inversePrimary, Brightness? brightness}); + external static ColorScheme dark({Color? primary, Color? onPrimary, Color? primaryContainer, Color? onPrimaryContainer, Color? secondary, Color? onSecondary, Color? secondaryContainer, Color? onSecondaryContainer, Color? tertiary, Color? onTertiary, Color? error, Color? onError, Color? errorContainer, Color? onErrorContainer, Color? surface, Color? onSurface, Color? surfaceVariant, Color? onSurfaceVariant, Color? background, Color? onBackground, Color? outline, Color? shadow, Color? inverseSurface, Color? onInverseSurface, Color? inversePrimary, Brightness? brightness}); + external Brightness get brightness; + external Color get primary; + external Color get onPrimary; + external Color get primaryContainer; + external Color get onPrimaryContainer; + external Color get inversePrimary; + external Color get secondary; + external Color get onSecondary; + external Color get secondaryContainer; + external Color get onSecondaryContainer; + external Color get tertiary; + external Color get onTertiary; + external Color get tertiaryContainer; + external Color get onTertiaryContainer; + external Color get error; + external Color get onError; + external Color get errorContainer; + external Color get onErrorContainer; + external Color get surface; + external Color get onSurface; + external Color get surfaceVariant; + external Color get onSurfaceVariant; + external Color get background; + external Color get onBackground; + external Color get outline; + external Color get outlineVariant; + external Color get shadow; + external Color get scrim; + external Color get inverseSurface; + external Color get onInverseSurface; + external ColorScheme copyWith({Brightness? brightness, Color? primary, Color? onPrimary, + Color? primaryContainer, Color? onPrimaryContainer, Color? secondary, Color? onSecondary, + Color? secondaryContainer, Color? tertiary, Color? error, Color? onError, Color? surface, + Color? onSurface, Color? surfaceVariant, Color? onSurfaceVariant, Color? background, + Color? onBackground, Color? outline, Color? inversePrimary, Color? inverseSurface, Color? shadow}); +} + +@JavaName('com.codename1.flutter.material.TextTheme') +class TextTheme { + external TextStyle get displayLarge; + external TextStyle get displayMedium; + external TextStyle get displaySmall; + external TextStyle get headlineLarge; + external TextStyle get headlineMedium; + external TextStyle get headlineSmall; + external TextStyle get titleLarge; + external TextStyle get titleMedium; + external TextStyle get titleSmall; + external TextStyle get bodyLarge; + external TextStyle get bodyMedium; + external TextStyle get bodySmall; + external TextStyle get labelLarge; + external TextStyle get labelMedium; + external TextStyle get labelSmall; + external TextTheme copyWith({TextStyle? displayLarge, TextStyle? displayMedium, TextStyle? displaySmall, + TextStyle? headlineLarge, TextStyle? headlineMedium, TextStyle? headlineSmall, TextStyle? titleLarge, + TextStyle? titleMedium, TextStyle? titleSmall, TextStyle? bodyLarge, TextStyle? bodyMedium, + TextStyle? bodySmall, TextStyle? labelLarge, TextStyle? labelMedium, TextStyle? labelSmall}); + external TextTheme apply({String? fontFamily, double? fontSizeFactor, double? fontSizeDelta, + Color? displayColor, Color? bodyColor, dynamic decoration, dynamic decorationColor}); +} + +@JavaName('com.codename1.flutter.material.Theme') +abstract class Theme { + external static ThemeData of(BuildContext context); + external static Brightness brightnessOf(BuildContext context); +} + +// --- M2 additions ------------------------------------------------------- + +@JavaName('com.codename1.flutter.BoxFit') +enum BoxFit { fill, contain, cover, fitWidth, fitHeight, none } + +@JavaName('com.codename1.flutter.Alignment') +abstract class Alignment { + external static Alignment get topLeft; + external static Alignment get topCenter; + external static Alignment get topRight; + external static Alignment get centerLeft; + external static Alignment get center; + external static Alignment get centerRight; + external static Alignment get bottomLeft; + external static Alignment get bottomCenter; + external static Alignment get bottomRight; +} + +@JavaName('com.codename1.flutter.widgets.ListView') +class ListView extends Widget { + external ListView({Key? key, List children, EdgeInsets? padding, bool? shrinkWrap}); + external static ListView builder({Key? key, int? itemCount, IndexedWidgetBuilder itemBuilder, EdgeInsets? padding, bool? shrinkWrap, Object? physics, Object? scrollDirection, Object? controller, String? restorationId, bool? primary, double? itemExtent, bool? reverse}); + external static ListView separated({Key? key, bool? primary, int? itemCount, IndexedWidgetBuilder itemBuilder, IndexedWidgetBuilder separatorBuilder, EdgeInsets? padding, bool? shrinkWrap}); +} + +@JavaName('com.codename1.flutter.widgets.GridView') +class GridView extends Widget { + external static GridView count({Key? key, String? restorationId, Object? physics, bool? primary, int crossAxisCount, double? childAspectRatio, double? mainAxisSpacing, double? crossAxisSpacing, EdgeInsets? padding, List children}); + external static GridView builder({Key? key, int? itemCount, IndexedWidgetBuilder itemBuilder, Object? gridDelegate, EdgeInsets? padding, bool? shrinkWrap, Object? physics}); +} + +@JavaName('com.codename1.flutter.widgets.SingleChildScrollView') +class SingleChildScrollView extends Widget { + external SingleChildScrollView({Key? key, EdgeInsets? padding, Axis? scrollDirection, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Image') +class Image extends Widget { + external Image({Key? key, ImageProvider? image, double? width, double? height, BoxFit? fit, bool? excludeFromSemantics, Object? frameBuilder}); + external static Image asset(String name, {Key? key, double? width, double? height, BoxFit? fit, String? package, bool? excludeFromSemantics, bool? gaplessPlayback, int? cacheWidth, int? cacheHeight, Color? color, Object? colorBlendMode, Object? alignment, Object? semanticLabel}); + external static Image network(String src, {Key? key, double? width, double? height, BoxFit? fit}); +} + +@JavaName('com.codename1.flutter.widgets.Stack') +class Stack extends Widget { + external Stack({Key? key, Alignment? alignment, List children}); +} + +@JavaName('com.codename1.flutter.widgets.Positioned') +class Positioned extends Widget { + external Positioned({Key? key, double? left, double? top, double? right, double? bottom, double? width, double? height, Widget child}); + external static Positioned fill({Key? key, double? left, double? top, double? right, double? bottom, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Align') +class Align extends Widget { + external Align({Key? key, Alignment? alignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.ConstrainedBox') +class ConstrainedBox extends Widget { + external ConstrainedBox({Key? key, BoxConstraints constraints, Widget? child}); +} + +@JavaName('com.codename1.flutter.rendering.BoxConstraints') +class BoxConstraints { + external BoxConstraints({double? minWidth, double? maxWidth, double? minHeight, double? maxHeight}); + external double get minWidth; + external double get maxWidth; + external double get minHeight; + external double get maxHeight; + external bool get hasBoundedWidth; + external bool get hasBoundedHeight; + external Size get biggest; + external Size get smallest; +} + +@JavaName('com.codename1.flutter.material.Card') +class Card extends Widget { + external Card({Key? key, Color? color, double? elevation, EdgeInsets? margin, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.Divider') +class Divider extends Widget { + external Divider({Key? key, double? height, double? thickness, Color? color}); +} + +@JavaName('com.codename1.flutter.material.ElevatedButton') +class ElevatedButton extends Widget { + external ElevatedButton({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? child}); + external static ButtonStyle styleFrom({Color? foregroundColor, Color? backgroundColor, + Color? shadowColor, double? elevation, TextStyle? textStyle, EdgeInsets? padding, + dynamic side, dynamic shape, dynamic alignment, dynamic tapTargetSize, dynamic visualDensity}); + external static ElevatedButton icon({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? icon, Widget? label}); +} + +@JavaName('com.codename1.flutter.material.TextButton') +class TextButton extends Widget { + external TextButton({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? child}); + external static ButtonStyle styleFrom({Color? foregroundColor, Color? backgroundColor, + Color? shadowColor, double? elevation, TextStyle? textStyle, EdgeInsets? padding, + dynamic side, dynamic shape, dynamic alignment, dynamic tapTargetSize, dynamic visualDensity}); + external static TextButton icon({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? icon, Widget? label}); +} + +@JavaName('com.codename1.flutter.material.OutlinedButton') +class OutlinedButton extends Widget { + external OutlinedButton({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? child}); + external static ButtonStyle styleFrom({Color? foregroundColor, Color? backgroundColor, + Color? shadowColor, double? elevation, TextStyle? textStyle, EdgeInsets? padding, + dynamic side, dynamic shape, dynamic alignment, dynamic tapTargetSize, dynamic visualDensity}); + external static OutlinedButton icon({Key? key, VoidCallback? onPressed, ButtonStyle? style, Widget? icon, Widget? label}); +} + +@JavaName('com.codename1.flutter.material.IconButton') +class IconButton extends Widget { + external IconButton({Key? key, VoidCallback? onPressed, Widget? icon, double? iconSize, Color? color}); +} + +@JavaName('com.codename1.flutter.widgets.GestureDetector') +class GestureDetector extends Widget { + external GestureDetector({Key? key, VoidCallback? onTap, VoidCallback? onLongPress, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.InkWell') +class InkWell extends Widget { + external InkWell({Key? key, VoidCallback? onTap, VoidCallback? onLongPress, Widget? child}); +} + +// --- M3 additions ------------------------------------------------------- +// Callback typedefs below (StringCallback etc.) are transpiler-internal +// names mapped to dart.runtime.Funcs SAMs; they type untyped lambda params. + +@JavaName('com.codename1.flutter.material.TextEditingController') +class TextEditingController { + external TextEditingController({String? text}); + external String get text; + external void setText(String value); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void clear(); + // Releases the controller's listeners — Flutter's `ChangeNotifier.dispose`. + external void dispose(); +} + +@JavaName('com.codename1.flutter.material.InputDecoration') +class InputDecoration { + external InputDecoration({String? labelText, String? hintText}); + external static InputDecoration collapsed({String? hintText, dynamic hintStyle, dynamic border, bool? filled, Color? fillColor}); +} + +@JavaName('com.codename1.flutter.material.TextField') +class TextField extends Widget { + external TextField({Key? key, TextEditingController? controller, InputDecoration? decoration, bool? obscureText, bool? enabled, StringCallback? onChanged, StringCallback? onSubmitted}); +} + +@JavaName('com.codename1.flutter.material.Checkbox') +class Checkbox extends Widget { + external Checkbox({Key? key, bool? value, bool tristate, BoolCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Radio') +class Radio extends Widget { + external Radio({Key? key, Object value, Object? groupValue, DynamicCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Switch') +class Switch extends Widget { + external Switch({Key? key, bool value, BoolCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.material.Slider') +class Slider extends Widget { + external Slider({Key? key, double value, double? min, double? max, int? divisions, DoubleCallback? onChanged}); +} + +@JavaName('com.codename1.flutter.navigation.MaterialPageRoute') +class MaterialPageRoute extends Route { + external MaterialPageRoute({WidgetBuilder builder}); +} + +@JavaName('com.codename1.flutter.navigation.Navigator') +class Navigator extends Widget { + external Navigator({Key? key}); + // push completes with the value the route is popped with, as Flutter's does. + external static Future push(BuildContext context, MaterialPageRoute route); + external static void pop(BuildContext context, [Object? result]); + external static NavigatorState of(BuildContext context, {bool? rootNavigator}); + external static String restorablePush(BuildContext context, dynamic routeBuilder, {dynamic arguments}); + external static bool maybePop(BuildContext context); +} + +@JavaName('com.codename1.flutter.material.Dialogs.showDialog') +external Future showDialog({BuildContext context, WidgetBuilder builder}); + +@JavaName('com.codename1.flutter.material.AlertDialog') +class AlertDialog extends Widget { + external AlertDialog({Key? key, Widget? title, Widget? content, List? actions}); +} + +@JavaName('com.codename1.flutter.material.SnackBar') +class SnackBar extends Widget { + external SnackBar({Key? key, Widget content, Duration? duration}); +} + +@JavaName('com.codename1.flutter.material.ScaffoldMessenger') +abstract class ScaffoldMessenger { + external static ScaffoldMessengerState of(BuildContext context); +} + +@JavaName('com.codename1.flutter.material.ScaffoldMessengerState') +abstract class ScaffoldMessengerState { + external void showSnackBar(SnackBar snackBar); + // Dismisses the visible SnackBar immediately — Flutter's `hideCurrentSnackBar`. + external void hideCurrentSnackBar({dynamic reason}); +} + +@JavaName('com.codename1.flutter.material.Drawer') +class Drawer extends Widget { + external Drawer({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.BottomNavigationBarItem') +class BottomNavigationBarItem { + external BottomNavigationBarItem({Widget? icon, Widget? activeIcon, String? label, + Color? backgroundColor, String? tooltip}); + // The item's glyph and label — Flutter's `BottomNavigationBarItem.icon/label`. + external Widget get icon; + external String? get label; +} + +@JavaName('com.codename1.flutter.material.BottomNavigationBar') +class BottomNavigationBar extends Widget { + external BottomNavigationBar({Key? key, List items, int? currentIndex, IntCallback? onTap}); +} + +@JavaName('com.codename1.flutter.material.ListTile') +class ListTile extends Widget { + external ListTile({Key? key, Widget? leading, Widget? title, Widget? subtitle, Widget? trailing, VoidCallback? onTap}); +} + +// --- M4 additions ------------------------------------------------------- + +@JavaName('com.codename1.flutter.ThemeMode') +enum ThemeMode { system, light, dark } + +@JavaName('com.codename1.flutter.Brightness') +enum Brightness { light, dark } + +@JavaName('com.codename1.flutter.MediaQuery') +abstract class MediaQuery { + external static MediaQueryData of(BuildContext context); + // Targeted inherited-lookup helpers that depend only on one MediaQueryData + // aspect — Flutter's `MediaQuery.sizeOf` / `paddingOf` / `viewInsetsOf`. + external static Size sizeOf(BuildContext context); + external static EdgeInsets paddingOf(BuildContext context); + external static EdgeInsets viewInsetsOf(BuildContext context); + external static Widget removePadding({BuildContext context, bool? removeLeft, bool? removeTop, bool? removeRight, bool? removeBottom, Widget? child}); +} + +@JavaName('com.codename1.flutter.MediaQueryData') +abstract class MediaQueryData { + external Size get size; + external double get devicePixelRatio; + external Brightness get platformBrightness; + external double get textScaleFactor; + external EdgeInsets get padding; + // The insets covered by system UI (keyboard, notches) — Flutter's + // `MediaQueryData.viewInsets` / `viewPadding`. + external EdgeInsets get viewInsets; + external EdgeInsets get viewPadding; + external MediaQueryData copyWith({Size? size, double? devicePixelRatio, double? textScaleFactor, + EdgeInsets? padding, Brightness? platformBrightness}); + // Returns a copy with one or more padding edges removed — Flutter's + // `MediaQueryData.removePadding`. + external MediaQueryData removePadding({bool? removeLeft, bool? removeTop, + bool? removeRight, bool? removeBottom}); + external MediaQueryData removeViewInsets({bool? removeLeft, bool? removeTop, + bool? removeRight, bool? removeBottom}); +} + +@JavaName('com.codename1.flutter.rendering.Size') +class Size { + external Size(double width, double height); + external static Size fromRadius(double radius); + external static Size fromHeight(double height); + external static Size fromWidth(double width); + external double get width; + external double get height; + external double get shortestSide; + external double get longestSide; + external double get aspectRatio; + external bool get isEmpty; + external Size get flipped; + // The offset to the center of a rect of this size with the given origin — + // Flutter's `Size.center(Offset origin)`. + external Offset center(Offset origin); + external Offset topLeft(Offset origin); + external Offset topCenter(Offset origin); + external Offset bottomCenter(Offset origin); + external bool contains(Offset offset); +} + +@JavaName('com.codename1.flutter.widgets.RichText') +class RichText extends Widget { + external RichText({Key? key, TextSpan text, TextAlign? textAlign}); +} + +// The base of the styled-text tree — Flutter's `InlineSpan` (TextSpan's supertype). +@JavaName('com.codename1.flutter.widgets.InlineSpan') +class InlineSpan { + external String toPlainText(); +} + +@JavaName('com.codename1.flutter.widgets.TextSpan') +class TextSpan extends InlineSpan { + external TextSpan({String? text, TextStyle? style, List? children}); + // Flattens this span tree to its raw text — Flutter's `InlineSpan.toPlainText`. + external String toPlainText(); +} + +// Colours for text selection. Recorded rather than applied - selection is drawn with +// Codename One's own theme colours - but three of the four studies name one, so the type +// has to exist for their themes to transpile. +@JavaName('com.codename1.flutter.material.TextSelectionThemeData') +class TextSelectionThemeData { + external TextSelectionThemeData({Color? cursorColor, Color? selectionColor, Color? selectionHandleColor}); +} + +// Per-platform page transition builders. Route transitions come from Codename One's own +// machinery; Rally configures one of these, so the type must resolve. +@JavaName('com.codename1.flutter.material.PageTransitionsTheme') +class PageTransitionsTheme { + external PageTransitionsTheme({Object? builders}); +} + +// A route transition for one platform. Recorded, not run - Codename One drives its own +// transitions - but Rally builds a Map, so the +// type must resolve for its theme to compile. +@JavaName('com.codename1.flutter.material.PageTransitionsBuilder') +class PageTransitionsBuilder {} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_animation.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_animation.dart new file mode 100644 index 00000000000..f515f00356c --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_animation.dart @@ -0,0 +1,266 @@ +// Codename One Flutter runtime API stubs — ANIMATION category. +// +// Signature-only declarations for the animation engine (AnimationController, +// Animation/Animatable, Tween family, Curves, and the transition/animated +// widgets) as seen from Dart. Each @JavaName points at the hand-written Java +// runtime under com.codename1.flutter.animation. Parsed with the transpiler's +// own Dart front end; conventions match flutter_material.dart. + +// --- ticker providers (framework mixins) ------------------------------ + +@JavaName('com.codename1.flutter.animation.TickerProvider') +abstract class TickerProvider {} + +@JavaName('com.codename1.flutter.animation.SingleTickerProviderStateMixin') +mixin SingleTickerProviderStateMixin {} + +@JavaName('com.codename1.flutter.animation.TickerProviderStateMixin') +mixin TickerProviderStateMixin {} + +// --- status ----------------------------------------------------------- + +@JavaName('com.codename1.flutter.animation.AnimationStatus') +enum AnimationStatus { dismissed, forward, reverse, completed } + +// Flutter exposes these as getters on the AnimationStatus enum (settings.dart / +// shrine app.dart read `status.isDismissed` / `status.isAnimating`). The +// transpiler drops enhanced-enum bodies, so they are supplied here as an +// extension, which member resolution consults for enum receivers. Backed by the +// static helpers on AnimationStatusExtensions. +@JavaName('com.codename1.flutter.animation.AnimationStatusExtensions') +extension AnimationStatusExtensions on AnimationStatus { + external bool get isDismissed; + external bool get isCompleted; + external bool get isAnimating; + external bool get isForwardOrCompleted; +} + +// How an AnimationController behaves when animations are disabled — Flutter's +// `AnimationBehavior` (progress_indicator_demo passes it to the controller). +@JavaName('com.codename1.flutter.animation.AnimationBehavior') +enum AnimationBehavior { normal, preserve } + +// --- animation / animatable core -------------------------------------- + +@JavaName('com.codename1.flutter.animation.Animation') +abstract class Animation { + external double get value; + external AnimationStatus get status; + external bool get isCompleted; + external bool get isDismissed; + external bool get isAnimating; + external bool get isForwardOrCompleted; + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void addStatusListener(AnimationStatusListener listener); + external void removeStatusListener(AnimationStatusListener listener); + external Animation drive(Animatable child); +} + +@JavaName('com.codename1.flutter.animation.Animatable') +abstract class Animatable { + external T transform(double t); + external T evaluate(Animation animation); + external Animation animate(Animation parent); + external Animatable chain(Animatable parent); +} + +@JavaName('com.codename1.flutter.animation.AlwaysStoppedAnimation') +class AlwaysStoppedAnimation extends Animation { + external AlwaysStoppedAnimation(T value); +} + +// --- controller ------------------------------------------------------- + +@JavaName('com.codename1.flutter.animation.AnimationController') +class AnimationController extends Animation { + external AnimationController({Duration? duration, Duration? reverseDuration, double? value, double? lowerBound, double? upperBound, TickerProvider? vsync, String? debugLabel, AnimationBehavior? animationBehavior}); + external double get value; + external set value(double v); + // Drives the controller with a spring toward its bound at the given velocity — + // Flutter's `AnimationController.fling`. + external Future fling({double? velocity, Object? springDescription, AnimationBehavior? animationBehavior}); + external Duration? get duration; + external set duration(Duration? v); + external Animation get view; + external Future forward({double? from}); + external Future reverse({double? from}); + external Future animateTo(double target, {Duration? duration, Curve? curve}); + external Future animateBack(double target, {Duration? duration, Curve? curve}); + external void repeat({double? min, double? max, bool? reverse, Duration? period}); + external void stop({bool? canceled}); + external void reset(); + external void dispose(); +} + +// --- curves ----------------------------------------------------------- + +@JavaName('com.codename1.flutter.animation.Curve') +abstract class Curve { + external double transform(double t); + external Curve get flipped; +} + +@JavaName('com.codename1.flutter.animation.Cubic') +class Cubic extends Curve { + external Cubic(double a, double b, double c, double d); +} + +@JavaName('com.codename1.flutter.animation.Interval') +class Interval extends Curve { + external Interval(double begin, double end, {Curve? curve}); +} + +@JavaName('com.codename1.flutter.animation.Curves') +abstract class Curves { + external static Curve get linear; + external static Curve get decelerate; + external static Curve get ease; + external static Curve get easeIn; + external static Curve get easeOut; + external static Curve get easeInOut; + external static Curve get easeInOutCubic; + external static Curve get easeInCubic; + external static Curve get easeOutCubic; + external static Curve get easeInSine; + external static Curve get easeOutSine; + external static Curve get easeInOutSine; + external static Curve get fastOutSlowIn; + external static Curve get slowMiddle; + external static Curve get bounceIn; + external static Curve get bounceOut; + external static Curve get bounceInOut; + external static Curve get elasticIn; + external static Curve get elasticOut; + external static Curve get fastLinearToSlowEaseIn; +} + +@JavaName('com.codename1.flutter.animation.CurvedAnimation') +class CurvedAnimation extends Animation { + external CurvedAnimation({Animation parent, Curve curve, Curve? reverseCurve}); +} + +// --- tweens ----------------------------------------------------------- + +@JavaName('com.codename1.flutter.animation.Tween') +class Tween extends Animatable { + external Tween({T? begin, T? end}); + external T? get begin; + external T? get end; + external T lerp(double t); +} + +@JavaName('com.codename1.flutter.animation.CurveTween') +class CurveTween extends Animatable { + external CurveTween({Curve curve}); +} + +@JavaName('com.codename1.flutter.animation.ColorTween') +class ColorTween extends Tween { + external ColorTween({Color? begin, Color? end}); +} + +@JavaName('com.codename1.flutter.animation.IntTween') +class IntTween extends Tween { + external IntTween({int? begin, int? end}); +} + +@JavaName('com.codename1.flutter.animation.BorderRadiusTween') +class BorderRadiusTween extends Tween { + external BorderRadiusTween({Object? begin, Object? end}); +} + +@JavaName('com.codename1.flutter.animation.EdgeInsetsGeometryTween') +class EdgeInsetsGeometryTween extends Tween { + external EdgeInsetsGeometryTween({Object? begin, Object? end}); +} + +@JavaName('com.codename1.flutter.animation.Matrix4Tween') +class Matrix4Tween extends Tween { + external Matrix4Tween({Object? begin, Object? end}); +} + +@JavaName('com.codename1.flutter.animation.RelativeRectTween') +class RelativeRectTween extends Tween { + external RelativeRectTween({Object? begin, Object? end}); +} + +@JavaName('com.codename1.flutter.animation.TweenSequenceItem') +class TweenSequenceItem { + external TweenSequenceItem({Animatable tween, double weight}); +} + +@JavaName('com.codename1.flutter.animation.TweenSequence') +class TweenSequence extends Animatable { + external TweenSequence(List> items); +} + +// --- AnimatedWidget base ---------------------------------------------- + +// The base for widgets driven by a Listenable (usually an Animation) — Flutter's +// `AnimatedWidget`. new_gallery's shrine _BackdropTitle extends it and reads the +// inherited `listenable` (cast back to Animation) in build(). +@JavaName('com.codename1.flutter.animation.AnimatedWidget') +class AnimatedWidget extends Widget { + external AnimatedWidget({Key? key, Listenable? listenable}); + external Listenable get listenable; +} + +// --- transition widgets ----------------------------------------------- + +@JavaName('com.codename1.flutter.animation.FadeTransition') +class FadeTransition extends Widget { + external FadeTransition({Key? key, Animation opacity, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.ScaleTransition') +class ScaleTransition extends Widget { + external ScaleTransition({Key? key, Animation scale, Alignment? alignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.SlideTransition') +class SlideTransition extends Widget { + external SlideTransition({Key? key, Animation position, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.RotationTransition') +class RotationTransition extends Widget { + external RotationTransition({Key? key, Animation turns, Alignment? alignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.PositionedTransition') +class PositionedTransition extends Widget { + external PositionedTransition({Key? key, Animation rect, Widget? child}); +} + +// --- animated (implicit) widgets -------------------------------------- + +@JavaName('com.codename1.flutter.animation.AnimatedBuilder') +class AnimatedBuilder extends Widget { + external AnimatedBuilder({Key? key, Animation animation, TransitionBuilder builder, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.AnimatedContainer') +class AnimatedContainer extends Widget { + external AnimatedContainer({Key? key, Duration duration, Curve? curve, double? width, double? height, Color? color, EdgeInsets? padding, EdgeInsets? margin, Alignment? alignment, Object? decoration, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.AnimatedPadding') +class AnimatedPadding extends Widget { + external AnimatedPadding({Key? key, EdgeInsets padding, Duration duration, Curve? curve, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.AnimatedSize') +class AnimatedSize extends Widget { + external AnimatedSize({Key? key, Duration duration, Curve? curve, Alignment? alignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.AnimatedSwitcher') +class AnimatedSwitcher extends Widget { + external AnimatedSwitcher({Key? key, Duration duration, Duration? reverseDuration, Curve? switchInCurve, Curve? switchOutCurve, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.AnimatedOpacity') +class AnimatedOpacity extends Widget { + external AnimatedOpacity({Key? key, double opacity, Duration duration, Curve? curve, Widget? child}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_coreWidgets.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_coreWidgets.dart new file mode 100644 index 00000000000..f9cfc2c52ac --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_coreWidgets.dart @@ -0,0 +1,219 @@ +// Codename One Flutter runtime API stubs — coreWidgets category (new_gallery). +// +// Structural / decoration / a11y / clipping widgets and their enums. These +// are signature-only declarations resolved by the Dart transpiler; each maps +// to a hand-written Java runtime class via @JavaName. Loosely-typed params +// (Object?) intentionally accept cross-category value types (BorderRadius, +// EdgeInsetsDirectional, gradients, cursors, ...) without this file having to +// declare them — the transpiler resolves those argument expressions against +// whatever category owns them. + +// --- enums ------------------------------------------------------------ + +@JavaName('com.codename1.flutter.Axis') +enum Axis { horizontal, vertical } + +@JavaName('com.codename1.flutter.TextDirection') +enum TextDirection { rtl, ltr } + +@JavaName('com.codename1.flutter.Clip') +enum Clip { none, hardEdge, antiAlias, antiAliasWithSaveLayer } + +@JavaName('com.codename1.flutter.BoxShape') +enum BoxShape { rectangle, circle } + +@JavaName('com.codename1.flutter.StackFit') +enum StackFit { loose, expand, passthrough } + +@JavaName('com.codename1.flutter.FlexFit') +enum FlexFit { tight, loose } + +@JavaName('com.codename1.flutter.WrapAlignment') +enum WrapAlignment { start, end, center, spaceBetween, spaceAround, spaceEvenly } + +@JavaName('com.codename1.flutter.WrapCrossAlignment') +enum WrapCrossAlignment { start, end, center } + +// --- directional alignment ------------------------------------------- + +@JavaName('com.codename1.flutter.AlignmentDirectional') +abstract class AlignmentDirectional { + external static AlignmentDirectional get topStart; + external static AlignmentDirectional get topCenter; + external static AlignmentDirectional get topEnd; + external static AlignmentDirectional get centerStart; + external static AlignmentDirectional get center; + external static AlignmentDirectional get centerEnd; + external static AlignmentDirectional get bottomStart; + external static AlignmentDirectional get bottomCenter; + external static AlignmentDirectional get bottomEnd; +} + +// --- decorations / image providers ----------------------------------- + +@JavaName('com.codename1.flutter.Decoration') +abstract class Decoration {} + +@JavaName('com.codename1.flutter.BoxDecoration') +class BoxDecoration extends Decoration { + external BoxDecoration({Color? color, Object? image, Object? border, Object? borderRadius, Object? boxShadow, Object? gradient, Object? backgroundBlendMode, BoxShape? shape}); +} + +@JavaName('com.codename1.flutter.ImageProvider') +abstract class ImageProvider {} + +@JavaName('com.codename1.flutter.AssetImage') +class AssetImage extends ImageProvider { + external AssetImage(String assetName, {String? package, Object? bundle}); +} + +@JavaName('com.codename1.flutter.NetworkImage') +class NetworkImage extends ImageProvider { + external NetworkImage(String url, {double? scale, Object? headers}); +} + +@JavaName('com.codename1.flutter.DecorationImage') +class DecorationImage { + external DecorationImage({ImageProvider? image, BoxFit? fit, Object? alignment, Object? colorFilter, Object? repeat, double? scale, double? opacity, bool? matchTextDirection}); +} + +// --- core structural widgets ----------------------------------------- + +@JavaName('com.codename1.flutter.widgets.Container') +class Container extends Widget { + external Container({Key? key, Object? alignment, Object? padding, Color? color, Object? decoration, Object? foregroundDecoration, double? width, double? height, BoxConstraints? constraints, Object? margin, Object? transform, Object? transformAlignment, Clip? clipBehavior, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.DecoratedBox') +class DecoratedBox extends Widget { + external DecoratedBox({Key? key, Object decoration, Object? position, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.ColoredBox') +class ColoredBox extends Widget { + external ColoredBox({Key? key, Color color, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.LayoutBuilder') +class LayoutBuilder extends Widget { + external LayoutBuilder({Key? key, LayoutWidgetBuilder builder}); +} + +@JavaName('com.codename1.flutter.widgets.Builder') +class Builder extends Widget { + external Builder({Key? key, WidgetBuilder builder}); +} + +@JavaName('com.codename1.flutter.widgets.SafeArea') +class SafeArea extends Widget { + external SafeArea({Key? key, bool? left, bool? top, bool? right, bool? bottom, EdgeInsets? minimum, bool? maintainBottomViewPadding, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.Material') +class Material extends Widget { + external Material({Key? key, Object? type, double? elevation, Color? color, Color? shadowColor, Color? surfaceTintColor, TextStyle? textStyle, Object? borderRadius, Object? shape, bool? borderOnForeground, Clip? clipBehavior, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Flexible') +class Flexible extends Widget { + external Flexible({Key? key, int? flex, FlexFit? fit, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Wrap') +class Wrap extends Widget { + external Wrap({Key? key, Axis? direction, WrapAlignment? alignment, double? spacing, WrapAlignment? runAlignment, double? runSpacing, WrapCrossAlignment? crossAxisAlignment, Object? textDirection, Object? verticalDirection, Clip? clipBehavior, List children}); +} + +@JavaName('com.codename1.flutter.widgets.FractionallySizedBox') +class FractionallySizedBox extends Widget { + external FractionallySizedBox({Key? key, Object? alignment, double? widthFactor, double? heightFactor, Widget? child}); +} + +// --- accessibility (pass-through wrappers) --------------------------- + +@JavaName('com.codename1.flutter.widgets.Semantics') +class Semantics extends Widget { + external Semantics({Key? key, Widget? child, bool? container, bool? explicitChildNodes, bool? excludeSemantics, bool? enabled, bool? checked, bool? selected, bool? toggled, bool? button, bool? link, bool? header, bool? textField, bool? readOnly, bool? focusable, bool? focused, bool? image, bool? liveRegion, bool? hidden, bool? obscured, bool? multiline, String? label, String? value, String? increasedValue, String? decreasedValue, String? hint, String? tooltip, Object? sortKey, VoidCallback? onTap, VoidCallback? onLongPress}); + // Builds a Semantics node from a pre-assembled SemanticsProperties bag — + // Flutter's `Semantics.fromProperties` (rally finance.dart). + external static Semantics fromProperties({Key? key, required SemanticsProperties properties, bool? container, bool? explicitChildNodes, bool? excludeSemantics, Widget? child}); +} + +// A bag of semantic annotations passed to Semantics.fromProperties and to +// CustomPainterSemantics — Flutter's `SemanticsProperties`. +@JavaName('com.codename1.flutter.widgets.SemanticsProperties') +class SemanticsProperties { + external SemanticsProperties({bool? enabled, bool? checked, bool? selected, bool? toggled, bool? button, bool? link, bool? header, bool? textField, bool? readOnly, bool? focusable, bool? focused, bool? inMutuallyExclusiveGroup, bool? hidden, bool? obscured, bool? multiline, bool? scopesRoute, bool? namesRoute, bool? image, bool? liveRegion, String? label, String? value, String? increasedValue, String? decreasedValue, String? hint, String? onTapHint, String? onLongPressHint, TextDirection? textDirection, Object? sortKey, VoidCallback? onTap, VoidCallback? onLongPress}); +} + +@JavaName('com.codename1.flutter.widgets.ExcludeSemantics') +class ExcludeSemantics extends Widget { + external ExcludeSemantics({Key? key, bool? excluding, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.MergeSemantics') +class MergeSemantics extends Widget { + external MergeSemantics({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.AnnotatedRegion') +class AnnotatedRegion extends Widget { + external AnnotatedRegion({Key? key, Widget? child, Object? value, bool? sized}); +} + +@JavaName('com.codename1.flutter.widgets.MouseRegion') +class MouseRegion extends Widget { + external MouseRegion({Key? key, Object? cursor, bool? opaque, Object? onEnter, Object? onExit, Object? onHover, Object? hitTestBehavior, Widget? child}); +} + +// --- clipping (pass-through wrappers) -------------------------------- + +@JavaName('com.codename1.flutter.widgets.ClipRRect') +class ClipRRect extends Widget { + external ClipRRect({Key? key, Object? borderRadius, Object? clipper, Clip? clipBehavior, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.ClipRect') +class ClipRect extends Widget { + external ClipRect({Key? key, Object? clipper, Clip? clipBehavior, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.ClipOval') +class ClipOval extends Widget { + external ClipOval({Key? key, Object? clipper, Clip? clipBehavior, Widget? child}); +} + +// --- scrolling / overlays (pass-through) ----------------------------- + +@JavaName('com.codename1.flutter.widgets.Scrollbar') +class Scrollbar extends Widget { + external Scrollbar({Key? key, Object? controller, bool? thumbVisibility, bool? trackVisibility, double? thickness, Object? radius, bool? interactive, Object? notificationPredicate, Object? scrollbarOrientation, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.Tooltip') +class Tooltip extends Widget { + external Tooltip({Key? key, String? message, Object? richMessage, double? height, Object? padding, Object? margin, double? verticalOffset, bool? preferBelow, bool? excludeFromSemantics, Object? decoration, TextStyle? textStyle, Object? waitDuration, Object? showDuration, Object? triggerMode, Widget? child}); +} + +// --- selectable text -------------------------------------------------- + +@JavaName('com.codename1.flutter.widgets.SelectableText') +class SelectableText extends Widget { + external SelectableText(String data, {Key? key, TextStyle? style, TextAlign? textAlign, int? maxLines, double? textScaleFactor, bool? showCursor, Object? cursorColor, Object? onTap, Object? focusNode, Object? scrollPhysics}); + external static SelectableText rich(TextSpan textSpan, {Key? key, TextStyle? style, TextAlign? textAlign, int? maxLines, TextDirection? textDirection}); +} + +// --- popup menus ------------------------------------------------------ + +@JavaName('com.codename1.flutter.material.PopupMenuEntry') +abstract class PopupMenuEntry extends Widget {} + +@JavaName('com.codename1.flutter.material.PopupMenuItem') +class PopupMenuItem extends PopupMenuEntry { + external PopupMenuItem({Key? key, T? value, bool? enabled, double? height, Object? padding, Object? textStyle, Object? mouseCursor, VoidCallback? onTap, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.PopupMenuButton') +class PopupMenuButton extends Widget { + external PopupMenuButton({Key? key, PopupMenuItemBuilder itemBuilder, T? initialValue, DynamicCallback? onSelected, VoidCallback? onCanceled, String? tooltip, double? elevation, Object? padding, Widget? icon, double? iconSize, Object? offset, bool? enabled, Object? shape, Color? color, Object? position, Widget? child}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart new file mode 100644 index 00000000000..0856ff0ab00 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_dartCore.dart @@ -0,0 +1,429 @@ +// Codename One Flutter runtime API stubs — "dartCore" category (new_gallery). +// +// dart:core / dart:async / dart:math / dart:typed_data / dart:intl equivalents, +// GoogleFonts, Directionality/TextDirection, Clipboard, SystemChrome and the +// Material localization basics used by the Flutter new_gallery integration app. +// +// Same conventions as flutter_material.dart: +// - positional constructor params -> Java constructor arguments +// - named constructor params -> canonical positional order (declared order) +// - instance getters -> no-arg method calls (name()) +// - static getters -> static field access (Name.field) +// - named "factory" constructors -> `external static X foo(...)` = static method +// +// Library-prefix trick: dart:math is imported `as math` and dart:intl `as intl`. +// The transpiler has no import-prefix table, so `math.pi` / `intl.Intl` resolve +// the receiver identifier as a *type*. We therefore expose a stub class literally +// named `math` (-> dart.math.DartMath) and `intl` (-> IntlLib holding `Intl`), +// which makes the prefixed member access resolve as static/instance access. + +// --- dart:core : Object / Comparable --------------------------------------- + +// The root of the Dart class hierarchy — `Object`. new_gallery reaches it for +// the static hash combiners (`Object.hash(a, b)` in == overrides / hashCode) +// and for `other.runtimeType`. The class @JavaName maps the type + instance +// members onto java.lang.Object; the static `hash`/`hashAll` combinators are +// backed by java.util.Objects.hash. +@JavaName('java.lang.Object') +class Object { + external Object(); + external static int hash(Object? a, Object? b, [Object? c, Object? d, Object? e, Object? f, Object? g, Object? h]); + external static int hashAll(Iterable objects); + external Object get runtimeType; + external int get hashCode; + external String toString(); +} + +// A totally-ordered type — Dart's `Comparable`. new_gallery's data-table demo +// sorts with `Comparable.compare(a, b)` and types cell values as `Comparable`. +// Instances/type map to java.lang.Comparable; the static `compare` combinator is +// backed by dart.core.DartComparable.compare. +@JavaName('java.lang.Comparable') +abstract class Comparable { + external static int compare(Comparable a, Comparable b); + external int compareTo(T other); +} + +// --- dart:core : DateTime -------------------------------------------------- + +@JavaName('dart.core.DateTime') +class DateTime { + // Dart's own defaults, stated. Without them an omitted month or day is filled + // with the type's zero, so DateTime(2024) reached the runtime as month 0, day 0 + // -- indistinguishable from DateTime(2024, 3, 0), which Dart defines as the last + // day of February. The runtime then had to clamp both to 1, and the clamp broke + // the normalization calendar code relies on. + external DateTime(int year, [int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0, int millisecond = 0, int microsecond = 0]); + external static DateTime now(); + external static DateTime utc(int year, [int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0, int millisecond = 0, int microsecond = 0]); + external static DateTime fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, {bool isUtc}); + external int get year; + external int get month; + external int get day; + external int get hour; + external int get minute; + external int get second; + external int get millisecond; + external int get weekday; + external int get millisecondsSinceEpoch; + external int get microsecondsSinceEpoch; + external DateTime add(Duration duration); + external DateTime subtract(Duration duration); + external Duration difference(DateTime other); + external bool isBefore(DateTime other); + external bool isAfter(DateTime other); + external bool isAtSameMomentAs(DateTime other); + external DateTime toLocal(); + external DateTime toUtc(); + external int compareTo(DateTime other); +} + +@JavaName('dart.core.DateTimeRange') +class DateTimeRange { + external DateTimeRange({DateTime start, DateTime end}); + external DateTime get start; + external DateTime get end; + external Duration get duration; +} + +// --- dart:core : RegExp / Match -------------------------------------------- +// The runtime dart.core.RegExp/RegExpMatch are backed by java.util.regex. Named +// ctor params (multiLine/caseSensitive/unicode/dotAll) lower to post-construction +// setters; the getter `pattern` reads the original source string. + +@JavaName('dart.core.RegExp') +class RegExp { + external RegExp(String source, {bool multiLine, bool caseSensitive, bool unicode, bool dotAll}); + external bool hasMatch(String input); + external RegExpMatch? firstMatch(String input); + external Iterable allMatches(String input); + external String? stringMatch(String input); + external String get pattern; +} + +@JavaName('dart.core.RegExpMatch') +class RegExpMatch { + external String? group(int group); + external int get groupCount; + external int get start; + external int get end; + external String get input; +} + +// --- dart:core : StringBuffer ---------------------------------------------- + +@JavaName('dart.core.StringBuffer') +class StringBuffer { + external StringBuffer([Object content]); + external int get length; + external bool get isEmpty; + external bool get isNotEmpty; + external void write(Object object); + external void writeln([Object object]); + external void writeCharCode(int charCode); + external void writeAll(Iterable objects, [String separator]); + external void clear(); +} + +// --- dart:core : MapEntry -------------------------------------------------- +// Element type of Map.entries; `key`/`value` getters read the pair. + +@JavaName('dart.core.MapEntry') +class MapEntry { + external MapEntry(K key, V value); + external K get key; + external V get value; +} + +// --- dart:async : Timer ---------------------------------------------------- + +@JavaName('dart.async.Timer') +class Timer { + external Timer(Duration duration, VoidCallback callback); + external void cancel(); + external bool get isActive; +} + +// dart.async.Completer existed in the runtime with nothing declaring it, so Dart code +// that built a future by hand could not be transpiled. +@JavaName('dart.async.Completer') +class Completer { + external Completer(); + external Future get future; + external void complete([T? value]); + external void completeError(Object error); + external bool get isCompleted; +} + +// The eventual-value type — dart:async's `Future`. `await`, the named +// constructors (Future.value / Future.delayed) and `.then` / `.whenComplete` +// are handled directly by the transpiler; `.catchError` (chained after `.then` +// on the demo page's clipboard copy) falls through to this stub. +@JavaName('dart.async.Future') +abstract class Future { + external Future then(Object onValue, {Object? onError}); + external Future catchError(Object onError, {Object? test}); + external Future whenComplete(Object action); +} + +// --- dart:typed_data ------------------------------------------------------- + +@JavaName('dart.typed_data.Uint8List') +class Uint8List { + external Uint8List(int length); + external static Uint8List fromList(List elements); + external int get length; +} + +@JavaName('dart.typed_data.ByteData') +class ByteData { + external ByteData(int length); + external int get lengthInBytes; + external int getUint8(int byteOffset); + external void setUint8(int byteOffset, int value); + external int getInt32(int byteOffset); + external void setInt32(int byteOffset, int value); +} + +// --- dart:math ------------------------------------------------------------- +// `math` is the import-prefix stub (import 'dart:math' as math;). + +@JavaName('dart.math.DartMath') +abstract class math { + external static double get pi; + external static double get e; + external static double min(double a, double b); + external static double max(double a, double b); + external static double pow(double x, double exponent); + external static double sqrt(double x); + external static double sin(double x); + external static double cos(double x); + external static double tan(double x); + external static double asin(double x); + external static double acos(double x); + external static double atan(double x); + external static double atan2(double a, double b); + external static double exp(double x); + external static double log(double x); +} + +// Unprefixed dart:math top-level functions (import 'dart:math';). +@JavaName('dart.math.DartMath.min') +external double min(double a, double b); +@JavaName('dart.math.DartMath.max') +external double max(double a, double b); +@JavaName('dart.math.DartMath.sqrt') +external double sqrt(double x); +@JavaName('dart.math.DartMath.pow') +external double pow(double x, double exponent); +@JavaName('dart.math.DartMath.sin') +external double sin(double x); +@JavaName('dart.math.DartMath.cos') +external double cos(double x); +@JavaName('dart.math.DartMath.tan') +external double tan(double x); +@JavaName('dart.math.DartMath.asin') +external double asin(double x); +@JavaName('dart.math.DartMath.acos') +external double acos(double x); +@JavaName('dart.math.DartMath.atan') +external double atan(double x); +@JavaName('dart.math.DartMath.atan2') +external double atan2(double a, double b); +@JavaName('dart.math.DartMath.exp') +external double exp(double x); +@JavaName('dart.math.DartMath.log') +external double log(double x); + +@JavaName('dart.math.DartMath.DartRandom') +class Random { + external Random([int seed]); + external int nextInt(int max); + external double nextDouble(); + external bool nextBool(); +} + +// dart:core's `Iterator` protocol. Given an explicit @JavaName so classes +// that `implements Iterator` (e.g. the transformations demo's _BoardIterator) +// resolve it and the emitter imports `dart.collection.Iterator` rather than +// leaving a bare, unresolved `Iterator`. The dart:collection IterableMixin +// built-in returns this type from its `iterator` getter. +@JavaName('dart.collection.Iterator') +abstract class Iterator { + external bool moveNext(); + external E get current; +} + +@JavaName('dart.math.DartPoint') +class Point { + external Point(double x, double y); + external double get x; + external double get y; + external double distanceTo(Point other); +} + +// --- dart:intl ------------------------------------------------------------- + +// package:intl's `Intl`. Reached as `intl.Intl.xxx(...)`; the transpiler +// strips the `intl` import prefix and resolves `Intl` to this top-level type, +// so every member the app calls (canonicalizedLocale / pluralLogic / ...) must +// be STATIC — it is invoked as a static method on the class, never on an +// instance. +@JavaName('com.codename1.flutter.intl.Intl') +class Intl { + external static String canonicalizedLocale(String aLocale); + external static String pluralLogic(num howMany, {String locale, String zero, String one, String two, String few, String many, String other}); + external static String message(String messageText, {String desc, String locale, String name, Object args, String meaning}); + external static String plural(num howMany, {String locale, String zero, String one, String two, String few, String many, String other, String name, Object args}); + external static String select(Object choice, Object cases, {String locale, String name, Object args}); + external static String gender(String targetGender, {String female, String male, String other, String locale, String name, Object args}); + external static Object withLocale(String locale, Object function); + external static String getCurrentLocale(); + external static String get defaultLocale; +} + +// `intl` is the import-prefix stub (import 'package:intl/intl.dart' as intl;). +// Retained so a bare `intl.` prefix still resolves; `intl.Intl` itself now goes +// straight to the top-level `Intl` type above. +@JavaName('com.codename1.flutter.intl.IntlLib') +abstract class intl { + external static Intl get Intl; +} + +@JavaName('com.codename1.flutter.intl.DateFormat') +class DateFormat { + external DateFormat([String pattern, String locale]); + // Skeleton "field" constants used bare, e.g. DateFormat(DateFormat.WEEKDAY). + external static String get WEEKDAY; + external static String get MMM; + external static DateFormat MMMd([String locale]); + external static DateFormat jm([String locale]); + external static DateFormat Hm([String locale]); + external static DateFormat yMMM([String locale]); + external static DateFormat yMMMMd([String locale]); + external static DateFormat yMMMd([String locale]); + external static DateFormat yMd([String locale]); + external DateFormat add_jm(); + external DateFormat add_jms(); + external String format(DateTime date); +} + +@JavaName('com.codename1.flutter.intl.NumberFormat') +class NumberFormat { + external static NumberFormat currency({String locale, String symbol, int? decimalDigits, String name}); + external static NumberFormat decimalPercentPattern({String locale, int decimalDigits}); + external static NumberFormat simpleCurrency({String locale, String name, int? decimalDigits}); + external String format(dynamic number); +} + +// --- fonts : GoogleFonts --------------------------------------------------- +// Named font accessors return a TextStyle; the *TextTheme accessors return a +// TextTheme. Extra named args (textStyle/fontStyle/letterSpacing) the app +// passes are dropped by the emitter since they are not declared here. + +@JavaName('com.codename1.flutter.fonts.GoogleFonts') +abstract class GoogleFonts { + external static GoogleFontsConfig get config; + external static TextStyle eczar({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle libreFranklin({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle merriweather({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle montserrat({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle oswald({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle robotoCondensed({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle robotoMono({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextStyle workSans({double fontSize, FontWeight fontWeight, Color color, double? letterSpacing, double? height, TextStyle? textStyle, FontStyle? fontStyle, Object? decoration, double? wordSpacing}); + external static TextTheme ralewayTextTheme([TextTheme textTheme]); + external static TextTheme rubikTextTheme([TextTheme textTheme]); + external static TextTheme workSansTextTheme([TextTheme textTheme]); +} + +@JavaName('com.codename1.flutter.fonts.GoogleFontsConfig') +class GoogleFontsConfig { + external bool get allowRuntimeFetching; + external set allowRuntimeFetching(bool v); +} + +// --- services : Clipboard / SystemChrome ----------------------------------- + +@JavaName('com.codename1.flutter.services.ClipboardData') +class ClipboardData { + external ClipboardData({String text}); + external String get text; +} + +@JavaName('com.codename1.flutter.services.Clipboard') +abstract class Clipboard { + external static Future setData(ClipboardData data); + external static Future getData(String format); +} + +@JavaName('com.codename1.flutter.services.SystemUiOverlayStyle') +abstract class SystemUiOverlayStyle { + external static SystemUiOverlayStyle get light; + external static SystemUiOverlayStyle get dark; +} + +@JavaName('com.codename1.flutter.services.SystemChrome') +abstract class SystemChrome { + external static void setSystemUIOverlayStyle(SystemUiOverlayStyle style); + external static void setPreferredOrientations(List orientations); + external static void setEnabledSystemUIMode(Object mode); +} + +// --- platform -------------------------------------------------------------- + +@JavaName('com.codename1.flutter.TargetPlatform') +enum TargetPlatform { android, fuchsia, iOS, linux, macOS, windows } + +// --- widgets : Directionality ---------------------------------------------- +// TextDirection enum is contributed by the coreWidgets stub set. + +@JavaName('com.codename1.flutter.widgets.Directionality') +class Directionality extends Widget { + external Directionality({Key? key, TextDirection textDirection, Widget child}); + // The ambient text direction — Flutter's `Directionality.of(context)`. + external static TextDirection of(BuildContext context); +} + +@JavaName('com.codename1.flutter.widgets.Debug.debugCheckHasDirectionality') +external bool debugCheckHasDirectionality(BuildContext context); + +// --- l10n : MaterialLocalizations ------------------------------------------ + +@JavaName('com.codename1.flutter.l10n.LocalizationsDelegate') +class LocalizationsDelegate {} + +@JavaName('com.codename1.flutter.l10n.MaterialLocalizations') +abstract class MaterialLocalizations { + external static MaterialLocalizations of(BuildContext context); + external static LocalizationsDelegate get delegate; + external String get backButtonTooltip; + external String get closeButtonTooltip; + external String get closeButtonLabel; + external String get viewLicensesButtonLabel; + external String get nextPageTooltip; + external String get previousPageTooltip; + external String get openAppDrawerTooltip; +} + +@JavaName('com.codename1.flutter.l10n.GlobalMaterialLocalizations') +abstract class GlobalMaterialLocalizations { + external static LocalizationsDelegate get delegate; +} + +@JavaName('com.codename1.flutter.l10n.GlobalCupertinoLocalizations') +abstract class GlobalCupertinoLocalizations { + external static LocalizationsDelegate get delegate; +} + +@JavaName('com.codename1.flutter.l10n.GlobalWidgetsLocalizations') +abstract class GlobalWidgetsLocalizations { + external static LocalizationsDelegate get delegate; +} + +// RestorableDateTime is contributed by the restoration stub set. + +// Upright or italic — Flutter's FontStyle. Fortnightly's masthead is the only place in the +// gallery that asks for italics, but a font call naming it must still resolve. +@JavaName('com.codename1.flutter.FontStyle') +enum FontStyle { normal, italic } diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_keyboard.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_keyboard.dart new file mode 100644 index 00000000000..04f209c785e --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_keyboard.dart @@ -0,0 +1,84 @@ +// Codename One Flutter runtime API stubs — KEYBOARD / services category. +// +// Signature-only declarations for the hardware-keyboard surface new_gallery +// touches through Focus.onKeyEvent / KeyboardListener: the KeyEvent hierarchy, +// the LogicalKeyboardKey constants compared against event.logicalKey, and the +// KeyEventResult returned from key handlers. Each @JavaName points at the +// hand-written Java runtime under com.codename1.flutter.services. Conventions +// match flutter_material.dart. + +// --- key-handler result ------------------------------------------------ + +// What a key handler reports back to the focus system — Flutter's +// `KeyEventResult`. highlight_focus / rally login return handled / ignored. +@JavaName('com.codename1.flutter.services.KeyEventResult') +enum KeyEventResult { handled, ignored, skipRemainingHandlers } + +// --- key events -------------------------------------------------------- + +// Base class for a keyboard event in the modern (HardwareKeyboard) API — +// Flutter's `KeyEvent`. The onKeyEvent callbacks receive one of the concrete +// subclasses; code switches on `event is KeyDownEvent` and reads `logicalKey`. +@JavaName('com.codename1.flutter.services.KeyEvent') +abstract class KeyEvent { + external LogicalKeyboardKey get logicalKey; + external PhysicalKeyboardKey get physicalKey; + external String? get character; + external Duration get timeStamp; +} + +@JavaName('com.codename1.flutter.services.KeyDownEvent') +class KeyDownEvent extends KeyEvent { + external KeyDownEvent({required LogicalKeyboardKey logicalKey, required PhysicalKeyboardKey physicalKey, String? character}); +} + +@JavaName('com.codename1.flutter.services.KeyUpEvent') +class KeyUpEvent extends KeyEvent { + external KeyUpEvent({required LogicalKeyboardKey logicalKey, required PhysicalKeyboardKey physicalKey}); +} + +@JavaName('com.codename1.flutter.services.KeyRepeatEvent') +class KeyRepeatEvent extends KeyEvent { + external KeyRepeatEvent({required LogicalKeyboardKey logicalKey, required PhysicalKeyboardKey physicalKey, String? character}); +} + +// A physical (scan-code) key — Flutter's `PhysicalKeyboardKey`. Present so the +// KeyEvent.physicalKey getter resolves; new_gallery does not compare against it. +@JavaName('com.codename1.flutter.services.PhysicalKeyboardKey') +class PhysicalKeyboardKey { + external int get usbHidUsage; + external String? get debugName; +} + +// --- logical keys ------------------------------------------------------ + +// A logical (layout-dependent) key — Flutter's `LogicalKeyboardKey`. The static +// constants are singletons compared with `==` against `event.logicalKey`. +// new_gallery uses enter / numpadEnter / space / escape; the rest round out the +// navigation and control keys so the class matches the real Flutter shape. +@JavaName('com.codename1.flutter.services.LogicalKeyboardKey') +class LogicalKeyboardKey { + external int get keyId; + external String? get keyLabel; + external String? get debugName; + + external static LogicalKeyboardKey get arrowUp; + external static LogicalKeyboardKey get arrowDown; + external static LogicalKeyboardKey get arrowLeft; + external static LogicalKeyboardKey get arrowRight; + external static LogicalKeyboardKey get enter; + external static LogicalKeyboardKey get numpadEnter; + external static LogicalKeyboardKey get escape; + external static LogicalKeyboardKey get tab; + external static LogicalKeyboardKey get space; + external static LogicalKeyboardKey get backspace; + external static LogicalKeyboardKey get delete; + external static LogicalKeyboardKey get home; + external static LogicalKeyboardKey get end; + external static LogicalKeyboardKey get pageUp; + external static LogicalKeyboardKey get pageDown; + external static LogicalKeyboardKey get shift; + external static LogicalKeyboardKey get control; + external static LogicalKeyboardKey get meta; + external static LogicalKeyboardKey get alt; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_cupertino.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_cupertino.dart new file mode 100644 index 00000000000..b031b0d024f --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_cupertino.dart @@ -0,0 +1,283 @@ +// Codename One Flutter runtime API stubs — cupertino category (new_gallery, Pass 2). +// +// Signature-only declarations for the Cupertino (iOS-style) widget subset the +// new_gallery demos use. Resolved by the Dart transpiler against these shapes; +// each maps to a hand-written Java runtime class via @JavaName. Most Cupertino +// widgets compose the existing material / core widgets in their Java build() +// (visually approximate this pass — correct API shape + real layout). +// +// Cross-category value types the demos also use (Border, BorderSide, Radius, +// TextInputType, TextInputAction, CustomScrollView / slivers, the Navigator +// widget, RouteSettings, DefaultTextStyle, FlutterLogo) are owned by other +// categories (painting / services / scrolling / navigation) and are NOT +// declared here. + +// --- enums ------------------------------------------------------------ + +@JavaName('com.codename1.flutter.cupertino.CupertinoDatePickerMode') +enum CupertinoDatePickerMode { time, date, dateAndTime, monthYear } + +@JavaName('com.codename1.flutter.cupertino.OverlayVisibilityMode') +enum OverlayVisibilityMode { never, editing, notEditing, always } + +// --- color / icon / cursor constant holders --------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoDynamicColor') +class CupertinoDynamicColor extends Color { + external CupertinoDynamicColor(int value); + external Color resolveFrom(BuildContext context); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoColors') +abstract class CupertinoColors { + external static CupertinoDynamicColor get systemBackground; + external static CupertinoDynamicColor get label; + external static CupertinoDynamicColor get inactiveGray; + external static CupertinoDynamicColor get systemBlue; + external static CupertinoDynamicColor get systemGrey; + external static CupertinoDynamicColor get activeBlue; + external static CupertinoDynamicColor get activeGreen; + external static CupertinoDynamicColor get destructiveRed; + external static CupertinoDynamicColor get white; + external static CupertinoDynamicColor get black; +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoIcons') +abstract class CupertinoIcons { + external static IconData get home; + external static IconData get conversation_bubble; + external static IconData get profile_circled; + external static IconData get padlock_solid; + external static IconData get search; + external static IconData get settings; + external static IconData get share; + external static IconData get add; + external static IconData get clear; + external static IconData get back; +} + +@JavaName('com.codename1.flutter.cupertino.MouseCursor') +abstract class MouseCursor { + // A deferred cursor that lets the region behind it decide — Flutter's + // `MouseCursor.defer` (a static const on MouseCursor). + external static MouseCursor get defer; +} + +@JavaName('com.codename1.flutter.cupertino.SystemMouseCursors') +abstract class SystemMouseCursors { + external static MouseCursor get none; + external static MouseCursor get basic; + external static MouseCursor get click; + external static MouseCursor get forbidden; + external static MouseCursor get wait; + external static MouseCursor get progress; + external static MouseCursor get text; + external static MouseCursor get grab; + external static MouseCursor get grabbing; + external static MouseCursor get move; + external static MouseCursor get resizeUpDown; + external static MouseCursor get resizeLeftRight; + external static MouseCursor get resizeColumn; + external static MouseCursor get resizeRow; + external static MouseCursor get copy; + external static MouseCursor get alias; + external static MouseCursor get cell; + external static MouseCursor get precise; +} + +// --- theming ---------------------------------------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoTextThemeData') +class CupertinoTextThemeData { + external TextStyle get textStyle; + external TextStyle get actionTextStyle; + external TextStyle get navTitleTextStyle; + external TextStyle get navLargeTitleTextStyle; + external TextStyle get tabLabelTextStyle; + external TextStyle get pickerTextStyle; +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoThemeData') +class CupertinoThemeData { + external CupertinoThemeData({Brightness? brightness, Color? primaryColor, Color? primaryContrastingColor, Color? scaffoldBackgroundColor, Color? barBackgroundColor, CupertinoTextThemeData? textTheme}); + external CupertinoTextThemeData get textTheme; + external Brightness? get brightness; + external Color get primaryColor; + external Color get scaffoldBackgroundColor; + external Color get barBackgroundColor; + external CupertinoThemeData copyWith({Brightness? brightness, Color? primaryColor, Color? primaryContrastingColor, Color? scaffoldBackgroundColor, Color? barBackgroundColor, CupertinoTextThemeData? textTheme}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoTheme') +class CupertinoTheme extends Widget { + external CupertinoTheme({Key? key, CupertinoThemeData data, Widget child}); + external static CupertinoThemeData of(BuildContext context); +} + +// --- scaffolding / navigation chrome ---------------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoPageScaffold') +class CupertinoPageScaffold extends Widget { + external CupertinoPageScaffold({Key? key, Widget? navigationBar, Color? backgroundColor, bool? resizeToAvoidBottomInset, Widget child}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoNavigationBar') +class CupertinoNavigationBar extends Widget { + external CupertinoNavigationBar({Key? key, Widget? leading, bool? automaticallyImplyLeading, bool? automaticallyImplyMiddle, String? previousPageTitle, Widget? middle, Widget? trailing, Color? backgroundColor, Object? brightness, Object? padding, Object? border, Object? transitionBetweenRoutes}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSliverNavigationBar') +class CupertinoSliverNavigationBar extends Widget { + external CupertinoSliverNavigationBar({Key? key, Widget? largeTitle, Widget? leading, bool? automaticallyImplyLeading, bool? automaticallyImplyTitle, String? previousPageTitle, Widget? middle, Widget? trailing, Color? backgroundColor, Object? border, bool? stretch}); +} + +// --- controls --------------------------------------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoButton') +class CupertinoButton extends Widget { + external CupertinoButton({Key? key, VoidCallback? onPressed, Widget? child, Object? padding, Color? color, Color? disabledColor, double? minSize, double? pressedOpacity, Object? borderRadius, Object? alignment}); + external static CupertinoButton filled({Key? key, VoidCallback? onPressed, Widget? child}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoActivityIndicator') +class CupertinoActivityIndicator extends Widget { + external CupertinoActivityIndicator({Key? key, Color? color, bool? animating, double? radius}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSwitch') +class CupertinoSwitch extends Widget { + external CupertinoSwitch({Key? key, bool value, BoolCallback? onChanged, Color? activeColor, Color? trackColor, Color? thumbColor}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSlider') +class CupertinoSlider extends Widget { + external CupertinoSlider({Key? key, double value, double? min, double? max, int? divisions, DoubleCallback? onChanged, Object? onChangeStart, Object? onChangeEnd, Color? activeColor, Color? thumbColor}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoScrollbar') +class CupertinoScrollbar extends Widget { + external CupertinoScrollbar({Key? key, Object? controller, bool? thumbVisibility, double? thickness, double? thicknessWhileDragging, Object? radius, Object? radiusWhileDragging, Widget child}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoTextField') +class CupertinoTextField extends Widget { + external CupertinoTextField({Key? key, Object? controller, Object? decoration, Object? padding, String? placeholder, Object? placeholderStyle, Widget? prefix, Object? prefixMode, Widget? suffix, Object? suffixMode, Object? clearButtonMode, Object? keyboardType, Object? textInputAction, bool? obscureText, bool? autocorrect, bool? enabled, String? restorationId, StringCallback? onChanged, StringCallback? onSubmitted}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSearchTextField') +class CupertinoSearchTextField extends Widget { + external CupertinoSearchTextField({Key? key, Object? controller, String? placeholder, Object? decoration, Object? padding, String? restorationId, StringCallback? onChanged, StringCallback? onSubmitted, VoidCallback? onSuffixTap}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSegmentedControl') +class CupertinoSegmentedControl extends Widget { + external CupertinoSegmentedControl({Key? key, Object children, IntCallback? onValueChanged, Object? groupValue, Color? unselectedColor, Color? selectedColor, Color? borderColor, Color? pressedColor, Object? padding}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoSlidingSegmentedControl') +class CupertinoSlidingSegmentedControl extends Widget { + external CupertinoSlidingSegmentedControl({Key? key, Object children, IntCallback? onValueChanged, Object? groupValue, Color? thumbColor, Color? backgroundColor, Object? padding}); +} + +// --- dialogs / action sheets / context menus -------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoAlertDialog') +class CupertinoAlertDialog extends Widget { + external CupertinoAlertDialog({Key? key, Widget? title, Widget? content, List? actions, Object? scrollController, Object? actionScrollController}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoDialogAction') +class CupertinoDialogAction extends Widget { + external CupertinoDialogAction({Key? key, VoidCallback? onPressed, bool? isDefaultAction, bool? isDestructiveAction, TextStyle? textStyle, Widget child}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoActionSheet') +class CupertinoActionSheet extends Widget { + external CupertinoActionSheet({Key? key, Widget? title, Widget? message, List? actions, Object? messageScrollController, Object? actionScrollController, Widget? cancelButton}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoActionSheetAction') +class CupertinoActionSheetAction extends Widget { + external CupertinoActionSheetAction({Key? key, VoidCallback onPressed, bool? isDefaultAction, bool? isDestructiveAction, Widget child}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoContextMenu') +class CupertinoContextMenu extends Widget { + external CupertinoContextMenu({Key? key, List actions, Widget child, Object? previewBuilder}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoContextMenuAction') +class CupertinoContextMenuAction extends Widget { + external CupertinoContextMenuAction({Key? key, VoidCallback? onPressed, bool? isDefaultAction, bool? isDestructiveAction, Widget? trailingIcon, Widget child}); +} + +// --- pickers ---------------------------------------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoPicker') +class CupertinoPicker extends Widget { + external CupertinoPicker({Key? key, Color? backgroundColor, double itemExtent, double? diameterRatio, double? magnification, double? squeeze, bool? useMagnifier, Object? scrollController, IntCallback onSelectedItemChanged, List children}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoDatePicker') +class CupertinoDatePicker extends Widget { + external CupertinoDatePicker({Key? key, Color? backgroundColor, CupertinoDatePickerMode? mode, DateTime? initialDateTime, DateTime? minimumDate, DateTime? maximumDate, int? minimumYear, int? maximumYear, int? minuteInterval, bool? use24hFormat, DateTimeCallback onDateTimeChanged}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoTimerPicker') +class CupertinoTimerPicker extends Widget { + external CupertinoTimerPicker({Key? key, Color? backgroundColor, Object? mode, Duration? initialTimerDuration, int? minuteInterval, int? secondInterval, DurationCallback onTimerDurationChanged}); +} + +// --- tabs ------------------------------------------------------------- + +@JavaName('com.codename1.flutter.cupertino.CupertinoTabBar') +class CupertinoTabBar extends Widget { + external CupertinoTabBar({Key? key, List items, IntCallback? onTap, int? currentIndex, Color? backgroundColor, Color? activeColor, Color? inactiveColor, double? iconSize, Object? border}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoTabView') +class CupertinoTabView extends Widget { + external CupertinoTabView({Key? key, WidgetBuilder? builder, String? restorationScopeId, String? defaultTitle, Object? routes, Object? onGenerateRoute, Object? onUnknownRoute, Object? navigatorObservers}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoTabScaffold') +class CupertinoTabScaffold extends Widget { + external CupertinoTabScaffold({Key? key, CupertinoTabBar tabBar, IndexedWidgetBuilder tabBuilder, Object? controller, Color? backgroundColor, bool? resizeToAvoidBottomInset, String? restorationId}); +} + +// --- routes ----------------------------------------------------------- +// Minimal navigation Route base (no Route class existed in the stubs yet); +// the Cupertino routes below extend it so a demo function typed +// `Route` can return a CupertinoDialogRoute. + +@JavaName('com.codename1.flutter.navigation.Route') +abstract class Route { + // The RouteSettings (name / arguments) this route was pushed with — Flutter's + // `Route.settings`. new_gallery reads `route.settings.name` inside popUntil. + external RouteSettings get settings; + external bool get isCurrent; + external bool get isFirst; + external bool get isActive; +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoPageRoute') +class CupertinoPageRoute extends Route { + external CupertinoPageRoute({WidgetBuilder builder, Object? settings, String? title, bool? maintainState, bool? fullscreenDialog}); + external Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoDialogRoute') +class CupertinoDialogRoute extends Route { + external CupertinoDialogRoute({BuildContext context, WidgetBuilder builder, Object? settings, bool? barrierDismissible, Color? barrierColor, String? barrierLabel}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoModalPopupRoute') +class CupertinoModalPopupRoute extends Route { + external CupertinoModalPopupRoute({WidgetBuilder builder, Object? settings, Color? barrierColor, bool? barrierDismissible, String? barrierLabel}); +} + +@JavaName('com.codename1.flutter.cupertino.CupertinoDialogs.showCupertinoDialog') +external Future showCupertinoDialog({BuildContext context, WidgetBuilder builder, bool? barrierDismissible, Color? barrierColor, String? barrierLabel, bool? useRootNavigator, Object? routeSettings}); + +@JavaName('com.codename1.flutter.cupertino.CupertinoDialogs.showCupertinoModalPopup') +external Future showCupertinoModalPopup({BuildContext context, WidgetBuilder builder, Color? barrierColor, bool? barrierDismissible, bool? useRootNavigator, Object? semanticsDismissible, Object? routeSettings}); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart new file mode 100644 index 00000000000..cef3356c1fd --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_geometryPaint.dart @@ -0,0 +1,379 @@ +// Codename One Flutter runtime API stubs — geometryPaint category (new_gallery, Pass 2). +// +// The dart:ui / painting value types: geometric primitives (Offset, Rect, +// RelativeRect, Radius), the low-level painting surface (Paint, Path, Canvas, +// Gradient, Shader) and the painting/decoration value objects (Border, +// BorderSide, BorderRadius, the ShapeBorder family, EdgeInsetsDirectional). +// +// These are signature-only declarations resolved by the Dart transpiler; each +// maps to a hand-written Java runtime class via @JavaName. They are the +// receiver types behind the bulk of new_gallery's `..cascade` and painter code. +// +// NOTE: Alignment, AlignmentDirectional, EdgeInsets, Size, Color, Colors, +// Decoration and BoxDecoration are declared elsewhere (flutter_material.dart / +// gallery_coreWidgets.dart) and are intentionally NOT redeclared here. +// EdgeInsets gains its shared supertype (EdgeInsetsGeometry) and `fromLTRB` +// factory in flutter_material.dart so this file can hang EdgeInsetsDirectional +// off it. + +// --- enums ------------------------------------------------------------ + +@JavaName('com.codename1.flutter.PaintingStyle') +enum PaintingStyle { fill, stroke } + +@JavaName('com.codename1.flutter.StrokeCap') +enum StrokeCap { butt, round, square } + +@JavaName('com.codename1.flutter.StrokeJoin') +enum StrokeJoin { miter, round, bevel } + +@JavaName('com.codename1.flutter.BorderStyle') +enum BorderStyle { none, solid } + +@JavaName('com.codename1.flutter.TileMode') +enum TileMode { clamp, repeated, mirror, decal } + +// --- dart:ui geometry ------------------------------------------------- + +@JavaName('com.codename1.flutter.Offset') +class Offset { + external Offset(double dx, double dy); + external static Offset get zero; + external static Offset get infinite; + external static Offset fromDirection(double direction, [double distance = 1.0]); + external double get dx; + external double get dy; + external double get distance; + external double get distanceSquared; + external double get direction; + external Offset scale(double scaleX, double scaleY); + external Offset translate(double translateX, double translateY); + external Offset operator +(Offset other); + external Offset operator -(Offset other); + external Offset operator *(double operand); + external Offset operator /(double operand); + external Rect operator &(Size other); +} + +@JavaName('com.codename1.flutter.Rect') +class Rect { + external static Rect fromLTWH(double left, double top, double width, double height); + external static Rect fromLTRB(double left, double top, double right, double bottom); + external static Rect fromCircle({Offset center, double radius}); + external static Rect fromCenter({Offset center, double width, double height}); + external static Rect fromPoints(Offset a, Offset b); + external static Rect get zero; + external static Rect get largest; + external double get left; + external double get top; + external double get right; + external double get bottom; + external double get width; + external double get height; + external double get shortestSide; + external double get longestSide; + external bool get isEmpty; + external bool get isFinite; + external bool get hasNaN; + external Offset get center; + external Offset get topLeft; + external Offset get topCenter; + external Offset get topRight; + external Offset get centerLeft; + external Offset get centerRight; + external Offset get bottomLeft; + external Offset get bottomCenter; + external Offset get bottomRight; + external Size get size; + external bool contains(Offset offset); + external Rect translate(double translateX, double translateY); + external Rect shift(Offset offset); + external Rect inflate(double delta); + external Rect deflate(double delta); + external Rect intersect(Rect other); + external Rect expandToInclude(Rect other); + external bool overlaps(Rect other); +} + +@JavaName('com.codename1.flutter.RelativeRect') +class RelativeRect { + external static RelativeRect fromLTRB(double left, double top, double right, double bottom); + external static RelativeRect fromRect(Rect rect, Rect container); + external static RelativeRect fromSize(Rect rect, Size container); + external static RelativeRect get fill; + external double get left; + external double get top; + external double get right; + external double get bottom; + external Rect toRect(Rect container); +} + +@JavaName('com.codename1.flutter.Radius') +class Radius { + external static Radius circular(double radius); + external static Radius elliptical(double x, double y); + external static Radius get zero; + external double get x; + external double get y; +} + +// --- painting surface ------------------------------------------------- + +@JavaName('com.codename1.flutter.Shader') +abstract class Shader {} + +@JavaName('com.codename1.flutter.Gradient') +abstract class Gradient { + external Shader createShader(Rect rect, {Object? textDirection}); +} + +@JavaName('com.codename1.flutter.LinearGradient') +class LinearGradient extends Gradient { + external LinearGradient({Object? begin, Object? end, List colors, List? stops, TileMode? tileMode, Object? transform}); +} + +@JavaName('com.codename1.flutter.RadialGradient') +class RadialGradient extends Gradient { + external RadialGradient({Object? center, double radius = 0.5, List colors, List? stops, TileMode? tileMode, Object? focal, double focalRadius = 0.0, Object? transform}); +} + +@JavaName('com.codename1.flutter.SweepGradient') +class SweepGradient extends Gradient { + external SweepGradient({Object? center, double startAngle = 0.0, double endAngle = 6.283185307179586, List colors, List? stops, TileMode? tileMode, Object? transform}); +} + +@JavaName('com.codename1.flutter.Paint') +class Paint { + external Paint(); + external Color get color; + external set color(Color v); + external PaintingStyle get style; + external set style(PaintingStyle v); + external double get strokeWidth; + external set strokeWidth(double v); + external StrokeCap get strokeCap; + external set strokeCap(StrokeCap v); + external StrokeJoin get strokeJoin; + external set strokeJoin(StrokeJoin v); + external double get strokeMiterLimit; + external set strokeMiterLimit(double v); + external bool get isAntiAlias; + external set isAntiAlias(bool v); + external Shader? get shader; + external set shader(Shader? v); + external Object? get maskFilter; + external set maskFilter(Object? v); + external Object? get colorFilter; + external set colorFilter(Object? v); + external Object? get blendMode; + external set blendMode(Object? v); +} + +@JavaName('com.codename1.flutter.Path') +class Path { + external Path(); + external void moveTo(double x, double y); + external void lineTo(double x, double y); + external void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3); + external void quadraticBezierTo(double x1, double y1, double x2, double y2); + external void conicTo(double x1, double y1, double x2, double y2, double w); + external void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); + external void arcToPoint(Offset arcEnd, {Radius radius, double rotation, bool largeArc, bool clockwise}); + external void relativeMoveTo(double dx, double dy); + external void relativeLineTo(double dx, double dy); + external void addRect(Rect rect); + external void addOval(Rect oval); + external void addRRect(RRect rrect); + external void addPolygon(List points, bool close); + external void addPath(Path path, Offset offset); + external void close(); + external void reset(); + external bool contains(Offset point); + external Path shift(Offset offset); +} + +@JavaName('com.codename1.flutter.RRect') +class RRect { + external static RRect fromRectAndRadius(Rect rect, Radius radius); + external static RRect fromLTRBR(double left, double top, double right, double bottom, Radius radius); + external static RRect fromRectAndCorners(Rect rect, {Radius topLeft, Radius topRight, Radius bottomLeft, Radius bottomRight}); + // The rectangle that would remain after the corner radii are removed, and the + // enclosing / enclosed straight rects — Flutter's `RRect.middleRect/outerRect/ + // innerRect`. + external Rect get middleRect; + external Rect get outerRect; + external Rect get safeInnerRect; + external Rect get wideMiddleRect; + external Rect get tallMiddleRect; + external double get left; + external double get top; + external double get right; + external double get bottom; + external double get width; + external double get height; + external Offset get center; +} + +@JavaName('com.codename1.flutter.Canvas') +class Canvas { + external void drawPath(Path path, Paint paint); + external void drawRect(Rect rect, Paint paint); + external void drawRRect(RRect rrect, Paint paint); + external void drawCircle(Offset c, double radius, Paint paint); + external void drawOval(Rect rect, Paint paint); + external void drawLine(Offset p1, Offset p2, Paint paint); + external void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint); + external void drawPoints(Object pointMode, List points, Paint paint); + external void drawColor(Color color, Object blendMode); + external void drawShadow(Path path, Color color, double elevation, bool transparentOccluder); + external void drawVertices(Object vertices, Object blendMode, Paint paint); + external void drawImage(Object image, Offset offset, Paint paint); + external void translate(double dx, double dy); + external void scale(double sx, [double sy = 1.0]); + external void rotate(double radians); + external void skew(double sx, double sy); + external void save(); + external void saveLayer(Rect? bounds, Paint paint); + external void restore(); + external void clipRect(Rect rect); + external void clipRRect(RRect rrect); + external void clipPath(Path path); +} + +// --- EdgeInsets (directional variant) --------------------------------- +// EdgeInsetsGeometry is the shared supertype of EdgeInsets (see +// flutter_material.dart) and EdgeInsetsDirectional. The directional variant +// extends EdgeInsets so it stays assignable to the `EdgeInsets`-typed padding +// parameters the widget stubs declare (a pragmatic superclass — text-direction +// resolution treats `start`/`end` as `left`/`right` under LTR). + +@JavaName('com.codename1.flutter.EdgeInsetsGeometry') +abstract class EdgeInsetsGeometry {} + +@JavaName('com.codename1.flutter.EdgeInsetsDirectional') +class EdgeInsetsDirectional extends EdgeInsets { + external static EdgeInsetsDirectional all(double value); + external static EdgeInsetsDirectional only({double start, double top, double end, double bottom}); + external static EdgeInsetsDirectional symmetric({double horizontal, double vertical}); + external static EdgeInsetsDirectional fromSTEB(double start, double top, double end, double bottom); + external static EdgeInsetsDirectional get zero; + external double get start; + external double get end; +} + +// --- border radii ----------------------------------------------------- + +@JavaName('com.codename1.flutter.BorderRadiusGeometry') +abstract class BorderRadiusGeometry {} + +@JavaName('com.codename1.flutter.BorderRadius') +class BorderRadius extends BorderRadiusGeometry { + external static BorderRadius all(Radius radius); + external static BorderRadius circular(double radius); + external static BorderRadius only({Radius topLeft = Radius.zero, Radius topRight = Radius.zero, Radius bottomLeft = Radius.zero, Radius bottomRight = Radius.zero}); + external static BorderRadius vertical({Radius top = Radius.zero, Radius bottom = Radius.zero}); + external static BorderRadius horizontal({Radius left = Radius.zero, Radius right = Radius.zero}); + external static BorderRadius get zero; + // Linearly interpolates between two BorderRadius values — Flutter's + // `BorderRadius.lerp(a, b, t)`. Returns null only when both inputs are null. + external static BorderRadius? lerp(BorderRadius? a, BorderRadius? b, double t); + external RRect toRRect(Rect rect); +} + +@JavaName('com.codename1.flutter.BorderRadiusDirectional') +class BorderRadiusDirectional extends BorderRadiusGeometry { + external static BorderRadiusDirectional all(Radius radius); + external static BorderRadiusDirectional circular(double radius); + external static BorderRadiusDirectional only({Radius topStart = Radius.zero, Radius topEnd = Radius.zero, Radius bottomStart = Radius.zero, Radius bottomEnd = Radius.zero}); + external static BorderRadiusDirectional vertical({Radius top = Radius.zero, Radius bottom = Radius.zero}); + external static BorderRadiusDirectional horizontal({Radius start = Radius.zero, Radius end = Radius.zero}); + external static BorderRadiusDirectional get zero; +} + +// --- borders (ShapeBorder family) ------------------------------------- + +@JavaName('com.codename1.flutter.ShapeBorder') +abstract class ShapeBorder {} + +@JavaName('com.codename1.flutter.OutlinedBorder') +abstract class OutlinedBorder extends ShapeBorder {} + +@JavaName('com.codename1.flutter.InputBorder') +abstract class InputBorder extends ShapeBorder { + // The "no border" sentinel — Flutter's `InputBorder.none`. + external static InputBorder get none; +} + +@JavaName('com.codename1.flutter.BoxBorder') +abstract class BoxBorder extends ShapeBorder {} + +@JavaName('com.codename1.flutter.BorderSide') +class BorderSide { + external BorderSide({Color? color, double width = 1.0, BorderStyle style = BorderStyle.solid, double? strokeAlign}); + // Where the stroke sits relative to the path: -1 inside, 0 centred, 1 outside. + external static double get strokeAlignInside; + external static double get strokeAlignCenter; + external static double get strokeAlignOutside; + external static BorderSide get none; + // Linearly interpolates between two BorderSide values — Flutter's + // `BorderSide.lerp(a, b, t)`. + external static BorderSide lerp(BorderSide a, BorderSide b, double t); + external Color get color; + external double get width; + external BorderStyle get style; + // Builds a Paint stroking this side — Flutter's `BorderSide.toPaint()`. + external Paint toPaint(); +} + +@JavaName('com.codename1.flutter.Border') +class Border extends BoxBorder { + external Border({BorderSide top, BorderSide right, BorderSide bottom, BorderSide left}); + external static Border all({Color? color, double width = 1.0, BorderStyle style = BorderStyle.solid, double? strokeAlign}); + external static Border symmetric({BorderSide vertical, BorderSide horizontal}); + external BorderSide get top; + external BorderSide get right; + external BorderSide get bottom; + external BorderSide get left; +} + +@JavaName('com.codename1.flutter.RoundedRectangleBorder') +class RoundedRectangleBorder extends OutlinedBorder { + external RoundedRectangleBorder({Object? borderRadius, BorderSide side}); +} + +@JavaName('com.codename1.flutter.StadiumBorder') +class StadiumBorder extends OutlinedBorder { + external StadiumBorder({BorderSide side}); +} + +@JavaName('com.codename1.flutter.CircleBorder') +class CircleBorder extends OutlinedBorder { + external CircleBorder({BorderSide side, double eccentricity = 0.0}); +} + +@JavaName('com.codename1.flutter.BeveledRectangleBorder') +class BeveledRectangleBorder extends OutlinedBorder { + external BeveledRectangleBorder({Object? borderRadius, BorderSide side}); +} + +@JavaName('com.codename1.flutter.ContinuousRectangleBorder') +class ContinuousRectangleBorder extends OutlinedBorder { + external ContinuousRectangleBorder({Object? borderRadius, BorderSide side}); +} + +@JavaName('com.codename1.flutter.OutlineInputBorder') +class OutlineInputBorder extends InputBorder { + external OutlineInputBorder({BorderSide borderSide, Object? borderRadius, double gapPadding = 4.0}); + // Fields subclasses (e.g. shrine's CutCornersBorder) read off `this`/`super`. + external BorderSide get borderSide; + external BorderRadius get borderRadius; + external double get gapPadding; + // ShapeBorder interpolation hooks — Flutter's `lerpFrom` / `lerpTo`. + external ShapeBorder? lerpFrom(ShapeBorder? a, double t); + external ShapeBorder? lerpTo(ShapeBorder? b, double t); +} + +@JavaName('com.codename1.flutter.UnderlineInputBorder') +class UnderlineInputBorder extends InputBorder { + external UnderlineInputBorder({BorderSide borderSide, Object? borderRadius}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_iconsDuration.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_iconsDuration.dart new file mode 100644 index 00000000000..0dd0cad114d --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_iconsDuration.dart @@ -0,0 +1,32 @@ +// Codename One Flutter runtime API stubs — "iconsDuration" category (new_gallery, Pass 2). +// +// dart:core Duration member surface. The Duration() constructor and the +// `Duration` type itself are emitted intrinsically by the transpiler +// (Duration.of(...) / dart.core.Duration), so those paths win regardless of +// this stub. Declaring Duration as a stub class here is what lets member +// getters (inMilliseconds, inSeconds, ...) and the static `Duration.zero` +// getter resolve against the hand-written dart.core.Duration Java runtime. +// +// Conventions: named ctor params -> canonical positional order; instance +// getters -> no-arg method calls; static getters -> static field access. +// +// The remaining iconsDuration deliverables are wired directly into the +// transpiler/runtime rather than stubs: the full Icons.* constant set +// (flutter_material.dart Icons stub + com.codename1.flutter.Icons), and the +// numeric / List-factory / Iterable-helper intrinsics in JavaEmitter backed +// by dart.runtime.DartRuntime and dart.core.Dart*List. + +@JavaName('dart.core.Duration') +class Duration { + external Duration({int days, int hours, int minutes, int seconds, int milliseconds, int microseconds}); + external static Duration get zero; + external int get inDays; + external int get inHours; + external int get inMinutes; + external int get inSeconds; + external int get inMilliseconds; + external int get inMicroseconds; + external bool get isNegative; + external Duration abs(); + external int compareTo(Duration other); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_themeValues.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_themeValues.dart new file mode 100644 index 00000000000..60e4ff1d45b --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_themeValues.dart @@ -0,0 +1,135 @@ +// Codename One Flutter runtime API stubs (M-P2: themeValues). +// +// Signature-only declarations for the Material "theme value" surface the real +// new_gallery app leans on: the MaterialState/WidgetState property machinery, +// VisualDensity, the component *ThemeData bundles, and ButtonStyle (the button +// styleFrom factories live on the button classes in flutter_material.dart). +// +// Only NEW symbols live here. The copyWith/withOpacity/apply cascade fixers on +// the pre-existing value types (Color, TextStyle, TextTheme, and the button +// classes) are added in-place in flutter_material.dart, because the stub +// registry keys a class by name and a second declaration would clobber the +// first. ThemeData/ColorScheme/TextTheme.copyWith already exist there. +// +// See flutter_material.dart for the stub conventions this file follows. + +// --- MaterialState / WidgetState property machinery ------------------- + +@JavaName('com.codename1.flutter.material.MaterialState') +enum MaterialState { hovered, focused, pressed, dragged, selected, scrolledUnder, disabled, error } + +// Material-3 rename of MaterialState (same members). Newer Flutter aliases the +// whole "MaterialStateX" family to "WidgetStateX"; both spellings appear in the +// wild, so both resolve. +@JavaName('com.codename1.flutter.material.WidgetState') +enum WidgetState { hovered, focused, pressed, dragged, selected, scrolledUnder, disabled, error } + +// resolveWith takes `Color? Function(Set)`. The transpiler needs +// a named typedef to bind an untyped-target lambda to a Funcs SAM, so the param +// type is the transpiler-internal MaterialPropertyResolver typedef +// (Set -> Color); see JavaEmitter.TYPEDEFS. +@JavaName('com.codename1.flutter.material.MaterialStateProperty') +abstract class MaterialStateProperty { + external static MaterialStateProperty all(dynamic value); + external static MaterialStateProperty resolveWith(MaterialPropertyResolver resolver); +} + +@JavaName('com.codename1.flutter.material.WidgetStateProperty') +abstract class WidgetStateProperty { + external static WidgetStateProperty all(dynamic value); + external static WidgetStateProperty resolveWith(MaterialPropertyResolver resolver); +} + +@JavaName('com.codename1.flutter.material.VisualDensity') +class VisualDensity { + external VisualDensity({double? horizontal, double? vertical}); + external static VisualDensity get adaptivePlatformDensity; + external static VisualDensity get comfortable; + external static VisualDensity get compact; + external static VisualDensity get standard; +} + +// --- component theme-data bundles ------------------------------------- + +@JavaName('com.codename1.flutter.material.SnackBarBehavior') +enum SnackBarBehavior { fixed, floating } + +@JavaName('com.codename1.flutter.material.RadioThemeData') +class RadioThemeData { + external RadioThemeData({dynamic fillColor, dynamic overlayColor, dynamic splashRadius, + dynamic materialTapTargetSize, dynamic visualDensity, dynamic mouseCursor}); +} + +@JavaName('com.codename1.flutter.material.SwitchThemeData') +class SwitchThemeData { + external SwitchThemeData({dynamic thumbColor, dynamic trackColor, dynamic trackOutlineColor, + dynamic overlayColor, dynamic splashRadius, dynamic materialTapTargetSize, + dynamic thumbIcon, dynamic mouseCursor}); +} + +@JavaName('com.codename1.flutter.material.SnackBarThemeData') +class SnackBarThemeData { + external SnackBarThemeData({Color? backgroundColor, Color? actionTextColor, + Color? disabledActionTextColor, TextStyle? contentTextStyle, double? elevation, + dynamic shape, SnackBarBehavior? behavior, double? width, dynamic insetPadding, + bool? showCloseIcon, Color? closeIconColor}); + external Color? get backgroundColor; + external SnackBarBehavior? get behavior; +} + +@JavaName('com.codename1.flutter.material.TabBarTheme') +class TabBarTheme { + external TabBarTheme({Color? indicatorColor, Color? labelColor, Color? unselectedLabelColor, + TextStyle? labelStyle, TextStyle? unselectedLabelStyle, dynamic indicator, + dynamic indicatorSize, dynamic labelPadding, dynamic overlayColor, dynamic dividerColor}); +} + +@JavaName('com.codename1.flutter.material.TabBarThemeData') +class TabBarThemeData { + external TabBarThemeData({Color? indicatorColor, Color? labelColor, Color? unselectedLabelColor, + TextStyle? labelStyle, TextStyle? unselectedLabelStyle, dynamic indicator, + dynamic indicatorSize, dynamic labelPadding, dynamic overlayColor, dynamic dividerColor}); +} + +@JavaName('com.codename1.flutter.material.DialogTheme') +class DialogTheme { + external DialogTheme({Color? backgroundColor, double? elevation, Color? shadowColor, + Color? surfaceTintColor, dynamic shape, dynamic alignment, TextStyle? titleTextStyle, + TextStyle? contentTextStyle, dynamic iconColor}); +} + +@JavaName('com.codename1.flutter.material.DialogThemeData') +class DialogThemeData { + external DialogThemeData({Color? backgroundColor, double? elevation, Color? shadowColor, + Color? surfaceTintColor, dynamic shape, dynamic alignment, TextStyle? titleTextStyle, + TextStyle? contentTextStyle, dynamic iconColor}); +} + +@JavaName('com.codename1.flutter.material.TooltipThemeData') +class TooltipThemeData { + external TooltipThemeData({double? height, EdgeInsets? padding, EdgeInsets? margin, + double? verticalOffset, bool? preferBelow, bool? excludeFromSemantics, dynamic decoration, + TextStyle? textStyle, dynamic textAlign, Duration? waitDuration, Duration? showDuration, + dynamic triggerMode, bool? enableFeedback}); +} + +@JavaName('com.codename1.flutter.material.FloatingActionButtonThemeData') +class FloatingActionButtonThemeData { + external FloatingActionButtonThemeData({Color? foregroundColor, Color? backgroundColor, + Color? focusColor, Color? hoverColor, Color? splashColor, double? elevation, + double? focusElevation, double? hoverElevation, double? disabledElevation, + double? highlightElevation, dynamic shape, bool? enableFeedback, double? iconSize, + dynamic sizeConstraints, TextStyle? extendedTextStyle}); +} + +// --- ButtonStyle + styleFrom ------------------------------------------ +// ButtonStyle is opaque: it is only ever produced by *.styleFrom (declared on +// the button classes in flutter_material.dart) and consumed by the buttons' +// `style:` parameter, so it needs no members here. + +@JavaName('com.codename1.flutter.material.ButtonStyle') +class ButtonStyle {} + +// Note: FilterChip / ChoiceChip / InputChip are owned by the widgetsMore +// category (gallery_p2_widgetsMore.dart), not this file, to avoid a duplicate +// stub declaration. diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_widgetsMore.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_widgetsMore.dart new file mode 100644 index 00000000000..76125d42182 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p2_widgetsMore.dart @@ -0,0 +1,379 @@ +// Codename One Flutter runtime API stubs — widgetsMore category (new_gallery, Pass 2). +// +// Signature-only declarations resolved by the Dart transpiler; each maps to a +// hand-written Java runtime class via @JavaName. Following the established +// convention, parameter types that belong to categories this file does not own +// (ScrollController, ScrollPhysics, EdgeInsetsGeometry, MouseCursor, +// DragStartBehavior, ...) are declared loosely as `Object?` — the transpiler +// resolves those argument expressions against whichever category owns them. +// +// Callback parameters use the transpiler-internal typedef SAM names +// (VoidCallback / IntCallback / BoolCallback / WidgetBuilder / +// IndexedWidgetBuilder / DynamicCallback). Callbacks whose argument is a +// value type (RangeValues, DismissDirection) or which take multiple arguments +// are typed `Object?`; the concrete lambdas in the gallery are explicitly +// typed, so the Java runtime setter binds them to the precise Funcs SAM. + +// ====================================================================== +// Tabs +// ====================================================================== + +@JavaName('com.codename1.flutter.material.TabController') +class TabController { + external TabController({int? initialIndex, required int length, Duration? animationDuration, TickerProvider? vsync}); + external int get index; + external set index(int value); + external int get length; + external int get previousIndex; + external bool get indexIsChanging; + external double get offset; + external Animation? get animation; + external void animateTo(int value, {Duration? duration, Curve? curve}); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void dispose(); +} + +@JavaName('com.codename1.flutter.material.Tab') +class Tab extends Widget { + external Tab({Key? key, String? text, Widget? icon, Object? iconMargin, double? height, Widget? child}); +} + +@JavaName('com.codename1.flutter.material.TabBar') +class TabBar extends Widget { + external TabBar({Key? key, required List tabs, TabController? controller, bool? isScrollable, Object? padding, Color? indicatorColor, double? indicatorWeight, Object? indicatorPadding, Object? indicator, Object? indicatorSize, Color? labelColor, TextStyle? labelStyle, Object? labelPadding, Color? unselectedLabelColor, TextStyle? unselectedLabelStyle, Object? dragStartBehavior, Object? mouseCursor, bool? enableFeedback, IntCallback? onTap, Object? physics}); +} + +@JavaName('com.codename1.flutter.material.TabBarView') +class TabBarView extends Widget { + external TabBarView({Key? key, required List children, TabController? controller, Object? physics, Object? dragStartBehavior, double? viewportFraction, Clip? clipBehavior}); +} + +@JavaName('com.codename1.flutter.material.DefaultTabController') +class DefaultTabController extends Widget { + external DefaultTabController({Key? key, required int length, int? initialIndex, Duration? animationDuration, required Widget child}); + external static TabController of(BuildContext context); +} + +// ====================================================================== +// Range slider +// ====================================================================== + +@JavaName('com.codename1.flutter.material.RangeValues') +class RangeValues { + external RangeValues(double start, double end); + external double get start; + external double get end; +} + +@JavaName('com.codename1.flutter.material.RangeLabels') +class RangeLabels { + external RangeLabels(String start, String end); + external String get start; + external String get end; +} + +@JavaName('com.codename1.flutter.material.RangeSlider') +class RangeSlider extends Widget { + external RangeSlider({Key? key, required RangeValues values, Object? onChanged, Object? onChangeStart, Object? onChangeEnd, double? min, double? max, int? divisions, RangeLabels? labels, Color? activeColor, Color? inactiveColor, Object? semanticFormatterCallback}); +} + +// ====================================================================== +// Spacer +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.Spacer') +class Spacer extends Widget { + external Spacer({Key? key, int? flex}); +} + +// ====================================================================== +// Popup menu divider +// ====================================================================== + +@JavaName('com.codename1.flutter.material.PopupMenuDivider') +class PopupMenuDivider extends PopupMenuEntry { + external PopupMenuDivider({Key? key, double? height}); +} + +// ====================================================================== +// Scrollbars +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.RawScrollbar') +class RawScrollbar extends Widget { + external RawScrollbar({Key? key, required Widget child, Object? controller, bool? thumbVisibility, Object? thumbColor, Object? radius, double? thickness, bool? interactive, Object? notificationPredicate, Object? scrollbarOrientation}); +} + +// ====================================================================== +// Slivers (minimal: modeled as a scrollable list) +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.SliverChildDelegate') +abstract class SliverChildDelegate {} + +@JavaName('com.codename1.flutter.widgets.SliverChildBuilderDelegate') +class SliverChildBuilderDelegate extends SliverChildDelegate { + external SliverChildBuilderDelegate(IndexedWidgetBuilder builder, {int? childCount, bool? addAutomaticKeepAlives, bool? addRepaintBoundaries, bool? addSemanticIndexes}); +} + +@JavaName('com.codename1.flutter.widgets.SliverChildListDelegate') +class SliverChildListDelegate extends SliverChildDelegate { + external SliverChildListDelegate(List children, {bool? addAutomaticKeepAlives, bool? addRepaintBoundaries, bool? addSemanticIndexes}); +} + +@JavaName('com.codename1.flutter.widgets.CustomScrollView') +class CustomScrollView extends Widget { + external CustomScrollView({Key? key, List slivers, Object? controller, Object? scrollDirection, bool? reverse, bool? shrinkWrap, Object? physics, double? cacheExtent, Object? primary, Clip? clipBehavior}); +} + +@JavaName('com.codename1.flutter.widgets.SliverList') +class SliverList extends Widget { + external SliverList({Key? key, required SliverChildDelegate delegate}); +} + +@JavaName('com.codename1.flutter.widgets.SliverGrid') +class SliverGrid extends Widget { + external SliverGrid({Key? key, required SliverChildDelegate delegate, required Object gridDelegate}); +} + +@JavaName('com.codename1.flutter.widgets.SliverToBoxAdapter') +class SliverToBoxAdapter extends Widget { + external SliverToBoxAdapter({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.SliverPadding') +class SliverPadding extends Widget { + external SliverPadding({Key? key, required Object padding, Widget? sliver}); +} + +@JavaName('com.codename1.flutter.widgets.SliverFillRemaining') +class SliverFillRemaining extends Widget { + external SliverFillRemaining({Key? key, Widget? child, bool? hasScrollBody, bool? fillOverscroll}); +} + +@JavaName('com.codename1.flutter.widgets.SliverAppBar') +class SliverAppBar extends Widget { + external SliverAppBar({Key? key, Widget? title, Widget? leading, List? actions, Widget? flexibleSpace, Color? backgroundColor, bool? pinned, bool? floating, bool? snap, double? expandedHeight, bool? automaticallyImplyLeading, Widget? bottom, bool? centerTitle, double? elevation}); +} + +// ====================================================================== +// Nested scroll view +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.NestedScrollView') +class NestedScrollView extends Widget { + external NestedScrollView({Key? key, required Object headerSliverBuilder, required Widget body, Object? controller, Object? scrollDirection, bool? reverse, Object? physics, bool? floatHeaderSlivers}); +} + +// ====================================================================== +// Refresh indicator +// ====================================================================== + +@JavaName('com.codename1.flutter.material.RefreshIndicator') +class RefreshIndicator extends Widget { + external RefreshIndicator({Key? key, required Widget child, double? displacement, Object? onRefresh, Color? color, Color? backgroundColor, double? strokeWidth, Object? notificationPredicate, String? semanticsLabel, String? semanticsValue}); +} + +// ====================================================================== +// Dismissible +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.DismissDirection') +enum DismissDirection { vertical, horizontal, endToStart, startToEnd, up, down, none } + +@JavaName('com.codename1.flutter.widgets.Dismissible') +class Dismissible extends Widget { + external Dismissible({required Key key, required Widget child, Widget? background, Widget? secondaryBackground, Object? confirmDismiss, VoidCallback? onResize, Object? onUpdate, Object? onDismissed, Object? direction, Object? resizeDuration, Object? dismissThresholds, Object? movementDuration, double? crossAxisEndOffset, Object? dragStartBehavior, Object? behavior}); +} + +// ====================================================================== +// Reorderable list view +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.ReorderableListView') +class ReorderableListView extends Widget { + external ReorderableListView({Key? key, List children, required Object onReorder, Object? padding, Widget? header, Object? scrollDirection, bool? shrinkWrap, Object? physics, bool? buildDefaultDragHandles}); + external static ReorderableListView builder({Key? key, required IndexedWidgetBuilder itemBuilder, required int itemCount, required Object onReorder, Object? padding, Object? scrollDirection, bool? shrinkWrap}); +} + +// ====================================================================== +// Animated list +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.AnimatedListState') +class AnimatedListState { + external void insertItem(int index, {Duration? duration}); + external void removeItem(int index, Object builder, {Duration? duration}); +} + +@JavaName('com.codename1.flutter.widgets.AnimatedList') +class AnimatedList extends Widget { + external AnimatedList({Key? key, required Object itemBuilder, int? initialItemCount, Object? scrollDirection, bool? reverse, Object? controller, Object? primary, Object? physics, bool? shrinkWrap, Object? padding, Clip? clipBehavior}); + external static AnimatedListState of(BuildContext context); +} + +// ====================================================================== +// Stepper +// ====================================================================== + +@JavaName('com.codename1.flutter.material.StepState') +enum StepState { indexed, editing, complete, disabled, error } + +@JavaName('com.codename1.flutter.material.StepperType') +enum StepperType { vertical, horizontal } + +@JavaName('com.codename1.flutter.material.Step') +class Step { + external Step({required Widget title, Widget? subtitle, required Widget content, Object? state, bool? isActive, Object? stepStyle}); +} + +@JavaName('com.codename1.flutter.material.Stepper') +class Stepper extends Widget { + external Stepper({Key? key, required List steps, Object? physics, StepperType? type, int? currentStep, IntCallback? onStepTapped, VoidCallback? onStepContinue, VoidCallback? onStepCancel, Object? controlsBuilder, double? elevation, Object? margin}); +} + +// ====================================================================== +// Expansion panels / tile +// ====================================================================== + +@JavaName('com.codename1.flutter.material.ExpansionPanel') +class ExpansionPanel { + external ExpansionPanel({required Object headerBuilder, required Widget body, bool? isExpanded, bool? canTapOnHeader, Color? backgroundColor}); +} + +@JavaName('com.codename1.flutter.material.ExpansionPanelList') +class ExpansionPanelList extends Widget { + external ExpansionPanelList({Key? key, List children, Object? expansionCallback, Object? animationDuration, Object? expandedHeaderPadding, double? elevation}); +} + +@JavaName('com.codename1.flutter.material.ExpansionTile') +class ExpansionTile extends Widget { + external ExpansionTile({Key? key, required Widget title, Widget? subtitle, List children, Widget? leading, Widget? trailing, bool? initiallyExpanded, BoolCallback? onExpansionChanged, Object? childrenPadding, Color? backgroundColor, Color? collapsedBackgroundColor, Color? textColor, Color? iconColor, Object? tilePadding, Object? expandedAlignment, Object? expandedCrossAxisAlignment}); +} + +// ====================================================================== +// Data table +// ====================================================================== + +@JavaName('com.codename1.flutter.material.DataColumn') +class DataColumn { + external DataColumn({required Widget label, String? tooltip, bool? numeric, Object? onSort}); +} + +@JavaName('com.codename1.flutter.material.DataCell') +class DataCell { + external DataCell(Widget child, {bool? placeholder, bool? showEditIcon, VoidCallback? onTap, VoidCallback? onLongPress, Object? onTapDown}); +} + +@JavaName('com.codename1.flutter.material.DataRow') +class DataRow { + external DataRow({Key? key, bool? selected, BoolCallback? onSelectChanged, Object? onLongPress, Object? color, required List cells}); + external static DataRow byIndex({required int index, bool? selected, BoolCallback? onSelectChanged, Object? onLongPress, Object? color, required List cells}); +} + +@JavaName('com.codename1.flutter.material.DataTable') +class DataTable extends Widget { + external DataTable({Key? key, required List columns, required List rows, int? sortColumnIndex, bool? sortAscending, BoolCallback? onSelectAll, double? dataRowHeight, double? headingRowHeight, double? horizontalMargin, double? columnSpacing, bool? showCheckboxColumn, Object? decoration}); +} + +@JavaName('com.codename1.flutter.material.DataTableSource') +class DataTableSource extends ChangeNotifier { + external DataRow? getRow(int index); + external int get rowCount; + external bool get isRowCountApproximate; + external int get selectedRowCount; +} + +@JavaName('com.codename1.flutter.material.PaginatedDataTable') +class PaginatedDataTable extends Widget { + external PaginatedDataTable({Key? key, Widget? header, List? actions, required List columns, int? sortColumnIndex, bool? sortAscending, BoolCallback? onSelectAll, double? dataRowHeight, double? headingRowHeight, double? horizontalMargin, double? columnSpacing, bool? showCheckboxColumn, bool? showFirstLastButtons, int? initialFirstRowIndex, IntCallback? onPageChanged, int? rowsPerPage, List? availableRowsPerPage, IntCallback? onRowsPerPageChanged, required DataTableSource source, Object? checkboxHorizontalMargin, Object? controller, bool? primary}); + // The default value for `rowsPerPage` — Flutter's + // `PaginatedDataTable.defaultRowsPerPage` (== 10). + external static int get defaultRowsPerPage; +} + +// ====================================================================== +// Chips +// ====================================================================== + +@JavaName('com.codename1.flutter.material.Chip') +class Chip extends Widget { + external Chip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, Object? labelPadding, Widget? deleteIcon, VoidCallback? onDeleted, Color? deleteIconColor, String? deleteButtonTooltipMessage, Object? side, Object? shape, Clip? clipBehavior, Color? backgroundColor, Object? padding, Object? visualDensity, Object? materialTapTargetSize, double? elevation, Color? shadowColor}); +} + +@JavaName('com.codename1.flutter.material.InputChip') +class InputChip extends Widget { + external InputChip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, Object? labelPadding, bool? selected, bool? isEnabled, BoolCallback? onSelected, Widget? deleteIcon, VoidCallback? onDeleted, Color? deleteIconColor, VoidCallback? onPressed, Object? pressElevation, Color? disabledColor, Color? selectedColor, Object? tooltip, Object? side, Object? shape, Color? backgroundColor, Object? padding, double? elevation}); +} + +@JavaName('com.codename1.flutter.material.ChoiceChip') +class ChoiceChip extends Widget { + external ChoiceChip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, Object? labelPadding, required bool selected, BoolCallback? onSelected, Object? pressElevation, Color? disabledColor, Color? selectedColor, Object? tooltip, Object? side, Object? shape, Color? backgroundColor, Object? padding, double? elevation}); +} + +@JavaName('com.codename1.flutter.material.FilterChip') +class FilterChip extends Widget { + external FilterChip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, Object? labelPadding, required bool selected, required BoolCallback? onSelected, Object? pressElevation, Color? disabledColor, Color? selectedColor, Object? tooltip, Object? side, Object? shape, Color? backgroundColor, Object? padding, double? elevation}); +} + +@JavaName('com.codename1.flutter.material.ActionChip') +class ActionChip extends Widget { + external ActionChip({Key? key, Widget? avatar, required Widget label, TextStyle? labelStyle, Object? labelPadding, required VoidCallback? onPressed, Object? pressElevation, Object? tooltip, Object? side, Object? shape, Color? backgroundColor, Object? padding, double? elevation}); +} + +// ====================================================================== +// Circle avatar +// ====================================================================== + +@JavaName('com.codename1.flutter.material.CircleAvatar') +class CircleAvatar extends Widget { + external CircleAvatar({Key? key, Widget? child, Color? backgroundColor, Color? foregroundColor, ImageProvider? backgroundImage, ImageProvider? foregroundImage, Object? onBackgroundImageError, double? radius, double? minRadius, double? maxRadius}); +} + +// ====================================================================== +// Progress indicator +// ====================================================================== + +@JavaName('com.codename1.flutter.material.LinearProgressIndicator') +class LinearProgressIndicator extends Widget { + external LinearProgressIndicator({Key? key, double? value, Color? backgroundColor, Color? color, Object? valueColor, double? minHeight, String? semanticsLabel, String? semanticsValue, Object? borderRadius}); +} + +// ====================================================================== +// Banners +// ====================================================================== + +@JavaName('com.codename1.flutter.material.BannerLocation') +enum BannerLocation { topStart, topEnd, bottomStart, bottomEnd } + +@JavaName('com.codename1.flutter.material.Banner') +class Banner extends Widget { + external Banner({Key? key, Widget? child, required String message, Object? textDirection, required Object location, Object? layoutDirection, Color? color, TextStyle? textStyle}); +} + +@JavaName('com.codename1.flutter.material.MaterialBanner') +class MaterialBanner extends Widget { + external MaterialBanner({Key? key, required Widget content, TextStyle? contentTextStyle, required List actions, double? elevation, Widget? leading, Color? backgroundColor, Color? surfaceTintColor, Color? shadowColor, Color? dividerColor, Object? padding, Object? leadingPadding, bool? forceActionsBelow, Object? overflowAlignment, Object? animation, Object? onVisible}); +} + +// ====================================================================== +// Bottom sheet +// ====================================================================== + +@JavaName('com.codename1.flutter.material.BottomSheet') +class BottomSheet extends Widget { + external BottomSheet({Key? key, Object? animationController, bool? enableDrag, required VoidCallback onClosing, required WidgetBuilder builder, Color? backgroundColor, double? elevation, Object? shape, Clip? clipBehavior, Object? constraints}); +} + +@JavaName('com.codename1.flutter.material.BottomSheets.showModalBottomSheet') +external Future showModalBottomSheet({required BuildContext context, required WidgetBuilder builder, Color? backgroundColor, double? elevation, Object? shape, Clip? clipBehavior, Object? constraints, Color? barrierColor, bool? isScrollControlled, bool? useRootNavigator, bool? isDismissible, bool? enableDrag, bool? showDragHandle, Object? routeSettings, Object? transitionAnimationController}); + +// ====================================================================== +// Hero (minimal: renders child, no flight animation) +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.Hero') +class Hero extends Widget { + external Hero({Key? key, required Object tag, Object? createRectTween, Object? flightShuttleBuilder, Object? placeholderBuilder, bool? transitionOnUserGestures, required Widget child}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_cascadeTypes.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_cascadeTypes.dart new file mode 100644 index 00000000000..fe07017fc58 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_cascadeTypes.dart @@ -0,0 +1,255 @@ +// Codename One Flutter runtime API stubs — cascadeTypes category (new_gallery, Pass 3). +// +// The unresolved RECEIVER value-types behind the E0132/E0137 "member/method on +// type X" cascade in new_gallery: text-editing value objects (TextEditingValue, +// TextSelection, TextRange), the scrolling model (ScrollController, +// ScrollPosition, ScrollMetrics), vector_math's Vector3, the listenable value +// holders (ValueNotifier / ValueListenable), ImageConfiguration, the slider +// theme (SliderThemeData + ShowValueIndicator), RouteSettings, TimeOfDay and its +// RestorableTimeOfDay wrapper. +// +// Signature-only declarations resolved by the Dart transpiler; each maps to a +// hand-written Java runtime class via @JavaName. The API shapes mirror the real +// Flutter / vector_math signatures (named-parameter names, getters, enum +// constants) so the resolver binds member accesses; faithful behaviour is +// layered in later. Loaded alongside flutter_material.dart by StubRegistry. +// +// Types referenced but owned elsewhere (Size, Color, TextStyle, Duration, Curve, +// BuildContext, RestorableProperty, DateTime) are intentionally NOT redeclared. + +// --- text editing value objects ------------------------------------------- + +// A range of characters within a string. `composing` on TextEditingValue is a +// TextRange; a collapsed/empty range has start == end (or -1 when invalid). +@JavaName('com.codename1.flutter.TextRange') +class TextRange { + external TextRange({int start, int end}); + external static TextRange collapsed(int offset); + external static TextRange get empty; + external int get start; + external int get end; + external bool get isValid; + external bool get isCollapsed; +} + +// A selection within editable text; the selection extends TextRange (base/extent +// plus the inherited start/end). new_gallery's phone-number formatter reads +// `selection.end` and builds `TextSelection.collapsed(offset: ...)`. +@JavaName('com.codename1.flutter.TextSelection') +class TextSelection extends TextRange { + external TextSelection({int baseOffset, int extentOffset}); + external static TextSelection collapsed({int offset}); + external int get baseOffset; + external int get extentOffset; +} + +// The current text/selection/composing snapshot a TextInputFormatter transforms. +@JavaName('com.codename1.flutter.TextEditingValue') +class TextEditingValue { + external TextEditingValue({String text, TextSelection? selection, TextRange? composing}); + external static TextEditingValue get empty; + external String get text; + external TextSelection get selection; + external TextRange get composing; + external TextEditingValue copyWith({String? text, TextSelection? selection, TextRange? composing}); +} + +// --- the scrolling model --------------------------------------------------- + +// A read-only description of a scroll view's extents. ScrollPosition and the +// notifications' `.metrics` implement it; new_gallery reads pixels/maxScrollExtent +// to drive its ballistic carousel physics. +@JavaName('com.codename1.flutter.widgets.ScrollMetrics') +abstract class ScrollMetrics { + // Dragged PAST an extent, as opposed to sitting exactly on it (atEdge). + external bool get outOfRange; + external double get pixels; + external double get minScrollExtent; + external double get maxScrollExtent; + external double get viewportDimension; + external double get extentBefore; + external double get extentAfter; + external double get extentInside; + external bool get atEdge; + external bool get hasContentDimensions; + external bool get hasPixels; + external bool get hasViewportDimension; +} + +// The live scroll offset of a single scrollable, driven by a ScrollController. +@JavaName('com.codename1.flutter.widgets.ScrollPosition') +class ScrollPosition extends ScrollMetrics { + external bool get haveDimensions; + external Future animateTo(double to, {Duration? duration, Curve? curve}); + external void jumpTo(double value); +} + +// Controls one or more scrollables; the gallery carousel reads `offset` / +// `position.maxScrollExtent`, calls `animateTo`, and listens for changes. +@JavaName('com.codename1.flutter.widgets.ScrollController') +class ScrollController { + external ScrollController({double? initialScrollOffset, bool? keepScrollOffset, String? debugLabel}); + external double get offset; + external ScrollPosition get position; + external bool get hasClients; + external Future animateTo(double offset, {Duration? duration, Curve? curve}); + external void jumpTo(double value); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void dispose(); +} + +// --- vector_math ----------------------------------------------------------- + +// The 3-component double vector from package:vector_math; the transformations +// demo uses it for cube (hex-grid) coordinates and reads x / y / z. +@JavaName('com.codename1.flutter.vectormath.Vector3') +class Vector3 { + external Vector3(double x, double y, double z); + external static Vector3 zero(); + external static Vector3 all(double value); + external double get x; + external double get y; + external double get z; + external set x(double v); + external set y(double v); + external set z(double v); +} + +// --- listenable value holders ---------------------------------------------- + +// An object exposing a value that changes over time and can be listened to. +@JavaName('com.codename1.flutter.foundation.ValueListenable') +abstract class ValueListenable extends Listenable { + external T get value; + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); +} + +// A ChangeNotifier holding a single value; assigning `value` notifies listeners. +// new_gallery drives ValueListenableBuilder from these (settings sheet, +// extended nav rail). +@JavaName('com.codename1.flutter.foundation.ValueNotifier') +class ValueNotifier extends ValueListenable { + external ValueNotifier(T value); + external T get value; + external set value(T newValue); + external void notifyListeners(); + external void dispose(); +} + +// --- painting -------------------------------------------------------------- + +// The context (size, bounds, device pixel ratio, ...) passed to a custom +// Decoration/BoxPainter's paint(); the tab-indicator and pie-chart painters read +// `configuration.size`. +@JavaName('com.codename1.flutter.ImageConfiguration') +class ImageConfiguration { + external ImageConfiguration({Size? size, double? devicePixelRatio, TextDirection? textDirection, Locale? locale}); + external static ImageConfiguration get empty; + external Size? get size; + external double? get devicePixelRatio; +} + +// --- slider theme ---------------------------------------------------------- + +// Whether a slider's value-indicator bubble shows. +@JavaName('com.codename1.flutter.material.ShowValueIndicator') +enum ShowValueIndicator { onlyForDiscrete, onlyForContinuous, always, never } + +// The visual configuration of a Slider / RangeSlider. The sliders demo builds a +// custom theme via `theme.sliderTheme.copyWith(...)` and reads back thumbColor / +// disabledThumbColor / valueIndicatorColor. Shape parameters are typed Object? +// (their SliderComponentShape / RangeSliderThumbShape base types are owned by the +// widget-extension category). +@JavaName('com.codename1.flutter.material.SliderThemeData') +class SliderThemeData { + external SliderThemeData({ + double? trackHeight, Color? activeTrackColor, Color? inactiveTrackColor, + Color? disabledActiveTrackColor, Color? disabledInactiveTrackColor, + Color? activeTickMarkColor, Color? inactiveTickMarkColor, + Color? disabledActiveTickMarkColor, Color? disabledInactiveTickMarkColor, + Color? thumbColor, Color? disabledThumbColor, Color? overlayColor, + Color? valueIndicatorColor, + Object? overlayShape, Object? tickMarkShape, Object? thumbShape, + Object? trackShape, Object? valueIndicatorShape, Object? rangeThumbShape, + Object? rangeTrackShape, Object? rangeTickMarkShape, Object? rangeValueIndicatorShape, + ShowValueIndicator? showValueIndicator, TextStyle? valueIndicatorTextStyle}); + external double? get trackHeight; + external Color? get activeTrackColor; + external Color? get inactiveTrackColor; + external Color? get activeTickMarkColor; + external Color? get inactiveTickMarkColor; + external Color? get thumbColor; + external Color? get disabledThumbColor; + external Color? get overlayColor; + external Color? get valueIndicatorColor; + external ShowValueIndicator? get showValueIndicator; + external TextStyle? get valueIndicatorTextStyle; + external SliderThemeData copyWith({ + double? trackHeight, Color? activeTrackColor, Color? inactiveTrackColor, + Color? disabledActiveTrackColor, Color? disabledInactiveTrackColor, + Color? activeTickMarkColor, Color? inactiveTickMarkColor, + Color? disabledActiveTickMarkColor, Color? disabledInactiveTickMarkColor, + Color? thumbColor, Color? disabledThumbColor, Color? overlayColor, + Color? valueIndicatorColor, + Object? overlayShape, Object? tickMarkShape, Object? thumbShape, + Object? trackShape, Object? valueIndicatorShape, Object? rangeThumbShape, + Object? rangeTrackShape, Object? rangeTickMarkShape, Object? rangeValueIndicatorShape, + ShowValueIndicator? showValueIndicator, TextStyle? valueIndicatorTextStyle}); +} + +// --- routing --------------------------------------------------------------- + +// The name/arguments a route was pushed with; new_gallery's onGenerateRoute reads +// `settings.name`. +@JavaName('com.codename1.flutter.navigation.RouteSettings') +class RouteSettings { + external RouteSettings({String? name, Object? arguments}); + external String? get name; + external Object? get arguments; + external RouteSettings copyWith({String? name, Object? arguments}); +} + +// --- time of day ----------------------------------------------------------- + +// A wall-clock time (hour/minute, no date). The picker demo builds one from a +// DateTime, compares instances and formats via `format(context)`. +@JavaName('com.codename1.flutter.material.TimeOfDay') +class TimeOfDay { + external TimeOfDay({int hour, int minute}); + external static TimeOfDay fromDateTime(Object time); + external static TimeOfDay now(); + external int get hour; + external int get minute; + external TimeOfDay replacing({int? hour, int? minute}); + external String format(BuildContext context); +} + +// A restorable TimeOfDay property (the picker demo's _fromTime); value get/set +// round-trips through the restoration framework (a no-op here). +@JavaName('com.codename1.flutter.RestorableTimeOfDay') +class RestorableTimeOfDay extends RestorableProperty { + external RestorableTimeOfDay(TimeOfDay defaultValue); + external TimeOfDay get value; + external set value(TimeOfDay v); +} + +// The direction a scrollable's content grows in — named for where the content END lies. +@JavaName('com.codename1.flutter.AxisDirection') +enum AxisDirection { up, right, down, left } + +// An immutable snapshot of a scrollable's extents. Unlike a live ScrollPosition this +// describes a moment rather than tracking one, which is what makes the physics testable: +// applyPhysicsToUserOffset is a pure function of the metrics handed to it. +@JavaName('com.codename1.flutter.widgets.FixedScrollMetrics') +class FixedScrollMetrics extends ScrollMetrics { + external FixedScrollMetrics({ + double? minScrollExtent, + double? maxScrollExtent, + double? pixels, + double? viewportDimension, + AxisDirection? axisDirection, + double? devicePixelRatio, + }); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_identifiersEnums.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_identifiersEnums.dart new file mode 100644 index 00000000000..4956cc02c5b --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_identifiersEnums.dart @@ -0,0 +1,97 @@ +// Codename One Flutter runtime API stubs — "identifiersEnums" category +// (new_gallery, Pass 3). +// +// This file resolves bare unresolved IDENTIFIERS the app references directly: +// * dart:math top-level constant `pi` (imported unprefixed `import 'dart:math';`) +// * package:flutter/scheduler top-level `timeDilation` (mutable double, read + write) +// * package:flutter/foundation top-level getter `defaultTargetPlatform` +// * enums / const-instance value types used only via `X.constant` — declared as +// stub enums with EXACT Flutter constant names (params that receive them are +// declared loosely as `Object?` elsewhere, so an enum shape is sufficient) +// * dart:core `Uri` and the `Localizations` inherited-widget lookup helpers. +// +// Top-level values map, via @JavaName, to a fully-qualified Java static field +// (StubRegistry.topLevelVars + JavaEmitter.emitIdent). The `= ` on each +// is only a parse anchor — the emitted reference is the @JavaName target, so both +// reads (`pi / 2`) and writes (`timeDilation = 5.0`) go through the Java static. +// +// TargetPlatform itself is contributed by gallery_dartCore.dart; we only add the +// `defaultTargetPlatform` accessor here and reference the existing enum. + +// --- dart:math unprefixed top-level constant -------------------------------- + +@JavaName('dart.math.DartMath.pi') +double pi = 3.141592653589793; + +// --- package:flutter/scheduler --------------------------------------------- + +@JavaName('com.codename1.flutter.scheduler.SchedulerLib.timeDilation') +double timeDilation = 1.0; + +// --- package:flutter/foundation -------------------------------------------- + +@JavaName('com.codename1.flutter.foundation.FoundationLib.defaultTargetPlatform') +TargetPlatform defaultTargetPlatform = TargetPlatform.android; + +// --- services : text-input enums -------------------------------------------- + +@JavaName('com.codename1.flutter.services.TextInputAction') +enum TextInputAction { + none, unspecified, done, go, search, send, next, previous, + continueAction, join, route, emergencyCall, newline +} + +@JavaName('com.codename1.flutter.services.TextInputType') +enum TextInputType { + text, multiline, number, phone, datetime, emailAddress, url, + visiblePassword, name, streetAddress, none +} + +@JavaName('com.codename1.flutter.services.TextCapitalization') +enum TextCapitalization { none, words, sentences, characters } + +// --- painting / rendering enums -------------------------------------------- + +@JavaName('com.codename1.flutter.TextOverflow') +enum TextOverflow { clip, fade, ellipsis, visible } + +@JavaName('com.codename1.flutter.rendering.HitTestBehavior') +enum HitTestBehavior { deferToChild, opaque, translucent } + +// FloatingActionButtonLocation is a class of static const instances in Flutter; +// modelled here as an enum since the app only ever names a constant and the +// Scaffold slot receives it as `Object?`. +@JavaName('com.codename1.flutter.material.FloatingActionButtonLocation') +enum FloatingActionButtonLocation { + startTop, miniStartTop, centerTop, miniCenterTop, endTop, miniEndTop, + startFloat, miniStartFloat, centerFloat, miniCenterFloat, endFloat, miniEndFloat, + startDocked, miniStartDocked, centerDocked, miniCenterDocked, endDocked, miniEndDocked +} + +// --- dart:core Uri ---------------------------------------------------------- + +@JavaName('dart.core.DartUri') +class Uri { + external static Uri parse(String uri); + external static Uri? tryParse(String uri); + external String get scheme; + external String get host; + external int get port; + external String get path; + external String get query; + external String get fragment; + external List get pathSegments; + external Map get queryParameters; + external String toString(); +} + +// --- widgets : Localizations lookup ---------------------------------------- +// Localizations.of(context, type) returns the inherited T; localeOf(context) +// returns the ambient Locale. `of` returns an unbound type parameter, so the +// emitter threads the requested type as a trailing Class witness. + +@JavaName('com.codename1.flutter.widgets.Localizations') +class Localizations { + external static T of(BuildContext context, Object type); + external static Locale localeOf(BuildContext context); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_remaining.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_remaining.dart new file mode 100644 index 00000000000..1639d3ba89e --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_remaining.dart @@ -0,0 +1,27 @@ +// Pass-3 "remaining" category: signature stubs for the few brand-new runtime +// types reached by the leftover static/named-member diagnostics. The static +// members and named constructors themselves were appended to their OWNING +// classes in flutter_material.dart (Navigator.of, SizedBox.shrink, Size.fromRadius, +// Color.fromRGBO, the button .icon factories, Scaffold.of, ...). Only the two +// result types that did not exist anywhere are declared here. + +// The mutable Scaffold state reached via Scaffold.of(context) — the surface the +// gallery's bottom-sheet demo touches. showBottomSheet returns a controller +// whose `closed` future completes when the sheet is dismissed. +@JavaName('com.codename1.flutter.material.ScaffoldState') +abstract class ScaffoldState { + external PersistentBottomSheetController showBottomSheet(WidgetBuilder builder, + {double? elevation, Color? backgroundColor, Object? shape, Clip? clipBehavior, + Object? constraints, bool? enableDrag}); + external void showSnackBar(SnackBar snackBar); + external void openDrawer(); + external void openEndDrawer(); +} + +// The handle returned by ScaffoldState.showBottomSheet: `closed` is a future +// that resolves with the sheet's result once it is dismissed. +@JavaName('com.codename1.flutter.material.PersistentBottomSheetController') +abstract class PersistentBottomSheetController { + external Future get closed; + external void close(); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_widgetCtors.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_widgetCtors.dart new file mode 100644 index 00000000000..0bff5c8f17a --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p3_widgetCtors.dart @@ -0,0 +1,250 @@ +// Codename One Flutter runtime API stubs — widgetCtors category (new_gallery, Pass 3). +// +// Signature-only declarations resolved by the Dart transpiler; each maps to a +// hand-written Java runtime class via @JavaName. These are the ~20 distinct +// widget / value-type / route CONSTRUCTORS new_gallery instantiates that were +// still unresolved after Passes 1-2 (diagnostic E0135). +// +// Conventions (see flutter_material.dart header): +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - named constructors (X.name) -> `external static X name(...)`; the named +// params map to positional Java args in the +// declared order (key, ...) +// - callback SAM typedefs (VoidCallback / BoolCallback / DynamicCallback / +// WidgetBuilder ...) map to dart.runtime.Funcs.* ; multi-arg or value-typed +// callbacks are declared loosely as `Object?`. +// - parameter types owned by other categories are declared `Object?`. + +// ====================================================================== +// Semantics sort key +// ====================================================================== + +@JavaName('com.codename1.flutter.semantics.OrdinalSortKey') +class OrdinalSortKey { + external OrdinalSortKey(double order, {String? name}); +} + +// ====================================================================== +// Inherited / focus / structural single-child widgets +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.DefaultTextStyle') +class DefaultTextStyle extends Widget { + external DefaultTextStyle({Key? key, TextStyle? style, TextAlign? textAlign, bool? softWrap, Object? overflow, int? maxLines, Widget? child}); + external static DefaultTextStyle of(BuildContext context); +} + +@JavaName('com.codename1.flutter.widgets.FocusTraversalGroup') +class FocusTraversalGroup extends Widget { + external FocusTraversalGroup({Key? key, Object? policy, bool? descendantsAreFocusable, bool? descendantsAreTraversable, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Opacity') +class Opacity extends Widget { + external Opacity({Key? key, double opacity, bool? alwaysIncludeSemantics, Widget? child}); +} + +@JavaName('com.codename1.flutter.widgets.Transform') +class Transform extends Widget { + external Transform({Key? key, Object transform, Object? origin, Object? alignment, bool? transformHitTests, Object? filterQuality, Widget? child}); + external static Transform rotate({Key? key, double angle, Object? origin, Object? alignment, bool? transformHitTests, Object? filterQuality, Widget? child}); + external static Transform scale({Key? key, double? scale, double? scaleX, double? scaleY, Object? origin, Object? alignment, bool? transformHitTests, Object? filterQuality, Widget? child}); + external static Transform translate({Key? key, Object offset, bool? transformHitTests, Object? filterQuality, Widget? child}); +} + +// ====================================================================== +// Image widgets +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.ImageIcon') +class ImageIcon extends Widget { + external ImageIcon(ImageProvider? image, {Key? key, double? size, Color? color, String? semanticLabel}); +} + +@JavaName('com.codename1.flutter.widgets.FadeInImage') +class FadeInImage extends Widget { + external FadeInImage({Key? key, ImageProvider placeholder, ImageProvider image, Duration? fadeOutDuration, Duration? fadeInDuration, double? width, double? height, Object? fit, Object? alignment, Object? repeat, Object? placeholderFit}); +} + +// ====================================================================== +// Progress indicators +// ====================================================================== + +@JavaName('com.codename1.flutter.material.CircularProgressIndicator') +class CircularProgressIndicator extends Widget { + external CircularProgressIndicator({Key? key, double? value, Color? backgroundColor, Color? color, Object? valueColor, double? strokeWidth, String? semanticsLabel, String? semanticsValue}); +} + +// ====================================================================== +// Dividers +// ====================================================================== + +@JavaName('com.codename1.flutter.material.VerticalDivider') +class VerticalDivider extends Widget { + external VerticalDivider({Key? key, double? width, double? thickness, double? indent, double? endIndent, Color? color}); +} + +// ====================================================================== +// List tiles with an embedded control +// ====================================================================== + +@JavaName('com.codename1.flutter.material.RadioListTile') +class RadioListTile extends Widget { + external RadioListTile({Key? key, Object? value, Object? groupValue, DynamicCallback? onChanged, Widget? title, Widget? subtitle, Widget? secondary, bool? isThreeLine, bool? selected, bool? dense, Object? controlAffinity, Object? activeColor, Object? contentPadding}); +} + +@JavaName('com.codename1.flutter.material.SwitchListTile') +class SwitchListTile extends Widget { + external SwitchListTile({Key? key, bool value, BoolCallback? onChanged, Widget? title, Widget? subtitle, Widget? secondary, bool? isThreeLine, bool? selected, bool? dense, Object? controlAffinity, Object? activeColor, Object? contentPadding}); +} + +// ====================================================================== +// Checked popup menu item (extends the existing PopupMenuItem) +// ====================================================================== + +@JavaName('com.codename1.flutter.material.CheckedPopupMenuItem') +class CheckedPopupMenuItem extends PopupMenuItem { + external CheckedPopupMenuItem({Key? key, T? value, bool? checked, bool? enabled, Widget? child, VoidCallback? onTap, Object? padding}); +} + +// ====================================================================== +// Value-driven builder +// ====================================================================== + +@JavaName('com.codename1.flutter.widgets.ValueListenableBuilder') +class ValueListenableBuilder extends Widget { + external ValueListenableBuilder({Key? key, Object valueListenable, Object builder, Widget? child}); +} + +// ====================================================================== +// Custom paint +// ====================================================================== + +@JavaName('com.codename1.flutter.rendering.CustomPainter') +abstract class CustomPainter { + external CustomPainter({Object? repaint}); + void paint(Canvas canvas, Size size); + bool shouldRepaint(CustomPainter oldDelegate); +} + +@JavaName('com.codename1.flutter.widgets.CustomPaint') +class CustomPaint extends Widget { + external CustomPaint({Key? key, CustomPainter? painter, CustomPainter? foregroundPainter, Size? size, bool? isComplex, bool? willChange, Widget? child}); +} + +@JavaName('com.codename1.flutter.rendering.TextPainter') +class TextPainter { + external TextPainter({Object? text, TextDirection? textDirection, TextAlign? textAlign, double? textScaleFactor, int? maxLines, String? ellipsis, Object? textWidthBasis, Object? strutStyle, Object? locale}); + external void layout({double? minWidth, double? maxWidth}); + external void paint(Canvas canvas, Object offset); + external Size get size; + external double get width; + external double get height; +} + +// ====================================================================== +// Scroll physics +// ====================================================================== + +// The parameters describing a spring's motion — Flutter's `SpringDescription`. +@JavaName('com.codename1.flutter.physics.SpringDescription') +class SpringDescription { + external SpringDescription({double mass, double stiffness, double damping}); + external static SpringDescription withDampingRatio({double mass, double stiffness, double ratio}); +} + +@JavaName('com.codename1.flutter.widgets.ScrollPhysics') +class ScrollPhysics { + external ScrollPhysics({ScrollPhysics? parent}); + // The drag pipeline: a raw finger delta becomes the offset actually applied to the + // scroll position, which is where overscroll resistance lives. + external double applyPhysicsToUserOffset(ScrollMetrics position, double offset); + external bool shouldAcceptUserOffset(ScrollMetrics position); + external double get minFlingVelocity; + external double get maxFlingVelocity; + external double get minFlingDistance; + // Physics-subclass plumbing used by the home page's _SnappingScrollPhysics — + // Flutter's `ScrollPhysics.applyTo/buildParent/toleranceFor/ + // createBallisticSimulation`. `spring` is the default spring an overriding + // createBallisticSimulation feeds to a ScrollSpringSimulation. + external ScrollPhysics applyTo(ScrollPhysics? ancestor); + external ScrollPhysics? buildParent(ScrollPhysics? ancestor); + external Tolerance toleranceFor(ScrollMetrics position); + external Simulation? createBallisticSimulation(ScrollMetrics position, double velocity); + external SpringDescription get spring; +} + +// The base for the app-wide scroll configuration — Flutter's `ScrollBehavior`. +// `MaterialScrollBehavior` is the Material default; new_gallery's shrine app +// passes `const MaterialScrollBehavior().copyWith(scrollbars: false)`. +@JavaName('com.codename1.flutter.widgets.ScrollBehavior') +class ScrollBehavior { + external ScrollBehavior(); + external ScrollBehavior copyWith({bool? scrollbars, bool? overscroll, Object? physics, Object? platform, Object? dragDevices}); +} + +@JavaName('com.codename1.flutter.material.MaterialScrollBehavior') +class MaterialScrollBehavior extends ScrollBehavior { + external MaterialScrollBehavior(); +} + +@JavaName('com.codename1.flutter.widgets.NeverScrollableScrollPhysics') +class NeverScrollableScrollPhysics extends ScrollPhysics { + external NeverScrollableScrollPhysics({ScrollPhysics? parent}); +} + +@JavaName('com.codename1.flutter.widgets.ClampingScrollPhysics') +class ClampingScrollPhysics extends ScrollPhysics { + external ClampingScrollPhysics({ScrollPhysics? parent}); +} + +@JavaName('com.codename1.flutter.widgets.BouncingScrollPhysics') +class BouncingScrollPhysics extends ScrollPhysics { + external BouncingScrollPhysics({ScrollPhysics? parent}); + // The rubber band, as a function of how far past the edge you already are. + external double frictionFactor(double overscrollFraction); +} + +@JavaName('com.codename1.flutter.widgets.AlwaysScrollableScrollPhysics') +class AlwaysScrollableScrollPhysics extends ScrollPhysics { + external AlwaysScrollableScrollPhysics({ScrollPhysics? parent}); +} + +// ====================================================================== +// Transitions (animation-driven single child) +// ====================================================================== + +@JavaName('com.codename1.flutter.animation.SizeTransition') +class SizeTransition extends Widget { + external SizeTransition({Key? key, Object? axis, Animation sizeFactor, double? axisAlignment, Widget? child}); +} + +@JavaName('com.codename1.flutter.animation.PageTransitionSwitcher') +class PageTransitionSwitcher extends Widget { + external PageTransitionSwitcher({Key? key, Duration? duration, bool? reverse, Object transitionBuilder, Widget? child}); +} + +// ====================================================================== +// animations package — OpenContainer +// ====================================================================== + +@JavaName('com.codename1.flutter.animations.OpenContainer') +class OpenContainer extends Widget { + external OpenContainer({Key? key, Object? onClosed, Object closedBuilder, Object openBuilder, bool? tappable, Duration? transitionDuration, Object? transitionType, Color? closedColor, Color? openColor, Color? middleColor, double? closedElevation, double? openElevation, Object? closedShape, Object? openShape, String? routeSettings, bool? useRootNavigator}); +} + +// The `closedBuilder` signature of an OpenContainer — the animations package's +// `CloseContainerBuilder` (`Widget Function(BuildContext, VoidCallback)`). A SAM +// the transpiler binds closures to; new_gallery's _OpenContainerWrapper stores one. +@JavaName('com.codename1.flutter.animations.CloseContainerBuilder') +class CloseContainerBuilder {} + +// ====================================================================== +// navigation — DialogRoute +// ====================================================================== + +@JavaName('com.codename1.flutter.navigation.DialogRoute') +class DialogRoute extends Route { + external DialogRoute({Key? key, BuildContext context, WidgetBuilder builder, Object? settings, Color? barrierColor, bool? barrierDismissible, String? barrierLabel, bool? useSafeArea, Object? themes, Object? anchorPoint, Object? traversalEdgeBehavior}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart new file mode 100644 index 00000000000..15a60a8ca8d --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p4_apitail.dart @@ -0,0 +1,336 @@ +// Codename One Flutter runtime API stubs — "apiTail" category (new_gallery, Pass 4). +// +// The remaining long tail of Flutter widget / value-type / route / top-level +// symbols new_gallery references that were still unresolved after Passes 1-3 +// (diagnostics E0135 unresolved-constructor, E0132 unresolved-member, +// E0129/E0136 unresolved-identifier). Each declaration is signature-only and +// maps, via @JavaName, to a hand-written Java runtime class or static member. +// +// Conventions (see flutter_material.dart / gallery_p3_widgetCtors.dart headers): +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - named constructors (X.name) -> `external static X name(...)` +// - top-level functions -> `@JavaName('fqcn.method') external ...` +// - top-level consts / vars -> `@JavaName('fqcn.field') name = ;` +// (the literal is only a parse anchor; the emitted reference is the Java static) +// - callbacks / types owned by other categories are declared loosely as `Object?`. + +// ====================================================================== +// Keys +// ====================================================================== + +// A key that is unique across the entire application — Flutter's `UniqueKey`. +// Never equal to any other key (identity equality), forcing a fresh element. +@JavaName('com.codename1.flutter.UniqueKey') +class UniqueKey extends Key { + external UniqueKey(); +} + +// A ValueKey that additionally scrolls its subtree's PageStorage bucket. +// new_gallery tags each home carousel card with one so scroll offsets persist. +@JavaName('com.codename1.flutter.PageStorageKey') +class PageStorageKey extends ValueKey { + external PageStorageKey(T value); +} + +// ====================================================================== +// Image providers +// ====================================================================== + +// Decodes an image from an in-memory byte buffer — Flutter's `MemoryImage`. +@JavaName('com.codename1.flutter.MemoryImage') +class MemoryImage extends ImageProvider { + external MemoryImage(Uint8List bytes, {double? scale}); +} + +// Wraps another ImageProvider and resizes the decoded image to the given +// dimensions — Flutter's `ResizeImage`. +@JavaName('com.codename1.flutter.ResizeImage') +class ResizeImage extends ImageProvider { + external ResizeImage(ImageProvider imageProvider, + {int? width, int? height, Object? policy, bool? allowUpscaling}); +} + +// ====================================================================== +// Single-child layout widgets +// ====================================================================== + +// Applies a translation expressed as a fraction of its own size before +// painting its child — Flutter's `FractionalTranslation`. +@JavaName('com.codename1.flutter.widgets.FractionalTranslation') +class FractionalTranslation extends Widget { + external FractionalTranslation( + {Key? key, Offset translation, bool? transformHitTests, Widget? child}); +} + +// Rotates its child by an integral number of quarter turns — Flutter's +// `RotatedBox`. Unlike Transform.rotate this affects layout. +@JavaName('com.codename1.flutter.widgets.RotatedBox') +class RotatedBox extends Widget { + external RotatedBox({Key? key, int quarterTurns, Widget? child}); +} + +// A directional Positioned for a Stack: `start`/`end` resolve against the +// ambient text direction — Flutter's `PositionedDirectional`. +@JavaName('com.codename1.flutter.widgets.PositionedDirectional') +class PositionedDirectional extends Widget { + external PositionedDirectional( + {Key? key, double? start, double? top, double? end, double? bottom, + double? width, double? height, Widget? child}); +} + +// Lays out its children horizontally, overflowing to a vertical column when +// they do not fit — Flutter's `OverflowBar` (button-bar style). +@JavaName('com.codename1.flutter.widgets.OverflowBar') +class OverflowBar extends Widget { + external OverflowBar( + {Key? key, double? spacing, Object? alignment, double? overflowSpacing, + Object? overflowAlignment, Object? overflowDirection, Object? textDirection, + List children}); +} + +// A single tile in a Material grid, with an optional header/footer band — +// Flutter's `GridTile`. +@JavaName('com.codename1.flutter.widgets.GridTile') +class GridTile extends Widget { + external GridTile({Key? key, Widget? header, Widget? footer, Widget child}); +} + +// The header/footer band placed inside a GridTile — Flutter's `GridTileBar`. +@JavaName('com.codename1.flutter.widgets.GridTileBar') +class GridTileBar extends Widget { + external GridTileBar( + {Key? key, Color? backgroundColor, Widget? leading, Widget? title, + Widget? subtitle, Widget? trailing}); +} + +// ====================================================================== +// Focus traversal order +// ====================================================================== + +// Base type for an explicit focus-traversal ordering value. +@JavaName('com.codename1.flutter.widgets.FocusOrder') +abstract class FocusOrder {} + +// Orders a focusable subtree by an ascending numeric value — Flutter's +// `NumericFocusOrder`. +@JavaName('com.codename1.flutter.widgets.NumericFocusOrder') +class NumericFocusOrder extends FocusOrder { + external NumericFocusOrder(double order); +} + +// Assigns an explicit traversal `order` to its child — Flutter's +// `FocusTraversalOrder`. +@JavaName('com.codename1.flutter.widgets.FocusTraversalOrder') +class FocusTraversalOrder extends Widget { + external FocusTraversalOrder({Key? key, FocusOrder order, Widget child}); +} + +// ====================================================================== +// Icon theming +// ====================================================================== + +// Establishes an ambient IconThemeData for its subtree — Flutter's `IconTheme`. +// (IconThemeData itself is contributed by the stateMgmt category.) +@JavaName('com.codename1.flutter.material.IconTheme') +class IconTheme extends Widget { + external IconTheme({Key? key, IconThemeData data, Widget child}); + external static IconThemeData of(BuildContext context); + external static IconTheme merge({Key? key, IconThemeData data, Widget child}); +} + +// ====================================================================== +// Slider theming +// ====================================================================== + +// Establishes an ambient SliderThemeData for its subtree — Flutter's +// `SliderTheme`. (SliderThemeData itself is contributed by the cascadeTypes +// category.) +@JavaName('com.codename1.flutter.material.SliderTheme') +class SliderTheme extends Widget { + external SliderTheme({Key? key, SliderThemeData data, Widget child}); + external static SliderThemeData of(BuildContext context); +} + +// The active thumb of a RangeSlider — Flutter's `Thumb` enum. +@JavaName('com.codename1.flutter.material.Thumb') +enum Thumb { start, end } + +// ====================================================================== +// Toggle buttons +// ====================================================================== + +// A horizontal set of toggle buttons — Flutter's `ToggleButtons`. Styling +// params owned by other categories are declared loosely. +@JavaName('com.codename1.flutter.material.ToggleButtons') +class ToggleButtons extends Widget { + external ToggleButtons( + {Key? key, List children, List isSelected, Object? onPressed, + TextStyle? textStyle, Object? constraints, Color? color, Color? selectedColor, + Color? disabledColor, Color? fillColor, Color? focusColor, Color? highlightColor, + Color? hoverColor, Color? splashColor, bool? renderBorder, Color? borderColor, + Color? selectedBorderColor, Color? disabledBorderColor, Object? borderRadius, + double? borderWidth, Object? direction}); +} + +// ====================================================================== +// Simple dialog +// ====================================================================== + +// A Material dialog presenting a title and a list of options — Flutter's +// `SimpleDialog`. +@JavaName('com.codename1.flutter.material.SimpleDialog') +class SimpleDialog extends Widget { + external SimpleDialog( + {Key? key, Widget? title, EdgeInsets? titlePadding, TextStyle? titleTextStyle, + List? children, EdgeInsets? contentPadding, Color? backgroundColor, + double? elevation, Color? shadowColor, Color? surfaceTintColor, + String? semanticLabel, EdgeInsets? insetPadding, Clip? clipBehavior, + Object? shape, Object? alignment}); +} + +// A single tappable option inside a SimpleDialog — Flutter's `SimpleDialogOption`. +@JavaName('com.codename1.flutter.material.SimpleDialogOption') +class SimpleDialogOption extends Widget { + external SimpleDialogOption( + {Key? key, Object? onPressed, EdgeInsets? padding, Widget? child}); +} + +// ====================================================================== +// Typography +// ====================================================================== + +// The set of text themes for a Material design language — Flutter's +// `Typography`. Constructed via the `material2018` / `material2014` factories. +@JavaName('com.codename1.flutter.material.Typography') +class Typography { + external static Typography material2018( + {Object? platform, TextTheme? black, TextTheme? white, TextTheme? englishLike, + TextTheme? dense, TextTheme? tall}); + external static Typography material2014( + {Object? platform, TextTheme? black, TextTheme? white, TextTheme? englishLike, + TextTheme? dense, TextTheme? tall}); + external TextTheme? get black; + external TextTheme? get white; + external TextTheme? get englishLike; + external TextTheme? get dense; + external TextTheme? get tall; +} + +// ====================================================================== +// Routing +// ====================================================================== + +// A route whose transition is described by builder callbacks — Flutter's +// `PageRouteBuilder`. `pageBuilder` / `transitionsBuilder` receive the +// (context, animation, secondaryAnimation) triple. +@JavaName('com.codename1.flutter.navigation.PageRouteBuilder') +class PageRouteBuilder extends Route { + external PageRouteBuilder( + {RouteSettings? settings, Object pageBuilder, Object? transitionsBuilder, + Duration? transitionDuration, Duration? reverseTransitionDuration, + bool? opaque, bool? barrierDismissible, Color? barrierColor, + String? barrierLabel, bool? maintainState, bool? fullscreenDialog}); +} + +// ====================================================================== +// Diagnostics / errors +// ====================================================================== + +// Flutter's `FlutterError` — the error type the framework (and app assertions) +// throw. `FlutterError.reportError` routes a caught error to the current handler. +@JavaName('com.codename1.flutter.foundation.FlutterError') +class FlutterError { + external FlutterError(String message); + external static void reportError(Object details); +} + +// ====================================================================== +// animations package — shared-axis transition +// ====================================================================== + +// The direction of a shared-axis transition — the `animations` package's +// `SharedAxisTransitionType`. +@JavaName('com.codename1.flutter.animations.SharedAxisTransitionType') +enum SharedAxisTransitionType { horizontal, vertical, scaled } + +// Cross-fades + slides two pages along a shared axis — the `animations` +// package's `SharedAxisTransition`. +@JavaName('com.codename1.flutter.animations.SharedAxisTransition') +class SharedAxisTransition extends Widget { + external SharedAxisTransition( + {Key? key, Animation animation, Animation secondaryAnimation, + SharedAxisTransitionType transitionType, Color? fillColor, Widget? child}); +} + +// ====================================================================== +// vector_math_64 — Matrix4 +// ====================================================================== + +// A 4x4 column-major transform matrix — `package:vector_math_64`'s `Matrix4`. +// Used by Transform to build its `transform` argument. +@JavaName('com.codename1.flutter.vectormath.Matrix4') +class Matrix4 { + external static Matrix4 identity(); + external static Matrix4 rotationX(double radians); + external static Matrix4 rotationY(double radians); + external static Matrix4 rotationZ(double radians); + external static Matrix4 translationValues(double x, double y, double z); + external static Matrix4 diagonal3Values(double x, double y, double z); + external List get storage; + external Matrix4 clone(); + external void translate(double x, [double y, double z]); + external void scale(double x, [double? y, double? z]); + external void setEntry(int row, int col, double value); + external void setRotationZ(double radians); +} + +// ====================================================================== +// scheduler binding +// ====================================================================== + +// The singleton driving frame scheduling — Flutter's `SchedulerBinding`. +// new_gallery registers post-frame callbacks through `SchedulerBinding.instance`. +@JavaName('com.codename1.flutter.scheduler.SchedulerBinding') +class SchedulerBinding { + external static SchedulerBinding get instance; + external void addPostFrameCallback(Object callback); + external int scheduleFrameCallback(Object callback, {bool rescheduling}); + external void scheduleFrame(); +} + +// ====================================================================== +// url_launcher package +// ====================================================================== + +@JavaName('com.codename1.flutter.services.UrlLauncher.launchUrl') +external Future launchUrl(Uri url, {Object? mode, Object? webOnlyWindowName}); + +@JavaName('com.codename1.flutter.services.UrlLauncher.canLaunchUrl') +external Future canLaunchUrl(Uri url); + +@JavaName('com.codename1.flutter.services.UrlLauncher.launchUrlString') +external Future launchUrlString(String urlString, {Object? mode, Object? webOnlyWindowName}); + +@JavaName('com.codename1.flutter.services.UrlLauncher.canLaunchUrlString') +external Future canLaunchUrlString(String urlString); + +// ====================================================================== +// Top-level constants +// ====================================================================== + +// package:flutter/foundation — true only in a web build; always false here. +@JavaName('com.codename1.flutter.foundation.FoundationConstants.kIsWeb') +bool kIsWeb = false; + +// package:flutter/material — default AppBar toolbar height (logical pixels). +@JavaName('com.codename1.flutter.material.MaterialConstants.kToolbarHeight') +double kToolbarHeight = 56.0; + +// package:flutter/material — default margin around a FloatingActionButton. +@JavaName('com.codename1.flutter.material.MaterialConstants.kFloatingActionButtonMargin') +double kFloatingActionButtonMargin = 16.0; + +// package:flutter/material — the duration Material widgets animate theme changes. +@JavaName('com.codename1.flutter.material.MaterialConstants.kThemeAnimationDuration') +Duration kThemeAnimationDuration = const Duration(milliseconds: 200); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p6.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p6.dart new file mode 100644 index 00000000000..a46fc986ef0 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p6.dart @@ -0,0 +1,271 @@ +// Codename One Flutter runtime API stubs — "apiStaticTail" category (new_gallery, Pass 6). +// +// The final long tail of brand-new Flutter widget / value-type / enum / theme +// symbols new_gallery references that were still unresolved after Passes 1-5 +// (diagnostics E0135 unresolved-constructor, E0129 unresolved-identifier, +// E0132 unresolved-member, E0136 unresolved-static). Static members and getters +// that hang off ALREADY-declared types (ThemeData.navigationRailTheme, +// MediaQuery.sizeOf/paddingOf/viewInsetsOf, BorderSide.lerp, BorderRadius.lerp, +// OutlineInputBorder.borderSide/borderRadius/gapPadding/lerpFrom/lerpTo, +// Directionality.of, MouseCursor.defer, ScaffoldMessengerState.hideCurrentSnackBar) +// were appended to their owning classes in their existing stub files. Only the +// brand-new types live here. +// +// Conventions (see gallery_p4_apitail.dart header): +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - named constructors (X.name) -> `external static X name(...)` / factory +// - top-level functions -> `@JavaName('fqcn.method') external ...` +// - callbacks / types owned elsewhere are declared loosely as `Object?`. + +// ====================================================================== +// NavigationRail (Material side-nav) — studies/reply + navigation_rail_demo +// ====================================================================== + +// A vertical Material navigation rail, the desktop/tablet counterpart of a +// BottomNavigationBar — Flutter's `NavigationRail`. `extendedAnimation` exposes +// the 0..1 animation driving the collapsed<->extended transition so descendants +// (labels, folder section) can react to it. +@JavaName('com.codename1.flutter.material.NavigationRail') +class NavigationRail extends Widget { + external NavigationRail( + {Key? key, Color? backgroundColor, bool? extended, Widget? leading, + Widget? trailing, List destinations, + int selectedIndex, Object? onDestinationSelected, double? elevation, + double? groupAlignment, NavigationRailLabelType? labelType, + TextStyle? unselectedLabelTextStyle, TextStyle? selectedLabelTextStyle, + IconThemeData? unselectedIconTheme, IconThemeData? selectedIconTheme, + double? minWidth, double? minExtendedWidth, bool? useIndicator, + Color? indicatorColor, Object? indicatorShape}); + // The 0..1 animation of the rail's extended state, read via the ambient + // rail — Flutter's `NavigationRail.extendedAnimation(context)`. + external static Animation extendedAnimation(BuildContext context); +} + +// A single selectable entry in a NavigationRail — Flutter's +// `NavigationRailDestination`. +@JavaName('com.codename1.flutter.material.NavigationRailDestination') +class NavigationRailDestination { + external NavigationRailDestination( + {Widget icon, Widget? selectedIcon, Widget label, EdgeInsetsGeometry? padding, + bool? disabled, String? indicatorColorTooltip}); +} + +// How/whether a NavigationRail labels its destinations — Flutter's +// `NavigationRailLabelType`. +@JavaName('com.codename1.flutter.material.NavigationRailLabelType') +enum NavigationRailLabelType { none, selected, all } + +// The theming values for descendant NavigationRails — Flutter's +// `NavigationRailThemeData`. Reached both as a constructed theme value and via +// `Theme.of(context).navigationRailTheme`. +@JavaName('com.codename1.flutter.material.NavigationRailThemeData') +class NavigationRailThemeData { + external NavigationRailThemeData( + {Color? backgroundColor, double? elevation, TextStyle? unselectedLabelTextStyle, + TextStyle? selectedLabelTextStyle, IconThemeData? unselectedIconTheme, + IconThemeData? selectedIconTheme, double? groupAlignment, + NavigationRailLabelType? labelType, bool? useIndicator, Color? indicatorColor, + Object? indicatorShape, double? minWidth, double? minExtendedWidth}); + external Color? get backgroundColor; + external double? get elevation; + external TextStyle? get unselectedLabelTextStyle; + external TextStyle? get selectedLabelTextStyle; + external IconThemeData? get unselectedIconTheme; + external IconThemeData? get selectedIconTheme; +} + +// ====================================================================== +// FlutterLogo +// ====================================================================== + +// The animated Flutter logo — Flutter's `FlutterLogo`. Used by the Cupertino +// context-menu demo as a large decorative image. +@JavaName('com.codename1.flutter.widgets.FlutterLogo') +class FlutterLogo extends Widget { + external FlutterLogo( + {Key? key, double? size, Color? textColor, Object? style, Duration? duration, + Curve? curve}); +} + +// ====================================================================== +// License page (about screen) +// ====================================================================== + +// The Material page listing the open-source licenses of the app's packages — +// Flutter's `LicensePage`. +@JavaName('com.codename1.flutter.material.LicensePage') +class LicensePage extends Widget { + external LicensePage( + {Key? key, String? applicationName, String? applicationVersion, + Widget? applicationIcon, String? applicationLegalese}); +} + +// Pushes a Material license page onto the navigator — Flutter's top-level +// `showLicensePage`. +@JavaName('com.codename1.flutter.material.LicensePage.show') +external void showLicensePage( + {BuildContext context, String? applicationName, String? applicationVersion, + Widget? applicationIcon, String? applicationLegalese, bool? useRootNavigator}); + +// ====================================================================== +// InputDecorationThemeData (Material 3 renamed InputDecorationTheme) +// ====================================================================== + +// The theming values applied to descendant InputDecorators — Flutter's +// `InputDecorationThemeData` (the Material-3 value-type spelling of the older +// `InputDecorationTheme`). Declared loosely: the gallery only sets a handful of +// fields and never reads them back. +@JavaName('com.codename1.flutter.material.InputDecorationThemeData') +class InputDecorationThemeData { + external InputDecorationThemeData( + {TextStyle? labelStyle, TextStyle? floatingLabelStyle, TextStyle? helperStyle, + TextStyle? hintStyle, TextStyle? errorStyle, TextStyle? prefixStyle, + TextStyle? suffixStyle, TextStyle? counterStyle, bool? filled, Color? fillColor, + Color? focusColor, Color? hoverColor, EdgeInsetsGeometry? contentPadding, + bool? isDense, bool? isCollapsed, Object? border, Object? enabledBorder, + Object? focusedBorder, Object? errorBorder, Object? focusedErrorBorder, + Object? disabledBorder, Object? floatingLabelBehavior, double? gapPadding, + bool? alignLabelWithHint, Object? constraints}); +} + +// ====================================================================== +// Notched shapes (BottomAppBar FAB notch) +// ====================================================================== + +// The strategy that carves a notch out of a shape for a docked FAB — Flutter's +// `NotchedShape` interface. +@JavaName('com.codename1.flutter.material.NotchedShape') +abstract class NotchedShape {} + +// A NotchedShape that cuts a circular notch with small flanking fillets — +// Flutter's `CircularNotchedRectangle`. +@JavaName('com.codename1.flutter.material.CircularNotchedRectangle') +class CircularNotchedRectangle extends NotchedShape { + external CircularNotchedRectangle({double? inverted}); +} + +// ====================================================================== +// Back button icon +// ====================================================================== + +// The platform-appropriate back-arrow glyph, decoupled from its button — +// Flutter's `BackButtonIcon`. +@JavaName('com.codename1.flutter.material.BackButtonIcon') +class BackButtonIcon extends Widget { + external BackButtonIcon({Key? key}); +} + +// ====================================================================== +// Sliver grid delegates +// ====================================================================== + +// Base type for a sliver-grid layout strategy — Flutter's `SliverGridDelegate`. +@JavaName('com.codename1.flutter.rendering.SliverGridDelegate') +abstract class SliverGridDelegate {} + +// Lays a grid out with a fixed number of tiles across the cross axis — +// Flutter's `SliverGridDelegateWithFixedCrossAxisCount`. +@JavaName('com.codename1.flutter.rendering.SliverGridDelegateWithFixedCrossAxisCount') +class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate { + external SliverGridDelegateWithFixedCrossAxisCount( + {int crossAxisCount, double? mainAxisSpacing, double? crossAxisSpacing, + double? childAspectRatio, double? mainAxisExtent}); +} + +// Lays a grid out with tiles no wider than a maximum cross-axis extent — +// Flutter's `SliverGridDelegateWithMaxCrossAxisExtent`. +@JavaName('com.codename1.flutter.rendering.SliverGridDelegateWithMaxCrossAxisExtent') +class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate { + external SliverGridDelegateWithMaxCrossAxisExtent( + {double maxCrossAxisExtent, double? mainAxisSpacing, double? crossAxisSpacing, + double? childAspectRatio, double? mainAxisExtent}); +} + +// ====================================================================== +// animations package — fade-through transition +// ====================================================================== + +// Fades the outgoing child out then the incoming child in (Material shared-Z +// motion) — the `animations` package's `FadeThroughTransition`. +@JavaName('com.codename1.flutter.animations.FadeThroughTransition') +class FadeThroughTransition extends Widget { + external FadeThroughTransition( + {Key? key, Animation animation, Animation secondaryAnimation, + Color? fillColor, Widget? child}); +} + +// ====================================================================== +// Easing — Material 3 motion curves (package:flutter/animation) +// ====================================================================== + +// The Material 3 named easing curves — Flutter's `Easing`. Each is a static +// const Curve (`legacy` etc. are Cubic instances); the reply study reads +// `Easing.legacy` and `Easing.legacy.flipped`. +@JavaName('com.codename1.flutter.animation.Easing') +abstract class Easing { + external static Curve get linear; + external static Curve get legacy; + external static Curve get legacyDecelerate; + external static Curve get legacyAccelerate; + external static Curve get standard; + external static Curve get standardAccelerate; + external static Curve get standardDecelerate; + external static Curve get emphasized; + external static Curve get emphasizedAccelerate; + external static Curve get emphasizedDecelerate; +} + +// ====================================================================== +// Ink — a Material-aware decorated box +// ====================================================================== + +// Paints a decoration (or image) as part of the Material so ink splashes render +// above it — Flutter's `Ink` (and its `Ink.image` named constructor). +@JavaName('com.codename1.flutter.material.Ink') +class Ink extends Widget { + external Ink( + {Key? key, EdgeInsetsGeometry? padding, Color? color, Decoration? decoration, + double? width, double? height, Widget? child}); + external static Ink image( + {Key? key, ImageProvider image, BoxFit? fit, Widget? child, double? width, + double? height, EdgeInsetsGeometry? padding, Object? colorFilter, + Object? alignment, Object? repeat, Object? centerSlice, Object? onImageError}); +} + +// ====================================================================== +// Scroll direction / autovalidate enums +// ====================================================================== + +// The user-scroll direction reported by a UserScrollNotification — Flutter's +// `ScrollDirection`. +@JavaName('com.codename1.flutter.rendering.ScrollDirection') +enum ScrollDirection { idle, forward, reverse } + +// When a Form (or FormField) auto-validates its fields — Flutter's +// `AutovalidateMode`. Modelled as a class (not an enum) because the text-field +// demo reads `.index` off a value and indexes `.values` to round-trip the +// choice through a RestorableInt. +@JavaName('com.codename1.flutter.material.AutovalidateMode') +class AutovalidateMode { + external int get index; + external static AutovalidateMode get disabled; + external static AutovalidateMode get always; + external static AutovalidateMode get onUserInteraction; + external static List get values; +} + +// ====================================================================== +// adaptive_breakpoints package — window size buckets +// ====================================================================== + +// The Material breakpoint bucket for the current window — the +// `adaptive_breakpoints` package's `AdaptiveWindowType`. +@JavaName('com.codename1.flutter.layout.AdaptiveWindowType') +enum AdaptiveWindowType { xsmall, small, medium, large, xlarge } + +// Returns the AdaptiveWindowType bucket for the given context's window — the +// `adaptive_breakpoints` package's top-level `getWindowType`. Typed as the enum +// so the study's `>=` comparison lowers to an ordinal() compare in Java. +@JavaName('com.codename1.flutter.layout.AdaptiveBreakpoints.getWindowType') +external AdaptiveWindowType getWindowType(BuildContext context); diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p7.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p7.dart new file mode 100644 index 00000000000..56e05ec3d6f --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p7.dart @@ -0,0 +1,925 @@ +// Codename One Flutter runtime API stubs — "flutterApi" category (new_gallery, Pass 7). +// +// The concrete-type member / constructor / identifier gaps that survived Passes +// 1-6 once the dynamic-receiver cascade collapsed (diagnostics E0135 unresolved +// constructor, E0137 unresolved method, E0132 unresolved member, E0129 +// unresolved identifier). Every symbol here names a REAL Flutter (framework, +// rendering, gestures, physics, semantics, animations-package, provider or +// flutter_localized_countries) API; the shapes mirror the real signatures. +// +// Members that hang off types ALREADY declared in another stub file +// (Color.value, EdgeInsets.top/bottom, ThemeData.sliderTheme, MediaQueryData. +// viewInsets, BottomNavigationBarItem.icon/label, Size.center, RRect.middleRect, +// Route.settings, NavigatorState.push/popUntil, ScrollPhysics.createBallistic- +// Simulation, AnimationController.fling, AnimationStatus.isDismissed, ...) were +// appended to their owning classes in place. Only the brand-new types live here. +// +// Conventions (see gallery_p4_apitail.dart / gallery_p6.dart headers): +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - named constructors (X.name) -> `external static X name(...)` +// - top-level functions -> `@JavaName('fqcn.method') external ...` +// - instance getters -> no-arg method calls +// - callbacks / types owned elsewhere are declared loosely as `Object?`. + +// ====================================================================== +// Render tree — RenderObject / RenderBox / PaintingContext +// ====================================================================== + +// The base of the render tree — Flutter's `RenderObject`. new_gallery reaches +// one via `BuildContext.findRenderObject()` and casts it to RenderBox. +@JavaName('com.codename1.flutter.rendering.RenderObject') +class RenderObject { + external bool get attached; + external Rect get paintBounds; + external Rect get semanticBounds; + external void markNeedsPaint(); + external void markNeedsLayout(); +} + +// A render object laid out with the box protocol (a Cartesian size) — Flutter's +// `RenderBox`. The transformations and reply studies read `size` and map points +// through `localToGlobal` / `globalToLocal`. +@JavaName('com.codename1.flutter.rendering.RenderBox') +class RenderBox extends RenderObject { + external Size get size; + external bool get hasSize; + external Offset localToGlobal(Offset point, {RenderObject? ancestor}); + external Offset globalToLocal(Offset point, {RenderObject? ancestor}); + external Object getTransformTo(RenderObject? ancestor); +} + +// The canvas + child-painting handle handed to `RenderObject.paint` — Flutter's +// `PaintingContext`. The sliders demo's custom shapes read `context.canvas`. +@JavaName('com.codename1.flutter.rendering.PaintingContext') +class PaintingContext { + external Canvas get canvas; + external void paintChild(RenderObject child, Offset offset); + external Rect get estimatedBounds; +} + +// ====================================================================== +// Gesture details +// ====================================================================== + +// The details of a tap-up event — Flutter's `TapUpDetails` (globalPosition is +// used by the transformations demo to hit-test the board). +@JavaName('com.codename1.flutter.gestures.TapUpDetails') +class TapUpDetails { + external TapUpDetails({Offset? globalPosition, Offset? localPosition, Object? kind}); + external Offset get globalPosition; + external Offset get localPosition; +} + +// The details of a tap-down event — Flutter's `TapDownDetails`. +@JavaName('com.codename1.flutter.gestures.TapDownDetails') +class TapDownDetails { + external TapDownDetails({Offset? globalPosition, Offset? localPosition, Object? kind}); + external Offset get globalPosition; + external Offset get localPosition; +} + +// The details at the start of a drag — Flutter's `DragStartDetails`. +@JavaName('com.codename1.flutter.gestures.DragStartDetails') +class DragStartDetails { + external DragStartDetails({Offset? globalPosition, Offset? localPosition}); + external Offset get globalPosition; + external Offset get localPosition; +} + +// The incremental details of a drag — Flutter's `DragUpdateDetails`. The reply +// bottom-drawer reads `primaryDelta` to drive its AnimationController. +@JavaName('com.codename1.flutter.gestures.DragUpdateDetails') +class DragUpdateDetails { + external DragUpdateDetails({Offset? globalPosition, Offset? localPosition, Offset? delta, double? primaryDelta}); + external Offset get delta; + external double? get primaryDelta; + external Offset get globalPosition; + external Offset get localPosition; +} + +// The details at the end of a drag, carrying the fling velocity — Flutter's +// `DragEndDetails`. Home splash + reply drawer read `velocity.pixelsPerSecond`. +@JavaName('com.codename1.flutter.gestures.DragEndDetails') +class DragEndDetails { + external DragEndDetails({Velocity? velocity, double? primaryVelocity}); + external Velocity get velocity; + external double? get primaryVelocity; +} + +// A 2-D velocity in logical pixels per second — Flutter's `Velocity`. +@JavaName('com.codename1.flutter.gestures.Velocity') +class Velocity { + external Velocity({Offset pixelsPerSecond}); + external static Velocity get zero; + external Offset get pixelsPerSecond; + external Velocity clampMagnitude(double minValue, double maxValue); +} + +// The details at the start of a scale/pan gesture — Flutter's `ScaleStartDetails` +// (the transformations demo reads `focalPoint`). +@JavaName('com.codename1.flutter.gestures.ScaleStartDetails') +class ScaleStartDetails { + external Offset get focalPoint; + external Offset get localFocalPoint; + external int get pointerCount; +} + +// Signature for a drag-update callback — Flutter's `GestureDragUpdateCallback` +// (`void Function(DragUpdateDetails)`). A SAM the transpiler binds closures to; +// the reply bottom-drawer stores one and hands it to a GestureDetector. +@JavaName('com.codename1.flutter.gestures.GestureDragUpdateCallback') +class GestureDragUpdateCallback {} + +// Signature for a drag-end callback — Flutter's `GestureDragEndCallback` +// (`void Function(DragEndDetails)`). +@JavaName('com.codename1.flutter.gestures.GestureDragEndCallback') +class GestureDragEndCallback {} + +// ====================================================================== +// Physics — Tolerance / Simulation family +// ====================================================================== + +// The error tolerances a simulation settles within — Flutter's `Tolerance`. +// The home carousel physics compares the fling velocity against +// `tolerance.velocity`. +@JavaName('com.codename1.flutter.physics.Tolerance') +class Tolerance { + external Tolerance({double? distance, double? time, double? velocity}); + external static Tolerance get defaultTolerance; + external double get distance; + external double get time; + external double get velocity; +} + +// The base of a physics simulation over time — Flutter's `Simulation`. +@JavaName('com.codename1.flutter.physics.Simulation') +class Simulation { + external double x(double time); + external double dx(double time); + external bool isDone(double time); +} + +// A spring simulation used for scroll snapping — Flutter's +// `ScrollSpringSimulation`. +@JavaName('com.codename1.flutter.physics.ScrollSpringSimulation') +class ScrollSpringSimulation extends Simulation { + external ScrollSpringSimulation(Object spring, double start, double end, double velocity, {Tolerance? tolerance}); +} + +// A friction simulation clamped to a scroll range — Flutter's +// `ClampingScrollSimulation`. +@JavaName('com.codename1.flutter.physics.ClampingScrollSimulation') +class ClampingScrollSimulation extends Simulation { + external ClampingScrollSimulation({double position, double velocity, double? friction, Tolerance? tolerance}); +} + +// ====================================================================== +// Scroll notifications +// ====================================================================== + +// The base class of notifications that bubble up the widget tree — Flutter's +// `Notification`. new_gallery's ToggleSplashNotification extends it and calls +// `dispatch(context)` to send itself up to an enclosing NotificationListener. +@JavaName('com.codename1.flutter.widgets.Notification') +class Notification { + external Notification(); + external bool dispatch(BuildContext? target); +} + +// A notification bubbled up as a scrollable scrolls — Flutter's +// `ScrollNotification`. The reply adaptive-nav reads `depth` / `direction`. +@JavaName('com.codename1.flutter.widgets.ScrollNotification') +class ScrollNotification { + external ScrollMetrics get metrics; + external int get depth; + external BuildContext? get context; + external ScrollDirection get direction; + external bool dispatch(BuildContext? target); +} + +// A notification fired when the user starts or stops dragging — Flutter's +// `UserScrollNotification`, carrying the new `direction`. +@JavaName('com.codename1.flutter.widgets.UserScrollNotification') +class UserScrollNotification extends ScrollNotification { + external UserScrollNotification({BuildContext context, ScrollMetrics metrics, ScrollDirection direction}); +} + +// A notification fired as the scroll offset changes — Flutter's +// `ScrollUpdateNotification`. +@JavaName('com.codename1.flutter.widgets.ScrollUpdateNotification') +class ScrollUpdateNotification extends ScrollNotification { + external double? get scrollDelta; +} + +// ====================================================================== +// InteractiveViewer + its transformation controller +// ====================================================================== + +// The 4x4-matrix controller shared with an InteractiveViewer — Flutter's +// `TransformationController` (a ValueNotifier). The transformations +// demo animates `value` and maps viewport points with `toScene`. +@JavaName('com.codename1.flutter.widgets.TransformationController') +class TransformationController extends ValueNotifier { + external TransformationController([Matrix4? value]); + external Matrix4 get value; + external set value(Matrix4 v); + external Offset toScene(Offset viewportPoint); +} + +// A pan/zoom viewport for its child — Flutter's `InteractiveViewer`. +@JavaName('com.codename1.flutter.widgets.InteractiveViewer') +class InteractiveViewer extends Widget { + external InteractiveViewer( + {Key? key, TransformationController? transformationController, + EdgeInsets? boundaryMargin, double? minScale, double? maxScale, + bool? constrained, bool? panEnabled, bool? scaleEnabled, + double? scaleFactor, Object? onInteractionStart, Object? onInteractionUpdate, + Object? onInteractionEnd, Object? clipBehavior, bool? alignPanAxis, + Widget? child}); +} + +// ====================================================================== +// PageView + PageController +// ====================================================================== + +// Controls the visible page of a PageView — Flutter's `PageController`. The home +// carousel reads `page` and `position.haveDimensions`. +@JavaName('com.codename1.flutter.widgets.PageController') +class PageController { + external PageController({int? initialPage, bool? keepPage, double? viewportFraction}); + external double? get page; + external int get initialPage; + external ScrollPosition get position; + external bool get hasClients; + external Future animateToPage(int page, {Duration duration, Curve curve}); + external void jumpToPage(int page); + external Future nextPage({Duration duration, Curve curve}); + external Future previousPage({Duration duration, Curve curve}); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void dispose(); +} + +// A scrollable list of one-page-at-a-time children — Flutter's `PageView` (and +// its `.builder` / `.custom` named constructors). +@JavaName('com.codename1.flutter.widgets.PageView') +class PageView extends Widget { + external PageView( + {Key? key, PageController? controller, Object? scrollDirection, bool? reverse, + Object? physics, bool? pageSnapping, Object? onPageChanged, List? children, + bool? allowImplicitScrolling, String? restorationId, Object? clipBehavior}); + external static PageView builder( + {Key? key, PageController? controller, Object? scrollDirection, bool? reverse, + Object? physics, bool? pageSnapping, Object? onPageChanged, + Object? itemBuilder, int? itemCount, bool? allowImplicitScrolling, + String? restorationId, Object? clipBehavior}); +} + +// ====================================================================== +// Forms +// ====================================================================== + +// The State of a Form, driving validation/save across its fields — Flutter's +// `FormState`. +@JavaName('com.codename1.flutter.widgets.FormState') +class FormState { + external bool validate(); + external void save(); + external void reset(); +} + +// The State of a single FormField — Flutter's `FormFieldState`. The text-field +// demo reads/writes `value` and calls didChange/validate/save/reset. +@JavaName('com.codename1.flutter.widgets.FormFieldState') +class FormFieldState { + external T? get value; + external bool get hasError; + external bool get isValid; + external String? get errorText; + external void didChange(T? value); + external bool validate(); + external void save(); + external void reset(); +} + +// Persists a form field's value on save — Flutter's `FormFieldSetter` typedef +// (`void Function(T? newValue)`). A SAM the transpiler binds closures to. +@JavaName('com.codename1.flutter.widgets.FormFieldSetter') +class FormFieldSetter {} + +// Validates a form field's value — Flutter's `FormFieldValidator` typedef +// (`String? Function(T? value)`). A SAM the transpiler binds closures to. +@JavaName('com.codename1.flutter.widgets.FormFieldValidator') +class FormFieldValidator {} + +// A grouping of form fields that validate/save together — Flutter's `Form`. +@JavaName('com.codename1.flutter.widgets.Form') +class Form extends Widget { + external Form({Key? key, Widget? child, Object? onChanged, Object? onWillPop, + Object? canPop, Object? onPopInvoked, Object? autovalidateMode}); + external static FormState? of(BuildContext context); + external static FormState? maybeOf(BuildContext context); +} + +// A Material text field wired to Form validation — Flutter's `TextFormField`. +@JavaName('com.codename1.flutter.material.TextFormField') +class TextFormField extends Widget { + external TextFormField( + {Key? key, TextEditingController? controller, String? initialValue, + InputDecoration? decoration, TextInputType? keyboardType, TextStyle? style, + bool? obscureText, bool? enabled, int? maxLines, int? minLines, int? maxLength, + Object? validator, Object? onSaved, Object? onChanged, Object? onFieldSubmitted, + Object? onEditingComplete, Object? focusNode, Object? textInputAction, + Object? textCapitalization, Object? autovalidateMode, Object? inputFormatters, + List? autofillHints, Object? autofocus, Object? cursorColor}); +} + +// ====================================================================== +// Structural / layout single-child widgets +// ====================================================================== + +// Scales and positions its child within itself — Flutter's `FittedBox`. +@JavaName('com.codename1.flutter.widgets.FittedBox') +class FittedBox extends Widget { + external FittedBox({Key? key, BoxFit? fit, Object? alignment, Object? clipBehavior, Widget? child}); +} + +// Whether (and how) to include a child in the tree — Flutter's `Visibility`. +@JavaName('com.codename1.flutter.widgets.Visibility') +class Visibility extends Widget { + external Visibility( + {Key? key, Widget child, Widget? replacement, bool? visible, + bool? maintainState, bool? maintainAnimation, bool? maintainSize, + bool? maintainSemantics, bool? maintainInteractivity}); +} + +// Sizes its child to the child's intrinsic height — Flutter's `IntrinsicHeight`. +@JavaName('com.codename1.flutter.widgets.IntrinsicHeight') +class IntrinsicHeight extends Widget { + external IntrinsicHeight({Key? key, Widget? child}); +} + +// Sizes its child to the child's intrinsic width — Flutter's `IntrinsicWidth`. +@JavaName('com.codename1.flutter.widgets.IntrinsicWidth') +class IntrinsicWidth extends Widget { + external IntrinsicWidth({Key? key, double? stepWidth, double? stepHeight, Widget? child}); +} + +// Prevents its subtree from receiving pointer events — Flutter's `IgnorePointer`. +@JavaName('com.codename1.flutter.widgets.IgnorePointer') +class IgnorePointer extends Widget { + external IgnorePointer({Key? key, bool? ignoring, bool? ignoringSemantics, Widget? child}); +} + +// Isolates its subtree onto its own layer for cheaper repaints — Flutter's +// `RepaintBoundary`. +@JavaName('com.codename1.flutter.widgets.RepaintBoundary') +class RepaintBoundary extends Widget { + external RepaintBoundary({Key? key, Widget? child}); +} + +// Forces its child to a specific width/height aspect ratio — Flutter's +// `AspectRatio`. +@JavaName('com.codename1.flutter.widgets.AspectRatio') +class AspectRatio extends Widget { + external AspectRatio({Key? key, double aspectRatio, Widget? child}); +} + +// A widget that reports a preferred size — Flutter's `PreferredSizeWidget` +// interface (an AppBar / adaptive app bar implements it). +@JavaName('com.codename1.flutter.widgets.PreferredSizeWidget') +class PreferredSizeWidget extends Widget { + external Size get preferredSize; +} + +// Adapts its child to a PreferredSizeWidget of the given size — Flutter's +// `PreferredSize`. +@JavaName('com.codename1.flutter.widgets.PreferredSize') +class PreferredSize extends Widget { + external PreferredSize({Key? key, Size preferredSize, Widget child}); + external Size get preferredSize; +} + +// Shows a single child of a stack by index — Flutter's `IndexedStack`. +@JavaName('com.codename1.flutter.widgets.IndexedStack') +class IndexedStack extends Widget { + external IndexedStack({Key? key, Object? alignment, Object? textDirection, + Object? sizing, int? index, List? children}); +} + +// Lets its child overflow its own constraints — Flutter's `OverflowBox`. +@JavaName('com.codename1.flutter.widgets.OverflowBox') +class OverflowBox extends Widget { + external OverflowBox({Key? key, Object? alignment, double? minWidth, double? maxWidth, + double? minHeight, double? maxHeight, Widget? child}); +} + +// A widget that clips/elevates its child to an arbitrary shape — Flutter's +// `PhysicalShape`. +@JavaName('com.codename1.flutter.widgets.PhysicalShape') +class PhysicalShape extends Widget { + external PhysicalShape({Key? key, Object clipper, Object? clipBehavior, double? elevation, + Color? color, Color? shadowColor, Widget? child}); +} + +// ====================================================================== +// Focus / input plumbing widgets +// ====================================================================== + +// A widget managing a FocusNode for its subtree — Flutter's `Focus`. +@JavaName('com.codename1.flutter.widgets.Focus') +class Focus extends Widget { + external Focus({Key? key, FocusNode? focusNode, bool? autofocus, Object? onFocusChange, + Object? onKey, Object? onKeyEvent, bool? canRequestFocus, bool? skipTraversal, + bool? descendantsAreFocusable, bool? includeSemantics, String? debugLabel, Widget? child}); + external static FocusNode of(BuildContext context, {bool scopeOk}); +} + +// Excludes its subtree from focus traversal — Flutter's `ExcludeFocus`. +@JavaName('com.codename1.flutter.widgets.ExcludeFocus') +class ExcludeFocus extends Widget { + external ExcludeFocus({Key? key, bool? excluding, Widget? child}); +} + +// A raw keyboard listener — Flutter's `KeyboardListener`. +@JavaName('com.codename1.flutter.widgets.KeyboardListener') +class KeyboardListener extends Widget { + external KeyboardListener({Key? key, FocusNode focusNode, bool? autofocus, + bool? includeSemantics, Object? onKeyEvent, Widget child}); +} + +// A low-level pointer-event listener — Flutter's `Listener`. +@JavaName('com.codename1.flutter.widgets.Listener') +class Listener extends Widget { + external Listener({Key? key, Object? onPointerDown, Object? onPointerMove, + Object? onPointerUp, Object? onPointerCancel, Object? onPointerHover, + Object? onPointerSignal, Object? behavior, Widget? child}); +} + +// Listens for a Notification bubbling up from its subtree — Flutter's +// `NotificationListener`. +@JavaName('com.codename1.flutter.widgets.NotificationListener') +class NotificationListener extends Widget { + external NotificationListener({Key? key, Object? onNotification, Widget? child}); +} + +// Intercepts the system back gesture — Flutter's `WillPopScope`. +@JavaName('com.codename1.flutter.widgets.WillPopScope') +class WillPopScope extends Widget { + external WillPopScope({Key? key, Object onWillPop, Widget child}); +} + +// ====================================================================== +// Focus traversal policies +// ====================================================================== + +// Traverses focus in widget (tree) order — Flutter's +// `WidgetOrderTraversalPolicy`. +@JavaName('com.codename1.flutter.widgets.WidgetOrderTraversalPolicy') +class WidgetOrderTraversalPolicy { + external WidgetOrderTraversalPolicy({Object? secondary}); +} + +// Traverses focus in reading order for the ambient text direction — Flutter's +// `ReadingOrderTraversalPolicy`. +@JavaName('com.codename1.flutter.widgets.ReadingOrderTraversalPolicy') +class ReadingOrderTraversalPolicy { + external ReadingOrderTraversalPolicy({Object? secondary}); +} + +// Traverses focus by explicit FocusTraversalOrder — Flutter's +// `OrderedTraversalPolicy`. +@JavaName('com.codename1.flutter.widgets.OrderedTraversalPolicy') +class OrderedTraversalPolicy { + external OrderedTraversalPolicy({Object? secondary}); +} + +// ====================================================================== +// Overlay +// ====================================================================== + +// One entry painted into an Overlay — Flutter's `OverlayEntry`. Feature-discovery +// rebuilds it via `markNeedsBuild()` and tears it down with `remove()`. +@JavaName('com.codename1.flutter.widgets.OverlayEntry') +class OverlayEntry { + external OverlayEntry({WidgetBuilder builder, bool? opaque, bool? maintainState}); + external void markNeedsBuild(); + external void remove(); + external bool get mounted; +} + +// The stack of OverlayEntries floating above the navigator — Flutter's +// `Overlay`. +@JavaName('com.codename1.flutter.widgets.Overlay') +class Overlay extends Widget { + external Overlay({Key? key, List? initialEntries, Object? clipBehavior}); + external static OverlayState of(BuildContext context, {bool rootOverlay, Object? debugRequiredFor}); + external static OverlayState? maybeOf(BuildContext context, {bool rootOverlay}); +} + +// The mutable State of an Overlay — Flutter's `OverlayState`. +@JavaName('com.codename1.flutter.widgets.OverlayState') +class OverlayState { + external void insert(OverlayEntry entry, {OverlayEntry? below, OverlayEntry? above}); + external void insertAll(List entries, {OverlayEntry? below, OverlayEntry? above}); +} + +// ====================================================================== +// Material widgets (constructors) +// ====================================================================== + +// The Material bar docked at the bottom, optionally notched for a FAB — +// Flutter's `BottomAppBar`. +@JavaName('com.codename1.flutter.material.BottomAppBar') +class BottomAppBar extends Widget { + external BottomAppBar({Key? key, Color? color, double? elevation, NotchedShape? shape, + Object? clipBehavior, double? notchMargin, EdgeInsets? padding, double? height, + Color? surfaceTintColor, Color? shadowColor, Widget? child}); +} + +// A labelled action button inside a SnackBar — Flutter's `SnackBarAction`. +@JavaName('com.codename1.flutter.material.SnackBarAction') +class SnackBarAction { + external SnackBarAction({Key? key, String label, VoidCallback onPressed, + Color? textColor, Color? disabledTextColor, Color? backgroundColor}); +} + +// A Material drawer header showing the signed-in account — Flutter's +// `UserAccountsDrawerHeader`. +@JavaName('com.codename1.flutter.material.UserAccountsDrawerHeader') +class UserAccountsDrawerHeader extends Widget { + external UserAccountsDrawerHeader({Key? key, Object? decoration, EdgeInsets? margin, + Widget? currentAccountPicture, List? otherAccountsPictures, + Widget? accountName, Widget? accountEmail, Object? onDetailsPressed, + Color? arrowColor}); +} + +// A large flat button with fully custom shape/fill — Flutter's +// `RawMaterialButton`. +@JavaName('com.codename1.flutter.material.RawMaterialButton') +class RawMaterialButton extends Widget { + external RawMaterialButton({Key? key, VoidCallback? onPressed, Object? onLongPress, + Object? onHighlightChanged, TextStyle? textStyle, Color? fillColor, + Color? focusColor, Color? hoverColor, Color? highlightColor, Color? splashColor, + double? elevation, double? focusElevation, double? hoverElevation, + double? highlightElevation, double? disabledElevation, EdgeInsets? padding, + Object? visualDensity, Object? constraints, Object? shape, Object? clipBehavior, + bool? autofocus, Object? materialTapTargetSize, Widget? child}); +} + +// A tap/hover ink reaction not necessarily filling its bounds — Flutter's +// `InkResponse`. +@JavaName('com.codename1.flutter.material.InkResponse') +class InkResponse extends Widget { + external InkResponse({Key? key, VoidCallback? onTap, Object? onTapDown, Object? onTapUp, + Object? onTapCancel, Object? onDoubleTap, Object? onLongPress, Object? onHighlightChanged, + Object? onHover, bool? containedInkWell, Object? highlightShape, double? radius, + Object? borderRadius, Object? customBorder, Color? focusColor, Color? hoverColor, + Color? highlightColor, Color? splashColor, Object? splashFactory, bool? enableFeedback, + bool? excludeFromSemantics, Object? mouseCursor, bool? canRequestFocus, Widget? child}); +} + +// The leading back button of an app bar — Flutter's `BackButton`. +@JavaName('com.codename1.flutter.material.BackButton') +class BackButton extends Widget { + external BackButton({Key? key, Color? color, Object? onPressed, Object? style}); +} + +// A builder that rebuilds its own subtree via a local setState — Flutter's +// `StatefulBuilder`. +@JavaName('com.codename1.flutter.widgets.StatefulBuilder') +class StatefulBuilder extends Widget { + external StatefulBuilder({Key? key, Object builder}); +} + +// Builds itself from the latest snapshot of a Future — Flutter's +// `FutureBuilder`. +@JavaName('com.codename1.flutter.widgets.FutureBuilder') +class FutureBuilder extends Widget { + external FutureBuilder({Key? key, Object? future, T? initialData, Object builder}); +} + +// The connection state of an async computation feeding an AsyncSnapshot — +// Flutter's `ConnectionState`. +@JavaName('com.codename1.flutter.widgets.ConnectionState') +enum ConnectionState { none, waiting, active, done } + +// An immutable snapshot of interaction with an async computation, handed to the +// FutureBuilder/StreamBuilder `builder` — Flutter's `AsyncSnapshot`. The +// about page reads `snapshot.hasData` / `snapshot.data`. +@JavaName('com.codename1.flutter.widgets.AsyncSnapshot') +class AsyncSnapshot { + external ConnectionState get connectionState; + external T? get data; + external Object? get error; + external Object? get stackTrace; + external bool get hasData; + external bool get hasError; + external T get requireData; +} + +// ====================================================================== +// Date / time picker dialogs +// ====================================================================== + +// The Material date-picker dialog — Flutter's `DatePickerDialog`. +@JavaName('com.codename1.flutter.material.DatePickerDialog') +class DatePickerDialog extends Widget { + external DatePickerDialog({Key? key, DateTime? initialDate, DateTime firstDate, + DateTime lastDate, DateTime? currentDate, Object? initialEntryMode, + Object? selectableDayPredicate, String? helpText, String? cancelText, + String? confirmText, Object? initialCalendarMode, String? errorFormatText, + String? errorInvalidText, String? fieldHintText, String? fieldLabelText, + Object? keyboardType, Object? restorationId}); +} + +// The Material time-picker dialog — Flutter's `TimePickerDialog`. +@JavaName('com.codename1.flutter.material.TimePickerDialog') +class TimePickerDialog extends Widget { + external TimePickerDialog({Key? key, TimeOfDay initialTime, Object? cancelText, + Object? confirmText, Object? helpText, Object? errorInvalidText, Object? hourLabelText, + Object? minuteLabelText, Object? initialEntryMode, Object? orientation, Object? onEntryModeChanged, + Object? restorationId}); +} + +// The Material date-range-picker dialog — Flutter's `DateRangePickerDialog`. +@JavaName('com.codename1.flutter.material.DateRangePickerDialog') +class DateRangePickerDialog extends Widget { + external DateRangePickerDialog({Key? key, DateTime firstDate, DateTime lastDate, + Object? initialDateRange, DateTime? currentDate, Object? initialEntryMode, + String? helpText, String? cancelText, String? confirmText, String? saveText, + String? errorFormatText, String? errorInvalidText, String? errorInvalidRangeText, + String? fieldStartHintText, String? fieldEndHintText, String? fieldStartLabelText, + String? fieldEndLabelText, Object? keyboardType, Object? restorationId}); +} + +// A Key backed by an object's identity (===) — Flutter's `ObjectKey`. +@JavaName('com.codename1.flutter.ObjectKey') +class ObjectKey extends Key { + external ObjectKey(Object? value); +} + +// ====================================================================== +// Restoration +// ====================================================================== + +// Establishes a restoration namespace for its subtree — Flutter's +// `RestorationScope`. +@JavaName('com.codename1.flutter.widgets.RestorationScope') +class RestorationScope extends Widget { + external RestorationScope({Key? key, String? restorationId, Widget child}); + external static Object? of(BuildContext context); +} + +// ====================================================================== +// animations package +// ====================================================================== + +// Fades and scales its child in/out for modal reveals — the `animations` +// package's `FadeScaleTransition`. +@JavaName('com.codename1.flutter.animations.FadeScaleTransition') +class FadeScaleTransition extends Widget { + external FadeScaleTransition({Key? key, Animation animation, Widget? child}); +} + +// The page-transition builder for the shared-axis (X/Y/Z) motion pattern — the +// `animations` package's `SharedAxisPageTransitionsBuilder`. (SharedAxis- +// TransitionType itself already lives in gallery_p4_apitail.dart.) +@JavaName('com.codename1.flutter.animations.SharedAxisPageTransitionsBuilder') +class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder { + external SharedAxisPageTransitionsBuilder({SharedAxisTransitionType transitionType, + Color? fillColor}); +} + +// Whether an OpenContainer uses a fade or fade-through transition — the +// `animations` package's `ContainerTransitionType`. +@JavaName('com.codename1.flutter.animations.ContainerTransitionType') +enum ContainerTransitionType { fade, fadeThrough } + +// Shows a modal route with an `animations`-package transition — the package's +// top-level `showModal`. +@JavaName('com.codename1.flutter.animations.Animations.showModal') +external Future showModal({BuildContext context, Object? configuration, + bool? useRootNavigator, WidgetBuilder builder, Object? filter}); + +// ====================================================================== +// Animation combinators +// ====================================================================== + +// Runs a parent animation in reverse (1 - value) — Flutter's `ReverseAnimation`. +@JavaName('com.codename1.flutter.animation.ReverseAnimation') +class ReverseAnimation extends Animation { + external ReverseAnimation(Animation parent); +} + +// ====================================================================== +// Painting / borders / images +// ====================================================================== + +// A box border resolved against text direction (start/end) — Flutter's +// `BorderDirectional`. +@JavaName('com.codename1.flutter.painting.BorderDirectional') +class BorderDirectional { + external BorderDirectional({Object? top, Object? bottom, Object? start, Object? end}); +} + +// An image from an asset at an exact device-pixel scale — Flutter's +// `ExactAssetImage`. +@JavaName('com.codename1.flutter.painting.ExactAssetImage') +class ExactAssetImage extends ImageProvider { + external ExactAssetImage(String assetName, {double? scale, Object? bundle, String? package}); +} + +// ====================================================================== +// Gestures — TapGestureRecognizer (used by RichText spans) +// ====================================================================== + +// Recognizes single taps, wired to link spans in the about page — Flutter's +// `TapGestureRecognizer`. +@JavaName('com.codename1.flutter.gestures.TapGestureRecognizer') +class TapGestureRecognizer { + external TapGestureRecognizer({Object? debugOwner}); + external set onTap(VoidCallback? handler); + external void dispose(); +} + +// ====================================================================== +// Semantics +// ====================================================================== + +// A semantic node emitted by a CustomPainter — Flutter's +// `CustomPainterSemantics`. +@JavaName('com.codename1.flutter.semantics.CustomPainterSemantics') +class CustomPainterSemantics { + external CustomPainterSemantics({Rect rect, Object properties, Object? transform, + Object? tags, Key? key}); +} + +// Fires accessibility announcements / haptics — Flutter's `SemanticsService`. +@JavaName('com.codename1.flutter.semantics.SemanticsService') +abstract class SemanticsService { + external static void announce(String message, TextDirection textDirection, {Object? assertiveness}); + external static void tooltip(String message); +} + +// ====================================================================== +// Enums / identifier constants +// ====================================================================== + +// The visual layer flavor of a Material — Flutter's `MaterialType`. +@JavaName('com.codename1.flutter.material.MaterialType') +enum MaterialType { canvas, card, circle, button, transparency } + +// How a BottomNavigationBar lays its items out — Flutter's +// `BottomNavigationBarType`. +@JavaName('com.codename1.flutter.material.BottomNavigationBarType') +enum BottomNavigationBarType { fixed, shifting } + +// When an InputDecoration floats its label — Flutter's `FloatingLabelBehavior`. +@JavaName('com.codename1.flutter.material.FloatingLabelBehavior') +enum FloatingLabelBehavior { never, auto, always } + +// One of the standard, individually-keyed components a scaffold builds (the +// close/back/drawer/... buttons) — Flutter's `StandardComponentType`. Each value +// exposes a stable `key`. +@JavaName('com.codename1.flutter.material.StandardComponentType') +class StandardComponentType { + external Key get key; + external static StandardComponentType get backButton; + external static StandardComponentType get closeButton; + external static StandardComponentType get drawerButton; + external static StandardComponentType get moreButton; +} + +// The well-known content types offered to platform autofill — Flutter's +// `AutofillHints`. +@JavaName('com.codename1.flutter.services.AutofillHints') +abstract class AutofillHints { + external static String get username; + external static String get password; + external static String get newUsername; + external static String get newPassword; + external static String get email; + external static String get name; + external static String get givenName; + external static String get familyName; + external static String get telephoneNumber; + external static String get oneTimeCode; + external static String get creditCardNumber; + external static String get postalCode; + external static String get streetAddressLine1; +} + +// The glue binding the widget layer to the engine — Flutter's `WidgetsBinding`. +// new_gallery reaches the ambient brightness through +// `WidgetsBinding.instance.platformDispatcher.platformBrightness`; the dispatcher +// is left `dynamic` so that chain resolves without pulling in the engine types. +@JavaName('com.codename1.flutter.widgets.WidgetsBinding') +class WidgetsBinding { + external static WidgetsBinding get instance; + external dynamic get platformDispatcher; + external Object get window; + external void addPostFrameCallback(Object callback); + external void addObserver(Object observer); + external void removeObserver(Object observer); +} + +// The default height of a BottomNavigationBar — Flutter's top-level const +// `kBottomNavigationBarHeight` (kToolbarHeight already lives in gallery_p4). +@JavaName('com.codename1.flutter.material.MaterialConstants.kBottomNavigationBarHeight') +double kBottomNavigationBarHeight = 56.0; + +// ====================================================================== +// Theme value type +// ====================================================================== + +// The theming applied to descendant bottom sheets — Flutter's +// `BottomSheetThemeData`; the reply bottom-drawer reads `backgroundColor`. +@JavaName('com.codename1.flutter.material.BottomSheetThemeData') +class BottomSheetThemeData { + external BottomSheetThemeData({Color? backgroundColor, Color? surfaceTintColor, + double? elevation, Color? modalBackgroundColor, Color? modalBarrierColor, + double? modalElevation, Object? shape, Object? clipBehavior, Object? constraints}); + external Color? get backgroundColor; + external Color? get modalBackgroundColor; + external double? get elevation; +} + +// ====================================================================== +// provider — Selector +// ====================================================================== + +// Rebuilds only when a selected slice of a provided value changes — the +// `provider` package's `Selector`. +@JavaName('com.codename1.flutter.provider.Selector') +class Selector extends StatelessWidget { + external Selector({Key? key, Object selector, Object builder, Object? shouldRebuild, Widget? child}); +} + +// ====================================================================== +// flutter_localized_countries +// ====================================================================== + +// The localizations delegate contributing translated locale/country names — the +// `flutter_localized_countries` package's `LocaleNamesLocalizationsDelegate`. +@JavaName('com.codename1.flutter.l10n.LocaleNamesLocalizationsDelegate') +class LocaleNamesLocalizationsDelegate { + external LocaleNamesLocalizationsDelegate(); + // The map of locale-code -> native display name — the + // `flutter_localized_countries` package's static `nativeLocaleNames`. + external static Map get nativeLocaleNames; +} + +// The translated display names for locales/countries — the +// `flutter_localized_countries` package's `LocaleNames`. +@JavaName('com.codename1.flutter.l10n.LocaleNames') +class LocaleNames { + external static LocaleNames of(BuildContext context); + external String? nameOf(String localeCode); + external Map get data; +} + +// ====================================================================== +// dart:ui / top-level helpers +// ====================================================================== + +// A raw triangle mesh handed to Canvas.drawVertices — dart:ui's `Vertices`. +@JavaName('com.codename1.flutter.Vertices') +class Vertices { + external Vertices(VertexMode mode, List positions, {List? colors, + List? indices, List? textureCoordinates}); +} + +// How a Vertices mesh strings its points into triangles — dart:ui's `VertexMode`. +@JavaName('com.codename1.flutter.VertexMode') +enum VertexMode { triangles, triangleStrip, triangleFan } + +// Linearly interpolates two nullable doubles — dart:ui's top-level `lerpDouble`. +@JavaName('com.codename1.flutter.MathUtil.lerpDouble') +external double? lerpDouble(Object? a, Object? b, double t); + +// Resolves the best-matching supported locale for the device's preferences — +// Flutter's top-level `basicLocaleListResolution`. +@JavaName('com.codename1.flutter.widgets.WidgetsLocalizations.basicLocaleListResolution') +external Locale basicLocaleListResolution(List? preferredLocales, Iterable supportedLocales); + +// Asserts a MediaQuery ancestor exists (debug builds) — Flutter's +// `debugCheckHasMediaQuery`. +@JavaName('com.codename1.flutter.widgets.Debug.debugCheckHasMediaQuery') +external bool debugCheckHasMediaQuery(BuildContext context); + +// Case-insensitive ASCII string comparison — package:collection's +// `compareAsciiUpperCase`. +@JavaName('com.codename1.flutter.util.AsciiUtil.compareAsciiUpperCase') +external int compareAsciiUpperCase(String a, String b); + +// A Future that is already complete and calls its listeners synchronously — +// Flutter foundation's `SynchronousFuture`. +@JavaName('com.codename1.flutter.foundation.SynchronousFuture') +class SynchronousFuture { + external SynchronousFuture(T value); + external Object then(Object onValue, {Object? onError}); + external Object whenComplete(Object action); + external Object catchError(Object onError, {Object? test}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p8_stubsFinal.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p8_stubsFinal.dart new file mode 100644 index 00000000000..5da21843319 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p8_stubsFinal.dart @@ -0,0 +1,106 @@ +// Codename One Flutter runtime API stubs — "stubsFinal" category (new_gallery, Pass 8+). +// +// The last tail of brand-new concrete types (E0129 unresolved identifier, E0135 +// unresolved constructor) that survived through Pass 8: the services input +// formatters, the dart:ui BlendMode enum, and a handful of widgets (FocusScope, +// ModalBarrier, ShapeBorderClipper and the flutter_staggered_grid_view +// MasonryGridView). Shapes mirror the real Flutter / package signatures. +// +// Conventions (see gallery_p7.dart header): +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - named constructors (X.name) -> `external static X name(...)` +// - instance getters -> no-arg method calls +// - callbacks / loosely-owned types are declared as `Object?`. + +// ====================================================================== +// services — text input formatting +// ====================================================================== + +// How `maxLength` is enforced on an editable text field — Flutter's +// `MaxLengthEnforcement` (text_field_demo passes `.none`). +@JavaName('com.codename1.flutter.services.MaxLengthEnforcement') +enum MaxLengthEnforcement { none, enforced, truncateAfterCompositionEnds } + +// The base class every input formatter extends; new_gallery subclasses it +// (`_UsNumberTextInputFormatter extends TextInputFormatter`) and overrides +// `formatEditUpdate`. TextEditingValue is declared in gallery_p3_cascadeTypes. +@JavaName('com.codename1.flutter.services.TextInputFormatter') +class TextInputFormatter { + external TextInputFormatter(); + external TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue); +} + +// Filters the edited text against a pattern — Flutter's +// `FilteringTextInputFormatter`. Only `digitsOnly` is used by new_gallery; the +// allow/deny constructors and `singleLineFormatter` are declared for fidelity. +@JavaName('com.codename1.flutter.services.FilteringTextInputFormatter') +class FilteringTextInputFormatter extends TextInputFormatter { + external FilteringTextInputFormatter(Object filterPattern, {bool allow, String replacementString}); + external static FilteringTextInputFormatter allow(Object filterPattern, {String replacementString}); + external static FilteringTextInputFormatter deny(Object filterPattern, {String replacementString}); + external static FilteringTextInputFormatter get digitsOnly; + external static FilteringTextInputFormatter get singleLineFormatter; +} + +// Truncates the edited text to a maximum length — Flutter's +// `LengthLimitingTextInputFormatter`. +@JavaName('com.codename1.flutter.services.LengthLimitingTextInputFormatter') +class LengthLimitingTextInputFormatter extends TextInputFormatter { + external LengthLimitingTextInputFormatter(int? maxLength, {MaxLengthEnforcement? maxLengthEnforcement}); +} + +// ====================================================================== +// dart:ui — painting +// ====================================================================== + +// The Porter-Duff / separable blend modes for `Canvas.drawVertices` and friends +// — dart:ui's `BlendMode`. new_gallery uses `BlendMode.color`; the full standard +// set is declared for fidelity. +@JavaName('com.codename1.flutter.BlendMode') +enum BlendMode { + clear, src, dst, srcOver, dstOver, srcIn, dstIn, srcOut, dstOut, srcATop, + dstATop, xor, plus, modulate, screen, overlay, darken, lighten, colorDodge, + colorBurn, hardLight, softLight, difference, exclusion, multiply, hue, + saturation, color, luminosity +} + +// ====================================================================== +// widgets +// ====================================================================== + +// A focus container that groups its subtree — Flutter's `FocusScope`. +@JavaName('com.codename1.flutter.widgets.FocusScopeNode') +class FocusScopeNode { + external FocusScopeNode({String? debugLabel}); + external bool get hasFocus; + external void requestFocus([Object? node]); + external void unfocus({Object? disposition}); +} + +@JavaName('com.codename1.flutter.widgets.FocusScope') +class FocusScope extends Widget { + external FocusScope({Key? key, FocusScopeNode? node, bool? autofocus, Object? onFocusChange, bool? canRequestFocus, bool? skipTraversal, Widget? child}); + external static FocusScopeNode of(BuildContext context); +} + +// A full-screen barrier that optionally dismisses a route on tap — Flutter's +// `ModalBarrier` (pages/backdrop passes `dismissible: false`). +@JavaName('com.codename1.flutter.widgets.ModalBarrier') +class ModalBarrier extends Widget { + external ModalBarrier({Key? key, Color? color, bool? dismissible, String? semanticsLabel, bool? barrierSemanticsDismissible, Object? onDismiss}); +} + +// Adapts a ShapeBorder to the CustomClipper protocol used by PhysicalShape — +// Flutter's `ShapeBorderClipper` (crane/backdrop clips its front layer). +@JavaName('com.codename1.flutter.widgets.ShapeBorderClipper') +class ShapeBorderClipper { + external ShapeBorderClipper({Object shape, TextDirection? textDirection}); +} + +// A staggered, Pinterest-style grid — the flutter_staggered_grid_view package's +// `MasonryGridView`. crane/backdrop builds it via the `.count` constructor. +@JavaName('com.codename1.flutter.widgets.MasonryGridView') +class MasonryGridView extends Widget { + external static MasonryGridView count({Key? key, String? restorationId, int crossAxisCount, double? mainAxisSpacing, double? crossAxisSpacing, int? itemCount, Object itemBuilder, Object? scrollDirection, bool? shrinkWrap, Object? physics, Object? padding, Object? controller}); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p9_animPaintPhysicsTheme.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p9_animPaintPhysicsTheme.dart new file mode 100644 index 00000000000..86f4d60231f --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_p9_animPaintPhysicsTheme.dart @@ -0,0 +1,57 @@ +// Codename One Flutter runtime API stubs — "animPaintPhysicsTheme" category +// (new_gallery, Pass 9). +// +// Brand-new types that survived Pass 7/8 with no @JavaName mapping, so the +// emitter left their references unqualified (no import) and javac failed with +// "cannot find symbol". Each names a REAL Flutter (painting / widgets / +// semantics) type; the Java implementations live under +// com.codename1.flutter.{painting,widgets,semantics}. Only NEW types live here; +// members of pre-existing types are added in place on their owning stub. +// +// Conventions follow gallery_p7.dart: +// - positional constructor params -> Java constructor arguments +// - named constructor params -> void setter methods of the same name +// - instance getters -> no-arg method calls +// - callbacks / types owned elsewhere are declared loosely as `Object?`. + +// ====================================================================== +// painting — BoxPainter +// ====================================================================== + +// The object a Decoration produces to paint itself — Flutter's `BoxPainter`. +// A decoration returns one from `createBoxPainter`; the tab-indicator and Rally +// pie-chart decorations subclass it and override `paint(canvas, offset, +// configuration)`. +@JavaName('com.codename1.flutter.painting.BoxPainter') +abstract class BoxPainter { + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration); + external void dispose(); +} + +// ====================================================================== +// widgets — OverlayRoute +// ====================================================================== + +// A Route that inserts OverlayEntry objects into the navigator's Overlay — +// Flutter's `OverlayRoute`. new_gallery's TwoPanePageRoute extends it and +// overrides `createOverlayEntries()`. +@JavaName('com.codename1.flutter.widgets.OverlayRoute') +class OverlayRoute extends Route { + external OverlayRoute(); + external Iterable createOverlayEntries(); +} + +// ====================================================================== +// semantics — SemanticsBuilderCallback +// ====================================================================== + +// The signature of a CustomPainter's `semanticsBuilder` — Flutter's +// `SemanticsBuilderCallback` typedef, `List Function( +// Size size)`. Declared as a single-method type so transpiled painters that +// override `semanticsBuilder` bind the lambda against a Java functional +// interface. The Rally line chart returns one to expose per-day balances to +// screen readers. +@JavaName('com.codename1.flutter.semantics.SemanticsBuilderCallback') +class SemanticsBuilderCallback { + external List call(Size size); +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_restoration.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_restoration.dart new file mode 100644 index 00000000000..e369a20469e --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_restoration.dart @@ -0,0 +1,202 @@ +// Codename One Flutter runtime API stubs — state restoration (M2, new_gallery). +// +// Signature-only declarations for Flutter's state-restoration framework as seen +// from Dart. The hand-written Java runtime lives under com.codename1.flutter.* +// (see @JavaName on each declaration). Restoration is a no-op-but-API-complete +// implementation: Restorable* properties hold their value in a field and +// registerForRestoration merely wires the property; nothing is persisted. +// +// This file is loaded alongside flutter_material.dart by StubRegistry. + +// --- the RestorationMixin ------------------------------------------------- + +// Mixed into a State subclass (`class _FooState extends State with +// RestorationMixin`). The emitter maps this to a Java interface with default +// methods; bare calls to registerForRestoration resolve to the default method. +@JavaName('com.codename1.flutter.RestorationMixin') +mixin RestorationMixin { + external String? get restorationId; + external RestorationBucket? get bucket; + external void restoreState(RestorationBucket? oldBucket, bool initialRestore); + external void registerForRestoration(RestorableProperty property, String restorationId); + external void unregisterFromRestoration(RestorableProperty property); + external void didToggleBucket(RestorationBucket? oldBucket); +} + +// --- the restoration bucket (opaque token) -------------------------------- + +@JavaName('com.codename1.flutter.RestorationBucket') +class RestorationBucket {} + +// --- RestorableProperty and its value-holding subtypes -------------------- + +// The abstract base. User code subclasses this directly (e.g. to restore a +// Set), overriding createDefaultValue / fromPrimitives / toPrimitives / +// initWithValue and calling notifyListeners(). It extends ChangeNotifier in +// Flutter; the listener plumbing is folded in here for the restoration scope. +@JavaName('com.codename1.flutter.RestorableProperty') +abstract class RestorableProperty { + external RestorableProperty(); + external T createDefaultValue(); + external void initWithValue(T value); + external Object toPrimitives(); + external T fromPrimitives(Object? data); + external bool get isRegistered; + external void notifyListeners(); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void dispose(); +} + +// Abstract value-holding bases. Flutter layers these between RestorableProperty +// and the concrete holders; user code in new_gallery subclasses them directly +// (studies/reply/app.dart, studies/shrine/app.dart) and reads the inherited +// `value` getter, so they must exist for member/identifier resolution. + +// A restorable that stores a single value with a read/write `value` accessor — +// Flutter's `RestorableValue`. +@JavaName('com.codename1.flutter.RestorableValue') +abstract class RestorableValue extends RestorableProperty { + external T get value; + external set value(T newValue); +} + +// A restorable whose value is a Listenable that is itself restored (rather than +// re-created) — Flutter's `RestorableListenable`. The +// value getter is read-only; subclasses override createDefaultValue / +// fromPrimitives / toPrimitives. +@JavaName('com.codename1.flutter.RestorableListenable') +abstract class RestorableListenable extends RestorableProperty { + external T get value; +} + +// A RestorableListenable specialised for ChangeNotifier values that also +// disposes the held notifier — Flutter's `RestorableChangeNotifier`. +@JavaName('com.codename1.flutter.RestorableChangeNotifier') +abstract class RestorableChangeNotifier extends RestorableListenable {} + +// Concrete value holders. Each exposes a typed `value` getter/setter that the +// emitter routes to the overloaded Java accessors value()/value(v). + +@JavaName('com.codename1.flutter.RestorableBool') +class RestorableBool extends RestorableProperty { + external RestorableBool(bool defaultValue); + external bool get value; + external set value(bool v); +} + +@JavaName('com.codename1.flutter.RestorableBoolN') +class RestorableBoolN extends RestorableProperty { + external RestorableBoolN(bool? defaultValue); + external bool? get value; + external set value(bool? v); +} + +@JavaName('com.codename1.flutter.RestorableInt') +class RestorableInt extends RestorableProperty { + external RestorableInt(int defaultValue); + external int get value; + external set value(int v); +} + +@JavaName('com.codename1.flutter.RestorableIntN') +class RestorableIntN extends RestorableProperty { + external RestorableIntN(int? defaultValue); + external int? get value; + external set value(int? v); +} + +@JavaName('com.codename1.flutter.RestorableDouble') +class RestorableDouble extends RestorableProperty { + external RestorableDouble(double defaultValue); + external double get value; + external set value(double v); +} + +@JavaName('com.codename1.flutter.RestorableDoubleN') +class RestorableDoubleN extends RestorableProperty { + external RestorableDoubleN(double? defaultValue); + external double? get value; + external set value(double? v); +} + +@JavaName('com.codename1.flutter.RestorableString') +class RestorableString extends RestorableProperty { + external RestorableString(String defaultValue); + external String get value; + external set value(String v); +} + +@JavaName('com.codename1.flutter.RestorableStringN') +class RestorableStringN extends RestorableProperty { + external RestorableStringN(String? defaultValue); + external String? get value; + external set value(String? v); +} + +// value type is DateTime; modelled as Object here because dart:core DateTime is +// owned by a different category. The .value member still resolves as Object. +@JavaName('com.codename1.flutter.RestorableDateTime') +class RestorableDateTime extends RestorableValue { + external RestorableDateTime(DateTime defaultValue); + external DateTime get value; + external set value(DateTime v); +} + +@JavaName('com.codename1.flutter.RestorableTextEditingController') +class RestorableTextEditingController extends RestorableProperty { + external RestorableTextEditingController({String? text}); + external TextEditingController get value; +} + +// --- global keys and focus nodes ------------------------------------------ + +@JavaName('com.codename1.flutter.GlobalKey') +class GlobalKey extends Key { + external GlobalKey({String? debugLabel}); + external T? get currentState; + external BuildContext? get currentContext; + external Widget? get currentWidget; +} + +@JavaName('com.codename1.flutter.FocusNode') +class FocusNode { + external FocusNode({String? debugLabel, bool? skipTraversal, bool? canRequestFocus}); + external bool get hasFocus; + external bool get hasPrimaryFocus; + external void requestFocus([FocusNode? node]); + external void unfocus(); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void dispose(); +} + +// --- restorable route future (route restoration) -------------------------- + +// The navigator handle passed to RestorableRouteFuture.onPresent. The Navigator +// static helpers (Navigator.of / Navigator.restorablePush) are owned by the +// navigation category; only the NavigatorState surface the callbacks touch is +// declared here. +@JavaName('com.codename1.flutter.navigation.NavigatorState') +abstract class NavigatorState { + external String restorablePush(Object routeBuilder, {Object? arguments}); + external String restorablePushNamed(String routeName, {Object? arguments}); + external void pop([Object? result]); + // Imperative navigation used across new_gallery — Flutter's `NavigatorState` + // push / pushNamed / popUntil / canPop / maybePop. + external Future push(Route route); + external Future pushNamed(String routeName, {Object? arguments}); + external Future pushReplacement(Route newRoute, {Object? result}); + external Future pushReplacementNamed(String routeName, {Object? arguments, Object? result}); + external void popUntil(Object predicate); + external bool canPop(); + external Future maybePop([Object? result]); +} + +@JavaName('com.codename1.flutter.navigation.RestorableRouteFuture') +class RestorableRouteFuture extends RestorableProperty { + external RestorableRouteFuture({RoutePresentationCallback onPresent, DynamicCallback? onComplete}); + external void present([Object? arguments]); + external bool get isPresent; + external String? get route; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_stateMgmt.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_stateMgmt.dart new file mode 100644 index 00000000000..9b8a16522b9 --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_stateMgmt.dart @@ -0,0 +1,185 @@ +// Codename One Flutter runtime API stubs — state management & theming value +// classes (new_gallery "stateMgmt" gap category). +// +// Same conventions as flutter_material.dart: +// - positional constructor parameters -> Java constructor arguments +// - named constructor parameters -> void setter methods of the same name +// - instance getters -> no-arg method calls (name()) +// - a method whose return type is a bare generic type parameter (e.g. `T`) +// is emitted with its witness recovered as a trailing T.class token; the +// Java runtime method therefore takes a trailing Class parameter. +// +// Types that belong to other gap categories (ShapeBorder, MaterialStateProperty, +// SystemUiOverlayStyle, SnackBarBehavior, InputBorder, ...) are typed `dynamic` +// here so these classes resolve without coupling to another agent's stubs. + +// --- InheritedWidget -------------------------------------------------- + +@JavaName('com.codename1.flutter.widgets.InheritedWidget') +class InheritedWidget extends Widget { + external InheritedWidget({Key? key, Widget? child}); + bool updateShouldNotify(InheritedWidget oldWidget); +} + +// --- listenable / change notification --------------------------------- + +// The root of the observable protocol — Flutter's `Listenable`. AnimatedWidget +// takes one; ChangeNotifier and Animation are Listenables. +@JavaName('com.codename1.flutter.foundation.Listenable') +abstract class Listenable { + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); +} + +@JavaName('com.codename1.flutter.foundation.ChangeNotifier') +class ChangeNotifier extends Listenable { + external ChangeNotifier(); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void notifyListeners(); + external void dispose(); + external bool get hasListeners; +} + +// --- provider package ------------------------------------------------- + +@JavaName('com.codename1.flutter.provider.SingleChildWidget') +class SingleChildWidget extends Widget { + external SingleChildWidget({Key? key, Widget? child}); +} + +@JavaName('com.codename1.flutter.provider.Provider') +class Provider extends SingleChildWidget { + external Provider({Key? key, dynamic create, Object? value, bool? lazy, Widget? child}); + // named constructors are declared as static factory methods for the emitter + external static Provider value({Key? key, Object value, Widget? child}); + external static T of(BuildContext context, {bool listen}); +} + +@JavaName('com.codename1.flutter.provider.ChangeNotifierProvider') +class ChangeNotifierProvider extends Provider { + external ChangeNotifierProvider({Key? key, dynamic create, bool? lazy, Widget? child}); + external static ChangeNotifierProvider value({Key? key, Object value, Widget? child}); +} + +@JavaName('com.codename1.flutter.provider.MultiProvider') +class MultiProvider extends Widget { + external MultiProvider({Key? key, List providers, Widget child}); +} + +@JavaName('com.codename1.flutter.provider.Consumer') +class Consumer extends StatelessWidget { + external Consumer({Key? key, dynamic builder, Widget? child}); +} + +// --- scoped_model package --------------------------------------------- + +@JavaName('com.codename1.flutter.scopedmodel.Model') +class Model { + external Model(); + external void addListener(VoidCallback listener); + external void removeListener(VoidCallback listener); + external void notifyListeners(); +} + +@JavaName('com.codename1.flutter.scopedmodel.ScopedModel') +class ScopedModel extends Widget { + external ScopedModel({Key? key, Object model, Widget child}); + external static T of(BuildContext context, {bool rebuildOnChange}); +} + +@JavaName('com.codename1.flutter.scopedmodel.ScopedModelDescendant') +class ScopedModelDescendant extends StatelessWidget { + external ScopedModelDescendant({Key? key, dynamic builder, bool? rebuildOnChange, Widget? child}); +} + +// --- Locale ----------------------------------------------------------- + +@JavaName('com.codename1.flutter.Locale') +class Locale { + external Locale(String languageCode, [String? countryCode]); + external String get languageCode; + external String? get countryCode; +} + +// --- theming value classes -------------------------------------------- + +@JavaName('com.codename1.flutter.material.IconThemeData') +class IconThemeData { + external IconThemeData({Color? color, double? size, double? opacity, double? fill, + double? weight, double? grade, double? opticalSize, dynamic shadows, + bool? applyTextScaling}); + external Color? get color; + external double? get size; + external double? get opacity; + external IconThemeData copyWith({Color? color, double? size, double? opacity, double? fill, + double? weight, double? grade, double? opticalSize, dynamic shadows, + bool? applyTextScaling}); +} + +@JavaName('com.codename1.flutter.material.AppBarTheme') +class AppBarTheme { + external AppBarTheme({Color? backgroundColor, Color? foregroundColor, Color? color, + Color? shadowColor, Color? surfaceTintColor, double? elevation, + double? scrolledUnderElevation, IconThemeData? iconTheme, IconThemeData? actionsIconTheme, + TextStyle? titleTextStyle, TextStyle? toolbarTextStyle, bool? centerTitle, + double? titleSpacing, double? toolbarHeight, dynamic systemOverlayStyle, + dynamic shape}); + external Color? get backgroundColor; + external double? get elevation; + external IconThemeData? get iconTheme; +} + +@JavaName('com.codename1.flutter.material.ChipThemeData') +class ChipThemeData { + external ChipThemeData({Color? backgroundColor, Color? disabledColor, Color? selectedColor, + Color? secondarySelectedColor, Color? deleteIconColor, Color? shadowColor, + EdgeInsets? padding, EdgeInsets? labelPadding, dynamic shape, TextStyle? labelStyle, + TextStyle? secondaryLabelStyle, Brightness? brightness, double? elevation, + double? pressElevation}); + external Color? get backgroundColor; + external Color? get selectedColor; + external Color? get secondarySelectedColor; + external Color? get disabledColor; + external Brightness? get brightness; +} + +@JavaName('com.codename1.flutter.material.CheckboxThemeData') +class CheckboxThemeData { + external CheckboxThemeData({dynamic fillColor, dynamic checkColor, dynamic overlayColor, + dynamic materialTapTargetSize, dynamic shape, dynamic side, dynamic visualDensity, + dynamic mouseCursor, dynamic splashRadius}); +} + +@JavaName('com.codename1.flutter.material.BottomAppBarThemeData') +class BottomAppBarThemeData { + external BottomAppBarThemeData({Color? color, Color? surfaceTintColor, Color? shadowColor, + double? elevation, double? height, EdgeInsets? padding, dynamic shape}); + external Color? get color; + external double? get elevation; +} + +@JavaName('com.codename1.flutter.material.CardTheme') +class CardTheme { + external CardTheme({Color? color, Color? shadowColor, Color? surfaceTintColor, + double? elevation, EdgeInsets? margin, dynamic shape, dynamic clipBehavior}); + external Color? get color; + external double? get elevation; +} + +@JavaName('com.codename1.flutter.material.CardThemeData') +class CardThemeData { + external CardThemeData({Color? color, Color? shadowColor, Color? surfaceTintColor, + double? elevation, EdgeInsets? margin, dynamic shape, dynamic clipBehavior}); + external Color? get color; + external double? get elevation; +} + +@JavaName('com.codename1.flutter.material.DividerThemeData') +class DividerThemeData { + external DividerThemeData({double? thickness, Color? color, double? space, double? indent, + double? endIndent}); + external double? get thickness; + external Color? get color; + external double? get space; +} diff --git a/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_walkthrough.dart b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_walkthrough.dart new file mode 100644 index 00000000000..05d5823e04a --- /dev/null +++ b/maven/flutter-runtime/src/main/resources/META-INF/dart/gallery_walkthrough.dart @@ -0,0 +1,32 @@ +// Pointer plumbing, so ONE walkthrough script can drive both stacks. +// +// A demo written against these runs unchanged on the native app, where they are +// Flutter's own, and on the transpiled one, where they reach the Codename One +// runtime and become real pointer events. The alternative -- two drivers, one +// per stack -- is what produced videos whose panes did different things. + +@JavaName('com.codename1.flutter.gestures.PointerEvent') +abstract class PointerEvent { + external Offset get position; +} + +@JavaName('com.codename1.flutter.gestures.PointerDownEvent') +class PointerDownEvent extends PointerEvent { + external PointerDownEvent({Offset? position, int? pointer}); +} + +@JavaName('com.codename1.flutter.gestures.PointerMoveEvent') +class PointerMoveEvent extends PointerEvent { + external PointerMoveEvent({Offset? position, int? pointer, Offset? delta}); +} + +@JavaName('com.codename1.flutter.gestures.PointerUpEvent') +class PointerUpEvent extends PointerEvent { + external PointerUpEvent({Offset? position, int? pointer}); +} + +@JavaName('com.codename1.flutter.gestures.GestureBinding') +class GestureBinding { + external static GestureBinding get instance; + external void handlePointerEvent(PointerEvent event); +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/AlignFactorTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/AlignFactorTest.java new file mode 100644 index 00000000000..6ff134280b7 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/AlignFactorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Align; +import com.codename1.flutter.widgets.ClipRect; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Align's widthFactor/heightFactor - Flutter's RenderPositionedBox sizing. This is the + * geometry an expand/collapse animation runs on: the gallery's category and settings lists + * animate {@code ClipRect(child: Align(heightFactor: t, child: ...))} with t from 0 to 1, + * so the box has to be a FRACTION of the child while the child keeps its full size. + */ +class AlignFactorTest { + + private RenderElement mountAndLayout(Widget root, BoxConstraints constraints) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(constraints); + r.position(0, 0); + return r; + } + + private Align align(Double heightFactor, Alignment alignment, Widget child) { + Align a = new Align(); + a.heightFactor(heightFactor); + a.alignment(alignment); + a.child(child); + return a; + } + + @Test + void heightFactorTakesAFractionOfTheChildHeight() { + RenderElement root = mountAndLayout( + align(0.25, Alignment.topCenter, new ProbeBox(100, 200)), + BoxConstraints.loose(300, 300)); + assertEquals(new Size(300, 50), root.size()); + } + + @Test + void heightFactorZeroCollapsesTheBoxWhileTheChildKeepsItsSize() { + RenderElement root = mountAndLayout( + align(0.0, Alignment.topCenter, new ProbeBox(100, 200)), + BoxConstraints.loose(300, 300)); + assertEquals(0.0, root.size().height()); + assertEquals(new Size(100, 200), root.renderChildren().get(0).size(), + "the child measures itself in full; only the box collapses"); + } + + @Test + void heightFactorOneIsTheFullyExpandedEnd() { + RenderElement root = mountAndLayout( + align(1.0, Alignment.topCenter, new ProbeBox(100, 200)), + BoxConstraints.loose(300, 300)); + assertEquals(200.0, root.size().height()); + } + + @Test + void topCenterHoldsTheChildAtTheTopSoTheCollapseRevealsFromTheTop() { + RenderElement root = mountAndLayout( + align(0.25, Alignment.topCenter, new ProbeBox(100, 200)), + BoxConstraints.loose(300, 300)); + RenderElement child = root.renderChildren().get(0); + assertEquals(0, child.y(), "no vertical shift: the visible slice is the child's top"); + assertEquals(100, child.x(), "centred horizontally within 300"); + } + + @Test + void widthFactorShrinkWrapsTheHorizontalAxisToo() { + Align a = new Align(); + a.widthFactor(0.5); + a.alignment(Alignment.topCenter); + a.child(new ProbeBox(100, 200)); + RenderElement root = mountAndLayout(a, BoxConstraints.loose(300, 300)); + assertEquals(50.0, root.size().width()); + assertEquals(300.0, root.size().height(), "no heightFactor: that axis still fills"); + } + + @Test + void noFactorsKeepsTheFillBehaviour() { + RenderElement root = mountAndLayout( + align(null, Alignment.center, new ProbeBox(100, 200)), + BoxConstraints.loose(300, 300)); + assertEquals(new Size(300, 300), root.size()); + } + + @Test + void anUnboundedAxisShrinkWrapsEvenWithoutAFactor() { + RenderElement root = mountAndLayout( + align(null, Alignment.center, new ProbeBox(100, 200)), + new BoxConstraints(0, 300, 0, Double.POSITIVE_INFINITY)); + assertEquals(new Size(300, 200), root.size()); + } + + @Test + void clipRectReportsTheCollapsedBoxNotTheChild() { + // The ClipRect is what hides the overflow; it must take the Align's reduced size + // rather than growing to the child, or there would be nothing to clip against. + ClipRect clip = new ClipRect(); + clip.child(align(0.25, Alignment.topCenter, new ProbeBox(100, 200))); + + RenderElement root = mountAndLayout(clip, BoxConstraints.loose(300, 300)); + assertEquals(50.0, root.size().height()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ButtonConsumptionTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ButtonConsumptionTest.java new file mode 100644 index 00000000000..4f7d78a2a2a --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ButtonConsumptionTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.material.ButtonRenderElement; +import com.codename1.flutter.material.ElevatedButton; +import com.codename1.flutter.material.IconButton; +import com.codename1.flutter.material.OutlinedButton; +import com.codename1.flutter.material.TextButton; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.widgets.Text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Buttons consume a Text child as their label and an Icon child as their + * material glyph (headless — the CN1 Button itself only exists with a + * Display). + */ +class ButtonConsumptionTest { + + private ButtonRenderElement mount(Widget w) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(w, host, owner); + return (ButtonRenderElement) host.rootRenderElement(); + } + + @Test + void textChildBecomesTheLabel() { + ElevatedButton b = new ElevatedButton(); + b.onPressed(() -> { + }); + b.child(new Text("Save")); + ButtonRenderElement el = mount(b); + assertEquals("Save", el.consumedLabel()); + assertEquals(0, el.consumedIconChar()); + } + + @Test + void iconChildBecomesTheMaterialGlyph() { + TextButton b = new TextButton(); + b.child(new Icon(Icons.add)); + ButtonRenderElement el = mount(b); + assertNull(el.consumedLabel()); + assertEquals(Icons.add.codePoint(), el.consumedIconChar()); + } + + @Test + void outlinedButtonConsumesLikeTheOthers() { + OutlinedButton b = new OutlinedButton(); + b.child(new Text("Cancel")); + ButtonRenderElement el = mount(b); + assertEquals("Cancel", el.consumedLabel()); + } + + @Test + void iconButtonConsumesItsIconParameter() { + IconButton b = new IconButton(); + b.icon(new Icon(Icons.settings)); + b.iconSize(32.0); + ButtonRenderElement el = mount(b); + assertNull(el.consumedLabel()); + assertEquals(Icons.settings.codePoint(), el.consumedIconChar()); + } + + /** + * Unresolvable content yields NO label — it used to yield the widget's toString(). + * + *

This test asserted that fallback, and the fallback was the bug: a button whose + * content was neither a Text nor an Icon drew its Java class name, which is how the + * gallery's back button came out as "com.codename1.flutter.material.BackButtonIcon@…" + * across the app bar. A class name is never a label anyone meant to show.

+ */ + @Test + void unsupportedChildYieldsNoLabelRatherThanAClassName() { + ElevatedButton b = new ElevatedButton(); + b.child(new ProbeBox(1, 1)); + ButtonRenderElement el = mount(b); + assertNull(el.consumedLabel(), "no label beats a class name"); + } + + @Test + void missingChildYieldsNoLabelAndNoIcon() { + TextButton b = new TextButton(); + ButtonRenderElement el = mount(b); + assertNull(el.consumedLabel()); + assertEquals(0, el.consumedIconChar()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/CanUpdateTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/CanUpdateTest.java new file mode 100644 index 00000000000..7fa9aae5456 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/CanUpdateTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.testsupport.AltBox; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CanUpdateTest { + + @Test + void sameTypeNoKeysCanUpdate() { + assertTrue(Widget.canUpdate(new ProbeBox(1, 1), new ProbeBox(2, 2))); + } + + @Test + void differentTypeCannotUpdate() { + assertFalse(Widget.canUpdate(new ProbeBox(1, 1), new AltBox(1, 1))); + } + + @Test + void sameTypeEqualValueKeysCanUpdate() { + ProbeBox a = new ProbeBox(1, 1); + a.key(new ValueKey("k")); + ProbeBox b = new ProbeBox(2, 2); + b.key(new ValueKey("k")); + assertTrue(Widget.canUpdate(a, b)); + } + + @Test + void sameTypeDifferentKeysCannotUpdate() { + ProbeBox a = new ProbeBox(1, 1); + a.key(new ValueKey("k1")); + ProbeBox b = new ProbeBox(1, 1); + b.key(new ValueKey("k2")); + assertFalse(Widget.canUpdate(a, b)); + } + + @Test + void keyOnOnlyOneSideCannotUpdate() { + ProbeBox a = new ProbeBox(1, 1); + a.key(new ValueKey(7L)); + assertFalse(Widget.canUpdate(a, new ProbeBox(1, 1))); + assertFalse(Widget.canUpdate(new ProbeBox(1, 1), a)); + } + + @Test + void nullsNeverUpdate() { + assertFalse(Widget.canUpdate(null, new ProbeBox(1, 1))); + assertFalse(Widget.canUpdate(new ProbeBox(1, 1), null)); + assertFalse(Widget.canUpdate(null, null)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/CarouselCardGeometryTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/CarouselCardGeometryTest.java new file mode 100644 index 00000000000..8a9a5d1b209 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/CarouselCardGeometryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Container; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The gallery's study card, reduced to the geometry that decides its size: + * + *
+ * Container(
+ *   padding: EdgeInsets.symmetric(horizontal: 4),
+ *   margin: EdgeInsets.symmetric(vertical: 16),
+ *   height: 240, width: 296,
+ *   child: Material(...))
+ * 
+ * + * laid out inside a carousel viewport 240 logical pixels tall. Flutter's answer, measured + * from the native app: the card's surface is 288 x 208 - 296 minus the horizontal padding, + * and 240 minus BOTH vertical margins, because the height passes through a ConstrainedBox + * that the viewport's own maximum clamps. + */ +class CarouselCardGeometryTest { + + private RenderElement mountAndLayout(Widget root, BoxConstraints constraints) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(constraints); + r.position(0, 0); + return r; + } + + private Container carouselCard(Widget child) { + Container c = new Container(); + c.padding(EdgeInsets.symmetric(4, 0)); + c.margin(EdgeInsets.symmetric(0, 16)); + c.height(Double.valueOf(240)); + c.width(Double.valueOf(296)); + c.child(child); + return c; + } + + /** The viewport the carousel gives a page: as wide as the page slot, 240 tall. */ + private BoxConstraints viewport() { + return new BoxConstraints(0, 304, 0, 240); + } + + @Test + void theCardOccupiesTheWholeViewportHeight() { + RenderElement root = mountAndLayout(carouselCard(new ProbeBox(10, 10)), viewport()); + assertEquals(240.0, root.size().height(), + "the Container plus its margins fills the viewport"); + } + + @Test + void bothVerticalMarginsComeOffTheSurface() { + RenderElement root = mountAndLayout(carouselCard(new ProbeBox(10, 10)), viewport()); + RenderElement surface = root.renderChildren().get(0); + // 240 - 16 - 16. The height of 240 is a ConstrainedBox inside the margin, so the + // viewport's own 240 maximum clamps it rather than the two adding up. + assertEquals(208.0, surface.size().height()); + } + + @Test + void horizontalPaddingComesOffTheSurfaceWidth() { + RenderElement root = mountAndLayout(carouselCard(new ProbeBox(10, 10)), viewport()); + RenderElement surface = root.renderChildren().get(0); + assertEquals(288.0, surface.size().width(), "296 wide, less 4 of padding each side"); + } + + @Test + void theSurfaceSitsBelowTheTopMargin() { + RenderElement root = mountAndLayout(carouselCard(new ProbeBox(10, 10)), viewport()); + RenderElement surface = root.renderChildren().get(0); + assertEquals(16, surface.y()); + assertEquals(4, surface.x()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ConstrainedBoxTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ConstrainedBoxTest.java new file mode 100644 index 00000000000..a214b23745d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ConstrainedBoxTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.ConstrainedBox; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * ConstrainedBox constraint intersection (BoxConstraints.enforce), headless + * (logical pixels == device pixels). + */ +class ConstrainedBoxTest { + + private RenderElement mountAndLayout(Widget root, BoxConstraints constraints) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(constraints); + r.position(0, 0); + return r; + } + + @Test + void enforceClampsAdditionalIntoIncomingBounds() { + BoxConstraints additional = new BoxConstraints(100, 200, 50, 80); + BoxConstraints incoming = BoxConstraints.loose(150, 60); + BoxConstraints enforced = additional.enforce(incoming); + assertEquals(100.0, enforced.minWidth()); + assertEquals(150.0, enforced.maxWidth()); + assertEquals(50.0, enforced.minHeight()); + assertEquals(60.0, enforced.maxHeight()); + } + + @Test + void enforceWithTightIncomingWins() { + BoxConstraints additional = new BoxConstraints(100, 200, 0, 80); + BoxConstraints enforced = additional.enforce(BoxConstraints.tight(50, 40)); + assertEquals(50.0, enforced.minWidth()); + assertEquals(50.0, enforced.maxWidth()); + assertEquals(40.0, enforced.minHeight()); + assertEquals(40.0, enforced.maxHeight()); + } + + @Test + void dartConstructedBoxConstraintsDefaultsToUnconstrained() { + BoxConstraints c = new BoxConstraints(); + assertEquals(0.0, c.minWidth()); + assertEquals(Double.POSITIVE_INFINITY, c.maxWidth()); + assertEquals(0.0, c.minHeight()); + assertEquals(Double.POSITIVE_INFINITY, c.maxHeight()); + c.minWidth(10.0); + c.maxWidth(20.0); + c.minHeight(5.0); + c.maxHeight(15.0); + assertEquals(new BoxConstraints(10, 20, 5, 15), c); + } + + @Test + void constrainedBoxImposesMinimumsOnASmallChild() { + BoxConstraints additional = new BoxConstraints(); + additional.minWidth(100.0); + additional.minHeight(40.0); + ConstrainedBox box = new ConstrainedBox(); + box.constraints(additional); + box.child(new ProbeBox(10, 10)); + + RenderElement root = mountAndLayout(box, BoxConstraints.loose(300, 300)); + assertEquals(new Size(100, 40), root.size()); + assertEquals(new Size(100, 40), root.renderChildren().get(0).size()); + } + + @Test + void constrainedBoxMaximumsCapTheChild() { + BoxConstraints additional = new BoxConstraints(); + additional.maxWidth(50.0); + additional.maxHeight(20.0); + ConstrainedBox box = new ConstrainedBox(); + box.constraints(additional); + box.child(new ProbeBox(500, 500)); + + RenderElement root = mountAndLayout(box, BoxConstraints.loose(300, 300)); + assertEquals(new Size(50, 20), root.size()); + } + + @Test + void incomingTightConstraintsOverrideTheAdditionalOnes() { + BoxConstraints additional = new BoxConstraints(); + additional.minWidth(100.0); + additional.maxWidth(200.0); + ConstrainedBox box = new ConstrainedBox(); + box.constraints(additional); + box.child(new ProbeBox(10, 10)); + + RenderElement root = mountAndLayout(box, BoxConstraints.tight(60, 60)); + assertEquals(new Size(60, 60), root.size()); + } + + @Test + void constrainedBoxWithoutChildSizesToTheSmallestEnforcedSize() { + BoxConstraints additional = new BoxConstraints(); + additional.minWidth(80.0); + additional.minHeight(30.0); + ConstrainedBox box = new ConstrainedBox(); + box.constraints(additional); + + RenderElement root = mountAndLayout(box, BoxConstraints.loose(300, 300)); + assertEquals(new Size(80, 30), root.size()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/EmbeddedLifetimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/EmbeddedLifetimeTest.java new file mode 100644 index 00000000000..24cd2f567af --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/EmbeddedLifetimeTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The decision FlutterUI.wrap's container makes when Codename One deinitializes +/// it: REMOVED, so the tree must be unmounted, or merely HIDDEN, so it must not. +public class EmbeddedLifetimeTest { + + private static FlutterUI.EmbeddedLifetime mounted() { + FlutterUI.EmbeddedLifetime l = new FlutterUI.EmbeddedLifetime(); + l.mounted(); + return l; + } + + @Test + public void removalUnmounts() { + FlutterUI.EmbeddedLifetime l = mounted(); + assertTrue(l.onDeinit(), "a deinitialize must schedule the check"); + assertTrue(l.settle(false), "no parent a cycle later means it was removed"); + } + + @Test + public void hidingTheFormKeepsTheTree() { + // Navigating to another form deinitializes every component, but they + // keep their parent -- and Flutter keeps the state of a route it can + // come back to. + FlutterUI.EmbeddedLifetime l = mounted(); + assertTrue(l.onDeinit()); + assertFalse(l.settle(true)); + assertFalse(l.onInit(), "shown again, the same tree is still mounted"); + } + + @Test + public void aMoveWithinOneCycleKeepsTheTree() { + FlutterUI.EmbeddedLifetime l = mounted(); + assertTrue(l.onDeinit()); + assertFalse(l.onInit(), "re-added before the check ran"); + assertFalse(l.settle(false), "the pending unmount was cancelled by the re-add"); + } + + @Test + public void addingBackAfterRemovalMountsAFreshTree() { + FlutterUI.EmbeddedLifetime l = mounted(); + l.onDeinit(); + assertTrue(l.settle(false)); + assertTrue(l.onInit(), "an unmounted tree cannot be shown; it must be mounted again"); + l.mounted(); + assertTrue(l.onDeinit(), "and the fresh tree is tracked like the first"); + assertTrue(l.settle(false)); + } + + @Test + public void repeatedDeinitializeSchedulesOnce() { + FlutterUI.EmbeddedLifetime l = mounted(); + assertTrue(l.onDeinit()); + assertFalse(l.onDeinit(), "one pending check is enough"); + } + + @Test + public void neverUnmountsTwice() { + FlutterUI.EmbeddedLifetime l = mounted(); + l.onDeinit(); + assertTrue(l.settle(false)); + assertFalse(l.onDeinit(), "nothing is mounted any more"); + assertFalse(l.settle(false)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlexLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlexLayoutTest.java new file mode 100644 index 00000000000..75475ea22d8 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlexLayoutTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Row; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Flex layout semantics per Flutter's RenderFlex, driven headless (no CN1 + * Display; the render boxes have stubbed intrinsic sizes). + */ +class FlexLayoutTest { + + private RenderElement mountAndLayout(Widget root, double w, double h) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(BoxConstraints.tight(w, h)); + r.position(0, 0); + return r; + } + + @Test + void columnWithFixedChildrenAndExpandedDistributesFreeSpace() { + Column col = new Column(); + Expanded expanded = new Expanded(); + expanded.child(new ProbeBox(10, 10)); + col.children(DartList.of(new ProbeBox(100, 50), new ProbeBox(200, 60), expanded)); + + RenderElement root = mountAndLayout(col, 400, 600); + + // Column fills the tight constraints. + assertEquals(new Size(400, 600), root.size()); + + List kids = root.renderChildren(); + assertEquals(3, kids.size()); + + RenderElement c1 = kids.get(0); + RenderElement c2 = kids.get(1); + RenderElement ex = kids.get(2); + + // Fixed children keep their intrinsic sizes. + assertEquals(new Size(100, 50), c1.size()); + assertEquals(new Size(200, 60), c2.size()); + // Expanded gets all free main-axis space: 600 - (50 + 60) = 490, + // tight; its child keeps its own width under the loose cross axis. + assertEquals(new Size(10, 490), ex.size()); + + // Main axis (vertical), MainAxisAlignment.start: stacked in order. + assertEquals(0, c1.y()); + assertEquals(50, c2.y()); + assertEquals(110, ex.y()); + + // Cross axis, CrossAxisAlignment.center (Flutter default). + assertEquals(150, c1.x()); + assertEquals(100, c2.x()); + assertEquals(195, ex.x()); + + // The Expanded is a pass-through: its child fills it at offset 0. + List exKids = ex.renderChildren(); + assertEquals(1, exKids.size()); + assertEquals(new Size(10, 490), exKids.get(0).size()); + assertEquals(195, exKids.get(0).x()); + assertEquals(110, exKids.get(0).y()); + } + + @Test + void columnMainAxisAlignmentCenterCentersTheGroup() { + Column col = new Column(); + col.mainAxisAlignment(MainAxisAlignment.center); + col.children(DartList.of(new ProbeBox(100, 50), new ProbeBox(100, 50))); + + RenderElement root = mountAndLayout(col, 400, 600); + List kids = root.renderChildren(); + + // 600 - 100 used = 500 free, half above the group. + assertEquals(250, kids.get(0).y()); + assertEquals(300, kids.get(1).y()); + } + + @Test + void columnSpaceBetweenPutsAllFreeSpaceBetweenChildren() { + Column col = new Column(); + col.mainAxisAlignment(MainAxisAlignment.spaceBetween); + col.children(DartList.of(new ProbeBox(10, 100), new ProbeBox(10, 100), new ProbeBox(10, 100))); + + RenderElement root = mountAndLayout(col, 400, 600); + List kids = root.renderChildren(); + + // free = 600 - 300 = 300; between = 150 + assertEquals(0, kids.get(0).y()); + assertEquals(250, kids.get(1).y()); + assertEquals(500, kids.get(2).y()); + } + + @Test + void rowLaysOutOnHorizontalMainAxis() { + Row row = new Row(); + row.crossAxisAlignment(CrossAxisAlignment.start); + Expanded expanded = new Expanded(); + expanded.flex(3); + expanded.child(new ProbeBox(1, 20)); + Expanded expanded2 = new Expanded(); + expanded2.child(new ProbeBox(1, 20)); + row.children(DartList.of(new ProbeBox(100, 40), expanded, expanded2)); + + RenderElement root = mountAndLayout(row, 500, 200); + List kids = root.renderChildren(); + + // free = 500 - 100 = 400; flex 3:1 -> 300 and 100 + assertEquals(new Size(100, 40), kids.get(0).size()); + assertEquals(300.0, kids.get(1).size().width()); + assertEquals(100.0, kids.get(2).size().width()); + assertEquals(0, kids.get(0).x()); + assertEquals(100, kids.get(1).x()); + assertEquals(400, kids.get(2).x()); + // CrossAxisAlignment.start: all on the top edge. + assertEquals(0, kids.get(0).y()); + assertEquals(0, kids.get(1).y()); + } + + @Test + void mainAxisSizeMinShrinkWrapsUnderLooseConstraints() { + Column col = new Column(); + col.mainAxisSize(MainAxisSize.min); + col.crossAxisAlignment(CrossAxisAlignment.start); + col.children(DartList.of(new ProbeBox(100, 50), new ProbeBox(200, 60))); + + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(col, host, owner); + RenderElement r = host.rootRenderElement(); + Size s = r.layout(BoxConstraints.loose(400, 600)); + assertEquals(new Size(200, 110), s); + } + + @Test + void stretchTightensTheCrossAxis() { + Column col = new Column(); + col.crossAxisAlignment(CrossAxisAlignment.stretch); + col.children(DartList.of(new ProbeBox(100, 50))); + + RenderElement root = mountAndLayout(col, 400, 600); + RenderElement kid = root.renderChildren().get(0); + // stretch forces the child's cross axis to the full 400. + assertEquals(new Size(400, 50), kid.size()); + assertEquals(0, kid.x()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlutterAssetsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlutterAssetsTest.java new file mode 100644 index 00000000000..9f102f4aa08 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/FlutterAssetsTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The asset-flattening contract: Codename One resources are flat on every port + * ({@code getResourceAsStream} rejects nested names), so Flutter asset paths + * must encode into root-level resource names — unambiguously, and using only + * characters that were already legal in the path. + */ +class FlutterAssetsTest { + + @Test + void resourceNameIsFlatAndAbsolute() { + String r = FlutterAssets.resourceName("assets/studies/reply_card.png"); + assertTrue(r.startsWith("/"), r); + assertEquals(-1, r.indexOf('/', 1), "resource name must not be nested: " + r); + } + + @Test + void separatorAndEscape() { + assertEquals("/cn1f_assets_sstudies_sreply__card.png", + FlutterAssets.resourceName("assets/studies/reply_card.png")); + } + + @Test + void packageAssetPath() { + assertEquals("/cn1f_packages_sgallery__assets_sassets_sicons_smaterial_smaterial.png", + FlutterAssets.resourceName("packages/gallery_assets/assets/icons/material/material.png")); + } + + @Test + void leadingSlashesIgnored() { + assertEquals(FlutterAssets.flatName("assets/a.png"), FlutterAssets.flatName("/assets/a.png")); + } + + @Test + void extensionSurvivesForNativeBundlers() { + assertTrue(FlutterAssets.flatName("assets/fonts/Roboto_Bold.ttf").endsWith(".ttf")); + } + + /** The doubling rule is what keeps '_' as a separator unambiguous. */ + @Test + void underscoreVersusSeparatorDoNotCollide() { + Set seen = new HashSet(); + String[] paths = { + "a/b.png", // separator + "a_b.png", // literal underscore at root + "a/b/c.png", + "a_b/c.png", + "a/b_c.png", + // Adjacent underscore and separator: the doubling rule encoded both + // of these as "a___b.png", and one asset overwrote the other. + "a_/b.png", + "a/_b.png", + "a__/b.png", + "a/__b.png", + "a_/_b.png", + }; + for (String p : paths) { + assertTrue(seen.add(FlutterAssets.flatName(p)), + "collision on " + p + " -> " + FlutterAssets.flatName(p)); + } + } + + @Test + void neverStartsWithReservedRawPrefix() { + assertTrue(FlutterAssets.flatName("raw/thing.png").startsWith("cn1f_")); + } + + // ------------------------------------------------------------------ + // Density variants + // ------------------------------------------------------------------ + + @Test + void variantsPreferTheSmallestAtLeastAsDenseAsTheScreen() { + String[] c = FlutterAssets.variantCandidates("assets/icons/material.png", 3); + assertEquals("assets/icons/3.0x/material.png", c[0]); + assertEquals("assets/icons/4.0x/material.png", c[1]); + } + + @Test + void variantsFallBackDownwardThenToTheUnscaledAsset() { + String[] c = FlutterAssets.variantCandidates("assets/icons/material.png", 3); + assertEquals("assets/icons/2.0x/material.png", c[2]); + assertEquals("assets/icons/1.5x/material.png", c[3]); + assertEquals("assets/icons/material.png", c[c.length - 1], + "the unscaled asset is always the last resort"); + } + + @Test + void aDenserScreenThanAnyVariantTakesTheDensestAvailable() { + String[] c = FlutterAssets.variantCandidates("a/b.png", 5); + assertEquals("a/4.0x/b.png", c[0]); + assertEquals("a/3.0x/b.png", c[1]); + } + + @Test + void anUnscaledScreenSkipsVariantsFirst() { + String[] c = FlutterAssets.variantCandidates("a/b.png", 1); + assertEquals("a/1.5x/b.png", c[0], "1.5x is the smallest variant at least as dense as 1x"); + assertEquals("a/b.png", c[c.length - 1]); + } + + @Test + void variantOfARootLevelAsset() { + assertEquals("2.0x/b.png", FlutterAssets.variantCandidates("b.png", 2)[0]); + } + + @Test + void ratioOfIdentifiesTheLoadedDensity() { + assertEquals(3.0, FlutterAssets.ratioOf("a/3.0x/b.png", "a/b.png"), 0.001); + assertEquals(1.5, FlutterAssets.ratioOf("a/1.5x/b.png", "a/b.png"), 0.001); + assertEquals(1.0, FlutterAssets.ratioOf("a/b.png", "a/b.png"), 0.001); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/InheritedDependencyTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/InheritedDependencyTest.java new file mode 100644 index 00000000000..51e28aff0ff --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/InheritedDependencyTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.InheritedWidget; +import com.codename1.flutter.widgets.Padding; + +import dart.core.DartList; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Reading an InheritedWidget creates a DEPENDENCY, and changing it rebuilds the readers. + * + *

The tree here is deliberately the shape the gallery actually uses — a StatefulWidget + * holding the value, an InheritedWidget publishing it, and a consumer several layers below + * — because a flatter arrangement does not exercise the thing that matters. An earlier + * attempt at this feature passed a flat version of these tests and rendered whole pages + * blank on a device, so "the harness agrees" is only worth something if the harness is + * shaped like the real tree.

+ */ +class InheritedDependencyTest { + + /** An inherited widget carrying one value, with the usual "did it change?" test. */ + static class Model extends InheritedWidget { + final int value; + + Model(int value, Widget child) { + this.value = value; + child(child); + } + + @Override + public boolean updateShouldNotify(InheritedWidget oldWidget) { + return ((Model) oldWidget).value != value; + } + } + + /** Reads the model on every build — the consumer, buried a few layers down. */ + static class Reader extends StatelessWidget { + int builds; + int lastSeen = -1; + + @Override + public Widget build(BuildContext context) { + builds++; + Model m = context.dependOnInheritedWidgetOfExactType(Model.class); + lastSeen = m == null ? -1 : m.value; + return new ProbeBox(1, 1); + } + } + + /** Wraps its child a few levels deep, so the consumer is not a direct child. */ + private static Widget buried(Widget child) { + Padding inner = new Padding(); + inner.padding(EdgeInsets.all(1)); + inner.child(child); + Column col = new Column(); + col.children(DartList.of((Widget) inner)); + Padding outer = new Padding(); + outer.padding(EdgeInsets.all(1)); + outer.child(col); + return outer; + } + + /** + * The gallery's ModelBinding: a StatefulWidget that owns the value and republishes it + * through an InheritedWidget on setState. + */ + static class Binding extends StatefulWidget { + final Reader reader; + + Binding(Reader reader) { + this.reader = reader; + } + + @Override + public State createState() { + return new BindingState(); + } + + class BindingState extends State { + int value = 1; + + void publish(int v) { + setState(() -> value = v); + } + + @Override + public Widget build(BuildContext context) { + return new Model(value, buried(widget().reader)); + } + } + } + + private BuildOwner owner; + + private Binding.BindingState mount(Reader reader) { + owner = new BuildOwner(); + Binding b = new Binding(reader); + Element root = FlutterUI.mount(b, new RenderHost(), owner); + return (Binding.BindingState) ((StatefulElement) root).state(); + } + + @Test + @DisplayName("a buried reader sees the new value when the model is republished") + void republishingRebuildsTheReader() { + Reader reader = new Reader(); + Binding.BindingState state = mount(reader); + assertEquals(1, reader.lastSeen, "the reader should see the initial value"); + + state.publish(2); + owner.flushSync(); + + assertEquals(2, reader.lastSeen, "the reader must see the NEW value"); + } + + @Test + @DisplayName("republishing the SAME value does not disturb the reader") + void anUnchangedValueDoesNotNotify() { + Reader reader = new Reader(); + Binding.BindingState state = mount(reader); + int buildsAfterMount = reader.builds; + + state.publish(1); // same value: updateShouldNotify returns false + owner.flushSync(); + + assertEquals(1, reader.lastSeen); + assertTrue(reader.builds >= buildsAfterMount, "no element lost"); + } + + @Test + @DisplayName("the subtree survives the notification - this is what blanked pages before") + void theSubtreeIsStillIntactAfterAChange() { + Reader reader = new Reader(); + Binding.BindingState state = mount(reader); + + state.publish(2); + owner.flushSync(); + state.publish(3); + owner.flushSync(); + + assertEquals(3, reader.lastSeen); + // The reader must still be MOUNTED and still producing its component. The previous + // attempt notified before this element's own subtree had been rebuilt, and the + // elements the dependents rebuilt into were unmounted moments later - which is + // exactly what an intact-looking test suite failed to catch. + assertNotNull(reader, "reader widget still referenced"); + assertTrue(reader.builds >= 3, "reader rebuilt on each change, got " + reader.builds); + } + + @Test + @DisplayName("several readers at different depths all see the change") + void everyDependentIsNotified() { + Reader shallow = new Reader(); + Reader deep = new Reader(); + owner = new BuildOwner(); + Binding b = new Binding(deep) { + }; + // Two readers under one model: one direct, one buried. + Column pair = new Column(); + pair.children(DartList.of((Widget) shallow, buried(deep))); + final Model[] held = new Model[1]; + StatelessWidget host = new StatelessWidget() { + @Override + public Widget build(BuildContext context) { + held[0] = new Model(9, pair); + return held[0]; + } + }; + FlutterUI.mount(host, new RenderHost(), owner); + + assertEquals(9, shallow.lastSeen); + assertEquals(9, deep.lastSeen); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/KeyIdentityTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/KeyIdentityTest.java new file mode 100644 index 00000000000..93db784c979 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/KeyIdentityTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.widgets.SizedBox; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Keys and locales compare the way Flutter compares them, and a GlobalKey +/// reaches the element its widget is mounted as. +public class KeyIdentityTest { + + static final class Probe extends StatefulWidget { + @Override + public State createState() { + return new ProbeState(); + } + } + + static final class ProbeState extends State { + @Override + public Widget build(BuildContext context) { + return new SizedBox(); + } + } + + @Test + public void separateUniqueKeysAreUnequal() { + UniqueKey a = new UniqueKey(); + UniqueKey b = new UniqueKey(); + assertNotEquals(a, b, "a fresh UniqueKey exists to force a new element"); + assertEquals(a, a); + Widget w1 = new SizedBox(); + w1.key(a); + Widget w2 = new SizedBox(); + w2.key(b); + assertFalse(Widget.canUpdate(w1, w2), "different unique keys must not reuse an element"); + } + + @Test + public void separateGlobalKeysAreUnequal() { + assertNotEquals(new GlobalKey(), new GlobalKey()); + } + + @Test + public void valueKeysStillCompareByValue() { + assertEquals(new ValueKey("a"), new ValueKey("a")); + } + + @Test + public void aGlobalKeyReachesItsMountedState() { + GlobalKey key = new GlobalKey(); + Probe probe = new Probe(); + probe.key(key); + Element root = FlutterUI.mount(probe, new RenderHost(), new BuildOwner()); + + assertTrue(key.currentState() instanceof ProbeState, "currentState is the mounted State"); + assertSame(root, key.currentContext()); + assertSame(probe, key.currentWidget()); + + FlutterUI.unmountTree(root); + assertNull(key.currentState(), "unmounting releases it"); + } + + @Test + public void aGlobalKeyFollowsTheNewConfigurationOfItsStatefulElement() { + GlobalKey key = new GlobalKey(); + Probe first = new Probe(); + first.key(key); + Element root = FlutterUI.mount(first, new RenderHost(), new BuildOwner()); + Object state = key.currentState(); + + Probe second = new Probe(); + second.key(key); + root.update(second); + + assertSame(second, key.currentWidget(), "currentWidget follows the update"); + assertSame(state, key.currentState(), "the State is kept"); + FlutterUI.unmountTree(root); + } + + @Test + public void localesCompareByValue() { + assertEquals(new Locale("en", "US"), new Locale("en", "US")); + assertEquals(new Locale("en", "US").hashCode(), new Locale("en", "US").hashCode()); + assertNotEquals(new Locale("en", "US"), new Locale("en", "GB")); + assertEquals(new Locale("en"), new Locale("en")); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/LayoutBuilderLatchTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/LayoutBuilderLatchTest.java new file mode 100644 index 00000000000..c92af431f04 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/LayoutBuilderLatchTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.widgets.LayoutBuilder; +import com.codename1.flutter.widgets.SizedBox; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A LayoutBuilder's builder must not be handed a speculative, unbounded box. + * + *

Codename One asks a box how wide it would like to be -- a real layout call, + * not a dry one, with the width unbounded -- before it lays the box out at the + * size it will occupy. A Flutter builder is allowed to LATCH: the gallery's + * 2D-transformations demo centres its board against the first + * {@code constraints.maxWidth} it is shown and never recomputes. So a builder + * that runs against infinity once has decided the screen, and the board drew in + * the corner for the life of the route.

+ */ +class LayoutBuilderLatchTest { + + /** The constraints each builder call was given, in order. */ + private final List seen = new ArrayList(); + + private LayoutBuilder recordingBuilder() { + LayoutBuilder lb = new LayoutBuilder(); + lb.builder(new dart.runtime.Funcs.Func2() { + @Override + public Widget call(BuildContext context, BoxConstraints constraints) { + seen.add(constraints); + return new SizedBox(); + } + }); + return lb; + } + + private RenderElement mount(Widget root) { + FlutterUI.mount(root, new RenderHost(), new BuildOwner()); + return null; + } + + @Test + void anUnboundedPassIsSatOutSoTheFirstBuildSeesTheRealBox() { + LayoutBuilder lb = recordingBuilder(); + RenderHost host = new RenderHost(); + FlutterUI.mount(lb, host, new BuildOwner()); + RenderElement r = host.rootRenderElement(); + + // How Codename One measures: real pass, width unbounded. + r.layout(new BoxConstraints(0, Double.POSITIVE_INFINITY, 0, 550)); + assertTrue(seen.isEmpty(), "the builder latched onto an unbounded measurement"); + + // Then the box it will actually occupy. + r.layout(BoxConstraints.tight(343, 550)); + assertEquals(1, seen.size(), "the builder should have run exactly once by now"); + assertEquals(343.0, seen.get(0).maxWidth(), 0.001); + assertEquals(550.0, seen.get(0).maxHeight(), 0.001); + } + + @Test + void aViewportChildBuildsIMMEDIATELY() { + // The regression this pins. A vertical list hands its child a TIGHT + // width and an unbounded height, and Flutter runs the builder against + // exactly that. Sitting it out returns a zero size the list then keeps, + // which emptied the reply study's whole mail list -- while the diff + // score went DOWN, because blank background differs from the reference + // less than mis-rendered cards do. + LayoutBuilder lb = recordingBuilder(); + RenderHost host = new RenderHost(); + FlutterUI.mount(lb, host, new BuildOwner()); + RenderElement r = host.rootRenderElement(); + + r.layout(new BoxConstraints(367, 367, 0, Double.POSITIVE_INFINITY)); + assertEquals(1, seen.size(), "a viewport child must not be sat out"); + assertEquals(367.0, seen.get(0).maxWidth(), 0.001); + } + + @Test + void aGenuinelyUnboundedLayoutStillBuilds() { + // A viewport's child really is unbounded and Flutter runs the builder + // against infinity, so sitting out MUST NOT mean never building. + LayoutBuilder lb = recordingBuilder(); + RenderHost host = new RenderHost(); + FlutterUI.mount(lb, host, new BuildOwner()); + RenderElement r = host.rootRenderElement(); + + BoxConstraints unbounded = new BoxConstraints(0, 300, 0, Double.POSITIVE_INFINITY); + r.layout(unbounded); + r.layout(unbounded); + assertEquals(1, seen.size(), "the builder never ran on a truly unbounded layout"); + assertTrue(Double.isInfinite(seen.get(0).maxHeight())); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/MaterialSwatchTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/MaterialSwatchTest.java new file mode 100644 index 00000000000..f9c7985756c --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/MaterialSwatchTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.codename1.generated.flutter.MaterialAccentColor; +import com.codename1.generated.flutter.MaterialColor; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/// Pins the material palettes to their real per-shade values. +/// +/// Every shade used to resolve to the swatch's primary, so the colors demo drew +/// each palette as one flat block of ten identical rows and nothing reported a +/// problem. A swatch that answers the same colour for every key is the failure +/// this guards against, which is why the distinctness assertions matter as much +/// as the spot values. +class MaterialSwatchTest { + + @Test + void redSwatchCarriesTheMaterialShades() { + MaterialColor red = Colors.red; + assertEquals(0xFFF44336L, red.value(), "primary"); + assertEquals(0xFFFFEBEEL, red.idx(50).value()); + assertEquals(0xFFE57373L, red.idx(300).value()); + assertEquals(0xFFF44336L, red.idx(500).value(), "500 is the primary"); + assertEquals(0xFFB71C1CL, red.idx(900).value()); + } + + @Test + void greyCarriesItsTwoExtraShades() { + // Grey is the one swatch with 350 and 850; a table built for exactly ten + // keys would silently answer the primary for those two. + assertEquals(0xFFEEEEEEL, Colors.grey.idx(200).value()); + assertEquals(0xFFD6D6D6L, Colors.grey.idx(350).value()); + assertEquals(0xFF303030L, Colors.grey.idx(850).value()); + } + + @Test + void accentSwatchCarriesItsFourShades() { + MaterialAccentColor pink = Colors.pinkAccent; + assertEquals(0xFFFF80ABL, pink.idx(100).value()); + assertEquals(0xFFFF4081L, pink.idx(200).value(), "200 is the primary"); + assertEquals(0xFFF50057L, pink.idx(400).value()); + assertEquals(0xFFC51162L, pink.idx(700).value()); + } + + @Test + void valueIsTheUnsignedArgbWordDartWouldPrint() { + // The colors demo renders color.value.toRadixString(16); a signed word + // makes every opaque colour negative and the demo prints "#000-1412". + assertTrue(Colors.red.value() > 0, "an opaque colour is a positive int in Dart"); + assertEquals("fff44336", Long.toString(Colors.red.value(), 16)); + } + + @Test + void everyShadeOfAPaletteIsDistinct() { + for (MaterialColor swatch : new MaterialColor[] { + Colors.red, Colors.pink, Colors.purple, Colors.deepPurple, + Colors.indigo, Colors.blue, Colors.lightBlue, Colors.cyan, + Colors.teal, Colors.green, Colors.lightGreen, Colors.lime, + Colors.yellow, Colors.amber, Colors.orange, Colors.deepOrange, + Colors.brown, Colors.grey, Colors.blueGrey }) { + Set seen = new HashSet(); + long[] keys = new long[] { 50, 100, 200, 300, 400, 500, 600, 700, 800, 900 }; + for (long key : keys) { + seen.add(Long.valueOf(swatch.idx(key).value())); + } + assertEquals((long) keys.length, (long) seen.size(), + "a palette must not repeat a colour across its shades"); + } + } + + @Test + void shadesGetDarkerAsTheKeyGrows() { + // Not a colour-science claim, just the Material invariant the demo shows: + // 50 is the lightest tint and 900 the darkest. + assertTrue(luminance(Colors.blue.idx(50)) > luminance(Colors.blue.idx(900))); + assertTrue(luminance(Colors.green.idx(100)) > luminance(Colors.green.idx(800))); + assertNotEquals(Colors.blue.idx(50).value(), Colors.blue.value()); + } + + private static long luminance(Color c) { + long v = c.value(); + return ((v >> 16) & 0xFF) * 3 + ((v >> 8) & 0xFF) * 6 + (v & 0xFF); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/MediaQueryTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/MediaQueryTest.java new file mode 100644 index 00000000000..f4de11c2b5c --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/MediaQueryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.Size; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MediaQueryData reports LOGICAL pixels: device pixels divided by the + * bucketed devicePixelRatio (Dp.scale()), matching Flutter. + */ +public class MediaQueryTest { + + @Test + public void sizeIsDevicePixelsDividedByRatio() { + MediaQueryData d = MediaQueryData.compute(1170, 2532, 3.0, Boolean.FALSE); + Size s = d.size(); + assertEquals(390.0, s.width(), 0.001); + assertEquals(844.0, s.height(), 0.001); + assertEquals(3.0, d.devicePixelRatio(), 0.001); + assertEquals(Brightness.light, d.platformBrightness()); + } + + @Test + public void ratioOneIsIdentity() { + MediaQueryData d = MediaQueryData.compute(800, 600, 1.0, null); + assertEquals(800.0, d.size().width(), 0.001); + assertEquals(600.0, d.size().height(), 0.001); + } + + @Test + public void darkModeFlagMapsToBrightness() { + assertEquals(Brightness.dark, + MediaQueryData.compute(100, 100, 2.0, Boolean.TRUE).platformBrightness()); + assertEquals(Brightness.light, + MediaQueryData.compute(100, 100, 2.0, null).platformBrightness(), + "unknown platform brightness defaults to light"); + } + + @Test + public void nonPositiveScaleFallsBackToOne() { + MediaQueryData d = MediaQueryData.compute(400, 400, 0, Boolean.FALSE); + assertEquals(1.0, d.devicePixelRatio(), 0.001); + assertEquals(400.0, d.size().width(), 0.001); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ReconciliationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ReconciliationTest.java new file mode 100644 index 00000000000..a421c83008b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ReconciliationTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.AltBox; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.testsupport.Toggler; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Element reconciliation driven synchronously via BuildOwner.flushSync (no + * Display, so nothing schedules on the EDT). + */ +class ReconciliationTest { + + private BuildOwner owner; + private RenderHost host; + + private Toggler.TogglerState mountToggler(Widget initialChild) { + owner = new BuildOwner(); + host = new RenderHost(); + Element root = FlutterUI.mount(new Toggler(initialChild), host, owner); + return (Toggler.TogglerState) ((StatefulElement) root).state(); + } + + @Test + void sameTypeRebuildReusesTheRenderElement() { + Toggler.TogglerState state = mountToggler(new ProbeBox(10, 10)); + assertEquals(1, state.initStateCalls); + + RenderElement before = host.rootRenderElement(); + assertTrue(before instanceof ProbeBox.ProbeBoxElement); + + final ProbeBox bigger = new ProbeBox(20, 20); + state.setState(() -> state.child = bigger); + owner.flushSync(); + + RenderElement after = host.rootRenderElement(); + assertSame(before, after, "same widget type must update the element in place"); + assertSame(bigger, after.widget(), "element must hold the new widget config"); + assertTrue(after.isMounted()); + + // The updated config drives layout. + assertEquals(new Size(20, 20), after.layout(BoxConstraints.loose(100, 100))); + } + + @Test + void differentTypeRebuildReplacesTheRenderElement() { + Toggler.TogglerState state = mountToggler(new ProbeBox(10, 10)); + RenderElement before = host.rootRenderElement(); + + state.setState(() -> state.child = new AltBox(5, 5)); + owner.flushSync(); + + RenderElement after = host.rootRenderElement(); + assertNotSame(before, after); + assertTrue(after instanceof AltBox.AltBoxElement); + assertFalse(before.isMounted(), "the replaced element must be unmounted"); + assertTrue(after.isMounted()); + } + + @Test + void statefulElementSurvivesParentRebuildAndDisposesOnRemoval() { + Toggler inner = new Toggler(new ProbeBox(1, 1)); + Toggler.TogglerState outer = mountToggler(inner); + + // Find the inner stateful element. + StatefulElement rootElement = (StatefulElement) host.rootElement(); + Element innerElement = rootElement.child(); + assertTrue(innerElement instanceof StatefulElement); + Toggler.TogglerState innerState = (Toggler.TogglerState) ((StatefulElement) innerElement).state(); + assertEquals(1, innerState.initStateCalls); + + // Parent rebuild with a same-type widget keeps the inner state alive. + state_setChild(outer, new Toggler(new ProbeBox(2, 2))); + assertSame(innerElement, rootElement.child(), "canUpdate match must reuse the stateful element"); + assertEquals(0, innerState.disposeCalls); + + // Replacing with a different type disposes the inner state. + state_setChild(outer, new AltBox(3, 3)); + assertEquals(1, innerState.disposeCalls); + assertFalse(innerElement.isMounted()); + } + + private void state_setChild(final Toggler.TogglerState s, final Widget w) { + s.setState(() -> s.child = w); + owner.flushSync(); + } + + @Test + void multiChildKeyedReconciliationReusesMovedChildren() { + ProbeBox a = keyed(new ProbeBox(10, 10), "a"); + ProbeBox b = keyed(new ProbeBox(20, 20), "b"); + ProbeBox c = keyed(new ProbeBox(30, 30), "c"); + + Column col1 = new Column(); + col1.children(DartList.of((Widget) a, b, c)); + + Toggler.TogglerState state = mountToggler(col1); + RenderElement flexBefore = host.rootRenderElement(); + List before = flexBefore.renderChildren(); + assertEquals(3, before.size()); + RenderElement elA = before.get(0); + RenderElement elC = before.get(2); + + // Reorder: c, a — b removed. + Column col2 = new Column(); + col2.children(DartList.of((Widget) keyed(new ProbeBox(30, 30), "c"), keyed(new ProbeBox(10, 10), "a"))); + state.setState(() -> state.child = col2); + owner.flushSync(); + + RenderElement flexAfter = host.rootRenderElement(); + assertSame(flexBefore, flexAfter); + List after = flexAfter.renderChildren(); + assertEquals(2, after.size()); + assertSame(elC, after.get(0), "keyed child c must be reused across the move"); + assertSame(elA, after.get(1), "keyed child a must be reused across the move"); + assertFalse(before.get(1).isMounted(), "removed keyed child b must be unmounted"); + } + + private static ProbeBox keyed(ProbeBox box, String key) { + box.key(new ValueKey(key)); + return box; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round10RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round10RuntimeTest.java new file mode 100644 index 00000000000..27b7815c108 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round10RuntimeTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.foundation.ChangeNotifier; +import com.codename1.flutter.foundation.Listeners; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// This round's runtime fixes: a listener removed during a notification is not +/// called, a disposed notifier stops mid-dispatch, and asset variants come from +/// the build's manifest at any scale. +class Round10RuntimeTest { + + @Test + void aListenerRemovedDuringDispatchIsNotCalled() { + final List listeners = new ArrayList(); + final List calls = new ArrayList(); + final Funcs.VoidFunc0 second = () -> calls.add("second"); + listeners.add(() -> { + calls.add("first"); + listeners.remove(second); + }); + listeners.add(second); + Listeners.notify(listeners); + assertEquals("[first]", calls.toString()); + } + + /** A notifier model, as an app's class mixing in ChangeNotifier is. */ + static final class Model implements ChangeNotifier { + void changed() { + notifyListeners(); + } + } + + @Test + void aNotifierDisposedByItsOwnListenerStopsThere() { + final Model model = new Model(); + final List calls = new ArrayList(); + model.addListener(() -> { + calls.add("first"); + model.dispose(); + }); + model.addListener(() -> calls.add("second")); + model.changed(); + assertEquals("[first]", calls.toString()); + } + + @Test + void theManifestGroupsVariantsAtAnyScale() { + Map> m = FlutterAssets.parseManifest( + "assets/a.png\nassets/2.5x/a.png\nassets/2x/a.png\npackages/p/assets/3.0x/only.png\nassets/notx/b.png\n"); + assertEquals(Arrays.asList("assets/a.png", "assets/2.5x/a.png", "assets/2x/a.png"), m.get("assets/a.png")); + assertEquals(Arrays.asList("packages/p/assets/3.0x/only.png"), m.get("packages/p/assets/only.png"), + "an asset bundled only as a variant is still found"); + assertEquals(Arrays.asList("assets/notx/b.png"), m.get("assets/notx/b.png")); + assertEquals(Arrays.asList("assets/a.png", "assets/2.5x/a.png", "assets/2x/a.png"), + Arrays.asList(FlutterAssets.variantCandidates("assets/a.png", 2.5, m))); + assertEquals(2.5, FlutterAssets.ratioOf("assets/2.5x/a.png", "assets/a.png"), 0.0); + assertEquals(2.0, FlutterAssets.ratioOf("assets/2x/a.png", "assets/a.png"), 0.0); + assertEquals(1.0, FlutterAssets.ratioOf("assets/a.png", "assets/a.png"), 0.0); + } + + @Test + void anAssetTheManifestDoesNotListFallsBackToTheFixedRatios() { + Map> m = FlutterAssets.parseManifest("assets/a.png\n"); + String[] c = FlutterAssets.variantCandidates("assets/staged.png", 2.0, m); + assertTrue(Arrays.asList(c).contains("assets/2.0x/staged.png")); + assertEquals("assets/staged.png", c[c.length - 1]); + } + + @Test + void onlyAPositiveDecimalAndAnXIsARatioDirectory() { + assertTrue(FlutterAssets.isRatioDir("2x")); + assertTrue(FlutterAssets.isRatioDir("2.5x")); + assertTrue(FlutterAssets.isRatioDir("10.0x")); + assertFalse(FlutterAssets.isRatioDir("x")); + assertFalse(FlutterAssets.isRatioDir("0x")); + assertFalse(FlutterAssets.isRatioDir(".5x")); + assertFalse(FlutterAssets.isRatioDir("2.x")); + assertFalse(FlutterAssets.isRatioDir("2.5.1x")); + assertFalse(FlutterAssets.isRatioDir("box")); + assertNull(FlutterAssets.parseManifest("").get("x")); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round13RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round13RuntimeTest.java new file mode 100644 index 00000000000..e679d746fae --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round13RuntimeTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.animation.AnimationController; +import com.codename1.flutter.animation.AnimationStatus; +import com.codename1.flutter.animation.IntTween; +import com.codename1.flutter.animation.ProxyAnimation; +import com.codename1.flutter.animation.ReverseAnimation; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Geometry and tween behaviour Flutter defines exactly, and animations derived from +/// another one forwarding its changes to their own listeners. +class Round13RuntimeTest { + + @Test + void aNaNRectIsNotFinite() { + assertFalse(Rect.fromLTRB(Double.NaN, 0, 10, 10).isFinite()); + assertFalse(Rect.fromLTRB(0, 0, Double.POSITIVE_INFINITY, 10).isFinite()); + assertTrue(Rect.fromLTRB(0, 0, 10, 10).isFinite()); + } + + @Test + void aNullBorderRadiusEndIsZero() { + BorderRadius r = BorderRadius.circular(10); + assertEquals(2.5, BorderRadius.lerp(null, r, 0.25).topLeft().x(), 1e-9, "null -> r scales by t"); + assertEquals(7.5, BorderRadius.lerp(r, null, 0.25).topLeft().x(), 1e-9, "r -> null scales by 1 - t"); + } + + @Test + void intTweenRoundsToTheNearest() { + IntTween t = new IntTween(); + t.begin(0); + t.end(1); + assertEquals(Integer.valueOf(1), t.lerp(0.6)); + assertEquals(Integer.valueOf(0), t.lerp(0.4)); + t.begin(0); + t.end(-1); + assertEquals(Integer.valueOf(-1), t.lerp(0.6), "rounding, not truncation toward zero"); + } + + @Test + void aReverseAnimationNotifiesItsListenersAndReversesStatus() { + AnimationController parent = new AnimationController(); + ReverseAnimation reverse = new ReverseAnimation(parent.view()); + final List values = new ArrayList(); + final List statuses = new ArrayList(); + Funcs.VoidFunc0 onValue = new Funcs.VoidFunc0() { + @Override + public void call() { + values.add(reverse.value()); + } + }; + reverse.addListener(onValue); + reverse.addStatusListener(new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + statuses.add(s); + } + }); + parent.value(0.25); + assertEquals(0.75, values.get(values.size() - 1), 1e-9); + parent.forward(); + assertTrue(statuses.contains(AnimationStatus.dismissed), "completed forward reads as dismissed: " + statuses); + reverse.removeListener(onValue); + int seen = values.size(); + parent.value(0.5); + assertEquals(seen, values.size(), "a removed listener hears nothing"); + } + + @Test + void aProxyAnimationForwardsItsParentAndAParentSwap() { + AnimationController a = new AnimationController(); + AnimationController b = new AnimationController(); + b.value(1.0); + ProxyAnimation proxy = new ProxyAnimation(a.view()); + final int[] notified = {0}; + proxy.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + notified[0]++; + } + }); + a.value(0.5); + assertEquals(1, notified[0], "the parent's change reaches the proxy's listener"); + proxy.parent(b.view()); + assertEquals(2, notified[0], "swapping to a parent at another value is a change"); + a.value(0.1); + assertEquals(2, notified[0], "the old parent is no longer followed"); + b.value(0.2); + assertEquals(3, notified[0]); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round14RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round14RuntimeTest.java new file mode 100644 index 00000000000..e02502ea34d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round14RuntimeTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.animation.AnimationController; +import com.codename1.flutter.foundation.Characters; +import com.codename1.flutter.material.Dialogs; +import com.codename1.flutter.material.Slider; +import com.codename1.flutter.material.SliderRenderElement; +import com.codename1.flutter.navigation.Navigator; +import com.codename1.flutter.provider.Selector; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// This round's runtime fixes: character counting, an awaitable dialog, Selector's +/// rebuild test, the slider's interaction callbacks and forward(from:). +class Round14RuntimeTest { + + @BeforeEach + void reset() { + Navigator.reset(); + Dialogs.reset(); + } + + @Test + void charactersAreUserPerceived() { + String family = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67"; // man ZWJ woman ZWJ girl + String flag = "\uD83C\uDDEE\uD83C\uDDF1"; // two regional indicators + String accented = "e\u0301"; // e + combining acute + String thumb = "\uD83D\uDC4D\uD83C\uDFFD"; // thumbs up + skin tone + assertEquals(1, Characters.count(family)); + assertEquals(1, Characters.count(flag)); + assertEquals(1, Characters.count(accented)); + assertEquals(1, Characters.count(thumb)); + assertEquals(3, Characters.count("a" + flag + "b")); + assertEquals("a" + flag, Characters.take("a" + flag + "b", 2), "never cut mid-character"); + assertEquals(family, Characters.take(family + family, 1)); + } + + @Test + void showDialogCompletesWithThePopResult() { + final Object[] result = {"pending"}; + Object f = Dialogs.showDialog(null, (context) -> new ProbeBox(5, 5)); + ((dart.async.Future) f).then(new Funcs.VoidFunc1() { + @Override + public void call(Object v) { + result[0] = v; + } + }); + assertEquals("pending", result[0], "still open"); + Navigator.pop(null, Boolean.TRUE); + assertEquals(Boolean.TRUE, result[0], "the future carries Navigator.pop's result"); + } + + /** A model the selector reads a slice of. */ + static final class Model implements com.codename1.flutter.foundation.ChangeNotifier { + String slice = "a"; + int unrelated; + + void touch() { + notifyListeners(); + } + } + + @Test + void aSelectorRebuildsOnlyWhenTheSelectionChanges() { + final int[] builds = {0}; + final Model model = new Model(); + Selector s = new Selector(); + s.providedType(Model.class); + s.selector(new Funcs.Func2() { + @Override + public String call(BuildContext c, Model m) { + return m.slice; + } + }); + s.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext c, String v, Widget child) { + builds[0]++; + return new ProbeBox(1, 1); + } + }); + com.codename1.flutter.provider.ChangeNotifierProvider p = + new com.codename1.flutter.provider.ChangeNotifierProvider(); + p.value(model); + p.child(s); + BuildOwner owner = new BuildOwner(); + FlutterUI.mount(p, new RenderHost(), owner); + assertEquals(1, builds[0]); + + model.unrelated++; + model.touch(); + owner.flushSync(); + assertEquals(1, builds[0], "an unrelated change leaves the selection, and the builder, alone"); + + model.slice = "b"; + model.touch(); + owner.flushSync(); + assertEquals(2, builds[0], "a changed selection rebuilds"); + + s.shouldRebuild(new Funcs.Func2() { + @Override + public Boolean call(String previous, String next) { + return Boolean.TRUE; + } + }); + model.touch(); + owner.flushSync(); + assertEquals(3, builds[0], "shouldRebuild overrides the equality test"); + } + + @Test + void aSliderReportsItsInteractionStartAndEnd() { + final List events = new ArrayList(); + Slider s = new Slider(); + s.value(2.0); + s.min(0.0); + s.max(10.0); + s.divisions(20L); + s.onChanged(v -> events.add("changed " + v)); + s.onChangeStart(v -> events.add("start " + v)); + s.onChangeEnd(v -> events.add("end " + v)); + SliderRenderElement e = (SliderRenderElement) FlutterUI.mount(s, new RenderHost(), new BuildOwner()); + e.userDragStarted(); + e.userDragged(15); + e.userDragEnded(); + e.userDragEnded(); + assertEquals("[start 2.0, changed 7.5, end 7.5]", events.toString()); + } + + @Test + void forwardFromNotifiesThroughTheValueSetter() { + AnimationController c = new AnimationController(); + final List seen = new ArrayList(); + c.addListener(() -> seen.add(c.value())); + c.forward(Double.valueOf(0.5)); + // Flutter's forward(from:) goes through the value setter, which announces the from + // value on its own before the run begins; the run then announces its frames (here, + // headless, its start and its end). Assigning the field dropped the setter's + // announcement, so dependants saw the from value only once the run had started. + assertEquals(java.util.Arrays.asList(0.5, 0.5, 1.0), seen); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round15RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round15RuntimeTest.java new file mode 100644 index 00000000000..52c4a97438b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round15RuntimeTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.foundation.Characters; +import com.codename1.flutter.provider.ChangeNotifierProvider; +import com.codename1.flutter.provider.Selector; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/// Combining marks of every script extend a character; a Selector mounted twice keeps +/// a cache per place; ObjectKey follows Dart's identical for boxed numbers. +class Round15RuntimeTest { + + @Test + void combiningMarksOfEveryScriptExtendTheCharacter() { + assertEquals(1, Characters.count("\u05D0\u05B0"), "Hebrew alef with sheva"); + assertEquals(1, Characters.count("\u0628\u064E"), "Arabic beh with fatha"); + assertEquals(1, Characters.count("\u0915\u093F"), "Devanagari ka with the i vowel sign"); + assertEquals(1, Characters.count("\u1100\u1161\u11A8"), "a Hangul syllable written as jamo"); + assertEquals("\u05D0\u05B0", Characters.take("\u05D0\u05B0\u05D1", 1)); + assertEquals(2, Characters.count("ab")); + } + + static final class Model implements com.codename1.flutter.foundation.ChangeNotifier { + String slice = "same"; + + void touch() { + notifyListeners(); + } + } + + @Test + void oneSelectorMountedTwiceCachesPerPlace() { + final List built = new ArrayList(); + Model model = new Model(); + Selector s = new Selector(); + s.providedType(Model.class); + s.selector(new Funcs.Func2() { + @Override + public String call(BuildContext c, Model m) { + return m.slice; + } + }); + s.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext c, String v, Widget child) { + built.add(v); + return new ProbeBox(1, 1); + } + }); + Column col = new Column(); + DartList kids = new DartList(); + kids.add(s); + kids.add(s); // the SAME widget instance in two places + col.children(kids); + ChangeNotifierProvider p = new ChangeNotifierProvider(); + p.value(model); + p.child(col); + BuildOwner owner = new BuildOwner(); + FlutterUI.mount(p, new RenderHost(), owner); + assertEquals(2, built.size(), "each place runs its own builder: " + built); + model.touch(); + owner.flushSync(); + assertEquals(2, built.size(), "an unchanged selection rebuilds neither place"); + } + + @Test + void objectKeysOfEqualBoxedNumbersAreTheSameKey() { + ObjectKey a = new ObjectKey(Long.valueOf(100000)); + ObjectKey b = new ObjectKey(Long.valueOf(100000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(new ObjectKey(new StringBuilder("x").toString()), new ObjectKey("x"), + "an object other than a number is still compared by identity"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round17RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round17RuntimeTest.java new file mode 100644 index 00000000000..7b240ca8bd8 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round17RuntimeTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.intl.DateFormat; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.StatefulBuilder; + +import dart.core.DateTime; +import dart.runtime.Funcs; + +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// setState in a StatefulBuilder rebuilds it; viewPadding carries the safe area; a UTC +/// time formats as itself even in the device's daylight-saving gap. +class Round17RuntimeTest { + + @Test + void statefulBuildersSetStateRebuildsIt() { + final int[] count = {0}; + final int[] builds = {0}; + final Object[] setter = new Object[1]; + StatefulBuilder sb = new StatefulBuilder(); + sb.builder(new Funcs.Func2, Widget>() { + @Override + public Widget call(BuildContext c, Funcs.VoidFunc1 setState) { + builds[0]++; + setter[0] = setState; + return new ProbeBox(1, 1); + } + }); + BuildOwner owner = new BuildOwner(); + FlutterUI.mount(sb, new RenderHost(), owner); + assertEquals(1, builds[0]); + @SuppressWarnings("unchecked") + Funcs.VoidFunc1 setState = (Funcs.VoidFunc1) setter[0]; + setState.call(() -> count[0]++); + owner.flushSync(); + assertEquals(1, count[0]); + assertEquals(2, builds[0], "the builder ran again after setState"); + } + + @Test + void viewPaddingCarriesTheSafeArea() { + MediaQueryData q = new MediaQueryData(new Size(400, 800), 1.0, Brightness.light, 1.0, + EdgeInsets.only(0, 44, 0, 34)); + assertEquals(44.0, q.viewPadding().top(), 0.0); + assertEquals(34.0, q.viewPadding().bottom(), 0.0); + } + + @Test + void aUtcTimeInTheLocalDaylightSavingGapFormatsAsItself() { + TimeZone saved = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/New_York")); + try { + // 02:30 on the day New York springs forward does not exist locally. + DateTime t = DateTime.utc(2024, 3, 10, 2, 30, 0, 0, 0); + assertEquals("02:30", DateFormat.Hm(null).format(t)); + } finally { + TimeZone.setDefault(saved); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round4RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round4RuntimeTest.java new file mode 100644 index 00000000000..d937944e037 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round4RuntimeTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.flutter.intl.NumberFormat; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// ValueNotifier uses Dart equality, stop(canceled: false) completes the run, and a +/// currency format honours an explicit zero digits. +class Round4RuntimeTest { + + @Test + void aNotifierComparesWithDartEquality() { + ValueNotifier n = new ValueNotifier(Long.valueOf(1)); + final int[] notified = {0}; + n.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + notified[0]++; + } + }); + n.value(Double.valueOf(1.0)); + assertEquals(0, notified[0], "1 == 1.0 in Dart: no change"); + n.value(Double.valueOf(Double.NaN)); + n.value(Double.valueOf(Double.NaN)); + assertEquals(2, notified[0], "NaN is never equal to itself, so both writes notify"); + } + + @Test + void anExplicitZeroDigitsIsHonoured() { + assertEquals("$12", NumberFormat.currency(null, "$", Long.valueOf(0), null).format(Long.valueOf(12))); + assertEquals("$12.00", NumberFormat.currency(null, "$", null, null).format(Long.valueOf(12)), + "omitted, it defaults to two"); + } + + @Test + void aUtcValueFormatsInUtc() { + java.util.TimeZone saved = java.util.TimeZone.getDefault(); + try { + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/New_York")); + String hm = com.codename1.flutter.intl.DateFormat.Hm(null) + .format(dart.core.DateTime.utc(2024, 1, 15, 12, 0, 0, 0, 0)); + assertEquals("12:00", hm, "not shifted by the device's offset"); + } finally { + java.util.TimeZone.setDefault(saved); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round6InheritedTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round6InheritedTest.java new file mode 100644 index 00000000000..27fa817532e --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/Round6InheritedTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.foundation.ChangeNotifier; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.InheritedWidget; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// A nested inherited widget is visible to its first children even under a non-empty +/// inherited map; a dependent State hears didChangeDependencies; and a ChangeNotifier's +/// listener entry goes when its last listener does. +class Round6InheritedTest { + + static final class Outer extends InheritedWidget { + Outer(Widget child) { + child(child); + } + } + + static final class Inner extends InheritedWidget { + final int value; + + Inner(int value, Widget child) { + this.value = value; + child(child); + } + + @Override + public boolean updateShouldNotify(InheritedWidget old) { + return ((Inner) old).value != value; + } + } + + static final class Reader extends StatelessWidget { + Inner seen; + + @Override + public Widget build(BuildContext context) { + seen = context.dependOnInheritedWidgetOfExactType(Inner.class); + return new ProbeBox(1, 1); + } + } + + /** + * Builds Outer(first nothing, then Inner(reader)). Outer has published by the time it + * is rebuilt, so the Inner it then mounts starts from a NON-EMPTY inherited map -- + * what every subtree in a real app has. On an initial mount the maps are still empty, + * lookups fall back to walking ancestors, and the publication order never shows. + */ + static final class Reveal extends StatefulWidget { + final Reader reader; + + Reveal(Reader reader) { + this.reader = reader; + } + + @Override + public State createState() { + return new RevealState(); + } + } + + static final class RevealState extends State { + boolean show; + + void reveal() { + setState(() -> show = true); + } + + @Override + public Widget build(BuildContext context) { + return new Outer(show ? new Inner(5, widget().reader) : new ProbeBox(1, 1)); + } + } + + @Test + void aNestedInheritedWidgetIsSeenByItsFirstChildren() { + Reader r = new Reader(); + BuildOwner owner = new BuildOwner(); + Element root = FlutterUI.mount(new Reveal(r), new RenderHost(), owner); + ((RevealState) ((StatefulElement) root).state()).reveal(); + owner.flushSync(); + assertTrue(r.seen != null, "the reader must find the Inner it was mounted under"); + assertEquals(5, r.seen.value); + } + + static final class Host extends StatefulWidget { + @Override + public State createState() { + return new HostState(); + } + } + + static final class HostState extends State { + int value = 1; + final Dependent dependent = new Dependent(); + + void publish(int v) { + setState(() -> value = v); + } + + @Override + public Widget build(BuildContext context) { + return new Inner(value, dependent); + } + } + + static final class Dependent extends StatefulWidget { + static int changes; + + @Override + public State createState() { + return new State() { + @Override + public void didChangeDependencies() { + changes++; + } + + @Override + public Widget build(BuildContext context) { + context.dependOnInheritedWidgetOfExactType(Inner.class); + return new ProbeBox(1, 1); + } + }; + } + } + + @Test + void aDependentStateHearsDidChangeDependencies() { + Dependent.changes = 0; + BuildOwner owner = new BuildOwner(); + Element root = FlutterUI.mount(new Host(), new RenderHost(), owner); + assertEquals(1, Dependent.changes, "once after initState, as in Flutter"); + ((HostState) ((StatefulElement) root).state()).publish(2); + owner.flushSync(); + assertEquals(2, Dependent.changes, "and again when the inherited widget it read changed"); + } + + static final class Model implements ChangeNotifier { + } + + @Test + void aNotifiersEntryGoesWithItsLastListener() { + Model m = new Model(); + Funcs.VoidFunc0 l = new Funcs.VoidFunc0() { + @Override + public void call() { + } + }; + m.notifyListeners(); + assertFalse(m.hasListeners(), "notifying with no listeners creates nothing"); + m.addListener(l); + assertTrue(m.hasListeners()); + m.removeListener(l); + assertFalse(m.hasListeners(), "and removing the last listener removes the entry"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/SafeAreaTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/SafeAreaTest.java new file mode 100644 index 00000000000..9593c83cf46 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/SafeAreaTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.MediaQuery; +import com.codename1.flutter.widgets.SafeArea; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * SafeArea INSETS its child, and MediaQuery reports the device's real insets. + * + *

Both were zero: padding defaulted to nothing and SafeArea rendered its child through. + * The result is a class of bug no error report can see — content under the notch, the status + * bar and the home indicator, and controls near the bottom edge that cannot be reached. The + * app throws nothing and looks plausible in a screenshot taken on a device without a notch, + * which is why a clean sweep said nothing about it.

+ */ +class SafeAreaTest { + + /** A MediaQuery with the insets a notched phone reports. */ + private static Widget withInsets(Widget child, double top, double bottom) { + MediaQuery q = new MediaQuery(); + q.data(new MediaQueryData(new Size(400, 800), 1.0, Brightness.light, 1.0, + EdgeInsets.fromLTRB(0, top, 0, bottom))); + q.child(child); + return q; + } + + /** + * Lays the tree out in a full-screen box and returns the CONTENT's render element. + * + *

The content, not the root: under a tight constraint the outer box still fills the + * screen — what a SafeArea changes is where the child sits inside it. Asserting the + * root's size would pass whether or not anything was inset, which is the same mistake + * that let this bug through in the first place.

+ */ + private static RenderElement contentOf(Widget root, double w, double h) { + Element e = FlutterUI.mount(root, new RenderHost(), new BuildOwner()); + RenderElement r = RenderElement.findRenderElement(e); + r.layout(BoxConstraints.tight(w, h)); + r.position(0, 0); + return deepest(r); + } + + /** The ProbeBox at the bottom of the subtree. */ + private static RenderElement deepest(RenderElement from) { + final RenderElement[] found = {null}; + from.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Element child) { + RenderElement r = RenderElement.findRenderElement(child); + if (r != null && r.widget() instanceof ProbeBox) { + found[0] = r; + } else if (r != null && found[0] == null) { + RenderElement deeper = deepest(r); + if (deeper != null && deeper.widget() instanceof ProbeBox) { + found[0] = deeper; + } + } + } + }); + return found[0] != null ? found[0] : from; + } + + @Test + @DisplayName("the metric math reports the safe-area insets it is given") + void computeCarriesThePadding() { + MediaQueryData d = MediaQueryData.compute(1125, 2436, 3.0, Boolean.FALSE, + EdgeInsets.fromLTRB(0, 47, 0, 34)); + + assertEquals(47, d.padding().top(), 1e-9); + assertEquals(34, d.padding().bottom(), 1e-9); + } + + @Test + @DisplayName("with no insets given the padding is zero, as before") + void computeDefaultsToZero() { + assertEquals(0, MediaQueryData.compute(400, 800, 1.0, Boolean.FALSE).padding().top(), + 1e-9); + } + + @Test + @DisplayName("a SafeArea shrinks its child by the top and bottom insets") + void safeAreaInsetsItsChild() { + SafeArea area = new SafeArea(); + area.child(new ProbeBox(400, 800)); + + RenderElement r = contentOf(withInsets(area, 47, 34), 400, 800); + + // Pushed clear of the notch, and short by both intrusions. + assertEquals(47, r.y(), "the content must start below the status bar"); + assertEquals(800 - 47 - 34, r.size().height(), 1e-9, + "and must not run under the home indicator"); + } + + @Test + @DisplayName("a disabled edge is not inset") + void aDisabledEdgeIsNotInset() { + SafeArea area = new SafeArea(); + area.bottom(false); + area.child(new ProbeBox(400, 800)); + + RenderElement r = contentOf(withInsets(area, 47, 34), 400, 800); + + assertEquals(47, r.y()); + assertEquals(800 - 47, r.size().height(), 1e-9, "only the top is avoided"); + } + + @Test + @DisplayName("minimum wins when the device reports less") + void minimumIsRespected() { + SafeArea area = new SafeArea(); + area.minimum(EdgeInsets.fromLTRB(0, 20, 0, 20)); + area.child(new ProbeBox(400, 800)); + + RenderElement r = contentOf(withInsets(area, 5, 5), 400, 800); + + assertEquals(20, r.y()); + assertEquals(800 - 20 - 20, r.size().height(), 1e-9); + } + + @Test + @DisplayName("with no intrusions a SafeArea costs nothing") + void noInsetsMeansNoWrapper() { + SafeArea area = new SafeArea(); + area.child(new ProbeBox(400, 800)); + + RenderElement r = contentOf(withInsets(area, 0, 0), 400, 800); + + assertEquals(0, r.y()); + assertEquals(800, r.size().height(), 1e-9); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollPhysicsPropsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollPhysicsPropsTest.java new file mode 100644 index 00000000000..66b9bd5459a --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollPhysicsPropsTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Hashtable; +import java.util.Map; + +/** + * The scroll-physics constants are installed as CONSTANTS. + * + *

This looks like a test of a spelling, and it is — but it is the spelling that decides + * whether the values do anything at all. {@code UIManager.buildTheme} sorts an incoming + * props table by the leading {@code @}: keys that have it become theme constants, keys + * that do not become ordinary style properties. Both are accepted silently, so a constant + * written without the prefix is stored, never read, and {@code getThemeConstant} goes on + * returning its default.

+ * + *

That is not hypothetical. The fling-distance constant was installed without the + * prefix and had no effect for the whole time it was believed to be fixing the scroll — + * on a device, where the only symptom was that the feel never quite matched and no error + * was ever raised.

+ */ +class ScrollPhysicsPropsTest { + + @Test + @DisplayName("every physics key is prefixed, or the value is silently ignored") + void everyKeyIsAThemeConstant() { + Hashtable props = FlutterUI.scrollPhysicsProps(); + assertTrue(!props.isEmpty(), "expected some physics constants"); + for (Map.Entry e : props.entrySet()) { + assertTrue(e.getKey().startsWith("@"), + "'" + e.getKey() + "' is missing the @ that makes it a theme constant, " + + "so UIManager will file it as a style property and " + + "getThemeConstant will keep returning the default"); + } + } + + @Test + @DisplayName("the fling distance matches Flutter's friction simulation") + void flingDistanceIsFlutterst() { + // CN1 coasts velocity * this/1000; Flutter's iOS FrictionSimulation (drag 0.135) + // travels -v/ln(0.135) = 0.4994 * v. 950 - the framework default - is 1.9x too far. + assertEquals("500", FlutterUI.scrollPhysicsProps().get("@DecayMotionScaleFactorInt")); + } + + @Test + @DisplayName("the rubber band matches BouncingScrollPhysics") + void rubberBandIsFluttersCoefficient() { + // Hundredths. Integrating Flutter's per-delta friction gives CN1's closed form with + // c = 0.52; the framework default is UIScrollView's 0.55. See RubberBandParityTest. + assertEquals("52", FlutterUI.scrollPhysicsProps().get("@rubberBandCoefficientInt")); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollablesTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollablesTest.java new file mode 100644 index 00000000000..d9ed82fdce6 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ScrollablesTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.GridView; +import com.codename1.flutter.widgets.ListView; +import com.codename1.flutter.widgets.ScrollRenderElement; +import com.codename1.flutter.widgets.SingleChildScrollView; + +import dart.core.DartList; +import dart.core.UnsupportedError; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Scrollable boundary layout (SingleChildScrollView/ListView/GridView), + * headless: the content subtree is laid out with a tight viewport width and + * an unbounded main axis; ListView.builder materializes its items eagerly. + */ +class ScrollablesTest { + + private RenderHost host; + + private ScrollRenderElement mountAndLayout(Widget root, BoxConstraints constraints) { + BuildOwner owner = new BuildOwner(); + host = new RenderHost(); + FlutterUI.mount(root, host, owner); + ScrollRenderElement r = (ScrollRenderElement) host.rootRenderElement(); + r.layout(constraints); + r.position(0, 0); + return r; + } + + private static RenderElement contentOf(ScrollRenderElement scroll) { + return RenderElement.findRenderElement(scroll.contentElement()); + } + + @Test + void builderMaterializesItemCountChildrenEagerly() { + final List builtIndexes = new ArrayList(); + final List contexts = new ArrayList(); + ListView lv = ListView.builder(null, 5L, (c, i) -> { + builtIndexes.add(i); + contexts.add(c); + return new ProbeBox(10, 20); + }, null, null, null, null, null, null, null, null, null); + + ScrollRenderElement scroll = mountAndLayout(lv, BoxConstraints.tight(100, 50)); + + assertEquals(List.of(0L, 1L, 2L, 3L, 4L), builtIndexes); + assertSame(scroll, contexts.get(0), "the scroll element is the builder's BuildContext"); + + RenderElement content = contentOf(scroll); + assertNotNull(content); + assertEquals(5, content.renderChildren().size()); + // stretched to the viewport width, stacked vertically, taller than + // the 50px viewport (that's what scrolls) + assertEquals(new Size(100, 100), content.size()); + assertEquals(new Size(100, 50), scroll.size()); + } + + @Test + void builderWithoutItemCountThrowsUnsupportedError() { + assertThrows(UnsupportedError.class, + () -> ListView.builder(null, null, (c, i) -> new ProbeBox(1, 1), + null, null, null, null, null, null, null, null, null)); + } + + @Test + void childrenModeStacksChildrenWithPadding() { + ListView lv = new ListView(); + lv.padding(EdgeInsets.all(5)); + lv.children(DartList.of((Widget) new ProbeBox(10, 10), new ProbeBox(10, 10), new ProbeBox(10, 10))); + + ScrollRenderElement scroll = mountAndLayout(lv, BoxConstraints.tight(100, 200)); + assertEquals(new Size(100, 200), scroll.size()); + + RenderElement content = contentOf(scroll); + // padding wrapper: 3 * 10 high + 10 padding, viewport-wide + assertEquals(new Size(100, 40), content.size()); + + // headless positioning goes through the scroll origin + List items = itemsOf(content); + assertEquals(3, items.size()); + assertEquals(5, items.get(0).x()); + assertEquals(5, items.get(0).y()); + assertEquals(15, items.get(1).y()); + assertEquals(25, items.get(2).y()); + // stretched to the padded viewport width + assertEquals(90.0, items.get(0).size().width()); + } + + @Test + void shrinkWrapSizesTheMainAxisToTheContent() { + ListView lv = new ListView(); + lv.shrinkWrap(true); + lv.children(DartList.of((Widget) new ProbeBox(10, 10), new ProbeBox(10, 30))); + + ScrollRenderElement scroll = mountAndLayout(lv, BoxConstraints.loose(100, 500)); + assertEquals(new Size(100, 40), scroll.size()); + } + + @Test + void singleChildScrollViewGivesTheChildAnUnboundedMainAxis() { + SingleChildScrollView sv = new SingleChildScrollView(); + sv.child(new ProbeBox(50, 1000)); + + ScrollRenderElement scroll = mountAndLayout(sv, BoxConstraints.tight(100, 200)); + assertEquals(new Size(100, 200), scroll.size()); + RenderElement content = contentOf(scroll); + // tight viewport width, free height + assertEquals(new Size(100, 1000), content.size()); + } + + @Test + void gridViewCountLaysOutRowsOfTightCells() { + DartList cells = new DartList(); + for (int i = 0; i < 6; i++) { + cells.add(new ProbeBox(1, 1)); + } + GridView gv = GridView.count(null, null, null, null, 2L, null, 10.0, 20.0, null, cells); + + ScrollRenderElement scroll = mountAndLayout(gv, BoxConstraints.tight(220, 500)); + RenderElement content = contentOf(scroll); + + // cellW = (220 - 20) / 2 = 100, ratio 1 -> cellH = 100 + List kids = content.renderChildren(); + assertEquals(6, kids.size()); + assertEquals(new Size(100, 100), kids.get(0).size()); + assertEquals(0, kids.get(0).x()); + assertEquals(0, kids.get(0).y()); + assertEquals(120, kids.get(1).x()); + assertEquals(0, kids.get(1).y()); + assertEquals(0, kids.get(2).x()); + assertEquals(110, kids.get(2).y()); + assertEquals(120, kids.get(3).x()); + assertEquals(110, kids.get(3).y()); + // 3 rows: 3*100 + 2*10 spacing + assertEquals(new Size(220, 320), content.size()); + } + + @Test + void gridViewAspectRatioShrinksTheCellHeight() { + DartList cells = new DartList(); + cells.add(new ProbeBox(1, 1)); + cells.add(new ProbeBox(1, 1)); + GridView gv = GridView.count(null, null, null, null, 2L, 2.0, null, null, null, cells); + + ScrollRenderElement scroll = mountAndLayout(gv, BoxConstraints.tight(200, 500)); + RenderElement content = contentOf(scroll); + // cellW = 100, ratio 2 -> cellH = 50 + assertEquals(new Size(100, 50), content.renderChildren().get(0).size()); + assertEquals(new Size(200, 50), content.size()); + } + + /** + * The probe items inside the ListView content (descending through the + * padding wrapper and the synthesized column). + */ + private static List itemsOf(RenderElement content) { + // content is the Padding render element; its child is the Column + RenderElement column = content.renderChildren().get(0); + return column.renderChildren(); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/StackLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/StackLayoutTest.java new file mode 100644 index 00000000000..8bfc07fa794 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/StackLayoutTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Positioned; +import com.codename1.flutter.widgets.Stack; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Stack/Positioned layout math per Flutter's RenderStack, driven headless. + */ +class StackLayoutTest { + + private RenderElement mountAndLayout(Widget root, BoxConstraints constraints) { + BuildOwner owner = new BuildOwner(); + RenderHost host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(constraints); + r.position(0, 0); + return r; + } + + @Test + void stackSizesToBiggestNonPositionedChildUnderLooseConstraints() { + Stack stack = new Stack(); + stack.children(DartList.of((Widget) new ProbeBox(100, 50), new ProbeBox(60, 120))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.loose(400, 600)); + assertEquals(new Size(100, 120), root.size()); + } + + @Test + void stackExpandsUnderTightConstraints() { + Stack stack = new Stack(); + stack.children(DartList.of((Widget) new ProbeBox(10, 10))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + assertEquals(new Size(400, 600), root.size()); + } + + @Test + void expandFitForcesNonPositionedChildrenToFillTheStack() { + // The gallery's study card: a Stack(fit: expand) over an image that also declares + // its own height. Flutter's tight constraints win, so the image covers the card; + // loosening instead left it at its own size, framed by the surface behind it. + Stack stack = new Stack(); + stack.fit(StackFit.expand); + stack.children(DartList.of((Widget) new ProbeBox(100, 240))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(296, 208)); + assertEquals(new Size(296, 208), root.size()); + assertEquals(new Size(296, 208), root.renderChildren().get(0).size(), + "expand must override the child's own size"); + } + + @Test + void looseFitLeavesTheChildItsOwnSize() { + // Same tree, default fit - the contrast that makes the case above meaningful. + Stack stack = new Stack(); + stack.children(DartList.of((Widget) new ProbeBox(100, 240))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(296, 208)); + assertEquals(new Size(296, 208), root.size()); + assertEquals(new Size(100, 208), root.renderChildren().get(0).size()); + } + + @Test + void passthroughFitHandsTheChildTheStacksOwnConstraints() { + Stack stack = new Stack(); + stack.fit(StackFit.passthrough); + stack.children(DartList.of((Widget) new ProbeBox(100, 50))); + + // Min constraints reach the child untouched, unlike loose. + RenderElement root = mountAndLayout(stack, new BoxConstraints(200, 400, 120, 600)); + assertEquals(new Size(200, 120), root.renderChildren().get(0).size()); + } + + @Test + void expandUnderAnUnboundedAxisDegradesToLooseRatherThanGoingInfinite() { + Stack stack = new Stack(); + stack.fit(StackFit.expand); + stack.children(DartList.of((Widget) new ProbeBox(100, 50))); + + // Unbounded height: there is no biggest to be tight to. + RenderElement root = mountAndLayout(stack, + new BoxConstraints(0, 400, 0, Double.POSITIVE_INFINITY)); + assertEquals(new Size(400, 50), root.renderChildren().get(0).size()); + assertEquals(new Size(400, 50), root.size()); + } + + @Test + void positionedChildrenIgnoreTheFit() { + // fit only governs NON-positioned children - a Positioned child still resolves + // against its own insets. + Stack stack = new Stack(); + stack.fit(StackFit.expand); + Positioned p = new Positioned(); + p.left(10.0); + p.top(20.0); + p.child(new ProbeBox(30, 30)); + stack.children(DartList.of((Widget) p, new ProbeBox(50, 50))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(0); + assertEquals(new Size(30, 30), positioned.size()); + assertEquals(10, positioned.x()); + assertEquals(20, positioned.y()); + assertEquals(new Size(400, 600), root.renderChildren().get(1).size()); + } + + @Test + void stackWithOnlyPositionedChildrenExpandsToBoundedAxes() { + Stack stack = new Stack(); + Positioned p = new Positioned(); + p.left(10.0); + p.top(10.0); + p.child(new ProbeBox(30, 30)); + stack.children(DartList.of((Widget) p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.loose(400, 600)); + assertEquals(new Size(400, 600), root.size()); + } + + @Test + void nonPositionedChildrenArePlacedByStackAlignment() { + Stack stack = new Stack(); + stack.alignment(Alignment.center); + stack.children(DartList.of((Widget) new ProbeBox(100, 50))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement kid = root.renderChildren().get(0); + assertEquals(150, kid.x()); + assertEquals(275, kid.y()); + + Stack bottomRight = new Stack(); + bottomRight.alignment(Alignment.bottomRight); + bottomRight.children(DartList.of((Widget) new ProbeBox(100, 50))); + RenderElement root2 = mountAndLayout(bottomRight, BoxConstraints.tight(400, 600)); + RenderElement kid2 = root2.renderChildren().get(0); + assertEquals(300, kid2.x()); + assertEquals(550, kid2.y()); + } + + @Test + void positionedLeftTopInsetsPlaceTheChild() { + Stack stack = new Stack(); + Positioned p = new Positioned(); + p.left(10.0); + p.top(20.0); + p.child(new ProbeBox(30, 40)); + stack.children(DartList.of((Widget) new ProbeBox(400, 600), p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(1); + assertEquals(10, positioned.x()); + assertEquals(20, positioned.y()); + assertEquals(new Size(30, 40), positioned.size()); + } + + @Test + void positionedRightBottomInsetsResolveAgainstStackBounds() { + Stack stack = new Stack(); + Positioned p = new Positioned(); + p.right(10.0); + p.bottom(5.0); + p.child(new ProbeBox(30, 40)); + stack.children(DartList.of((Widget) p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(0); + assertEquals(360, positioned.x()); + assertEquals(555, positioned.y()); + } + + @Test + void opposingInsetsTightenTheAxis() { + Stack stack = new Stack(); + Positioned p = new Positioned(); + p.left(10.0); + p.right(10.0); + p.top(0.0); + p.child(new ProbeBox(30, 40)); + stack.children(DartList.of((Widget) p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(0); + // left+right force the width to 400 - 10 - 10. + assertEquals(380.0, positioned.size().width()); + assertEquals(10, positioned.x()); + assertEquals(0, positioned.y()); + } + + @Test + void explicitExtentPlusOneInsetPlacesFromTheOppositeEdge() { + Stack stack = new Stack(); + Positioned p = new Positioned(); + p.width(50.0); + p.right(0.0); + p.top(10.0); + p.height(20.0); + p.child(new ProbeBox(5, 5)); + stack.children(DartList.of((Widget) p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(0); + assertEquals(new Size(50, 20), positioned.size()); + assertEquals(350, positioned.x()); + assertEquals(10, positioned.y()); + } + + @Test + void unresolvedPositionedAxisFallsBackToAlignment() { + Stack stack = new Stack(); + stack.alignment(Alignment.center); + Positioned p = new Positioned(); + p.left(10.0); + p.child(new ProbeBox(30, 40)); + stack.children(DartList.of((Widget) p)); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + RenderElement positioned = root.renderChildren().get(0); + assertEquals(10, positioned.x()); + // vertical axis unresolved: centered + assertEquals(280, positioned.y()); + } + + @Test + void zOrderMatchesChildOrder() { + Stack stack = new Stack(); + stack.children(DartList.of((Widget) new ProbeBox(50, 50), new ProbeBox(60, 60), new ProbeBox(70, 70))); + + RenderElement root = mountAndLayout(stack, BoxConstraints.tight(400, 600)); + List kids = root.renderChildren(); + // renderChildren is tree order == paint order (later on top) + assertEquals(50.0, kids.get(0).size().width()); + assertEquals(60.0, kids.get(1).size().width()); + assertEquals(70.0, kids.get(2).size().width()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/TextWrapTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/TextWrapTest.java new file mode 100644 index 00000000000..7f121285e47 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/TextWrapTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.widgets.TextRenderElement; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Word-wrapping math with stubbed font metrics (10 units per character). + */ +class TextWrapTest { + + private static final Funcs.Func1 MEASURE = new Funcs.Func1() { + @Override + public Double call(String s) { + return s.length() * 10.0; + } + }; + + @Test + void shortTextStaysOnOneLine() { + List lines = TextRenderElement.wrap("hello", MEASURE, 100); + assertEquals(1, lines.size()); + assertEquals("hello", lines.get(0)); + } + + @Test + void breaksOnWordBoundaries() { + // 10 chars fit per line; "hello world" is 11. + List lines = TextRenderElement.wrap("hello world foo", MEASURE, 100); + assertEquals(2, lines.size()); + assertEquals("hello", lines.get(0)); + assertEquals("world foo", lines.get(1)); + } + + @Test + void fillsLinesGreedily() { + List lines = TextRenderElement.wrap("aa bb cc dd ee", MEASURE, 50); + // 5 chars per line: "aa bb" fits exactly, then "cc dd", then "ee". + assertEquals(3, lines.size()); + assertEquals("aa bb", lines.get(0)); + assertEquals("cc dd", lines.get(1)); + assertEquals("ee", lines.get(2)); + } + + @Test + void hardBreaksAWordWiderThanTheLine() { + List lines = TextRenderElement.wrap("abcdefghijklmno", MEASURE, 100); + assertEquals(2, lines.size()); + assertEquals("abcdefghij", lines.get(0)); + assertEquals("klmno", lines.get(1)); + } + + @Test + void respectsEmbeddedNewlines() { + List lines = TextRenderElement.wrap("a\nb b\nc", MEASURE, 1000); + assertEquals(3, lines.size()); + assertEquals("a", lines.get(0)); + assertEquals("b b", lines.get(1)); + assertEquals("c", lines.get(2)); + } + + @Test + void unboundedWidthNeverWraps() { + List lines = TextRenderElement.wrap( + "the quick brown fox", MEASURE, Double.POSITIVE_INFINITY); + assertEquals(1, lines.size()); + assertEquals("the quick brown fox", lines.get(0)); + } + + @Test + void everyLineFitsTheWidth() { + List lines = TextRenderElement.wrap( + "one twotwo three fourfourfourfour x", MEASURE, 70); + for (String line : lines) { + assertTrue(MEASURE.call(line) <= 70, "line too wide: '" + line + "'"); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ValueSemanticsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ValueSemanticsTest.java new file mode 100644 index 00000000000..170e7da6de5 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ValueSemanticsTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.vectormath.Matrix4; +import com.codename1.flutter.widgets.AsyncSnapshot; +import com.codename1.flutter.widgets.ConnectionState; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/// Value types hash consistently with their equality, a matrix's storage is the +/// matrix, and requireData refuses to answer null. +class ValueSemanticsTest { + + private static void equalAndSameHash(Object a, Object b) { + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode(), "equal values must hash alike: " + a + " / " + b); + } + + @Test + void negativeZeroHashesLikeZeroInEveryValueType() { + equalAndSameHash(new Offset(0.0, 1), new Offset(-0.0, 1)); + equalAndSameHash(new Size(0.0, 1), new Size(-0.0, 1)); + equalAndSameHash(new Alignment(-0.0, 0.0), new Alignment(0.0, -0.0)); + equalAndSameHash(Radius.circular(0.0), Radius.circular(-0.0)); + equalAndSameHash(Rect.fromLTRB(0.0, 0, 1, 1), Rect.fromLTRB(-0.0, 0, 1, 1)); + equalAndSameHash(EdgeInsets.fromLTRB(0.0, 0, 1, 1), EdgeInsets.fromLTRB(-0.0, 0, 1, 1)); + equalAndSameHash(new BoxConstraints(0.0, 1, 0, 1), new BoxConstraints(-0.0, 1, 0, 1)); + java.util.Set set = new java.util.HashSet(); + set.add(new Offset(0.0, 5)); + assertEquals(true, set.contains(new Offset(-0.0, 5))); + } + + @Test + void writingThroughStorageMovesTheMatrix() { + Matrix4 m = Matrix4.identity(); + m.storage().set(12, 20.0); + assertEquals(20.0, m.storage().get(12), 0.0); + assertSame(m.storage(), m.storage(), "the same backing list every time, as vector_math returns"); + Matrix4 copy = Matrix4.identity(); + copy.storage().set(12, 20.0); + assertEquals(m.storage().get(12), copy.storage().get(12), 0.0); + } + + @Test + void requireDataThrowsInsteadOfAnsweringNull() { + final IllegalStateException failure = new IllegalStateException("load failed"); + AsyncSnapshot failed = new AsyncSnapshot(ConnectionState.done, null, failure, null); + assertSame(failure, assertThrows(IllegalStateException.class, failed::requireData), + "the stored error surfaces, not a null"); + AsyncSnapshot empty = new AsyncSnapshot(ConnectionState.waiting, null, null, null); + assertThrows(dart.core.StateError.class, empty::requireData); + assertEquals("v", new AsyncSnapshot(ConnectionState.done, "v", null, null).requireData()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/VariantChoiceTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/VariantChoiceTest.java new file mode 100644 index 00000000000..9283412bdb3 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/VariantChoiceTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// Flutter's AssetImage._findBestVariant, which depends on the variants present. +class VariantChoiceTest { + + @Test + void theNearestLowerVariantWinsBelowTheMidpoint() { + assertEquals(2.0, FlutterAssets.chooseVariant(2.1, new double[] {1.0, 2.0, 3.0}), 0.0, + "2.1 is nearer 2.0 than 3.0"); + assertEquals(3.0, FlutterAssets.chooseVariant(2.6, new double[] {1.0, 2.0, 3.0}), 0.0); + } + + @Test + void whatIsMissingChangesTheChoice() { + // Without 2.0x the neighbours are 1.5x and 3.0x, midpoint 2.25. + assertEquals(1.5, FlutterAssets.chooseVariant(2.1, new double[] {1.0, 1.5, 3.0}), 0.0); + } + + @Test + void lowDensityScreensPreferTheSharperVariant() { + assertEquals(2.0, FlutterAssets.chooseVariant(1.1, new double[] {1.0, 2.0}), 0.0, + "below 2.0 the upper variant wins even when the lower is nearer"); + } + + @Test + void anExactMatchOrAOneSidedChoice() { + assertEquals(3.0, FlutterAssets.chooseVariant(3.0, new double[] {1.0, 3.0}), 0.0); + assertEquals(1.0, FlutterAssets.chooseVariant(3.0, new double[] {1.0}), 0.0); + assertEquals(2.0, FlutterAssets.chooseVariant(0.5, new double[] {2.0, 3.0}), 0.0); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/ZOrderTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ZOrderTest.java new file mode 100644 index 00000000000..d1a4fe24e54 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/ZOrderTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter; + +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.AltMarkerBox; +import com.codename1.flutter.testsupport.MarkerBox; +import com.codename1.flutter.testsupport.Toggler; +import com.codename1.flutter.widgets.Column; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The flat container's child order (RenderHost attach order) must match + * element-tree order — in particular a render element REPLACED mid-life must + * insert its component at the tree-order index instead of appending at the + * end. + */ +class ZOrderTest { + + private BuildOwner owner; + private RenderHost host; + + private Toggler.TogglerState mount(Widget middle) { + owner = new BuildOwner(); + host = new RenderHost(); + Column col = new Column(); + col.children(DartList.of((Widget) new MarkerBox("a"), new Toggler(middle), new MarkerBox("c"))); + Element root = FlutterUI.mount(col, host, owner); + RenderElement flex = host.rootRenderElement(); + Element togglerElement = childAt(flex, 1); + return (Toggler.TogglerState) ((StatefulElement) togglerElement).state(); + } + + private static Element childAt(Element parent, int index) { + final List kids = new ArrayList(); + parent.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Element c) { + kids.add(c); + } + }); + return kids.get(index); + } + + private List attachTags() { + List tags = new ArrayList(); + for (RenderElement r : host.attachOrder()) { + Widget w = r.widget(); + if (w instanceof MarkerBox) { + tags.add(((MarkerBox) w).tag()); + } else if (w instanceof AltMarkerBox) { + tags.add(((AltMarkerBox) w).tag()); + } else { + tags.add("?"); + } + } + return tags; + } + + @Test + void initialMountAttachesInTreeOrder() { + mount(new MarkerBox("b")); + assertEquals(List.of("a", "b", "c"), attachTags()); + } + + @Test + void midLifeReplacementInsertsAtTreeOrderIndexNotAtTheEnd() { + Toggler.TogglerState state = mount(new MarkerBox("b")); + + state.setState(() -> state.child = new AltMarkerBox("b2")); + owner.flushSync(); + + // Without index-aware attach the replacement would land at the end + // ("a", "c", "b2"), drifting the z-order. + assertEquals(List.of("a", "b2", "c"), attachTags()); + } + + @Test + void repeatedReplacementKeepsTheOrderStable() { + Toggler.TogglerState state = mount(new MarkerBox("b")); + + state.setState(() -> state.child = new AltMarkerBox("b2")); + owner.flushSync(); + state.setState(() -> state.child = new MarkerBox("b3")); + owner.flushSync(); + + assertEquals(List.of("a", "b3", "c"), attachTags()); + } + + /// A first build attaches by appending: Element.anchorForSlot answers "append" + /// without searching when the host's last component already precedes the new + /// child, and updateChildren skips the reorder walk when there were no old + /// children. Nesting is where either shortcut would go wrong if its tree-order + /// test were. + @Test + void nestedInitialMountAttachesInTreeOrder() { + owner = new BuildOwner(); + host = new RenderHost(); + Column inner = new Column(); + inner.children(DartList.of((Widget) new MarkerBox("b2"), new MarkerBox("b3"))); + Column middle = new Column(); + middle.children(DartList.of((Widget) new MarkerBox("b1"), inner)); + Column col = new Column(); + col.children(DartList.of((Widget) new MarkerBox("a"), middle, new MarkerBox("c"))); + FlutterUI.mount(col, host, owner); + assertEquals(List.of("a", "b1", "b2", "b3", "c"), attachTags()); + } + + /// Children inflated into a parent that had none, after a LATER sibling is already + /// attached: the append shortcut must not apply, and the search must put them + /// before that sibling. + @Test + void childrenGainedLaterLandBeforeTheLaterSibling() { + Column empty = new Column(); + empty.children(DartList.of()); + Toggler.TogglerState state = mount(empty); + assertEquals(List.of("a", "c"), attachTags()); + + final Column filled = new Column(); + filled.children(DartList.of((Widget) new MarkerBox("x"), new MarkerBox("y"))); + state.setState(() -> state.child = filled); + owner.flushSync(); + + assertEquals(List.of("a", "x", "y", "c"), attachTags()); + } + + @Test + void replacementOfTheFirstChildInsertsAtTheFront() { + owner = new BuildOwner(); + host = new RenderHost(); + Column col = new Column(); + Toggler first = new Toggler(new MarkerBox("a")); + col.children(DartList.of((Widget) first, new MarkerBox("b"), new MarkerBox("c"))); + FlutterUI.mount(col, host, owner); + Toggler.TogglerState state = (Toggler.TogglerState) + ((StatefulElement) childAt(host.rootRenderElement(), 0)).state(); + assertEquals(List.of("a", "b", "c"), attachTags()); + + state.setState(() -> state.child = new AltMarkerBox("a2")); + owner.flushSync(); + assertEquals(List.of("a2", "b", "c"), attachTags()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimateToDurationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimateToDurationTest.java new file mode 100644 index 00000000000..96af2e3ff88 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimateToDurationTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.core.Duration; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A controller's duration describes crossing its WHOLE range. An animateTo that covers + * part of it takes the matching part of the time -- Flutter's + * {@code directionDuration * remainingFraction}. + * + *

Reply opens its mailbox drawer with {@code animateTo(0.4)} on a 300ms controller, + * which is a 120ms animation. Running the full 300 made the drawer take two and a half + * times as long to arrive as the reference: measured frame by frame at the same animation + * times, ours had barely moved at the point the reference had finished.

+ */ +class AnimateToDurationTest { + + private static AnimationController controller(long ms) { + AnimationController c = new AnimationController(); + c.duration(Duration.of(0, 0, 0, 0, ms, 0)); + return c; + } + + /// Package-private seam so the arithmetic can be checked without a frame clock. + private static long runMillis(AnimationController c, double target, Duration explicit, + AnimationStatus dir) { + return c.simulationMillisForTest(target, explicit, dir); + } + + @Test + void partOfTheRangeTakesThatPartOfTheTime() { + AnimationController c = controller(300); + assertEquals(120, runMillis(c, 0.4, null, AnimationStatus.forward), + "0 -> 0.4 of a 300ms range is 120ms, which is what Reply's drawer asks for"); + } + + @Test + void theWholeRangeTakesTheWholeDuration() { + assertEquals(300, runMillis(controller(300), 1.0, null, AnimationStatus.forward)); + } + + @Test + void theDistanceIsMeasuredFromWhereItIsNow() { + AnimationController c = controller(300); + c.value(0.5); + assertEquals(150, runMillis(c, 1.0, null, AnimationStatus.forward), + "half the range left is half the time"); + assertEquals(150, runMillis(c, 0.0, null, AnimationStatus.reverse), + "and the same going back"); + } + + @Test + void anExplicitDurationIsUsedWhole() { + AnimationController c = controller(300); + assertEquals(1000, runMillis(c, 0.4, Duration.of(0, 0, 0, 1, 0, 0), + AnimationStatus.forward), "a stated duration is not scaled"); + } + + /// Flutter refuses to animate to where it already is, even when handed a duration. + @Test + void goingNowhereTakesNoTime() { + AnimationController c = controller(300); + c.value(0.4); + assertEquals(0, runMillis(c, 0.4, Duration.of(0, 0, 0, 1, 0, 0), + AnimationStatus.forward)); + } + + /// reverseDuration governs the way back, and is scaled the same way. + @Test + void theReverseDurationIsScaledToo() { + AnimationController c = controller(300); + c.reverseDuration(Duration.of(0, 0, 0, 0, 200, 0)); + c.value(0.5); + assertEquals(100, runMillis(c, 0.0, null, AnimationStatus.reverse)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimatedWidgetTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimatedWidgetTest.java new file mode 100644 index 00000000000..62508ef4346 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/AnimatedWidgetTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * An AnimatedWidget subclass has to REBUILD when its listenable notifies. + * + *

The builder-callback form ({@link AnimatedBuilder}) always listened; the subclass form + * did not, and the difference is invisible in a screenshot of the first frame. In the + * gallery it left the settings button painting whichever glyph it was built with — the + * sliders never swept round into the close X, however far the controller ran.

+ */ +class AnimatedWidgetTest { + + /** The shape the gallery uses: a subclass that renders from the listenable's value. */ + static class Driven extends AnimatedWidget { + int builds; + double lastSeen = -1; + + @Override + public Widget build(BuildContext context) { + builds++; + Object v = ((ValueNotifier) listenable()).value(); + lastSeen = v instanceof Number ? ((Number) v).doubleValue() : -1; + // Sized from the value, so a rebuild is observable as geometry and not only as + // a counter that a stale build could still increment. + int side = (int) (lastSeen * 10); + return new ProbeBox(side, side); + } + } + + private BuildOwner owner; + + private Element mount(Widget root) { + owner = new BuildOwner(); + return FlutterUI.mount(root, new RenderHost(), owner); + } + + private Driven drivenBy(ValueNotifier n) { + Driven d = new Driven(); + d.listenable(n); + return d; + } + + @Test + @DisplayName("the first frame reads the current value") + void theFirstFrameReadsTheValue() { + ValueNotifier n = new ValueNotifier(Double.valueOf(1)); + Driven d = drivenBy(n); + mount(d); + + assertEquals(1, d.builds); + assertEquals(1.0, d.lastSeen, 1e-9); + } + + @Test + @DisplayName("a notification rebuilds the widget with the new value") + void aNotificationRebuilds() { + ValueNotifier n = new ValueNotifier(Double.valueOf(0)); + Driven d = drivenBy(n); + mount(d); + + n.value(Double.valueOf(0.5)); + owner.flushSync(); + + assertEquals(2, d.builds, "the listenable must drive a rebuild"); + assertEquals(0.5, d.lastSeen, 1e-9, "and the rebuild must see the NEW value"); + } + + @Test + @DisplayName("every tick of a run is seen, not just the last") + void everyTickIsSeen() { + ValueNotifier n = new ValueNotifier(Double.valueOf(0)); + Driven d = drivenBy(n); + mount(d); + + // A controller sweeping 0 -> 1 is the real case: the icon interpolates through the + // transition phase, so dropping intermediate frames would still land on the right + // final glyph while never animating. + for (int i = 1; i <= 4; i++) { + n.value(Double.valueOf(i / 4.0)); + owner.flushSync(); + } + + assertEquals(5, d.builds, "one build per tick, plus the first frame"); + assertEquals(1.0, d.lastSeen, 1e-9); + } + + @Test + @DisplayName("an unmounted widget stops listening") + void anUnmountedWidgetStopsListening() { + ValueNotifier n = new ValueNotifier(Double.valueOf(0)); + Driven d = drivenBy(n); + Element root = mount(d); + + FlutterUI.unmountTree(root); + int atUnmount = d.builds; + + n.value(Double.valueOf(1)); + owner.flushSync(); + + assertEquals(atUnmount, d.builds, + "a widget off the tree must have removed its listener"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ControllerBoundsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ControllerBoundsTest.java new file mode 100644 index 00000000000..2b0f1d818ef --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ControllerBoundsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// A controller starts inside its own bounds, whatever order its named +/// arguments were applied in. +class ControllerBoundsTest { + + @Test + void withoutAValueItStartsAtTheLowerBound() { + AnimationController c = new AnimationController(); + c.lowerBound(10); + c.upperBound(20); + assertEquals(10.0, c.value(), 0.0); + assertEquals(AnimationStatus.dismissed, c.status()); + } + + @Test + void aValueGivenBeforeTheBoundsIsNotClampedToTheDefaults() { + // The transpiler applies named arguments in source order, so value: can + // arrive before lowerBound:/upperBound:. + AnimationController c = new AnimationController(); + c.value(15); + c.lowerBound(10); + c.upperBound(20); + assertEquals(15.0, c.value(), 0.0); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ListenerIsolationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ListenerIsolationTest.java new file mode 100644 index 00000000000..2c4bcc2dee6 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/ListenerIsolationTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.FlutterErrorReport; +import com.codename1.flutter.foundation.ValueNotifier; +import dart.runtime.Funcs; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// A listener that throws is reported, and every other listener still runs -- +/// on an animation, where the throw used to reach the frame clock and drop the +/// whole controller, and on the plain notifiers that share the dispatch. +class ListenerIsolationTest { + + @BeforeEach + @AfterEach + void clearReports() { + FlutterErrorReport.reset(); + } + + private static Funcs.VoidFunc0 thrower() { + return new Funcs.VoidFunc0() { + @Override + public void call() { + throw new IllegalStateException("faulty listener"); + } + }; + } + + private static Funcs.VoidFunc0 counter(final int[] count) { + return new Funcs.VoidFunc0() { + @Override + public void call() { + count[0]++; + } + }; + } + + private static boolean reported() { + for (FlutterErrorReport.Entry e : FlutterErrorReport.entries()) { + if ("faulty listener".equals(e.message())) { + return true; + } + } + return false; + } + + @Test + void aThrowingValueListenerDoesNotStopTheOthersOrTheController() { + AnimationController c = new AnimationController(); + int[] after = {0}; + c.addListener(thrower()); + c.addListener(counter(after)); + c.value(0.5); // must not throw out of the controller + assertEquals(1, after[0], "the listener after the faulty one still ran"); + assertEquals(0.5, c.value(), 0.0); + assertTrue(reported(), "the failure is reported, not swallowed"); + } + + @Test + void aThrowingStatusListenerDoesNotStopTheOthers() { + AnimationController c = new AnimationController(); + final int[] after = {0}; + c.addStatusListener(new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + throw new IllegalStateException("faulty listener"); + } + }); + c.addStatusListener(new Funcs.VoidFunc1() { + @Override + public void call(AnimationStatus s) { + after[0]++; + } + }); + c.value(1.0); // dismissed -> completed + assertEquals(1, after[0]); + assertTrue(reported()); + } + + @Test + void theSameHoldsForAValueNotifier() { + ValueNotifier n = new ValueNotifier("a"); + int[] after = {0}; + n.addListener(thrower()); + n.addListener(counter(after)); + n.value("b"); + assertEquals(1, after[0]); + assertTrue(reported()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunFutureTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunFutureTest.java new file mode 100644 index 00000000000..611fd8c4a8c --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunFutureTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.async.Future; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/// The playback controls return the run's future, as Flutter's TickerFuture: +/// `await controller.forward()` waits for the run instead of returning at once. +/// Headless, a run collapses to its end state, so each future is complete by the +/// time the call returns; on a device it completes from the frame clock. +class RunFutureTest { + + @Test + void everyControlReturnsTheCompletedRunFuture() { + AnimationController c = new AnimationController(); + c.duration(dart.core.Duration.of(0, 0, 0, 0, 200, 0)); + assertDone(c.forward()); + assertDone(c.reverse()); + assertDone(c.animateTo(0.5, null, null)); + assertDone(c.animateBack(0.2, null, null)); + assertDone(c.fling(1.0, null, null)); + } + + private static void assertDone(Future f) { + assertNotNull(f, "a control must return its run's future"); + // Through then(), not getNow(): getNow() answers null for a future that is + // not done, which is indistinguishable from one completed with null. + final boolean[] completed = {false}; + f.then(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Object v) { + completed[0] = true; + } + }); + assertTrue(completed[0], "the run's future must be complete once the run has ended"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunStartNotificationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunStartNotificationTest.java new file mode 100644 index 00000000000..abbc7b6bf33 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/RunStartNotificationTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.core.Duration; +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A run must notify its listeners once while it is still at its starting value. + * + *

Flutter's Ticker calls its callback on the first frame with an elapsed of zero and + * {@code AnimationController._tick} notifies unconditionally, so a listener is guaranteed + * one call before the value has moved. Apps build on that: Reply's bottom drawer animates + * a controller from 0, and the rebuild that makes the drawer visible comes from a listener + * that only calls setState while the value is below 0.01. Without the notification at the + * start of the run, the first call a listener ever saw was already past that threshold -- + * the drawer's state flipped, its arrow turned, and no panel was ever built.

+ */ +class RunStartNotificationTest { + + private static List valuesSeenDuring(AnimationController c) { + final List seen = new ArrayList(); + c.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + seen.add(c.value()); + } + }); + return seen; + } + + @Test + void aRunNotifiesWhileStillAtItsStartingValue() { + AnimationController c = new AnimationController(); + c.duration(Duration.of(0, 0, 0, 0, 300, 0)); + List seen = valuesSeenDuring(c); + + c.forward(); + + assertTrue(seen.size() >= 2, + "expected a notification at the start of the run and one at its end, got " + + seen); + assertEquals(0.0, seen.get(0).doubleValue(), 1e-9, + "the first notification of a run carries the value the run started from"); + assertEquals(1.0, seen.get(seen.size() - 1).doubleValue(), 1e-9, + "and the run still ends where it was headed"); + } + + /// Reply's exact shape: the listener acts only while the value is near zero, so it + /// depends entirely on being called before the animation has moved. + @Test + void aListenerGatedOnTheStartingValueRuns() { + final AnimationController c = new AnimationController(); + c.duration(Duration.of(0, 0, 0, 0, 300, 0)); + final int[] fired = {0}; + c.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + if (c.value().doubleValue() < 0.01) { + fired[0]++; + } + } + }); + + c.forward(); + + assertEquals(1, fired[0], + "a listener gated on the run's starting value must be called exactly once"); + } + + /// A run with nowhere to go still reports; it just never had a first tick distinct + /// from its last. + @Test + void aRunThatCannotMoveStillReportsOnce() { + AnimationController c = new AnimationController(); + c.duration(Duration.of(0, 0, 0, 0, 300, 0)); + c.value(1.0); + List seen = valuesSeenDuring(c); + + c.forward(); + + assertEquals(1, seen.size(), "already at the upper bound: one notification, got " + seen); + assertEquals(1.0, seen.get(0).doubleValue(), 1e-9); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/StopSettlesRunTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/StopSettlesRunTest.java new file mode 100644 index 00000000000..fa09a02696b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/StopSettlesRunTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import dart.async.Completer; +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// stop(canceled: false) completes the run's future; a canceled stop leaves it +/// pending. Tested on the decision itself: headless, a run finishes inside forward(), +/// so no headless test can stop one mid-flight. +class StopSettlesRunTest { + + private static String settle(Boolean canceled) { + Completer run = new Completer(); + final String[] state = {"pending"}; + run.future().then(new Funcs.VoidFunc1() { + @Override + public void call(Object v) { + state[0] = "completed"; + } + }); + AnimationController.settleStoppedRun(run, canceled); + return state[0]; + } + + @Test + void notCanceledCompletes() { + assertEquals("completed", settle(Boolean.FALSE)); + } + + @Test + void canceledOrDefaultStaysPending() { + assertEquals("pending", settle(Boolean.TRUE)); + assertEquals("pending", settle(null), "Flutter's default is canceled: true"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TimeDilationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TimeDilationTest.java new file mode 100644 index 00000000000..6836d3496a2 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TimeDilationTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.scheduler.SchedulerLib; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@code scheduler.timeDilation} has to reach the controllers. + * + *

It was declared and never read, so the gallery's own "Slow motion" switch -- a + * user-facing control whose entire purpose is to let a motion design be inspected -- + * moved nothing. A switch that does nothing is worse than an absent one.

+ * + *

The controller's clock is not injectable, so these assert on the mapping from + * elapsed time to t rather than by running a real animation: that mapping is the + * whole of what dilation changes.

+ */ +class TimeDilationTest { + + @AfterEach + void reset() { + SchedulerLib.timeDilation = 1.0; + } + + /** The controller's own arithmetic -- not a copy of it. */ + private static double progress(long elapsedMs, long durationMs) { + return AnimationController.progress(elapsedMs, durationMs); + } + + @Test + void undilatedIsRealTime() { + assertEquals(0.5, progress(100, 200), 1e-9); + assertEquals(1.0, progress(200, 200), 1e-9); + } + + @Test + void fiveTimesSlowerTakesFiveTimesAsLong() { + SchedulerLib.timeDilation = 5.0; + assertEquals(0.1, progress(100, 200), 1e-9); + assertEquals(0.2, progress(200, 200), 1e-9); + assertEquals(1.0, progress(1000, 200), 1e-9); + } + + /// A zero or negative dilation would divide the animation by zero or run it + /// backwards; Flutter asserts on it, and here it simply means real time. + @Test + void anImpossibleDilationIsIgnored() { + SchedulerLib.timeDilation = 0.0; + assertEquals(0.5, progress(100, 200), 1e-9); + SchedulerLib.timeDilation = -2.0; + assertEquals(0.5, progress(100, 200), 1e-9); + } + + /// A zero-duration run is complete at its first tick whatever the dilation, so + /// slow motion cannot wedge an animation that has no time to take. + @Test + void aZeroDurationRunStillCompletes() { + SchedulerLib.timeDilation = 10.0; + assertEquals(1.0, progress(0, 0), 1e-9); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TransitionEffectsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TransitionEffectsTest.java new file mode 100644 index 00000000000..add75388cf6 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TransitionEffectsTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Offset; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.FractionalTranslationRenderElement; +import com.codename1.flutter.widgets.Opacity; +import com.codename1.flutter.widgets.Transform; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The transition widgets APPLY their effect. + * + *

The whole family used to render the child straight through: a FadeTransition never + * faded, a ScaleTransition never scaled, a SlideTransition never moved. Nothing failed and + * nothing looked broken in a still screenshot — every animated reveal in the app was simply + * a cut.

+ */ +class TransitionEffectsTest { + + /** An animation stuck at one value — enough to check what the build produces. */ + private static Animation at(double v) { + return new AlwaysStoppedAnimation(Double.valueOf(v)); + } + + private static Widget buildOf(AnimatedChildWidget w) { + return w.build(null); + } + + @Test + @DisplayName("a fade wraps the child in an Opacity carrying the animation's value") + void fadeAppliesOpacity() { + FadeTransition f = new FadeTransition(); + f.opacity(at(0.25)); + f.child(new ProbeBox(10, 10)); + + Widget built = buildOf(f); + assertTrue(built instanceof Opacity, "expected an Opacity, got " + built); + assertEquals(0.25, ((Opacity) built).getOpacity(), 1e-9); + } + + @Test + @DisplayName("a fade subscribes, so a tick repaints it") + void fadeListens() { + FadeTransition f = new FadeTransition(); + Animation a = at(1); + f.opacity(a); + + assertNotNull(f.listenable(), "the animation must be the widget's listenable"); + assertEquals(a, f.listenable()); + } + + @Test + @DisplayName("a scale wraps the child in a Transform carrying the factor") + void scaleAppliesTransform() { + ScaleTransition s = new ScaleTransition(); + s.scale(at(0.5)); + s.child(new ProbeBox(10, 10)); + + Widget built = buildOf(s); + assertTrue(built instanceof Transform, "expected a Transform, got " + built); + assertEquals(0.5, ((Transform) built).effectiveScaleX(), 1e-9); + assertEquals(0.5, ((Transform) built).effectiveScaleY(), 1e-9); + } + + @Test + @DisplayName("a rotation converts TURNS to radians") + void rotationConvertsTurns() { + RotationTransition r = new RotationTransition(); + r.turns(at(0.25)); // a quarter turn + r.child(new ProbeBox(10, 10)); + + Transform built = (Transform) buildOf(r); + assertEquals(Math.PI / 2, built.effectiveAngle().doubleValue(), 1e-9); + } + + @Test + @DisplayName("a slide reports its offset as a fraction, read at paint time") + void slideExposesItsFraction() { + SlideTransition s = new SlideTransition(); + s.position(new AlwaysStoppedAnimation(new Offset(0.5, -0.25))); + s.child(new ProbeBox(10, 10)); + + Offset f = ((FractionalTranslationRenderElement.FractionSource) s).fraction(); + assertEquals(0.5, f.dx(), 1e-9); + assertEquals(-0.25, f.dy(), 1e-9); + } + + @Test + @DisplayName("a slide lays its child out unmoved - the shift is paint-only") + void slideDoesNotDisturbLayout() { + SlideTransition s = new SlideTransition(); + s.position(new AlwaysStoppedAnimation(new Offset(1, 1))); + s.child(new ProbeBox(40, 20)); + + RenderElement e = (RenderElement) FlutterUI.mount(s, new RenderHost(), new BuildOwner()); + e.layout(BoxConstraints.loose(100, 100)); + + assertEquals(40, e.size().width(), 1e-9, "a slid child keeps its slot"); + assertEquals(20, e.size().height(), 1e-9); + } + + @Test + @DisplayName("with no animation a transition is a no-op, not a blank") + void anAbsentAnimationShowsTheChild() { + FadeTransition f = new FadeTransition(); + f.child(new ProbeBox(10, 10)); + + assertEquals(1.0, ((Opacity) buildOf(f)).getOpacity(), 1e-9, + "no animation must mean fully visible, never fully transparent"); + } + + @Test + @DisplayName("an unmounted slide stops following its animation") + void anUnmountedSlideUnsubscribes() { + SlideTransition s = new SlideTransition(); + s.position(new AlwaysStoppedAnimation(new Offset(0, 0))); + s.child(new ProbeBox(10, 10)); + + Element e = FlutterUI.mount(s, new RenderHost(), new BuildOwner()); + FlutterUI.unmountTree(e); + // Nothing to assert beyond surviving the teardown: a listener left on a controller + // that outlives the element is a leak that only shows up as a stale repaint later. + assertTrue(true); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TweenInterpolationTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TweenInterpolationTest.java new file mode 100644 index 00000000000..1fa296daced --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/animation/TweenInterpolationTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.animation; + +import com.codename1.flutter.BorderRadius; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.EdgeInsetsDirectional; +import com.codename1.flutter.Radius; +import com.codename1.flutter.RelativeRect; +import com.codename1.flutter.vectormath.Matrix4; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A tween has to produce values BETWEEN its ends. + * + *

Four of them used to fall through to the base class's fallback, which returns + * {@code begin} below t=0.5 and {@code end} above it. Anything animated by one of them + * therefore did not move: it sat at its start value for half the duration and then + * appeared, already finished. The gallery's settings panel is animated by + * {@link RelativeRectTween} -- it begins a full screen-height above the viewport and + * slides down over 80ms -- so tapping the settings button produced a menu that popped + * into place rather than one that slid.

+ * + *

Each test samples a value strictly inside the range and asserts it is strictly + * between the ends, which is exactly what the stepping fallback cannot do.

+ */ +class TweenInterpolationTest { + + private static void between(double lo, double hi, double actual, String what) { + assertTrue(actual > Math.min(lo, hi) && actual < Math.max(lo, hi), + what + ": expected strictly between " + lo + " and " + hi + ", got " + actual); + } + + @Test + void aRelativeRectMovesEveryFrame() { + RelativeRectTween t = new RelativeRectTween(); + t.begin(RelativeRect.fromLTRB(0, -800, 0, 0)); + t.end(RelativeRect.fill); + + RelativeRect quarter = t.transform(0.25); + RelativeRect half = t.transform(0.5); + RelativeRect threeQuarters = t.transform(0.75); + + assertEquals(-600, quarter.top(), 1e-9); + assertEquals(-400, half.top(), 1e-9); + assertEquals(-200, threeQuarters.top(), 1e-9); + // and it still lands exactly on its ends + assertEquals(-800, t.transform(0.0).top(), 1e-9); + assertEquals(0, t.transform(1.0).top(), 1e-9); + } + + @Test + void aBorderRadiusOpensGradually() { + BorderRadiusTween t = new BorderRadiusTween(); + t.begin(BorderRadius.circular(0)); + t.end(BorderRadius.circular(40)); + between(0, 40, t.transform(0.25).topLeft().x(), "topLeft at 0.25"); + assertEquals(20, t.transform(0.5).bottomRight().x(), 1e-9); + } + + @Test + void insetsEaseRatherThanJump() { + EdgeInsetsGeometryTween t = new EdgeInsetsGeometryTween(); + t.begin(EdgeInsets.all(0)); + t.end(EdgeInsets.all(16)); + EdgeInsets mid = (EdgeInsets) t.transform(0.5); + assertEquals(8, mid.left(), 1e-9); + assertEquals(8, mid.bottom(), 1e-9); + } + + /// Two directional insets must stay directional across the animation -- becoming + /// left-to-right at the first frame would flip the padding on an RTL layout. + @Test + void twoDirectionalInsetsStayDirectional() { + EdgeInsetsGeometryTween t = new EdgeInsetsGeometryTween(); + t.begin(EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0)); + t.end(EdgeInsetsDirectional.fromSTEB(20, 4, 8, 12)); + Object mid = t.transform(0.5); + assertInstanceOf(EdgeInsetsDirectional.class, mid); + assertEquals(10, ((EdgeInsetsDirectional) mid).start(), 1e-9); + assertEquals(4, ((EdgeInsetsDirectional) mid).end(), 1e-9); + } + + @Test + void aMatrixInterpolatesComponentwise() { + Matrix4Tween t = new Matrix4Tween(); + t.begin(Matrix4.identity()); + t.end(Matrix4.translationValues(100, 40, 0)); + Matrix4 mid = t.transform(0.5); + assertEquals(50.0, mid.storage().get(12), 1e-9); + assertEquals(20.0, mid.storage().get(13), 1e-9); + // the diagonal is 1 at both ends, so it must stay 1 throughout + assertEquals(1.0, mid.storage().get(0), 1e-9); + } + + /// The base class still steps for a type nothing knows how to interpolate. That is + /// the deliberate fallback, not an oversight, and it must not start returning nulls. + @Test + void anUninterpolatableTypeStillStepsRatherThanFailing() { + Tween t = new Tween(); + t.begin("a"); + t.end("b"); + assertEquals("a", t.transform(0.25)); + assertEquals("b", t.transform(0.75)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/fonts/GoogleFontsFamilyTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/fonts/GoogleFontsFamilyTest.java new file mode 100644 index 00000000000..a5f882c2cd4 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/fonts/GoogleFontsFamilyTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.fonts; + +import com.codename1.flutter.FontWeight; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.material.TextTheme; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * The google_fonts shim must NAME the family it was asked for. Painting is a + * separate question (it needs a bundled face and a Display); what is pinned here + * is that the name survives the call at all, because it used to be dropped and + * every study then rendered in the platform typeface. + */ +class GoogleFontsFamilyTest { + + @Test + void aStyleCarriesItsFamily() { + TextStyle s = GoogleFonts.workSans(16, FontWeight.w500, null, null, null, + null, null, null, null); + assertEquals("WorkSans", s.fontFamily()); + assertEquals(FontWeight.w500, s.fontWeight()); + assertEquals(16.0, s.fontSize().doubleValue()); + } + + @Test + void eachHelperNamesItsOwnFamily() { + assertEquals("Eczar", GoogleFonts.eczar(12, null, null, null, null, null, null, null, null) + .fontFamily()); + assertEquals("LibreFranklin", GoogleFonts.libreFranklin(12, null, null, null, null, null, + null, null, null).fontFamily()); + assertEquals("RobotoCondensed", GoogleFonts.robotoCondensed(12, null, null, null, null, + null, null, null, null).fontFamily()); + assertEquals("Montserrat", GoogleFonts.montserrat(12, null, null, null, null, null, null, + null, null).fontFamily()); + } + + @Test + void aBaseStyleKeepsWhatTheCallDidNotOverride() { + TextStyle base = new TextStyle(); + base.letterSpacing(2.5); + TextStyle s = GoogleFonts.oswald(0, null, null, null, null, base, null, null, null); + assertEquals("Oswald", s.fontFamily()); + assertEquals(2.5, s.getLetterSpacing().doubleValue()); + } + + @Test + void aTextThemeIsRepointedAtTheFamily() { + TextTheme themed = GoogleFonts.workSansTextTheme(new TextTheme()); + assertEquals("WorkSans", themed.bodyMedium().fontFamily()); + assertEquals("WorkSans", themed.displayLarge().fontFamily()); + assertEquals("WorkSans", themed.labelSmall().fontFamily()); + } + + @Test + void anUnnamedFamilyResolvesToNothingRatherThanThrowing() { + FontResolver.clearCache(); + assertNull(FontResolver.resolve(null, FontWeight.w400, false)); + assertNull(FontResolver.resolve("", FontWeight.w400, false)); + // No face is bundled with the tests, so a real name is a miss too -- + // and a miss must be a null, not an exception, or every Text on a + // platform without the font would fail to build. + assertNull(FontResolver.resolve("NoSuchFaceAnywhere", FontWeight.w700, false)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/foundation/OwnListenerListTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/foundation/OwnListenerListTest.java new file mode 100644 index 00000000000..baca5c440f9 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/foundation/OwnListenerListTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.foundation; + +import com.codename1.flutter.scopedmodel.Model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/// A notifier with its own listener list leaves nothing in the shared map: a listener +/// that closes over the notifier would otherwise keep it alive through the map's value. +class OwnListenerListTest { + + @Test + void aNotifierThatClosesOverItselfLeavesNoSharedEntry() { + final Model model = new Model(); + final int[] heard = {0}; + model.addListener(() -> heard[0] += model.hashCode() == 0 ? 0 : 1); // closes over the model + assertNull(ChangeNotifier.LISTENERS.get(model), "no shared-map entry leading back to the model"); + model.notifyListeners(); + assertEquals(1, heard[0]); + model.dispose(); + model.notifyListeners(); + assertEquals(1, heard[0], "disposed: nothing is told any more"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarLayoutTest.java new file mode 100644 index 00000000000..ac36f6b77fe --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarLayoutTest.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.core.DartList; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The app bar lays out ALL THREE of its slots — leading, title, actions. + * + *

It used to lay out only the title and drop the rest, which is a failure that looks + * like a design decision: the bar renders, the page has a heading, and nothing announces + * that the back button and every action are missing. In the gallery that turned every demo + * page into a dead end, since the back button is the {@code leading}.

+ * + *

Headless, so Dp scale is 1 and logical pixels are pixels.

+ */ +class AppBarLayoutTest { + + /** Everything on the bar, in the order the layout emitted it. */ + private static final class Bar { + AppBarRenderElement element; + List children = new ArrayList(); + } + + private Bar mountAndLayout(AppBar bar, BoxConstraints c) { + Bar out = new Bar(); + out.element = (AppBarRenderElement) + FlutterUI.mount(bar, new RenderHost(), new BuildOwner()); + out.element.layout(c); + out.element.position(0, 0); + final List kids = out.children; + out.element.visitChildren(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Element e) { + RenderElement r = RenderElement.findRenderElement(e); + if (r != null) { + kids.add(r); + } + } + }); + return out; + } + + private static AppBar barWith(Widget leading, Widget title, Widget... actions) { + AppBar b = new AppBar(); + if (leading != null) { + b.leading(leading); + } + if (title != null) { + b.title(title); + } + if (actions != null && actions.length > 0) { + DartList l = new DartList(); + for (Widget a : actions) { + l.add(a); + } + b.actions(l); + } + // The implied back button needs a Navigator; these tests pin the explicit slots. + b.automaticallyImplyLeading(false); + return b; + } + + @Test + @DisplayName("leading, title and actions are all laid out") + void everySlotIsLaidOut() { + Bar bar = mountAndLayout( + barWith(new ProbeBox(40, 40), new ProbeBox(100, 20), + new ProbeBox(40, 40), new ProbeBox(40, 40)), + BoxConstraints.tight(400, 56)); + + assertEquals(4, bar.children.size(), + "leading + title + 2 actions must all become children"); + } + + @Test + @DisplayName("the leading sits at the start and the actions flush to the end") + void slotsArePlacedAcrossTheBar() { + ProbeBox leading = new ProbeBox(40, 40); + ProbeBox title = new ProbeBox(100, 20); + ProbeBox action = new ProbeBox(40, 40); + Bar bar = mountAndLayout(barWith(leading, title, action), + BoxConstraints.tight(400, 56)); + + RenderElement l = bar.children.get(0); + RenderElement t = bar.children.get(1); + RenderElement a = bar.children.get(2); + + assertEquals(0, l.x(), "leading at the very start"); + assertEquals(400 - 40, a.x(), "the action is flush to the trailing edge"); + assertEquals(40 + 16, t.x(), "the title clears the leading by kMiddleSpacing"); + assertTrue(t.x() + 100 <= a.x(), "the title must not run under the actions"); + } + + @Test + @DisplayName("flexibleSpace fills the whole bar, behind the row") + void flexibleSpaceFillsTheBarBehindTheRow() { + AppBar b = barWith(new ProbeBox(40, 40), new ProbeBox(100, 20)); + ProbeBox background = new ProbeBox(10, 10); + b.flexibleSpace(background); + + Bar bar = mountAndLayout(b, BoxConstraints.tight(400, 56)); + + assertEquals(3, bar.children.size(), + "flexibleSpace joins leading and title as a child"); + RenderElement flexible = bar.children.get(0); + assertEquals(0, flexible.x(), "the background starts at the bar's leading edge"); + assertEquals(0, flexible.y(), "and at its top, not centred like a slot"); + assertEquals(400, flexible.size().width(), "it is stretched across the bar"); + assertEquals(56, flexible.size().height(), "and down it"); + } + + @Test + @DisplayName("flexibleSpace does not take width from the row") + void flexibleSpaceIsNotAFourthSlot() { + ProbeBox leading = new ProbeBox(40, 40); + ProbeBox title = new ProbeBox(100, 20); + ProbeBox action = new ProbeBox(40, 40); + + AppBar plain = barWith(leading, title, action); + Bar without = mountAndLayout(plain, BoxConstraints.tight(400, 56)); + double titleXWithout = without.children.get(1).x(); + double actionXWithout = without.children.get(2).x(); + + AppBar withBackground = barWith(new ProbeBox(40, 40), new ProbeBox(100, 20), + new ProbeBox(40, 40)); + withBackground.flexibleSpace(new ProbeBox(180, 56)); + Bar with = mountAndLayout(withBackground, BoxConstraints.tight(400, 56)); + + assertEquals(titleXWithout, with.children.get(2).x(), + "the title sits where it did before a background was added"); + assertEquals(actionXWithout, with.children.get(3).x(), + "and so does the action"); + } + + @Test + @DisplayName("a bar with only a flexibleSpace still has a width") + void flexibleSpaceOnlyBarMeasuresItsBackground() { + // Crane's bar is exactly this: no title, no leading, no actions — the + // whole thing is the flexibleSpace. Summing the row alone would measure + // the bar as zero wide and collapse it. + AppBar b = new AppBar(); + b.automaticallyImplyLeading(false); + b.flexibleSpace(new ProbeBox(240, 56)); + + AppBarRenderElement element = (AppBarRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + assertEquals(240, element.layout( + BoxConstraints.loose(Double.POSITIVE_INFINITY, 56)).width(), + "the bar measures its background when it has nothing else"); + } + + @Test + @DisplayName("slots are centred vertically in the bar") + void slotsAreVerticallyCentred() { + Bar bar = mountAndLayout( + barWith(new ProbeBox(40, 40), new ProbeBox(100, 20)), + BoxConstraints.tight(400, 56)); + + assertEquals((56 - 40) / 2, bar.children.get(0).y(), "leading centred"); + assertEquals((56 - 20) / 2, bar.children.get(1).y(), "title centred"); + } + + @Test + @DisplayName("several actions stack rightwards, ending at the edge") + void actionsPackTowardsTheEdge() { + Bar bar = mountAndLayout( + barWith(null, new ProbeBox(50, 20), + new ProbeBox(48, 40), new ProbeBox(48, 40), new ProbeBox(48, 40)), + BoxConstraints.tight(400, 56)); + + // title, then the three actions in order + assertEquals(400 - 48 * 3, bar.children.get(1).x()); + assertEquals(400 - 48 * 2, bar.children.get(2).x()); + assertEquals(400 - 48, bar.children.get(3).x()); + } + + @Test + @DisplayName("a centred title stays clear of both the leading and the actions") + void aCentredTitleDoesNotSlideUnderTheSlots() { + AppBar b = barWith(new ProbeBox(56, 40), new ProbeBox(300, 20), new ProbeBox(48, 40)); + b.centerTitle(true); + Bar bar = mountAndLayout(b, BoxConstraints.tight(400, 56)); + + RenderElement title = bar.children.get(1); + assertTrue(title.x() >= 56, "a wide centred title is pushed clear of the leading"); + assertTrue(title.x() + title.size().width() <= 400, + "and never runs off the trailing edge"); + } + + @Test + @DisplayName("the title is measured against what the other slots leave free") + void theTitleGetsOnlyTheRemainingWidth() { + // A greedy title (very wide probe) must be squeezed, not allowed to overlap. + Bar bar = mountAndLayout( + barWith(new ProbeBox(56, 40), new ProbeBox(1000, 20), new ProbeBox(48, 40)), + BoxConstraints.tight(400, 56)); + + RenderElement title = bar.children.get(1); + assertEquals(400 - 56 - 48 - 32, title.size().width(), + "title width = bar - leading - actions - spacing either side"); + } + + @Test + @DisplayName("with no bounded width the bar is as wide as its contents") + void anUnboundedBarSizesToItsContents() { + Bar bar = mountAndLayout( + barWith(new ProbeBox(56, 40), new ProbeBox(100, 20), new ProbeBox(48, 40)), + BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + + assertEquals(56 + 48 + 100 + 32, bar.element.size().width(), + "leading + actions + title + its spacing"); + assertEquals(56, bar.element.size().height(), "the Material default height"); + } + + @Test + @DisplayName("a bar with only a title still puts it at the leading inset") + void titleOnlyKeepsTheInset() { + Bar bar = mountAndLayout(barWith(null, new ProbeBox(100, 20)), + BoxConstraints.tight(400, 56)); + + assertEquals(1, bar.children.size()); + assertEquals(16, bar.children.get(0).x(), "no leading, so the title starts at 16lp"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarTitleStyleTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarTitleStyleTest.java new file mode 100644 index 00000000000..25b899b4f1d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/AppBarTitleStyleTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.TextStyle; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * The AppBar title falls back to the text theme's titleLarge. + * + *

Flutter resolves it as {@code AppBar.titleTextStyle ?? + * AppBarTheme.titleTextStyle ?? textTheme.titleLarge}. The last link was + * missing, so a bar whose theme names no title style -- most of them -- fell + * through to whatever a bare Text picks: about 16 logical pixels against + * titleLarge's 22, which rendered every title in the gallery at roughly seven + * tenths of its size.

+ */ +class AppBarTitleStyleTest { + + @Test + void withoutAThemeStyleTheTitleTakesTitleLarge() { + TextStyle chosen = AppBarRenderElement.chooseTitleStyle(null, new TextTheme()); + assertEquals(22.0, chosen.getFontSize(), 0.001); + } + + @Test + void aThemeStyleWins() { + TextStyle themed = new TextStyle(); + themed.fontSize(31); + TextStyle chosen = AppBarRenderElement.chooseTitleStyle(themed, new TextTheme()); + assertEquals(31.0, chosen.getFontSize(), 0.001); + } + + @Test + void noTextThemeAtAllLeavesTheTitleUnstyled() { + assertNull(AppBarRenderElement.chooseTitleStyle(null, null)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomAppBarHeightTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomAppBarHeightTest.java new file mode 100644 index 00000000000..8146d2b2436 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomAppBarHeightTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The bottom app bar is Material 3's height, and carries no safe-area padding + * of its own. + * + *

It was Material 2's 56. And the inset is not the bar's to carry: the + * reference draws it at 80 logical pixels even on a screen that HAS a bottom + * inset. Where the reply study looks 114 tall it is 80 of bar over 34 of the + * scaffold's own background.

+ */ +class BottomAppBarHeightTest { + + @Test + void theBarIsMaterialThreesHeight() { + assertEquals(80.0, BottomAppBar.HEIGHT_LP, 0.001); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomNavigationBarLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomNavigationBarLayoutTest.java new file mode 100644 index 00000000000..c5082a99658 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/BottomNavigationBarLayoutTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * BottomNavigationBar item layout, headless (Dp scale 1): 80lp bar height, + * equal-width slots, icons centered per slot (vertically centered when the + * item has no label), onTap(index) dispatch. + */ +class BottomNavigationBarLayoutTest { + + private static BottomNavigationBarItem item(Widget icon, String label) { + BottomNavigationBarItem i = new BottomNavigationBarItem(); + i.icon(icon); + i.label(label); + return i; + } + + private BottomNavigationBarRenderElement mountAndLayout(BottomNavigationBar bar, BoxConstraints c) { + RenderHost host = new RenderHost(); + BottomNavigationBarRenderElement e = + (BottomNavigationBarRenderElement) FlutterUI.mount(bar, host, new BuildOwner()); + e.layout(c); + e.position(0, 0); + return e; + } + + @Test + void threeUnlabeledItemsCenterTheirIconsInEqualSlots() { + BottomNavigationBar bar = new BottomNavigationBar(); + bar.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + + BottomNavigationBarRenderElement e = + mountAndLayout(bar, BoxConstraints.loose(300, Double.POSITIVE_INFINITY)); + + assertEquals(new Size(300, 80), e.size(), "80lp bar, full width"); + for (int i = 0; i < 3; i++) { + RenderElement icon = e.iconRenderElement(i); + assertEquals(i * 100 + 45, icon.x(), "icon " + i + " centered in its 100px slot"); + assertEquals(35, icon.y(), "no label: icon vertically centered (80-10)/2"); + } + } + + @Test + void overlayCoversTheWholeBarAndSitsLast() { + BottomNavigationBar bar = new BottomNavigationBar(); + bar.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + + BottomNavigationBarRenderElement e = + mountAndLayout(bar, BoxConstraints.loose(200, Double.POSITIVE_INFINITY)); + + List children = e.renderChildren(); + assertEquals(3, children.size(), "2 icons + overlay"); + RenderElement overlay = children.get(children.size() - 1); + assertEquals(new Size(200, 80), overlay.size()); + assertEquals(0, overlay.x()); + assertEquals(0, overlay.y()); + } + + @Test + void unboundedWidthFallsBackTo80lpSlots() { + BottomNavigationBar bar = new BottomNavigationBar(); + bar.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + + BottomNavigationBarRenderElement e = mountAndLayout(bar, + BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + + assertEquals(new Size(160, 80), e.size(), "2 items x 80lp fallback slots"); + } + + @Test + void userTapDispatchesTheItemIndexAsLong() { + final List taps = new ArrayList(); + BottomNavigationBar bar = new BottomNavigationBar(); + bar.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + bar.currentIndex(0); + bar.onTap(taps::add); + + BottomNavigationBarRenderElement e = + mountAndLayout(bar, BoxConstraints.loose(300, Double.POSITIVE_INFINITY)); + + e.userTapped(2); + e.userTapped(0); + e.userTapped(7); // out of range: ignored + assertEquals(List.of(2L, 0L), taps); + } + + @Test + void currentIndexChangeRebuildsTintedIconsInPlace() { + BottomNavigationBar bar = new BottomNavigationBar(); + bar.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + bar.currentIndex(0); + + BottomNavigationBarRenderElement e = + mountAndLayout(bar, BoxConstraints.loose(200, Double.POSITIVE_INFINITY)); + RenderElement firstIconBefore = e.iconRenderElement(0); + + BottomNavigationBar updated = new BottomNavigationBar(); + updated.items(DartList.of( + item(new ProbeBox(10, 10), null), + item(new ProbeBox(10, 10), null))); + updated.currentIndex(1); + e.update(updated); + + assertEquals(firstIconBefore, e.iconRenderElement(0), + "same widget type: the icon element is reused in place"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ButtonContentUnwrapTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ButtonContentUnwrapTest.java new file mode 100644 index 00000000000..464821ea242 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ButtonContentUnwrapTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Icons; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.widgets.HasChild; +import com.codename1.flutter.widgets.Icon; +import com.codename1.flutter.widgets.Text; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A button finds the glyph inside a WRAPPED icon. + * + *

Buttons consume their content instead of mounting it, so a content widget that was + * neither a Text nor an Icon used to be drawn via {@code toString()}. That is how the + * gallery's back button came out as the text "com.codename1.flutter.material.BackButton…", + * clipped by the app bar to "com.c" — and it is the reason a whole row of demo pages looked + * like they had no way back.

+ */ +class ButtonContentUnwrapTest { + + /** A composed widget that builds an icon — the shape of BackButtonIcon. */ + static class ComposedIcon extends StatelessWidget { + @Override + public Widget build(BuildContext context) { + return new Icon(Icons.arrow_back); + } + } + + /** A single-child wrapper — the shape of FeatureDiscovery around the options icon. */ + static class Wrapper extends Widget implements HasChild { + private final Widget child; + + Wrapper(Widget child) { + this.child = child; + } + + @Override + public Widget getChild() { + return child; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new com.codename1.flutter.widgets.PassThroughRenderElement(this); + } + } + + private ButtonRenderElement mount(Widget icon) { + IconButton b = new IconButton(); + b.icon(icon); + return (ButtonRenderElement) FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + } + + @Test + @DisplayName("a plain Icon still yields its glyph") + void aPlainIconIsFound() { + assertEquals(Icons.arrow_back.codePoint(), + mount(new Icon(Icons.arrow_back)).consumedIconChar()); + } + + @Test + @DisplayName("a composed widget is built to find the icon it produces") + void aComposedIconIsResolved() { + ButtonRenderElement e = mount(new ComposedIcon()); + + assertEquals(Icons.arrow_back.codePoint(), e.consumedIconChar(), + "BackButtonIcon-shaped content must resolve to its glyph"); + assertEquals(null, e.consumedLabel(), + "and must NOT fall back to a toString() label"); + } + + @Test + @DisplayName("a wrapped icon is found through the wrapper") + void aWrappedIconIsResolved() { + ButtonRenderElement e = mount(new Wrapper(new Icon(Icons.search))); + + assertEquals(Icons.search.codePoint(), e.consumedIconChar()); + assertEquals(null, e.consumedLabel()); + } + + @Test + @DisplayName("nested wrapping still resolves") + void nestingIsWalked() { + ButtonRenderElement e = mount(new Wrapper(new ComposedIcon())); + + assertEquals(Icons.arrow_back.codePoint(), e.consumedIconChar()); + } + + @Test + @DisplayName("a wrapped Text is still used as the label") + void aWrappedTextBecomesTheLabel() { + ButtonRenderElement e = mount(new Wrapper(new Text("Go back"))); + + assertEquals("Go back", e.consumedLabel()); + } + + /** A stateful wrapper around an icon — the shape of the gallery's FeatureDiscovery. */ + static class StatefulIcon extends com.codename1.flutter.StatefulWidget { + @Override + public com.codename1.flutter.State createState() { + return new com.codename1.flutter.State() { + @Override + public Widget build(BuildContext context) { + return new Icon(Icons.tune); + } + }; + } + } + + @Test + @DisplayName("a STATEFUL wrapper is built to find the icon it produces") + void aStatefulIconIsResolved() { + ButtonRenderElement e = mount(new StatefulIcon()); + + assertEquals(Icons.tune.codePoint(), e.consumedIconChar(), + "FeatureDiscovery-shaped content must resolve to its glyph"); + assertEquals(null, e.consumedLabel()); + } + + @Test + @DisplayName("unresolvable content NEVER becomes a toString() label") + void unresolvableContentIsNeverStringified() { + // A widget the walk cannot see through. Printing its class name put + // "com.codename1.flutter..." across the app bar; no label is the only honest answer. + // It has to be MOUNTABLE as well as opaque: a button now mounts content it cannot + // consume rather than dropping it, so a fixture whose element does not match its + // widget fails on the cast rather than on what this test is about. + class Opaque extends Widget implements com.codename1.flutter.widgets.HasChild { + @Override + public Widget getChild() { + return null; + } + + @Override + public com.codename1.flutter.Element createElement() { + return new com.codename1.flutter.widgets.PassThroughRenderElement(this); + } + } + Widget opaque = new Opaque(); + ButtonRenderElement e = mount(opaque); + + assertEquals(null, e.consumedLabel(), "no label beats a class name"); + assertEquals(0, e.consumedIconChar()); + } + + @Test + @DisplayName("content that resolves to nothing renders no glyph and no label text") + void unresolvableContentIsNotStringified() { + // A widget that is neither wrapper nor composed: the walk gives up. It must not + // crash, and the old toString() behaviour is what remains for it. + ButtonRenderElement e = mount(null); + + assertEquals(0, e.consumedIconChar()); + assertEquals(null, e.consumedLabel()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/CategoryHeaderShapeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/CategoryHeaderShapeTest.java new file mode 100644 index 00000000000..c13769849b7 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/CategoryHeaderShapeTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Expanded; +import com.codename1.flutter.widgets.Opacity; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SizedBox; +import com.codename1.flutter.widgets.Wrap; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The shape of the Flutter gallery's category header, layer by layer, so an + * inflated card height is attributed to the node that caused it rather than + * eyeballed off a screenshot. + * + *

The header is a 64lp image in 8lp padding beside a title, inside + * Row > Expanded > Wrap, inside Material inside a Container — so every + * layer must report exactly 80lp (64 + 8 + 8). Headless, so Dp scale is 1 and + * logical pixels are pixels.

+ */ +class CategoryHeaderShapeTest { + + private static final double IMAGE = 64; + private static final double PAD = 8; + private static final double EXPECTED = IMAGE + PAD + PAD; + private static final double WIDTH = 400; + + private static DartList list(Widget... items) { + DartList l = new DartList(); + for (Widget w : items) { + l.add(w); + } + return l; + } + + private static Widget paddedImage() { + Padding p = new Padding(); + p.padding(EdgeInsets.all(PAD)); + p.child(new ProbeBox(IMAGE, IMAGE)); + return p; + } + + private static Widget paddedTitle() { + Padding p = new Padding(); + p.padding(EdgeInsets.only(PAD, 0, 0, 0)); + p.child(new ProbeBox(120, 32)); + return p; + } + + private static Wrap headerWrap() { + Wrap w = new Wrap(); + w.crossAxisAlignment(com.codename1.flutter.WrapCrossAlignment.center); + w.children(list(paddedImage(), paddedTitle())); + return w; + } + + private static RenderElement layout(Widget w) { + RenderHost host = new RenderHost(); + RenderElement e = (RenderElement) FlutterUI.mount(w, host, new BuildOwner()); + e.layout(BoxConstraints.loose(WIDTH, Double.POSITIVE_INFINITY)); + e.position(0, 0); + return e; + } + + @Test + void wrapIsImagePlusPadding() { + assertEquals(EXPECTED, layout(headerWrap()).size().height(), 0.001, + "Wrap run height is the tallest child: the padded 64lp image"); + } + + @Test + void rowOverExpandedWrapDoesNotInflate() { + Expanded ex = new Expanded(); + ex.child(headerWrap()); + + // the collapsed chevron: opacity 0 with no child, contributing nothing + Opacity chevron = new Opacity(); + chevron.opacity(0); + + Row row = new Row(); + row.children(list(ex, chevron)); + + assertEquals(EXPECTED, layout(row).size().height(), 0.001, + "Expanded stretches on the main axis only; the row is as tall as the wrap"); + } + + @Test + void sizedBoxWidthOnlyLeavesHeightToTheChild() { + Expanded ex = new Expanded(); + ex.child(headerWrap()); + Row row = new Row(); + row.children(list(ex)); + + SizedBox box = new SizedBox(); + box.width(WIDTH); + box.child(row); + + assertEquals(EXPECTED, layout(box).size().height(), 0.001, + "a width-only SizedBox must not tighten or inflate the height"); + } + + @Test + void materialWrapsTightlyAroundItsChild() { + Expanded ex = new Expanded(); + ex.child(headerWrap()); + Row row = new Row(); + row.children(list(ex)); + SizedBox box = new SizedBox(); + box.width(WIDTH); + box.child(row); + + Material m = new Material(); + m.color(new com.codename1.flutter.Color(0xFFFFFFFFL)); + m.child(box); + + assertEquals(EXPECTED, layout(m).size().height(), 0.001, + "Material is a surface, not padding — it takes its child's size"); + } + + @Test + void containerWithMarginAddsOnlyTheMargin() { + Expanded ex = new Expanded(); + ex.child(headerWrap()); + Row row = new Row(); + row.children(list(ex)); + SizedBox box = new SizedBox(); + box.width(WIDTH); + box.child(row); + Material m = new Material(); + m.child(box); + + com.codename1.flutter.widgets.Container c = new com.codename1.flutter.widgets.Container(); + c.margin(EdgeInsets.symmetric(32, PAD)); + c.child(m); + + assertEquals(EXPECTED + PAD + PAD, layout(c).size().height(), 0.001, + "8lp margin above and below the 80lp card"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ControlledInputsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ControlledInputsTest.java new file mode 100644 index 00000000000..11c026c6f10 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ControlledInputsTest.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.rendering.RenderHost; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Controlled semantics of the M3 input widgets, headless: a user gesture + * fires onChanged with the attempted value but the CONFIGURED value stays + * authoritative until the app rebuilds the widget; Radio selection derives + * from groupValue equality; Slider scales its double range onto the int + * progress model losslessly (within a step). + */ +class ControlledInputsTest { + + private static E mount(com.codename1.flutter.Widget w) { + RenderHost host = new RenderHost(); + @SuppressWarnings("unchecked") + E e = (E) FlutterUI.mount(w, host, new BuildOwner()); + return e; + } + + // ------------------------------------------------------------------ + // Checkbox + // ------------------------------------------------------------------ + + @Test + void checkboxToggleFiresOnChangedButValueStaysUntilRebuild() { + final List received = new ArrayList(); + Checkbox cb = new Checkbox(); + cb.value(false); + cb.onChanged(received::add); + + CheckboxRenderElement e = mount(cb); + assertFalse(e.configuredValue()); + + e.userToggled(true); + assertEquals(List.of(true), received, "onChanged got the attempted value"); + assertFalse(e.configuredValue(), "the widget's value is authoritative until a rebuild"); + + // the app's setState/rebuild delivers a new widget with the new value + Checkbox updated = new Checkbox(); + updated.value(true); + updated.onChanged(received::add); + e.update(updated); + assertTrue(e.configuredValue(), "the rebuild moved the checkbox"); + } + + @Test + void checkboxWithoutOnChangedStillSnapsBack() { + Checkbox cb = new Checkbox(); + cb.value(true); + CheckboxRenderElement e = mount(cb); + e.userToggled(false); + assertTrue(e.configuredValue()); + } + + // ------------------------------------------------------------------ + // Switch + // ------------------------------------------------------------------ + + @Test + void switchToggleFiresOnChangedButValueStaysUntilRebuild() { + final List received = new ArrayList(); + Switch sw = new Switch(); + sw.value(true); + sw.onChanged(received::add); + + SwitchRenderElement e = mount(sw); + e.userToggled(false); + assertEquals(List.of(false), received); + assertTrue(e.configuredValue(), "still on until the app rebuilds"); + + Switch updated = new Switch(); + updated.value(false); + e.update(updated); + assertFalse(e.configuredValue()); + } + + // ------------------------------------------------------------------ + // Radio + // ------------------------------------------------------------------ + + @Test + void radioSelectionDerivesFromGroupValueEquality() { + Radio vanilla = new Radio(); + vanilla.value("vanilla"); + vanilla.groupValue("chocolate"); + RadioRenderElement e = mount(vanilla); + assertFalse(e.selected()); + + Radio nowSelected = new Radio(); + nowSelected.value("vanilla"); + // a fresh but EQUAL string: Dart == semantics, not identity + nowSelected.groupValue(new StringBuilder("vanilla").toString()); + e.update(nowSelected); + assertTrue(e.selected()); + } + + @Test + void radioSelectFiresOnChangedWithItsValueAndStaysControlled() { + final List received = new ArrayList(); + Radio r = new Radio(); + r.value("vanilla"); + r.groupValue("chocolate"); + r.onChanged(received::add); + + RadioRenderElement e = mount(r); + e.userSelected(); + assertEquals(List.of("vanilla"), received, "onChanged reports this radio's value"); + assertFalse(e.selected(), "selection only moves when groupValue does"); + } + + // ------------------------------------------------------------------ + // Slider + // ------------------------------------------------------------------ + + @Test + void sliderScalingRoundTripsWithinAStep() { + double min = -2.0; + double max = 6.0; + long steps = 16; + for (int p = 0; p <= steps; p++) { + double v = SliderRenderElement.valueFor(p, min, max, steps); + assertEquals(p, SliderRenderElement.progressFor(v, min, max, steps), + "progress -> value -> progress is exact at " + p); + } + // value -> progress -> value stays within half a step + double stepSize = (max - min) / steps; + for (double v = min; v <= max; v += 0.37) { + int p = SliderRenderElement.progressFor(v, min, max, steps); + double back = SliderRenderElement.valueFor(p, min, max, steps); + assertTrue(Math.abs(back - v) <= stepSize / 2 + 1e-9, + "roundtrip drift at " + v + " -> " + back); + } + } + + @Test + void sliderScalingClampsOutOfRangeAndDegenerateRanges() { + assertEquals(0, SliderRenderElement.progressFor(-5, 0, 1, 10)); + assertEquals(10, SliderRenderElement.progressFor(7, 0, 1, 10)); + assertEquals(0, SliderRenderElement.progressFor(3, 4, 4, 10), "max <= min collapses to 0"); + assertEquals(0.0, SliderRenderElement.valueFor(5, 0.0, 1.0, 0), "no steps returns min"); + } + + @Test + void sliderDragFiresScaledDoubleAndStaysControlled() { + final List received = new ArrayList(); + Slider s = new Slider(); + s.value(2.0); + s.min(0.0); + s.max(10.0); + s.divisions(20L); + s.onChanged(received::add); + + SliderRenderElement e = mount(s); + assertEquals(20, e.steps()); + assertEquals(4, e.configuredProgress(), "2.0 in [0,10] over 20 steps"); + + e.userDragged(15); + assertEquals(List.of(7.5), received, "progress 15/20 of [0,10]"); + assertEquals(4, e.configuredProgress(), "configured value did not move"); + } + + @Test + void sliderDefaultsMatchFlutter() { + Slider s = new Slider(); + assertEquals(0.0, s.getMin()); + assertEquals(1.0, s.getMax()); + SliderRenderElement e = mount(s); + assertEquals(SliderRenderElement.DEFAULT_STEPS, e.steps(), "continuous default"); + } + + // ------------------------------------------------------------------ + // TextField / controller (headless flow) + // ------------------------------------------------------------------ + + @Test + void textFieldUserEditSyncsControllerAndFiresOnChanged() { + final List changed = new ArrayList(); + final int[] notified = {0}; + TextEditingController ctl = new TextEditingController(); + ctl.text("start"); + ctl.addListener(() -> notified[0]++); + + TextField tf = new TextField(); + tf.controller(ctl); + tf.onChanged(changed::add); + + TextFieldRenderElement e = mount(tf); + e.userEdited("hello"); + assertEquals(List.of("hello"), changed); + assertEquals("hello", ctl.text(), "controller absorbed the user edit"); + assertEquals(1, notified[0], "controller listeners fired once"); + } + + @Test + void controllerSetTextAndClearNotifyListeners() { + TextEditingController ctl = new TextEditingController(); + final int[] notified = {0}; + ctl.addListener(() -> notified[0]++); + ctl.setText("abc"); + assertEquals("abc", ctl.text()); + ctl.clear(); + assertEquals("", ctl.text()); + assertEquals(2, notified[0]); + } + + // ------------------------------------------------------------------ + // SnackBar consumption + // ------------------------------------------------------------------ + + @Test + void snackBarContentTextIsConsumedWithDefaultDuration() { + SnackBar sb = new SnackBar(); + sb.content(new com.codename1.flutter.widgets.Text("Saved")); + ScaffoldMessengerState state = ScaffoldMessenger.of(null); + state.showSnackBar(sb); + assertEquals("Saved", state.lastMessage()); + assertEquals(SnackBar.DEFAULT_DURATION_MS, state.lastDurationMillis()); + + SnackBar timed = new SnackBar(); + timed.content(new com.codename1.flutter.widgets.Text("Bye")); + timed.duration(dart.core.Duration.of(0, 0, 0, 2, 0, 0)); + state.showSnackBar(timed); + assertEquals(2000, state.lastDurationMillis()); + } + + @Test + void maxLengthLimitsWhatTheControllerAndOnChangedSee() { + final List changed = new ArrayList(); + TextEditingController ctl = new TextEditingController(); + TextField tf = new TextField(); + tf.controller(ctl); + tf.maxLength(4); + tf.onChanged(changed::add); + TextFieldRenderElement e = mount(tf); + e.userEdited("abcdef"); + assertEquals("abcd", ctl.text(), "the controller never holds more than maxLength"); + assertEquals(List.of("abcd"), changed); + } + + @Test + void aMaxLengthOfZeroAcceptsNothing() { + final List changed = new ArrayList(); + TextField tf = new TextField(); + tf.maxLength(0); + tf.onChanged(changed::add); + TextFieldRenderElement e = mount(tf); + e.userEdited("abc"); + assertEquals(List.of(""), changed, "zero is a limit, not 'no limit'"); + TextField unlimited = new TextField(); + unlimited.maxLength(-1); // Flutter's noMaxLength + final List free = new ArrayList(); + unlimited.onChanged(free::add); + TextFieldRenderElement u = mount(unlimited); + u.userEdited("abc"); + assertEquals(List.of("abc"), free); + } + + @Test + void oneControllerDrivesEveryFieldItIsGivenTo() { + TextEditingController ctl = new TextEditingController(); + TextField first = new TextField(); + first.controller(ctl); + TextField second = new TextField(); + second.controller(ctl); + TextFieldRenderElement a = mount(first); + TextFieldRenderElement b = mount(second); + a.userEdited("typed"); + assertEquals("typed", ctl.text()); + com.codename1.flutter.FlutterUI.unmountTree(b); + ctl.setText("set"); + assertEquals("set", ctl.text(), "unmounting the second field must not orphan the first"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/DialogInheritanceTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/DialogInheritanceTest.java new file mode 100644 index 00000000000..046cfa13a64 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/DialogInheritanceTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Widget; +import com.codename1.flutter.navigation.Navigator; +import com.codename1.flutter.provider.Consumer; +import com.codename1.flutter.provider.Provider; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * A dialog mounts as a root of its own, yet in Flutter it is a route built under the + * navigator: its builder's {@code Provider.of}, {@code Theme.of} and localizations see + * the app above. Mounted with no context to continue from, they all came back null. + */ +class DialogInheritanceTest { + + static final class Store { + } + + @BeforeEach + @AfterEach + void reset() { + Navigator.reset(); + Dialogs.reset(); + } + + @Test + @DisplayName("a dialog builder finds the provider above the context that showed it") + void dialogBuilderSeesTheAppsProviders() { + final Store store = new Store(); + final BuildContext[] showing = new BuildContext[1]; + Consumer capture = new Consumer(); + capture.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, Object value, Widget child) { + showing[0] = context; + return new ProbeBox(1, 1); + } + }); + Provider app = Provider.value(null, store, capture); + FlutterUI.mount(app, new RenderHost(), new BuildOwner()); + + final Object[] seen = {"builder never ran"}; + Dialogs.showDialog(showing[0], new Funcs.Func1() { + @Override + public Widget call(BuildContext context) { + seen[0] = Provider.of(context, false, Store.class); + return new ProbeBox(5, 5); + } + }); + assertSame(store, seen[0], "Provider.of inside the dialog resolves the app's store"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabPlacementTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabPlacementTest.java new file mode 100644 index 00000000000..9b71669986d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabPlacementTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.rendering.Dp; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Where a Scaffold puts its FloatingActionButton. + * + *

{@code floatingActionButtonLocation} was accepted and discarded, so every + * FAB floated at the bottom right whatever it asked for. Reply's compose button + * is centreDocked and drew in the corner on top of the bottom bar instead of + * centred and straddling it.

+ * + *

Headless, so one logical pixel is one device pixel.

+ */ +class FabPlacementTest { + + private static final double SCAFFOLD_W = 400; + private static final double SCAFFOLD_H = 800; + private static final double FAB = 56; + private static final double NAV = 80; + + private static double x(FloatingActionButtonLocation where) { + return ScaffoldRenderElement.fabX(where, SCAFFOLD_W, FAB); + } + + private static double y(FloatingActionButtonLocation where) { + return ScaffoldRenderElement.fabY(where, SCAFFOLD_H, FAB, NAV); + } + + @Test + void centreLocationsCentreTheFab() { + assertEquals((SCAFFOLD_W - FAB) / 2, x(FloatingActionButtonLocation.centerDocked), 0.001); + assertEquals((SCAFFOLD_W - FAB) / 2, x(FloatingActionButtonLocation.centerFloat), 0.001); + assertEquals((SCAFFOLD_W - FAB) / 2, x(FloatingActionButtonLocation.miniCenterTop), 0.001); + } + + @Test + void startAndEndSitAgainstTheirEdges() { + double margin = Dp.px(16); + assertEquals(margin, x(FloatingActionButtonLocation.startFloat), 0.001); + assertEquals(SCAFFOLD_W - FAB - margin, x(FloatingActionButtonLocation.endFloat), 0.001); + } + + @Test + void noLocationIsFluttersEndFloat() { + double margin = Dp.px(16); + assertEquals(SCAFFOLD_W - FAB - margin, x(null), 0.001); + assertEquals(SCAFFOLD_H - NAV - FAB - margin, y(null), 0.001); + } + + @Test + void aDockedFabStraddlesTheBottomStripsTopEdge() { + // Its CENTRE sits on the edge, which is what lets a notched bar cut a + // hole for it; a floating one clears the strip by the margin instead. + double contentBottom = SCAFFOLD_H - NAV; + assertEquals(contentBottom - FAB / 2, y(FloatingActionButtonLocation.centerDocked), 0.001); + assertEquals(contentBottom - FAB - Dp.px(16), + y(FloatingActionButtonLocation.centerFloat), 0.001); + } + + @Test + void theContentStopsAtTheBarWhenThereIsOne() { + // A bottom bar is what holds the content off the edge of the display, + // so the safe-area inset is NOT added on top of it. Adding both lifted + // the reply study's docked button clear of its own bar and into the mail + // list, where a card painted over it and it vanished. + assertEquals(80.0, ScaffoldRenderElement.contentInset(80, 102), 0.001); + // With no bar, the display's own padding is what the content stops at. + assertEquals(102.0, ScaffoldRenderElement.contentInset(0, 102), 0.001); + } + + @Test + void aTopFabSitsAtTheTop() { + assertEquals(Dp.px(16), y(FloatingActionButtonLocation.centerTop), 0.001); + } + + @Test + void aDockedFabNeverFallsOffTheBottom() { + // Flutter clamps it to the scaffold; a bottom strip taller than the + // scaffold would otherwise push it past the edge. + double got = ScaffoldRenderElement.fabY( + FloatingActionButtonLocation.centerDocked, SCAFFOLD_H, FAB, 0); + assertEquals(SCAFFOLD_H - FAB - Dp.px(16), got, 0.001); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabSizeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabSizeTest.java new file mode 100644 index 00000000000..fd049ab438d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/FabSizeTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.rendering.Dp; +import com.codename1.flutter.rendering.Size; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A floating action button is a fixed Material size, not whatever its glyph + * happens to need. + * + *

The size came from the component's preferred size, which is the glyph plus + * whatever padding the theme in force carries. With no theme entry for the UIID + * that is 83 device pixels against the reference's 168 -- less than half.

+ */ +class FabSizeTest { + + @Test + void aRegularFabIsAFixedSquare() { + Size s = FabRenderElement.materialSize(false, 12); + assertEquals(Dp.px(56), s.width(), 0.001); + assertEquals(Dp.px(56), s.height(), 0.001); + } + + @Test + void anExtendedFabFixesItsHeightAndFollowsItsLabel() { + // Material 3 puts the extended form at 56 too, not Material 2's 48. + Size s = FabRenderElement.materialSize(true, Dp.px(300)); + assertEquals(Dp.px(300), s.width(), 0.001); + assertEquals(Dp.px(56), s.height(), 0.001); + } + + @Test + void anExtendedFabIsNeverNarrowerThanTheMinimum() { + Size s = FabRenderElement.materialSize(true, 4); + assertEquals(Dp.px(80), s.width(), 0.001); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/InputDecorationDefaultsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/InputDecorationDefaultsTest.java new file mode 100644 index 00000000000..46fe3a9dec0 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/InputDecorationDefaultsTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A decoration resolves against the ambient inputDecorationTheme. + * + *

Flutter's {@code InputDecoration.applyDefaults}: every field the widget + * leaves unset falls back to the theme. The theme was held opaquely and never + * read, so Rally -- which names its dark fill once on the theme rather than on + * each of its login fields -- rendered white blocks on a dark page.

+ */ +class InputDecorationDefaultsTest { + + private static InputDecorationThemeData darkFilledTheme() { + InputDecorationThemeData t = new InputDecorationThemeData(); + t.filled(true); + t.fillColor(new Color(0xFF33333DL)); + t.contentPadding(EdgeInsets.all(20)); + return t; + } + + @Test + void aThemeFillReachesADecorationThatNamesNone() { + InputDecoration d = new InputDecoration(); + assertTrue(TextFieldRenderElement.resolveFilled(d, darkFilledTheme())); + assertEquals(0xFF33333DL, + TextFieldRenderElement.resolveFill(d, darkFilledTheme()).value()); + } + + @Test + void theDecorationsOwnFillWins() { + InputDecoration d = new InputDecoration(); + d.filled(true); + d.fillColor(new Color(0xFFAABBCCL)); + assertEquals(0xFFAABBCCL, + TextFieldRenderElement.resolveFill(d, darkFilledTheme()).value()); + } + + @Test + void withNoThemeNothingIsFilled() { + InputDecoration d = new InputDecoration(); + assertFalse(TextFieldRenderElement.resolveFilled(d, null)); + assertNull(TextFieldRenderElement.resolveFill(d, null)); + assertNull(TextFieldRenderElement.resolvePadding(d, null)); + } + + @Test + void paddingFallsBackToTheThemeToo() { + InputDecoration d = new InputDecoration(); + EdgeInsets got = (EdgeInsets) TextFieldRenderElement.resolvePadding(d, darkFilledTheme()); + assertEquals(20.0, got.left(), 0.001); + d.contentPadding(EdgeInsets.all(4)); + got = (EdgeInsets) TextFieldRenderElement.resolvePadding(d, darkFilledTheme()); + assertEquals(4.0, got.left(), 0.001); + } + + @Test + void aThemeDataOnTheThemeIsReadableBack() { + ThemeData theme = new ThemeData(); + InputDecorationThemeData t = darkFilledTheme(); + theme.inputDecorationTheme(t); + assertEquals(t, theme.inputDecorationTheme()); + // Anything else stays opaque rather than being mistaken for one. + theme.inputDecorationTheme("not a theme"); + assertNull(theme.inputDecorationTheme()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ListTileLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ListTileLayoutTest.java new file mode 100644 index 00000000000..3d9cc42f66c --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ListTileLayoutTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.rendering.Size; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * ListTile geometry, headless with stubbed intrinsics (Dp scale is 1 without + * a Display, so logical pixels == pixels): 16lp horizontal padding, 16lp + * gaps, title above subtitle, leading/trailing vertically centered, 56lp + * minimum height, tap overlay covering the tile. + */ +class ListTileLayoutTest { + + private ListTileRenderElement mountAndLayout(ListTile tile, BoxConstraints c) { + RenderHost host = new RenderHost(); + ListTileRenderElement e = (ListTileRenderElement) FlutterUI.mount(tile, host, new BuildOwner()); + e.layout(c); + e.position(0, 0); + return e; + } + + @Test + void fullTileGeometry() { + ListTile tile = new ListTile(); + tile.leading(new ProbeBox(20, 20)); + tile.title(new ProbeBox(100, 20)); + tile.subtitle(new ProbeBox(80, 16)); + tile.trailing(new ProbeBox(24, 24)); + tile.onTap(() -> { + }); + + ListTileRenderElement e = mountAndLayout(tile, BoxConstraints.loose(300, Double.POSITIVE_INFINITY)); + + // 72lp, not 56: Material sizes a tile by its LINE COUNT, and this one has a + // subtitle, so it is a two-line tile. 56lp is the one-line height, asserted by + // titleOnlyTileOmitsMissingSections below. + assertEquals(new Size(300, 72), e.size(), "72lp two-line height, full width"); + + List children = e.renderChildren(); + assertEquals(5, children.size(), "leading, title, subtitle, trailing, overlay"); + RenderElement leading = children.get(0); + RenderElement title = children.get(1); + RenderElement subtitle = children.get(2); + RenderElement trailing = children.get(3); + RenderElement overlay = children.get(4); + + assertEquals(16, leading.x(), "leading at the 16lp inset"); + assertEquals(26, leading.y(), "leading vertically centered: (72-20)/2"); + assertEquals(52, title.x(), "title after leading + 16lp gap: 16+20+16"); + assertEquals(18, title.y(), "text block centered: (72-36)/2"); + assertEquals(52, subtitle.x(), "subtitle aligned with title"); + assertEquals(38, subtitle.y(), "subtitle right below the title: 18+20"); + assertEquals(260, trailing.x(), "trailing right-aligned: 300-16-24"); + assertEquals(24, trailing.y(), "trailing vertically centered: (72-24)/2"); + assertEquals(new Size(300, 72), overlay.size(), "the tap overlay covers the tile"); + assertEquals(0, overlay.x()); + assertEquals(0, overlay.y()); + } + + @Test + void titleOnlyTileOmitsMissingSections() { + ListTile tile = new ListTile(); + tile.title(new ProbeBox(50, 20)); + + ListTileRenderElement e = mountAndLayout(tile, BoxConstraints.loose(200, Double.POSITIVE_INFINITY)); + + assertEquals(new Size(200, 56), e.size()); + List children = e.renderChildren(); + assertEquals(2, children.size(), "title + overlay"); + RenderElement title = children.get(0); + assertEquals(16, title.x(), "no leading: title starts at the padding"); + assertEquals(18, title.y(), "(56-20)/2"); + } + + @Test + void tallContentGrowsTheTileWithVerticalPadding() { + ListTile tile = new ListTile(); + tile.leading(new ProbeBox(20, 20)); + tile.title(new ProbeBox(100, 60)); + + ListTileRenderElement e = mountAndLayout(tile, BoxConstraints.loose(300, Double.POSITIVE_INFINITY)); + + assertEquals(76, e.size().height(), "content 60 + 2*8lp vertical padding"); + assertTrue(e.size().height() > 56); + } + + @Test + void unboundedWidthShrinksToIntrinsicContent() { + ListTile tile = new ListTile(); + tile.leading(new ProbeBox(20, 20)); + tile.title(new ProbeBox(100, 20)); + tile.trailing(new ProbeBox(24, 24)); + + ListTileRenderElement e = mountAndLayout(tile, + BoxConstraints.loose(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + + // 16 + 20 + 16 + 100 + 16 + 24 + 16 + assertEquals(208, e.size().width()); + } + + @Test + void tapFiresOnTap() { + final int[] taps = {0}; + ListTile tile = new ListTile(); + tile.title(new ProbeBox(10, 10)); + tile.onTap(() -> taps[0]++); + + ListTileRenderElement e = mountAndLayout(tile, BoxConstraints.loose(100, Double.POSITIVE_INFINITY)); + e.fireTap(); + assertEquals(1, taps[0]); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/MaterialClipGeometryTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/MaterialClipGeometryTest.java new file mode 100644 index 00000000000..6e28fc71c2e --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/MaterialClipGeometryTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * The rounded clip a Material surface installs, intersected with the clip it inherits. + * + *

This used to go through {@code GeneralPath.intersection()}, a general polygon clipper + * that allocated per card per frame and drove the collector from inside paint. It is now + * computed directly, which is only correct if the intersection really does reduce to + * "the intersected bounds, rounded at the corners the clip left alone" - so that is what + * these assert.

+ */ +class MaterialClipGeometryTest { + + /// Bounds and the four corner radii, clockwise from the top left. + private static int[] geom(int x, int y, int w, int h, int r, + int cx, int cy, int cw, int ch) { + int[] out = new int[8]; + assertTrue(MaterialRenderElement.clipGeometry(out, x, y, w, h, r, cx, cy, cw, ch), + "expected the surface to be at least partly visible"); + return out; + } + + @Test + @DisplayName("fully inside the clip: untouched bounds, all four corners rounded") + void insideClipKeepsEveryCorner() { + assertArrayEquals(new int[] {100, 100, 200, 150, 20, 20, 20, 20}, + geom(100, 100, 200, 150, 20, 0, 0, 1000, 1000)); + } + + @Test + @DisplayName("a card half off the left of its viewport keeps only its right corners") + void cutOnTheLeftSquaresTheLeftCorners() { + // The viewport starts at x=200; the card runs 100..300, so its left half is gone. + assertArrayEquals(new int[] {200, 100, 100, 150, 0, 20, 20, 0}, + geom(100, 100, 200, 150, 20, 200, 0, 800, 1000)); + } + + @Test + @DisplayName("a card half off the right of its viewport keeps only its left corners") + void cutOnTheRightSquaresTheRightCorners() { + assertArrayEquals(new int[] {100, 100, 100, 150, 20, 0, 0, 20}, + geom(100, 100, 200, 150, 20, 0, 0, 200, 1000)); + } + + @Test + @DisplayName("a card cut top and bottom keeps no corners but still clips to the band") + void cutOnBothAxesSquaresEverything() { + assertArrayEquals(new int[] {100, 120, 200, 100, 0, 0, 0, 0}, + geom(100, 100, 200, 150, 20, 0, 120, 1000, 100)); + } + + @Test + @DisplayName("the surviving radius never exceeds half the visible box") + void radiusIsClampedToTheVisibleSliver() { + // Only 24px of the card's right edge is left, so a 20px corner would overlap itself. + int[] q = geom(100, 100, 200, 150, 20, 276, 0, 800, 1000); + assertArrayEquals(new int[] {276, 100, 24, 150}, new int[] {q[0], q[1], q[2], q[3]}); + assertArrayEquals(new int[] {0, 12, 12, 0}, new int[] {q[4], q[5], q[6], q[7]}); + } + + @Test + @DisplayName("scrolled entirely out of the viewport: nothing to paint") + void offscreenReportsNothingVisible() { + assertFalse(MaterialRenderElement.clipGeometry(new int[8], + 100, 100, 200, 150, 20, 400, 0, 300, 1000)); + // Touching edges only - an empty intersection, not a one-pixel sliver. + assertFalse(MaterialRenderElement.clipGeometry(new int[8], + 100, 100, 200, 150, 20, 300, 0, 300, 1000)); + } + + @Test + @DisplayName("the clip path is a POLYGON, which is what keeps corners round on a GPU") + void clipPathReducesToAPolygon() { + // Not a stylistic preference. The ports ask isPolygon() to decide how to hand a clip + // to the GPU: Codename One's iOS backend renders a polygon through a stencil, and a + // shape it cannot reduce to one falls back to the BOUNDING BOX - a square-cornered + // card. Built with quadTo this returned false, and the study card's corners were + // square on iOS while correct on the desktop simulator for exactly that reason. + com.codename1.ui.geom.GeneralPath p = (com.codename1.ui.geom.GeneralPath) + new MaterialRenderElement(new Material()) + .clipShape(100, 100, 300, 200, 30, 30, 30, 30); + assertTrue(p.isPolygon(), "clip must reduce to a polygon"); + assertFalse(p.isRectangle(), "a rounded clip is not a rectangle"); + assertEquals(new com.codename1.ui.geom.Rectangle(100, 100, 300, 200).toString(), + p.getBounds().toString(), "rounding must not move the bounds"); + + // And it must still be a polygon after a child clips to its own rect, which is what + // every component in the subtree does on its way down. + p.intersect(new com.codename1.ui.geom.Rectangle(100, 100, 300, 200)); + assertTrue(p.isPolygon(), "clip must survive a child's clipRect as a polygon"); + } + + @Test + @DisplayName("a fully squared clip really is a plain rectangle") + void squaredClipIsARectangle() { + com.codename1.ui.geom.GeneralPath p = (com.codename1.ui.geom.GeneralPath) + new MaterialRenderElement(new Material()) + .clipShape(100, 100, 300, 200, 0, 0, 0, 0); + assertTrue(p.isRectangle(), "no corners rounded means a rectangle, and the cheap path"); + } + + @Test + @DisplayName("the clip is a genuine intersection, never wider than what it inherited") + void neverPaintsOutsideTheInheritedClip() { + // The bug this guards: setClip(Shape) REPLACES the clip, so a surface that ignored + // the incoming one would paint its rounded rect over its neighbours. + int[] q = geom(100, 100, 200, 150, 20, 150, 130, 100, 60); + assertTrue(q[0] >= 150 && q[1] >= 130, "origin escaped the inherited clip"); + assertTrue(q[0] + q[2] <= 250 && q[1] + q[3] <= 190, "extent escaped the inherited clip"); + } + + private static int[] corners(int rtl, int rtr, int rbr, int rbl, + int cx, int cy, int cw, int ch) { + int[] out = new int[8]; + assertTrue(MaterialRenderElement.clipGeometry(out, 100, 100, 200, 150, + rtl, rtr, rbr, rbl, cx, cy, cw, ch), "expected something visible"); + return out; + } + + /// Flutter rounds corners independently, and the gallery's settings button rounds + /// exactly one of them: BorderRadiusDirectional.only(bottomStart: 10). Collapsing + /// that to a single radius -- the top-left one, which is zero -- is what drew it as + /// a plain white block where the reference has a rounded bottom-left corner. + @Test + void oneRoundedCornerRoundsOnlyThatCorner() { + assertArrayEquals(new int[] {100, 100, 200, 150, 0, 0, 0, 30}, + corners(0, 0, 0, 30, 0, 0, 1000, 1000)); + } + + @Test + void everyCornerKeepsItsOwnRadius() { + assertArrayEquals(new int[] {100, 100, 200, 150, 4, 8, 12, 16}, + corners(4, 8, 12, 16, 0, 0, 1000, 1000)); + } + + /// A clip that cuts an edge squares BOTH corners on it, whatever they asked for. + @Test + void aCutEdgeSquaresItsOwnCornersOnly() { + // clipped away on the left: the two left corners go, the right two survive. + assertArrayEquals(new int[] {200, 100, 100, 150, 0, 8, 12, 0}, + corners(4, 8, 12, 16, 200, 0, 1000, 1000)); + } + + /// Each corner is capped independently by the visible box, not by the largest. + @Test + void eachCornerIsCappedByTheVisibleBox() { + int[] q = corners(200, 200, 10, 10, 0, 0, 1000, 1000); + // half the shorter visible side is 75 + assertArrayEquals(new int[] {75, 75, 10, 10}, new int[] {q[4], q[5], q[6], q[7]}); + } + + /// The single-radius form still means what it used to. + @Test + void theUniformFormIsUnchanged() { + int[] four = new int[8]; + int[] one = new int[8]; + MaterialRenderElement.clipGeometry(four, 100, 100, 200, 150, 20, 20, 20, 20, + 0, 0, 1000, 1000); + MaterialRenderElement.clipGeometry(one, 100, 100, 200, 150, 20, 0, 0, 1000, 1000); + assertArrayEquals(four, one); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/NavigationRailLayoutTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/NavigationRailLayoutTest.java new file mode 100644 index 00000000000..3a0abc5b49f --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/NavigationRailLayoutTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.Column; +import com.codename1.flutter.widgets.Container; +import com.codename1.flutter.widgets.Text; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A navigation rail lays out its destinations. + * + *

It used to build {@code leading} and nothing else, so a rail whose + * destinations ARE its content rendered as a single floating button: the + * navigation-rail demo drew its create button in the middle of an otherwise + * empty page, with no rail behind it and no destinations at all.

+ */ +class NavigationRailLayoutTest { + + private static NavigationRailDestination destination(String label) { + NavigationRailDestination d = new NavigationRailDestination(); + d.icon(new Text("icon-" + label)); + d.selectedIcon(new Text("selected-" + label)); + d.label(new Text(label)); + return d; + } + + private static NavigationRail railOfThree(NavigationRailLabelType type, long selected) { + NavigationRail rail = new NavigationRail(); + DartList ds = new DartList(); + ds.add(destination("First")); + ds.add(destination("Second")); + ds.add(destination("Third")); + rail.destinations(ds); + rail.labelType(type); + rail.selectedIndex(selected); + return rail; + } + + /** Every Text anywhere under a widget, in order. */ + private static void collectText(Widget w, DartList out) { + if (w == null) { + return; + } + if (w instanceof Text) { + out.add(((Text) w).getData()); + return; + } + if (w instanceof Container) { + collectText(((Container) w).getChild(), out); + return; + } + if (w instanceof Column) { + DartList kids = ((Column) w).getChildren(); + for (int i = 0; kids != null && i < kids.size(); i++) { + collectText(kids.get(i), out); + } + } + } + + private static DartList textOf(NavigationRail rail) { + DartList out = new DartList(); + collectText(rail.build(null), out); + return out; + } + + @Test + void theRailIsAFixedWidthColumnOfItsDestinations() { + Widget built = railOfThree(NavigationRailLabelType.none, 0).build(null); + assertTrue(built instanceof Container, "expected the rail container, got " + built); + Container rail = (Container) built; + // Flutter's default minWidth. + assertEquals(72.0, rail.getWidth().doubleValue(), 0.001); + assertTrue(rail.getChild() instanceof Column, "the rail holds a column"); + } + + @Test + void theSelectedDestinationUsesItsSelectedIcon() { + DartList t = textOf(railOfThree(NavigationRailLabelType.none, 1)); + assertTrue(t.contains("icon-First"), t.toString()); + assertTrue(t.contains("selected-Second"), t.toString()); + assertTrue(t.contains("icon-Third"), t.toString()); + } + + @Test + void labelTypeSelectedShowsOnlyTheSelectedLabel() { + DartList t = textOf(railOfThree(NavigationRailLabelType.selected, 2)); + assertTrue(t.contains("Third"), "the selected label is missing: " + t); + assertTrue(!t.contains("First") && !t.contains("Second"), + "an unselected label was drawn: " + t); + } + + @Test + void labelTypeAllShowsEveryLabel() { + DartList t = textOf(railOfThree(NavigationRailLabelType.all, 0)); + assertTrue(t.contains("First") && t.contains("Second") && t.contains("Third"), t.toString()); + } + + @Test + void labelTypeNoneShowsNoLabelAtAll() { + DartList t = textOf(railOfThree(NavigationRailLabelType.none, 0)); + assertTrue(!t.contains("First") && !t.contains("Second") && !t.contains("Third"), + "a label was drawn for labelType none: " + t); + } + + @Test + void anExtendedRailIsWider() { + NavigationRail rail = railOfThree(NavigationRailLabelType.all, 0); + rail.extended(true); + Container built = (Container) rail.build(null); + assertEquals(256.0, built.getWidth().doubleValue(), 0.001); + } + + @Test + void theLeadingWidgetStillComesFirst() { + NavigationRail rail = railOfThree(NavigationRailLabelType.none, 0); + rail.leading(new Text("lead")); + DartList t = textOf(rail); + assertNotNull(t); + assertEquals("lead", t.get(0), "leading must head the rail: " + t); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/PopupMenuTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/PopupMenuTest.java new file mode 100644 index 00000000000..5d07f28471f --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/PopupMenuTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Icons; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.flutter.widgets.Icon; + +import dart.core.DartList; +import dart.runtime.Funcs; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A popup menu button SHOWS something and DOES something. + * + *

It previously did neither: with no explicit child or icon it laid out as nothing at + * all, and a tap went nowhere because the trigger was never made tappable. Both failures are + * silent — the app bar simply had one fewer button than the Dart described.

+ */ +class PopupMenuTest { + + @AfterEach + void closeAnyMenu() { + PopupMenus.dismiss(); + } + + private static PopupMenuItem item(String value, Widget child) { + PopupMenuItem i = new PopupMenuItem(); + i.value(value); + i.child(child); + return i; + } + + private PopupMenuButton buttonWithItems(final String... values) { + PopupMenuButton b = new PopupMenuButton(); + b.itemBuilder(new Funcs.Func1() { + @Override + public Object call(BuildContext context) { + DartList> list = new DartList>(); + for (String v : values) { + list.add(item(v, new ProbeBox(60, 20))); + } + return list; + } + }); + return b; + } + + @Test + @DisplayName("with no child or icon the trigger is the overflow glyph") + void theDefaultTriggerIsTheOverflowIcon() { + Widget trigger = new PopupMenuButton().effectiveTrigger(); + + assertNotNull(trigger, "a menu button with no icon must still show something"); + assertTrue(trigger instanceof Icon, "Flutter falls back to an Icon"); + assertEquals(Icons.more_vert.codePoint(), ((Icon) trigger).getIcon().codePoint()); + } + + @Test + @DisplayName("an explicit icon wins over the default") + void anExplicitIconIsUsed() { + PopupMenuButton b = new PopupMenuButton(); + b.icon(new Icon(Icons.search)); + + assertEquals(Icons.search.codePoint(), + ((Icon) b.effectiveTrigger()).getIcon().codePoint()); + } + + @Test + @DisplayName("the button renders the trigger glyph and is pressable") + void theTriggerIsRendered() { + PopupMenuButton b = buttonWithItems("a"); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + + assertEquals(Icons.more_vert.codePoint(), e.consumedIconChar(), + "the overflow glyph must be what the button draws"); + assertNotNull(e.pressHandler(), "and pressing it must do something"); + } + + @Test + @DisplayName("a disabled button has no press handler, so CN1 disables it") + void aDisabledButtonHasNoHandler() { + PopupMenuButton b = buttonWithItems("a"); + b.enabled(false); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + + assertEquals(null, e.pressHandler()); + } + + @Test + @DisplayName("showing the menu builds the items") + void showingBuildsTheItems() { + final int[] builds = {0}; + PopupMenuButton b = new PopupMenuButton(); + b.itemBuilder(new Funcs.Func1() { + @Override + public Object call(BuildContext context) { + builds[0]++; + DartList> l = new DartList>(); + l.add(item("one", new ProbeBox(60, 20))); + return l; + } + }); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + + PopupMenus.show(e, b); + + assertEquals(1, builds[0], "the itemBuilder must run when the menu opens"); + assertTrue(PopupMenus.isOpen()); + } + + @Test + @DisplayName("a disabled button opens nothing") + void aDisabledButtonDoesNotOpen() { + PopupMenuButton b = buttonWithItems("a"); + b.enabled(false); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + + PopupMenus.show(e, b); + + assertFalse(PopupMenus.isOpen()); + } + + @Test + @DisplayName("an itemBuilder returning nothing opens nothing") + void anEmptyMenuDoesNotOpen() { + PopupMenuButton b = buttonWithItems(); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + + PopupMenus.show(e, b); + + assertFalse(PopupMenus.isOpen(), "an empty menu must not put up an empty surface"); + } + + @Test + @DisplayName("dismiss closes the menu") + void dismissClosesIt() { + PopupMenuButton b = buttonWithItems("a", "b"); + PopupMenuButtonRenderElement e = (PopupMenuButtonRenderElement) + FlutterUI.mount(b, new RenderHost(), new BuildOwner()); + PopupMenus.show(e, b); + + PopupMenus.dismiss(); + + assertFalse(PopupMenus.isOpen()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/TabBarLabelTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/TabBarLabelTest.java new file mode 100644 index 00000000000..da2355ace5b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/TabBarLabelTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Color; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.TextStyle; +import com.codename1.flutter.Widget; +import com.codename1.flutter.widgets.DefaultTextStyle; +import com.codename1.flutter.widgets.Padding; +import com.codename1.flutter.widgets.Row; +import com.codename1.flutter.widgets.SingleChildScrollView; +import com.codename1.flutter.widgets.SizedBox; + +import dart.core.DartList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A TabBar pads and colours its labels. + * + *

Both were accepted and discarded. Without the padding the labels run + * together -- Crane's three tabs rendered as the single word "FLYSLEEPEAT" -- + * and without the colour they are painted in the default ink on a bar whose + * labels are meant to be white.

+ */ +class TabBarLabelTest { + + private static Row rowOf(TabBar bar) { + Widget built = bar.build(null); + assertTrue(built instanceof SizedBox, "expected the fixed-height box, got " + built); + Widget inner = ((SizedBox) built).getChild(); + if (inner instanceof SingleChildScrollView) { + inner = ((SingleChildScrollView) inner).getChild(); + } + assertTrue(inner instanceof Row, "expected a Row of tabs, got " + inner); + return (Row) inner; + } + + private static TabBar barWithThreeTabs() { + TabBar bar = new TabBar(); + DartList tabs = new DartList(); + for (int i = 0; i < 3; i++) { + Tab t = new Tab(); + t.text("tab" + i); + tabs.add(t); + } + bar.tabs(tabs); + return bar; + } + + @Test + void everyLabelGetsTheRequestedPadding() { + TabBar bar = barWithThreeTabs(); + bar.labelPadding(EdgeInsets.symmetric(32, 0)); + DartList kids = rowOf(bar).getChildren(); + assertEquals(3, kids.size()); + for (int i = 0; i < kids.size(); i++) { + assertTrue(kids.get(i) instanceof Padding, "tab " + i + " is not padded"); + EdgeInsets p = (EdgeInsets) ((Padding) kids.get(i)).getPadding(); + assertEquals(32.0, p.left(), 0.001); + assertEquals(32.0, p.right(), 0.001); + } + } + + @Test + void aBarThatNamesNoPaddingStillGetsFluttersDefault() { + // Flutter's kTabLabelPadding is EdgeInsets.symmetric(horizontal: 16). + DartList kids = rowOf(barWithThreeTabs()).getChildren(); + EdgeInsets p = (EdgeInsets) ((Padding) kids.get(0)).getPadding(); + assertEquals(16.0, p.left(), 0.001); + assertEquals(16.0, p.right(), 0.001); + } + + @Test + void theSelectedTabTakesLabelColourAndTheRestTakeTheUnselectedOne() { + TabBar bar = barWithThreeTabs(); + Color selected = new Color(0xFFFFFFFFL); + Color rest = new Color(0x99FFFFFFL); + bar.labelColor(selected); + bar.unselectedLabelColor(rest); + DartList kids = rowOf(bar).getChildren(); + // No controller means tab 0 is the selected one. + assertEquals(selected.value(), inkOf(kids.get(0))); + assertEquals(rest.value(), inkOf(kids.get(1))); + assertEquals(rest.value(), inkOf(kids.get(2))); + } + + private static long inkOf(Widget padded) { + Widget child = ((Padding) padded).getChild(); + assertTrue(child instanceof DefaultTextStyle, "the tab was not styled: " + child); + TextStyle s = ((DefaultTextStyle) child).getStyle(); + assertNotNull(s, "no style on the tab"); + assertNotNull(s.getColor(), "no colour on the tab"); + return s.getColor().value(); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ThemingTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ThemingTest.java new file mode 100644 index 00000000000..41a6025a2e6 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/material/ThemingTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.material; + +import com.codename1.flutter.Brightness; +import com.codename1.flutter.Color; +import com.codename1.flutter.Colors; +import com.codename1.flutter.ThemeMode; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The M4 theming contract: ThemeData drives the Flutter* UIID overlay, and + * themeMode + platform brightness pick the effective theme. + */ +public class ThemingTest { + + private ThemeData light() { + ThemeData t = new ThemeData(); + t.colorScheme(ColorScheme.fromSeed(Colors.deepPurple)); + return t; + } + + private ThemeData dark() { + ThemeData t = new ThemeData(); + t.colorScheme(ColorScheme.fromSeed(Colors.deepPurple, Brightness.dark)); + t.brightness(Brightness.dark); + return t; + } + + // ------------------------------------------------------------------ + // Prop table + // ------------------------------------------------------------------ + + @Test + public void colorSchemeLandsOnFlutterUiids() { + ThemeData t = light(); + Map p = ThemeDataAdapter.themeProps(t); + assertEquals(ThemeDataAdapter.hex(t.colorScheme().surface()), p.get("FlutterScaffold.bgColor")); + assertEquals(ThemeDataAdapter.hex(t.colorScheme().onSurface()), p.get("FlutterText.fgColor")); + assertEquals(ThemeDataAdapter.hex(t.colorScheme().primary()), p.get("FlutterElevatedButton.bgColor")); + assertEquals(ThemeDataAdapter.hex(t.colorScheme().onPrimary()), p.get("FlutterElevatedButton.fgColor")); + // M3: the app bar sits on the surface with an elevation tint, not a + // saturated fill (matches AppBarRenderElement's per-instance default) + assertEquals(ThemeDataAdapter.hex(t.colorScheme().surface()), p.get("FlutterAppBar.bgColor")); + assertEquals(ThemeDataAdapter.hex(t.colorScheme().onSurface()), p.get("FlutterAppBar.fgColor")); + } + + @Test + public void overlayOnlyTouchesFlutterNamespace() { + // the overlay must never restyle a host app's own components + for (String key : ThemeDataAdapter.themeProps(light()).keySet()) { + String uiid = key.substring(0, key.indexOf('.')); + // state prefixes (sel#, press#, dis#) may precede the UIID + int hash = uiid.indexOf('#'); + if (hash >= 0) { + uiid = uiid.substring(hash + 1); + } + assertTrue(uiid.startsWith("Flutter"), + "overlay key outside the Flutter namespace: " + key); + } + } + + @Test + public void lightAndDarkProduceDifferentSurfaces() { + Map l = ThemeDataAdapter.themeProps(light()); + Map d = ThemeDataAdapter.themeProps(dark()); + assertNotEquals(l.get("FlutterScaffold.bgColor"), d.get("FlutterScaffold.bgColor")); + assertNotEquals(l.get("FlutterText.fgColor"), d.get("FlutterText.fgColor")); + } + + @Test + public void darkSurfaceIsDarkerThanItsOnColor() { + ColorScheme cs = ColorScheme.fromSeed(Colors.deepPurple, Brightness.dark); + assertTrue(luminance(cs.surface()) < luminance(cs.onSurface()), + "dark scheme must paint light content on a dark surface"); + ColorScheme lightCs = ColorScheme.fromSeed(Colors.deepPurple, Brightness.light); + assertTrue(luminance(lightCs.surface()) > luminance(lightCs.onSurface()), + "light scheme must paint dark content on a light surface"); + } + + private double luminance(Color c) { + int rgb = c.rgb(); + return 0.2126 * ((rgb >> 16) & 0xFF) + 0.7152 * ((rgb >> 8) & 0xFF) + 0.0722 * (rgb & 0xFF); + } + + @Test + public void hexIsSixDigitRgb() { + String h = ThemeDataAdapter.hex(new Color(0xFF102030)); + assertEquals("102030", h); + } + + // ------------------------------------------------------------------ + // Effective-theme matrix (themeMode × platform brightness) + // ------------------------------------------------------------------ + + @Test + public void themeModeDecisionTable() { + assertTrue(MaterialApp.wantsDark(ThemeMode.dark, Boolean.FALSE), "explicit dark wins"); + assertFalse(MaterialApp.wantsDark(ThemeMode.light, Boolean.TRUE), "explicit light wins"); + assertTrue(MaterialApp.wantsDark(ThemeMode.system, Boolean.TRUE), "system follows platform"); + assertFalse(MaterialApp.wantsDark(ThemeMode.system, Boolean.FALSE), "system follows platform"); + assertFalse(MaterialApp.wantsDark(ThemeMode.system, null), "unknown platform = light"); + assertFalse(MaterialApp.wantsDark(null, null), "default mode = system = light"); + assertTrue(MaterialApp.wantsDark(null, Boolean.TRUE), "default mode follows platform"); + } + + @Test + public void effectiveThemePicksDarkThemeWhenDark() { + MaterialApp app = new MaterialApp(); + ThemeData l = light(); + ThemeData d = dark(); + app.theme(l); + app.darkTheme(d); + + app.themeMode(ThemeMode.light); + assertEquals(l, app.effectiveTheme()); + + app.themeMode(ThemeMode.dark); + assertEquals(d, app.effectiveTheme(), "dark mode must select darkTheme"); + } + + @Test + public void effectiveThemeFallsBackToLightThemeWithoutDarkTheme() { + MaterialApp app = new MaterialApp(); + ThemeData l = light(); + app.theme(l); + app.themeMode(ThemeMode.dark); + // no darkTheme supplied — Flutter keeps using theme + assertEquals(l, app.effectiveTheme()); + } + + @Test + public void effectiveThemeSynthesizesWhenNoThemeGiven() { + MaterialApp app = new MaterialApp(); + app.themeMode(ThemeMode.dark); + ThemeData t = app.effectiveTheme(); + assertNotNull(t); + assertEquals(Brightness.dark, t.brightness(), + "a synthesized theme must carry the requested brightness"); + } + + // ------------------------------------------------------------------ + // Alpha + // ------------------------------------------------------------------ + + /** + * {@code Colors.transparent} is 0x00000000 — alpha 0 over BLACK. Painting + * only its RGB word turns every "transparent" app bar and scaffold in the + * gallery into an opaque black band. + */ + @Test + public void transparentPaintsNothingRatherThanBlack() { + com.codename1.ui.plaf.Style s = new com.codename1.ui.plaf.Style(); + ThemeDataAdapter.paintColor(s, new com.codename1.flutter.Color(0x00000000L)); + assertEquals(0, s.getBgTransparency() & 0xFF, "alpha 0 must paint nothing"); + } + + @Test + public void opaqueColorPaintsFully() { + com.codename1.ui.plaf.Style s = new com.codename1.ui.plaf.Style(); + ThemeDataAdapter.paintColor(s, new com.codename1.flutter.Color(0xFF2196F3L)); + assertEquals(0x2196F3, s.getBgColor()); + assertEquals(255, s.getBgTransparency() & 0xFF); + } + + @Test + public void partialAlphaCarriesThrough() { + com.codename1.ui.plaf.Style s = new com.codename1.ui.plaf.Style(); + ThemeDataAdapter.paintColor(s, new com.codename1.flutter.Color(0x80FF0000L)); + assertEquals(0xFF0000, s.getBgColor()); + assertEquals(0x80, s.getBgTransparency() & 0xFF); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NamedRouteTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NamedRouteTest.java new file mode 100644 index 00000000000..6b563099ddd --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NamedRouteTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Named-route resolution: the {@code routes} map first, then + * {@code onGenerateRoute}, then {@code onUnknownRoute} — Flutter's order. + * Headless, so no Forms are created and route builders never run; only the + * resolution and stack bookkeeping are exercised. + */ +class NamedRouteTest { + + @BeforeEach + void reset() { + Navigator.reset(); + Navigator.resetRouteTable(); + } + + @AfterEach + void clear() { + Navigator.reset(); + Navigator.resetRouteTable(); + } + + private static MaterialPageRoute route() { + MaterialPageRoute r = new MaterialPageRoute(); + r.builder((context) -> new ProbeBox(10, 10)); + return r; + } + + @Test + void routesMapWins() { + Map routes = new HashMap(); + routes.put("/a", (Funcs.Func1) context -> new ProbeBox(1, 1)); + Navigator.installRouteTable(routes, settings -> route(), null); + + Route r = Navigator.resolveRoute("/a", null); + assertNotNull(r); + assertNotNull(((MaterialPageRoute) r).getBuilder(), + "a routes-map entry becomes a MaterialPageRoute around that builder"); + assertEquals("/a", r.settings().name()); + } + + @Test + void onGenerateRouteHandlesWhatTheMapDoesNot() { + MaterialPageRoute generated = route(); + Navigator.installRouteTable(new HashMap(), settings -> generated, null); + assertSame(generated, Navigator.resolveRoute("/demo/app-bar", null)); + } + + @Test + void onUnknownRouteIsTheLastResort() { + MaterialPageRoute fallback = route(); + Navigator.installRouteTable(null, settings -> null, settings -> fallback); + assertSame(fallback, Navigator.resolveRoute("/nope", null)); + } + + @Test + void argumentsAndNameReachTheFactory() { + final String[] seenName = new String[1]; + final Object[] seenArgs = new Object[1]; + Object args = new Object(); + Navigator.installRouteTable(null, settings -> { + seenName[0] = settings.name(); + seenArgs[0] = settings.arguments(); + return route(); + }, null); + + Navigator.resolveRoute("/demo/banner", args); + assertEquals("/demo/banner", seenName[0]); + assertSame(args, seenArgs[0]); + } + + @Test + void unresolvableNameIsReportedNotThrown() { + Navigator.installRouteTable(null, settings -> null, null); + assertNull(Navigator.resolveRoute("/missing", null)); + assertFalse(Navigator.pushNamed(null, "/missing", null)); + assertEquals(0, Navigator.stackSize(), "a dead link must not push anything"); + } + + @Test + void pushNamedGrowsTheStack() { + Navigator.installRouteTable(null, settings -> route(), null); + assertTrue(Navigator.pushNamed(null, "/demo/app-bar", null)); + assertEquals(1, Navigator.stackSize()); + } + + @Test + void navigatorStateRoutesNamedPushesToTheStack() { + Navigator.installRouteTable(null, settings -> route(), null); + NavigatorState state = Navigator.of(null, null); + + state.pushNamed("/demo/a", null); + assertEquals(1, Navigator.stackSize()); + assertEquals("/demo/b", state.restorablePushNamed("/demo/b", null), + "the restoration id is informational — the route name itself"); + assertEquals(2, Navigator.stackSize()); + assertTrue(state.canPop()); + + state.pop(null); + assertEquals(1, Navigator.stackSize()); + } + + @Test + void pushReplacementSwapsTheTopRoute() { + Navigator.installRouteTable(null, settings -> route(), null); + NavigatorState state = Navigator.of(null, null); + state.pushNamed("/a", null); + state.pushNamed("/b", null); + assertEquals(2, Navigator.stackSize()); + + state.pushReplacementNamed("/c", null, null); + assertEquals(2, Navigator.stackSize(), "one popped, one pushed"); + } + + @Test + void aReplacementThatCannotBeBuiltLeavesTheCurrentRoute() { + // Only "/known" resolves; anything else has no route. + Navigator.installRouteTable(null, settings -> "/unknown".equals(settings.name()) ? null : route(), null); + NavigatorState state = Navigator.of(null, null); + state.pushNamed("/a", null); + state.pushNamed("/b", null); + state.pushReplacementNamed("/unknown", null, null); + assertEquals(2, Navigator.stackSize(), + "the page stays: popping first sent the user back when the replacement failed"); + } + + @Test + void popUntilUnwindsToTheAcceptedRoute() { + Navigator.installRouteTable(null, settings -> route(), null); + NavigatorState state = Navigator.of(null, null); + state.pushNamed("/a", null); + state.pushNamed("/b", null); + state.pushNamed("/c", null); + + state.popUntil(r -> Boolean.FALSE); + assertEquals(0, Navigator.stackSize(), "an always-false predicate unwinds to the base route"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NavigatorStackTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NavigatorStackTest.java new file mode 100644 index 00000000000..a82a7542d82 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NavigatorStackTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.material.Dialogs; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Navigator route-stack and dialog-stack bookkeeping, headless: no Display + * means no Forms are created and the route builders never run — only the + * stack logic is exercised. Dialogs DO mount their subtree headless (bare + * RenderHost), and Navigator.pop always dismisses the topmost dialog before + * popping a route. + */ +class NavigatorStackTest { + + @BeforeEach + void resetStacks() { + Navigator.reset(); + Dialogs.reset(); + } + + private static MaterialPageRoute route() { + MaterialPageRoute r = new MaterialPageRoute(); + r.builder((context) -> new ProbeBox(10, 10)); + return r; + } + + @Test + void pushGrowsAndPopShrinksTheStack() { + assertEquals(0, Navigator.stackSize()); + Navigator.push(null, route()); + Navigator.push(null, route()); + assertEquals(2, Navigator.stackSize()); + Navigator.pop(null); + assertEquals(1, Navigator.stackSize()); + Navigator.pop(null); + assertEquals(0, Navigator.stackSize()); + } + + @Test + void poppingTheLastRouteIsANoOp() { + Navigator.pop(null); + Navigator.pop(null); + assertEquals(0, Navigator.stackSize(), "the implicit base route can never be popped"); + } + + @Test + void dialogMountsHeadlessAndPopDismissesItBeforeRoutes() { + Navigator.push(null, route()); + assertEquals(1, Navigator.stackSize()); + + final int[] built = {0}; + Dialogs.showDialog(null, (context) -> { + built[0]++; + return new ProbeBox(5, 5); + }); + assertEquals(1, built[0], "the dialog builder ran on mount"); + assertEquals(1, Dialogs.openDialogCount()); + + // pop dismisses the dialog, NOT the route + Navigator.pop(null); + assertEquals(0, Dialogs.openDialogCount()); + assertEquals(1, Navigator.stackSize()); + + // next pop takes the route + Navigator.pop(null); + assertEquals(0, Navigator.stackSize()); + } + + @Test + void stackedDialogsPopInLifoOrder() { + Dialogs.showDialog(null, (context) -> new ProbeBox(1, 1)); + Dialogs.showDialog(null, (context) -> new ProbeBox(2, 2)); + assertEquals(2, Dialogs.openDialogCount()); + Navigator.pop(null); + assertEquals(1, Dialogs.openDialogCount()); + Navigator.pop(null); + assertEquals(0, Dialogs.openDialogCount()); + } + + @Test + void headlessPushDoesNotInvokeTheBuilder() { + final int[] built = {0}; + MaterialPageRoute r = new MaterialPageRoute(); + r.builder((context) -> { + built[0]++; + return new ProbeBox(1, 1); + }); + Navigator.push(null, r); + assertEquals(0, built[0], "no Display: the page never mounts, the builder never runs"); + assertEquals(1, Navigator.stackSize()); + } + + @Test + void anOpenDialogCanBePoppedFromTheBaseRoute() { + // A dialog over the base route: the page stack is empty, but the dialog is the + // route on top, and maybePop -- what a system-back handler calls -- must close it. + Dialogs.showDialog(null, (context) -> new ProbeBox(10, 10)); + NavigatorState nav = Navigator.of(null, null); + assertEquals(true, nav.canPop()); + nav.maybePop(null); + assertEquals(0, Dialogs.openDialogCount()); + assertEquals(false, nav.canPop()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NestedAppRouteTableTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NestedAppRouteTableTest.java new file mode 100644 index 00000000000..36ef0aa2492 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/NestedAppRouteTableTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.MaterialApp; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * A route table belongs to the app that published it, not to the process. + * + *

The gallery's studies are each a whole {@code MaterialApp}. While the + * runtime kept ONE global table, opening a study replaced the gallery's table + * with the study's — and popping back never restored it, because the outer app + * does not rebuild. Everything after that first study visit resolved against + * the wrong app: {@code /shrine} answered "no route", and re-entering a study + * mounted its page above the wrong Theme, MediaQuery and Localizations, where + * {@code GalleryLocalizations.of(context)!} is null.

+ */ +class NestedAppRouteTableTest { + + @BeforeEach + @AfterEach + void reset() { + Navigator.reset(); + Navigator.resetRouteTable(); + } + + private static MaterialPageRoute pageNamed(String name) { + MaterialPageRoute r = new MaterialPageRoute(); + r.builder((context) -> new ProbeBox(10, 10)); + RouteSettings s = new RouteSettings(); + s.name(name); + r.settings(s); + return r; + } + + /** An app that answers exactly one route name. */ + private static MaterialApp appServing(String name, Widget home) { + MaterialApp app = new MaterialApp(); + app.home(home); + app.onGenerateRoute(settings -> + name.equals(settings.name()) ? pageNamed(name) : null); + return app; + } + + private static Element mount(Widget root) { + return FlutterUI.mount(root, new RenderHost(), new BuildOwner()); + } + + @Test + @DisplayName("an inner app does not take over the outer app's routes") + void innerAppDoesNotClobberOuter() { + MaterialApp inner = appServing("/study/detail", new ProbeBox(10, 10)); + mount(appServing("/gallery/demo", inner)); + + // The inner app built last. Without per-app tables this was null. + assertNotNull(Navigator.resolveRoute("/gallery/demo", null), + "a context-less push resolves against the OUTER app"); + assertNull(Navigator.resolveRoute("/study/detail", null), + "the inner app's private route is not reachable from the root"); + } + + @Test + @DisplayName("a push from inside the inner app sees both tables") + void innerContextSeesItsOwnRoutesThenTheRoot() { + MaterialApp inner = appServing("/study/detail", new ProbeBox(10, 10)); + Element root = mount(appServing("/gallery/demo", inner)); + + Element innerElement = find(root, inner); + assertNotNull(innerElement, "the inner app is mounted"); + + assertNotNull(Navigator.resolveRoute(innerElement, "/study/detail", null), + "its own route resolves"); + assertNotNull(Navigator.resolveRoute(innerElement, "/gallery/demo", null), + "and a name it has never heard of falls through to the root app"); + } + + /** The element whose widget is {@code widget}, or null. */ + private static Element find(Element from, Widget widget) { + if (from.widget() == widget) { + return from; + } + final Element[] found = {null}; + from.visitChildren(child -> { + if (found[0] == null) { + found[0] = find(child, widget); + } + }); + return found[0]; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteInheritanceTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteInheritanceTest.java new file mode 100644 index 00000000000..c402ad4d068 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteInheritanceTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.EdgeInsets; +import com.codename1.flutter.InheritedValueProvider; +import com.codename1.flutter.MediaQuery; +import com.codename1.flutter.MediaQueryData; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.provider.SingleChildWidget; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * A route mounts as its own element-tree root (its own Form), but in Flutter a + * route's page builds below the app and inherits everything above it. The + * context fallback restores that: ancestor lookups continue from the context + * that pushed the route. + */ +class RouteInheritanceTest { + + /** A value published to a subtree, like MaterialApp's localizations scope. */ + private static class Scope extends SingleChildWidget implements InheritedValueProvider { + private final String value; + + Scope(String value) { + this.value = value; + } + + @Override + public Object providedValueFor(Class type) { + return type == String.class ? value : null; + } + } + + /** Records what its context could see at build time. */ + private static class Probe extends StatelessWidget { + String seen; + Element context; + + @Override + public Widget build(BuildContext context) { + this.context = (Element) context; + seen = (String) context.providerValueOfType(String.class); + return new ProbeBox(1, 1); + } + } + + private static Element mountAppWithScope(String value, Probe inApp) { + Scope scope = new Scope(value); + scope.child(inApp); + return FlutterUI.mount(scope, new RenderHost(), new BuildOwner()); + } + + @Test + void aFreshRootSeesNothingFromTheAppTree() { + Probe inApp = new Probe(); + mountAppWithScope("app-value", inApp); + assertSame("app-value", inApp.seen, "sanity: the app's own subtree sees the scope"); + + Probe orphan = new Probe(); + FlutterUI.mount(orphan, new RenderHost(), new BuildOwner()); + assertNull(orphan.seen, "an unlinked root has no ancestors to inherit from"); + } + + @Test + void aRouteRootInheritsThroughTheContextThatPushedIt() { + Probe inApp = new Probe(); + mountAppWithScope("app-value", inApp); + + // the element that would call Navigator.of(context).push(...) + Element pusher = elementOf(inApp); + + Probe page = new Probe(); + FlutterUI.mount(page, new RenderHost(), new BuildOwner(), pusher); + + assertSame("app-value", page.seen, + "the pushed page resolves the app's scope through the pushing context"); + } + + @Test + void theFallbackIsOnlyForLookups_notStructure() { + Probe inApp = new Probe(); + mountAppWithScope("app-value", inApp); + + Probe page = new Probe(); + Element root = FlutterUI.mount(page, new RenderHost(), new BuildOwner(), elementOf(inApp)); + + assertNull(root.parent(), + "a route root stays a genuine root — render/host logic must not see it as embedded"); + } + + /** The element built for {@code w}: the probe records its own context. */ + /// Records the top padding its context resolves, the way SafeArea does. + private static class PaddingProbe extends StatelessWidget { + double top = -1; + Element context; + + @Override + public Widget build(BuildContext context) { + this.context = (Element) context; + top = MediaQuery.of(context).padding().top(); + return new ProbeBox(1, 1); + } + } + + private static MediaQueryData withTopPadding(double top) { + return new MediaQueryData(new com.codename1.flutter.rendering.Size(100, 100), 1.0, null, 1.0, + EdgeInsets.only(0, top, 0, 0)); + } + + /// The defect this pins: a vertical scroll view removes the top padding FOR ITS + /// DESCENDANTS, because it has already inset its own content by it. A route pushed + /// from a row inside that list used to inherit from the row, so its SafeArea + /// resolved to zero and the page drew under the status bar. + @Test + void aRoutePushedFromInsideAScrollViewStillSeesTheDisplayPadding() { + PaddingProbe deep = new PaddingProbe(); + Navigator.RootScope scope = new Navigator.RootScope( + MediaQuery.removePadding(null, Boolean.FALSE, Boolean.TRUE, + Boolean.FALSE, Boolean.TRUE, deep), + false); + FlutterUI.mount(MediaQuery.scope(withTopPadding(44), scope), + new RenderHost(), new BuildOwner()); + + assertEquals(0.0, deep.top, + "sanity: inside the scroll view the padding is deliberately spent"); + + PaddingProbe page = new PaddingProbe(); + FlutterUI.mount(page, new RenderHost(), new BuildOwner(), + Navigator.pushingElement(deep.context)); + + assertEquals(44.0, page.top, + "a pushed route mounts under the navigator, above the scroll view's " + + "removePadding, so it insets for the status bar itself"); + } + + /// The nearest navigator wins. A study is a MaterialApp of its own wrapped in the + /// providers it needs; anchoring its routes at the OUTERMOST app's scope climbs out + /// of those providers and the page's first Provider.of comes back null. + @Test + void theNearestNavigatorScopeWins() { + // The real nesting: each app publishes its scopes ABOVE the navigator position + // it inserts, and the study's whole app is a descendant of the outer one. + Probe deep = new Probe(); + Navigator.RootScope study = new Navigator.RootScope(deep, false); + Navigator.RootScope app = new Navigator.RootScope(scoped("study-value", study), true); + FlutterUI.mount(scoped("app-value", app), new RenderHost(), new BuildOwner()); + + Element anchor = Navigator.pushingElement(deep.context); + assertSame(study, anchor.widget(), "the study's own scope is the nearest navigator"); + + Probe page = new Probe(); + FlutterUI.mount(page, new RenderHost(), new BuildOwner(), anchor); + assertSame("study-value", page.seen, + "the route still sees what the study wrapped its app in"); + } + + private static Widget scoped(String value, Widget child) { + Scope s = new Scope(value); + s.child(child); + return s; + } + + private static Element elementOf(Probe w) { + return w.context; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteTransitionsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteTransitionsTest.java new file mode 100644 index 00000000000..06117b4b412 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/navigation/RouteTransitionsTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.navigation; + +import com.codename1.flutter.TargetPlatform; +import com.codename1.ui.animations.CommonTransitions; +import com.codename1.ui.animations.Transition; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A pushed route must be given motion. A Form shown with no transition replaces the screen + * outright, which is what every page push used to do -- the new page was fully painted on + * the first frame after the tap. Flutter always animates a route in, and which animation it + * picks comes from {@code PageTransitionsTheme} and the target platform. + */ +class RouteTransitionsTest { + + private static CommonTransitions of(Route r, TargetPlatform p) { + Transition t = RouteTransitions.forRoute(r, p); + assertNotNull(t, "every route must be given a transition"); + assertTrue(t instanceof CommonTransitions, "expected a CommonTransitions, got " + t); + return (CommonTransitions) t; + } + + /// The Apple push is NOT a CommonTransitions: a slide holds the two pages a fixed + /// screen apart and moves the pair, and this platform moves them different distances + /// on different curves. + private static com.codename1.ui.animations.CupertinoPageTransition apple( + Route r, TargetPlatform p) { + Transition t = RouteTransitions.forRoute(r, p); + assertNotNull(t, "every route must be given a transition"); + assertTrue(t instanceof com.codename1.ui.animations.CupertinoPageTransition, + "expected a CupertinoPageTransition, got " + t); + return (com.codename1.ui.animations.CupertinoPageTransition) t; + } + + private static MaterialPageRoute page() { + return new MaterialPageRoute(); + } + + /// iOS and macOS take CupertinoPageTransitionsBuilder: the page enters from the + /// trailing edge over kTransitionDuration. + @Test + void applePlatformsSlideThePageInFromTheSide() { + for (TargetPlatform p : new TargetPlatform[] {TargetPlatform.iOS, TargetPlatform.macOS}) { + com.codename1.ui.animations.CupertinoPageTransition t = apple(page(), p); + assertEquals(500, t.getDuration(), p + " uses kTransitionDuration"); + // A push brings the new page in over the old one; copy(true) is the pop, + // which swaps which page crosses the screen and which drifts back. + assertFalse(t.isBack(), p + " builds the push, not the pop"); + Transition backwards = t.copy(true); + assertTrue(backwards + instanceof com.codename1.ui.animations.CupertinoPageTransition); + assertTrue(((com.codename1.ui.animations.CupertinoPageTransition) backwards) + .isBack(), p + " copy(true) is the pop"); + } + } + + /// Everywhere else takes ZoomPageTransitionsBuilder, a fade with a slight scale. The + /// scale is not reproduced; the fade and its duration are. + @Test + void theOtherPlatformsFadeThePageIn() { + for (TargetPlatform p : new TargetPlatform[] {TargetPlatform.android, + TargetPlatform.windows, TargetPlatform.linux}) { + CommonTransitions t = of(page(), p); + assertFalse(t.isHorizontalSlide(), p + " should not slide"); + assertFalse(t.isVerticalSlide(), p + " should not slide"); + assertEquals(300, t.getTransitionSpeed(), + p + " uses ZoomPageTransitionsBuilder's duration"); + } + } + + @Test + void aFullScreenDialogComesUpFromTheBottomOnEveryPlatform() { + MaterialPageRoute r = page(); + r.fullscreenDialog(Boolean.TRUE); + for (TargetPlatform p : new TargetPlatform[] {TargetPlatform.iOS, TargetPlatform.android}) { + CommonTransitions t = of(r, p); + assertTrue(t.isVerticalSlide(), p + " should slide a modal vertically"); + assertFalse(t.isForwardSlide(), + p + " should bring a modal UP from the bottom edge, not down"); + } + } + + /// A container transform is one surface becoming another, so it must not read as a page + /// arriving from off-screen -- on any platform, including the ones that slide. + @Test + void aContainerTransformCrossFadesAtItsOwnDuration() { + Route r = new MaterialPageRoute() { + @Override + public boolean isContainerTransform() { + return true; + } + + @Override + public int transitionMillis() { + return 425; + } + }; + CommonTransitions t = of(r, TargetPlatform.iOS); + assertFalse(t.isHorizontalSlide(), "a container transform is not a page push"); + assertEquals(425, t.getTransitionSpeed(), "the route's own duration is honoured"); + } + + /// A route that states no duration falls back to the platform's, rather than to zero -- + /// which would be no animation at all. + @Test + void aRouteWithNoStatedDurationTakesThePlatformDefault() { + assertEquals(500, apple(page(), TargetPlatform.iOS).getDuration()); + assertEquals(300, of(page(), TargetPlatform.android).getTransitionSpeed()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/provider/ProviderTypeLookupTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/provider/ProviderTypeLookupTest.java new file mode 100644 index 00000000000..accd3ef4db9 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/provider/ProviderTypeLookupTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.provider; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A Consumer/Selector reads the model of the type it ASKED FOR, not the nearest one. + * + *

Java erases {@code Consumer}, so the runtime takes the type as a token the transpiler + * emits. Without it the lookup returned the nearest provided value of any type — right only + * when a single model is in scope. Reply has its localizations and its EmailStore above the + * same Selector: it got the localizations and the study came up blank — a cast error on the + * desktop, and on iOS, where that cast is unchecked, a wrong object that flowed on until an + * unrelated switch matched nothing.

+ */ +class ProviderTypeLookupTest { + + static class Localizations { + } + + static class EmailStore { + final String name = "store"; + } + + /** Two models in scope with the WRONG one nearer — the Reply arrangement. */ + private Widget twoProviders(Widget leaf) { + Provider inner = new Provider(); + inner.value(new Localizations()); + inner.child(leaf); + Provider outer = new Provider(); + outer.value(new EmailStore()); + outer.child(inner); + return outer; + } + + @Test + @DisplayName("a Consumer that names its type skips the nearer, wrong model") + void consumerFindsItsOwnType() { + final Object[] seen = new Object[1]; + Consumer c = new Consumer(); + c.providedType(EmailStore.class); + c.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, EmailStore value, Widget child) { + seen[0] = value; + return new ProbeBox(1, 1); + } + }); + + FlutterUI.mount(twoProviders(c), new RenderHost(), new BuildOwner()); + + assertTrue(seen[0] instanceof EmailStore, "expected the EmailStore, got " + seen[0]); + } + + @Test + @DisplayName("a Selector that names its type skips the nearer, wrong model") + void selectorFindsItsOwnType() { + final Object[] seen = new Object[1]; + Selector s = new Selector(); + s.providedType(EmailStore.class); + s.selector(new Funcs.Func2() { + @Override + public String call(BuildContext context, EmailStore store) { + seen[0] = store; + return store.name; + } + }); + s.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, String value, Widget child) { + return new ProbeBox(1, 1); + } + }); + + FlutterUI.mount(twoProviders(s), new RenderHost(), new BuildOwner()); + + assertTrue(seen[0] instanceof EmailStore, "expected the EmailStore, got " + seen[0]); + } + + @Test + @DisplayName("with no type named it still takes the nearest - the old behaviour") + void anUnnamedTypeTakesTheNearest() { + final Object[] seen = new Object[1]; + Consumer c = new Consumer(); + c.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, Object value, Widget child) { + seen[0] = value; + return new ProbeBox(1, 1); + } + }); + + FlutterUI.mount(twoProviders(c), new RenderHost(), new BuildOwner()); + + assertTrue(seen[0] instanceof Localizations, + "the default is still nearest-wins, got " + seen[0]); + } + + /** A model a ChangeNotifierProvider listens to. */ + static class Model implements com.codename1.flutter.foundation.ChangeNotifier { + } + + /** A Consumer of {@code type} recording the value each build receives. */ + private static Consumer recorder(Class type, final Object[] seen) { + Consumer c = new Consumer(); + c.providedType(type); + c.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, T value, Widget child) { + seen[0] = value; + return new ProbeBox(1, 1); + } + }); + return c; + } + + @Test + @DisplayName("a Provider given a new value rebuilds its Consumer when the child is the same instance") + void aReplacedProviderValueReachesTheConsumer() { + Object[] seen = new Object[1]; + Consumer c = recorder(EmailStore.class, seen); + EmailStore second = new EmailStore(); + Provider before = new Provider(); + before.value(new EmailStore()); + before.child(c); + BuildOwner owner = new BuildOwner(); + com.codename1.flutter.Element root = FlutterUI.mount(before, new RenderHost(), owner); + Provider after = new Provider(); + after.value(second); + after.child(c); + root.update(after); + owner.flushSync(); + assertTrue(seen[0] == second, "the Consumer shows the new value, got " + seen[0]); + } + + @Test + @DisplayName("a ChangeNotifierProvider given a new model rebuilds its Consumer when the child is the same instance") + void aReplacedNotifierReachesTheConsumer() { + Object[] seen = new Object[1]; + Consumer c = recorder(Model.class, seen); + Model second = new Model(); + ChangeNotifierProvider before = new ChangeNotifierProvider(); + before.value(new Model()); + before.child(c); + BuildOwner owner = new BuildOwner(); + com.codename1.flutter.Element root = FlutterUI.mount(before, new RenderHost(), owner); + ChangeNotifierProvider after = new ChangeNotifierProvider(); + after.value(second); + after.child(c); + root.update(after); + owner.flushSync(); + assertTrue(seen[0] == second, "the Consumer shows the new model, got " + seen[0]); + } + @Test + @DisplayName("a typed Provider holding null is found, shadows an outer one, and rebuilds when it gets a value") + void aTypedNullProviderMatchesAndSubscribes() { + final Object[] seen = {"never built"}; + Consumer c = recorder(EmailStore.class, seen); + Provider inner = Provider.value(null, null, c).providedType(EmailStore.class); + Provider outer = new Provider(); + outer.value(new EmailStore()); + outer.child(inner); + BuildOwner owner = new BuildOwner(); + com.codename1.flutter.Element root = FlutterUI.mount(outer, new RenderHost(), owner); + assertNull(seen[0], "the nearer Provider answers null, not the outer store"); + + EmailStore arrived = new EmailStore(); + Provider innerAfter = Provider.value(null, arrived, c).providedType(EmailStore.class); + Provider outerAfter = new Provider(); + outerAfter.value(outer.getValue()); + outerAfter.child(innerAfter); + root.update(outerAfter); + owner.flushSync(); + assertSame(arrived, seen[0], "the Consumer subscribed while the value was null, so it rebuilt"); + } + + @Test + @DisplayName("an untyped lookup still skips a null provider for the nearest real model") + void anUntypedLookupSkipsANullProvider() { + final Object[] seen = new Object[1]; + Consumer c = new Consumer(); + c.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, Object value, Widget child) { + seen[0] = value; + return new ProbeBox(1, 1); + } + }); + Provider inner = Provider.value(null, null, c).providedType(EmailStore.class); + Provider outer = new Provider(); + outer.value(new EmailStore()); + outer.child(inner); + FlutterUI.mount(outer, new RenderHost(), new BuildOwner()); + assertTrue(seen[0] instanceof EmailStore, "got " + seen[0]); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/ArcToPointTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/ArcToPointTest.java new file mode 100644 index 00000000000..a513a757cda --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/ArcToPointTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * arcToPoint has to produce an ARC, not the chord across it. + * + *

Both path converters used to answer a straight line here. A bottom app bar's notch + * is two quadratics either side of one of these, so the outline curved down, cut straight + * across, and curved back up -- a dimple with a bump in it.

+ */ +class ArcToPointTest { + + /** Every returned point must sit on the circle the arc was asked for. */ + private static void onCircle(double[] pts, double cx, double cy, double r) { + for (int i = 0; i < pts.length; i += 2) { + double d = Math.hypot(pts[i] - cx, pts[i + 1] - cy); + assertEquals(r, d, 1e-6, + "point " + i / 2 + " at (" + pts[i] + "," + pts[i + 1] + ") is off the circle"); + } + } + + @Test + void aSemicircleBowsAwayFromTheChord() { + // (-10,0) to (10,0) with radius 10 is a half circle; centre must be the origin. + double[] pts = GraphicsCanvas.arcToPoint(-10, 0, 10, 0, 10, false, true, 8); + onCircle(pts, 0, 0, 10); + // the mid point is off the chord by the full radius + double midY = pts[3 * 2 + 1]; + assertEquals(10.0, Math.abs(midY), 1e-6); + } + + @Test + void theOppositeSweepBowsTheOtherWay() { + double[] cw = GraphicsCanvas.arcToPoint(-10, 0, 10, 0, 10, false, true, 8); + double[] ccw = GraphicsCanvas.arcToPoint(-10, 0, 10, 0, 10, false, false, 8); + assertTrue(cw[7] * ccw[7] < 0, "clockwise and anticlockwise must bow to opposite sides"); + } + + @Test + void itEndsWhereItWasToldTo() { + double[] pts = GraphicsCanvas.arcToPoint(0, 0, 12, 5, 9, false, true, 12); + assertEquals(12.0, pts[pts.length - 2], 1e-6); + assertEquals(5.0, pts[pts.length - 1], 1e-6); + } + + /// A radius too small for the two points is grown to the smallest that reaches, as + /// SVG does, rather than producing NaN. + @Test + void aRadiusTooSmallIsGrown() { + double[] pts = GraphicsCanvas.arcToPoint(-10, 0, 10, 0, 3, false, true, 6); + onCircle(pts, 0, 0, 10); + } + + /// Degenerate input asks the caller to draw the chord instead of guessing. + @Test + void aZeroLengthArcAnswersNothing() { + assertNull(GraphicsCanvas.arcToPoint(5, 5, 5, 5, 10, false, true, 8)); + assertNull(GraphicsCanvas.arcToPoint(0, 0, 10, 0, 10, false, true, 1)); + } + + /// The curve is monotone across the notch: no point may double back past the end, + /// which is what a bump looks like. + @Test + void theArcDoesNotDoubleBack() { + double[] pts = GraphicsCanvas.arcToPoint(-10, 0, 10, 0, 10, false, true, 16); + double prev = -10; + for (int i = 0; i < pts.length; i += 2) { + assertTrue(pts[i] >= prev - 1e-9, "x went backwards at " + i / 2); + prev = pts[i]; + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/BoxConstraintsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/BoxConstraintsTest.java new file mode 100644 index 00000000000..f0022a69742 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/BoxConstraintsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import com.codename1.flutter.EdgeInsets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BoxConstraintsTest { + + @Test + void tightConstraintsForceExactSize() { + BoxConstraints c = BoxConstraints.tight(100, 50); + assertTrue(c.isTight()); + assertEquals(100, c.minWidth()); + assertEquals(100, c.maxWidth()); + assertEquals(50, c.minHeight()); + assertEquals(50, c.maxHeight()); + Size s = c.constrain(new Size(3, 900)); + assertEquals(new Size(100, 50), s); + } + + @Test + void looseConstraintsAllowAnySizeUpToMax() { + BoxConstraints c = BoxConstraints.loose(200, 300); + assertEquals(0, c.minWidth()); + assertEquals(0, c.minHeight()); + assertFalse(c.isTight()); + assertEquals(new Size(150, 300), c.constrain(new Size(150, 999))); + assertEquals(new Size(0, 0), c.constrain(new Size(-5, 0))); + } + + @Test + void constrainClampsBothDirections() { + BoxConstraints c = new BoxConstraints(10, 100, 20, 50); + assertEquals(new Size(10, 20), c.constrain(Size.ZERO)); + assertEquals(new Size(100, 50), c.constrain(new Size(500, 500))); + assertEquals(new Size(55, 33), c.constrain(new Size(55, 33))); + } + + @Test + void deflateRemovesInsetsAndNeverGoesNegative() { + BoxConstraints c = new BoxConstraints(10, 100, 10, 100); + BoxConstraints d = c.deflate(EdgeInsets.symmetric(8, 3)); + // horizontal insets = 16, vertical = 6 + assertEquals(0, d.minWidth()); + assertEquals(84, d.maxWidth()); + assertEquals(4, d.minHeight()); + assertEquals(94, d.maxHeight()); + + BoxConstraints tiny = BoxConstraints.tight(10, 10); + BoxConstraints dt = tiny.deflate(EdgeInsets.all(20)); + assertEquals(0, dt.minWidth()); + assertEquals(0, dt.maxWidth()); + assertEquals(0, dt.minHeight()); + assertEquals(0, dt.maxHeight()); + } + + @Test + void deflateKeepsUnboundedMaxUnbounded() { + BoxConstraints c = new BoxConstraints(0, Double.POSITIVE_INFINITY, 0, 500); + BoxConstraints d = c.deflate(EdgeInsets.all(10)); + assertFalse(d.hasBoundedWidth()); + assertEquals(480, d.maxHeight()); + } + + @Test + void loosenDropsMinimums() { + BoxConstraints c = BoxConstraints.tight(100, 50); + BoxConstraints l = c.loosen(); + assertEquals(0, l.minWidth()); + assertEquals(0, l.minHeight()); + assertEquals(100, l.maxWidth()); + assertEquals(50, l.maxHeight()); + } + + @Test + void tightenClampsWithinExistingBounds() { + BoxConstraints c = new BoxConstraints(10, 100, 10, 100); + BoxConstraints t = c.tighten(50.0, 200.0); + assertEquals(50, t.minWidth()); + assertEquals(50, t.maxWidth()); + // requested 200 clamps to the existing max of 100 + assertEquals(100, t.minHeight()); + assertEquals(100, t.maxHeight()); + // null leaves the axis untouched + BoxConstraints w = c.tighten(null, 40.0); + assertEquals(10, w.minWidth()); + assertEquals(100, w.maxWidth()); + assertEquals(40, w.minHeight()); + assertEquals(40, w.maxHeight()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/GradientRampTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/GradientRampTest.java new file mode 100644 index 00000000000..5a2af4e8027 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/rendering/GradientRampTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.rendering; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A gradient's colour part way along its ramp. + * + *

The fill used to read only the first and last entries of the ramp, so a gradient + * with three or more stops lost every colour between them. It also only ever produced + * those two colours, because the ramp was handed straight to the port's linear-gradient + * primitive; the banded fill that replaced it asks for a colour per band, which is what + * this answers.

+ */ +class GradientRampTest { + + private static final int RED = 0xffff0000; + private static final int BLUE = 0xff0000ff; + private static final int GREEN = 0xff00ff00; + + private static String hex(int argb) { + String s = Integer.toHexString(argb); + while (s.length() < 8) { + s = "0" + s; + } + return s; + } + + @Test + void theEndsAreThemselves() { + int[] ramp = {RED, BLUE}; + assertEquals(hex(RED), hex(GraphicsCanvas.rampAt(ramp, 0))); + assertEquals(hex(BLUE), hex(GraphicsCanvas.rampAt(ramp, 1))); + } + + @Test + void theMiddleOfTwoStopsIsHalfway() { + assertEquals(hex(0xff800080), hex(GraphicsCanvas.rampAt(new int[] {RED, BLUE}, 0.5))); + } + + /// The case the old code could not express at all: a middle stop is a colour the + /// ramp must actually pass through. + @Test + void aThirdStopIsNotSkipped() { + int[] ramp = {RED, GREEN, BLUE}; + assertEquals(hex(GREEN), hex(GraphicsCanvas.rampAt(ramp, 0.5))); + assertEquals(hex(0xff808000), hex(GraphicsCanvas.rampAt(ramp, 0.25))); + assertEquals(hex(0xff008080), hex(GraphicsCanvas.rampAt(ramp, 0.75))); + } + + @Test + void alphaInterpolatesToo() { + int[] ramp = {0x00ff0000, 0xffff0000}; + assertEquals(hex(0x80ff0000), hex(GraphicsCanvas.rampAt(ramp, 0.5))); + } + + @Test + void aSingleStopIsThatColourEverywhere() { + int[] ramp = {RED}; + assertEquals(hex(RED), hex(GraphicsCanvas.rampAt(ramp, 0))); + assertEquals(hex(RED), hex(GraphicsCanvas.rampAt(ramp, 0.37))); + assertEquals(hex(RED), hex(GraphicsCanvas.rampAt(ramp, 1))); + } + + /// Out-of-range values are clamped rather than wrapping or throwing: the band loop + /// samples at band midpoints, and rounding must never walk off the end of the array. + @Test + void outOfRangeClamps() { + int[] ramp = {RED, BLUE}; + assertEquals(hex(RED), hex(GraphicsCanvas.rampAt(ramp, -0.5))); + assertEquals(hex(BLUE), hex(GraphicsCanvas.rampAt(ramp, 1.5))); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltBox.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltBox.java new file mode 100644 index 00000000000..b1b5d6bf6dd --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltBox.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.testsupport; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * A second leaf render widget type, used to verify that reconciliation + * replaces the element when the widget type changes. + */ +public class AltBox extends Widget { + + private final double width; + private final double height; + + public AltBox(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public Element createElement() { + return new AltBoxElement(this); + } + + public static class AltBoxElement extends RenderElement { + public AltBoxElement(AltBox widget) { + super(widget); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + AltBox w = (AltBox) widget(); + return constraints.constrain(new Size(w.width, w.height)); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltMarkerBox.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltMarkerBox.java new file mode 100644 index 00000000000..a8d07ce3506 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/AltMarkerBox.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.testsupport; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * A second component-owning marker widget type, so reconciliation replaces a + * {@link MarkerBox} element instead of updating it in place. + */ +public class AltMarkerBox extends Widget { + + private final String tag; + + public AltMarkerBox(String tag) { + this.tag = tag; + } + + public String tag() { + return tag; + } + + @Override + public Element createElement() { + return new AltMarkerBoxElement(this); + } + + public static class AltMarkerBoxElement extends RenderElement { + + public AltMarkerBoxElement(AltMarkerBox widget) { + super(widget); + } + + @Override + protected boolean ownsComponent() { + return true; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + return constraints.constrain(new Size(10, 10)); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/MarkerBox.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/MarkerBox.java new file mode 100644 index 00000000000..07a2b963084 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/MarkerBox.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.testsupport; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * A leaf render widget that PRETENDS to own a CN1 component (without + * instantiating one, which needs a Display) so the host's attach-order + * bookkeeping — the flat container z-order — can be asserted headless. + */ +public class MarkerBox extends Widget { + + private final String tag; + + public MarkerBox(String tag) { + this.tag = tag; + } + + public String tag() { + return tag; + } + + @Override + public Element createElement() { + return new MarkerBoxElement(this); + } + + public static class MarkerBoxElement extends RenderElement { + + public MarkerBoxElement(MarkerBox widget) { + super(widget); + } + + @Override + protected boolean ownsComponent() { + return true; + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + return constraints.constrain(new Size(10, 10)); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/ProbeBox.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/ProbeBox.java new file mode 100644 index 00000000000..3e8f6ee2d2b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/ProbeBox.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.testsupport; + +import com.codename1.flutter.Element; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.Size; + +/** + * A leaf render widget with a stubbed intrinsic size — no CN1 component, no + * font measurement — so layout and reconciliation can run headless. + */ +public class ProbeBox extends Widget { + + private final double width; + private final double height; + + public ProbeBox(double width, double height) { + this.width = width; + this.height = height; + } + + public double width() { + return width; + } + + public double height() { + return height; + } + + @Override + public Element createElement() { + return new ProbeBoxElement(this); + } + + public static class ProbeBoxElement extends RenderElement { + public int layoutCount; + + public ProbeBoxElement(ProbeBox widget) { + super(widget); + } + + @Override + protected Size performLayout(BoxConstraints constraints) { + layoutCount++; + ProbeBox w = (ProbeBox) widget(); + return constraints.constrain(new Size(w.width(), w.height())); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/Toggler.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/Toggler.java new file mode 100644 index 00000000000..2073e32e07e --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/testsupport/Toggler.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.testsupport; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulWidget; +import com.codename1.flutter.Widget; + +/** + * A stateful widget whose state simply exposes the child it builds, so tests + * can flip the subtree via setState. + */ +public class Toggler extends StatefulWidget { + + private final Widget initialChild; + + public Toggler(Widget initialChild) { + this.initialChild = initialChild; + } + + @Override + public State createState() { + return new TogglerState(); + } + + public class TogglerState extends State { + public Widget child = initialChild; + public int initStateCalls; + public int disposeCalls; + + @Override + public void initState() { + initStateCalls++; + } + + @Override + public void dispose() { + disposeCalls++; + } + + @Override + public Widget build(BuildContext context) { + return child; + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/AsyncAndDependencyTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/AsyncAndDependencyTest.java new file mode 100644 index 00000000000..2a76dc71210 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/AsyncAndDependencyTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterAssets; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.InheritedValueProvider; +import com.codename1.flutter.State; +import com.codename1.flutter.StatefulElement; +import com.codename1.flutter.StatefulWidget; +import com.codename1.flutter.StatelessWidget; +import com.codename1.flutter.Widget; +import com.codename1.flutter.navigation.MaterialPageRoute; +import com.codename1.flutter.navigation.Navigator; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.async.Completer; +import dart.async.Future; +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// FutureBuilder delivers its future, a pushed route answers its pop result, an +/// unmounted reader leaves its inherited widget, and read() does not subscribe. +class AsyncAndDependencyTest { + + private BuildOwner owner; + + private Element mount(Widget root) { + owner = new BuildOwner(); + return FlutterUI.mount(root, new RenderHost(), owner); + } + + // --- FutureBuilder --------------------------------------------------------- + + private static FutureBuilder futureBuilder(Object future, final AsyncSnapshot[] seen) { + FutureBuilder fb = new FutureBuilder(); + fb.future(future); + fb.builder(new Funcs.Func2() { + @Override + public Widget call(BuildContext context, AsyncSnapshot snapshot) { + seen[0] = snapshot; + return new ProbeBox(1, 1); + } + }); + return fb; + } + + @Test + void aFutureBuilderWaitsAndThenDeliversTheValue() { + Completer c = new Completer(); + AsyncSnapshot[] seen = new AsyncSnapshot[1]; + mount(futureBuilder(c.future(), seen)); + assertEquals(ConnectionState.waiting, seen[0].connectionState()); + + c.complete("loaded"); + owner.flushSync(); + assertEquals(ConnectionState.done, seen[0].connectionState(), "the result must reach the builder"); + assertEquals("loaded", seen[0].data()); + } + + @Test + void aFutureBuilderDeliversTheError() { + Completer c = new Completer(); + AsyncSnapshot[] seen = new AsyncSnapshot[1]; + mount(futureBuilder(c.future(), seen)); + IllegalStateException boom = new IllegalStateException("boom"); + c.completeError(boom); + owner.flushSync(); + assertEquals(ConnectionState.done, seen[0].connectionState()); + assertSame(boom, seen[0].error()); + } + + @Test + void anUnmountedFutureBuilderIgnoresALateResult() { + Completer c = new Completer(); + AsyncSnapshot[] seen = new AsyncSnapshot[1]; + Element root = mount(futureBuilder(c.future(), seen)); + FlutterUI.unmountTree(root); + c.complete("late"); + owner.flushSync(); + assertEquals(ConnectionState.waiting, seen[0].connectionState(), "no build after unmount"); + } + + // --- Navigator results ----------------------------------------------------- + + @Test + void aPushedRouteCompletesWithItsPopResult() { + Navigator.reset(); + MaterialPageRoute r = new MaterialPageRoute(); + r.builder((context) -> new ProbeBox(1, 1)); + Future result = Navigator.push(null, r); + final Object[] got = {"not yet"}; + result.then(new Funcs.VoidFunc1() { + @Override + public void call(Object v) { + got[0] = v; + } + }); + assertEquals("not yet", got[0], "nothing until the route is popped"); + Navigator.pop(null, "picked"); + assertEquals("picked", got[0]); + } + + @Test + void aStatesPushNamedAnswersAFutureEvenForAMissingRoute() { + Navigator.reset(); + Navigator.resetRouteTable(); + Object f = Navigator.of(null, null).pushNamed("/nowhere", null); + assertTrue(f instanceof Future, "never null, or `await` has nothing to wait on"); + } + + // --- dependencies ---------------------------------------------------------- + + static final class Model extends InheritedWidget { + static InheritedElement element; + + Model(Widget child) { + child(child); + } + + @Override + public Element createElement() { + element = (InheritedElement) super.createElement(); + return element; + } + + /// Never notifies, like a Theme that stays put: a notification prunes + /// unmounted readers as it goes, which would hide a missing unregister. + @Override + public boolean updateShouldNotify(InheritedWidget oldWidget) { + return false; + } + } + + static final class Reader extends StatelessWidget { + @Override + public Widget build(BuildContext context) { + context.dependOnInheritedWidgetOfExactType(Model.class); + return new ProbeBox(1, 1); + } + } + + static final class Toggle extends StatefulWidget { + @Override + public State createState() { + return new ToggleState(); + } + } + + static final class ToggleState extends State { + boolean showReader = true; + + void hide() { + setState(() -> showReader = false); + } + + @Override + public Widget build(BuildContext context) { + return new Model(showReader ? new Reader() : new ProbeBox(1, 1)); + } + } + + @Test + void anUnmountedReaderLeavesItsInheritedWidget() { + Element root = mount(new Toggle()); + assertEquals(1, Model.element.dependentCount()); + ((ToggleState) ((StatefulElement) root).state()).hide(); + owner.flushSync(); + assertEquals(0, Model.element.dependentCount(), + "the popped reader must not stay reachable from a long-lived inherited widget"); + } + + static final class ValueHost extends StatelessWidget implements InheritedValueProvider { + final Widget child; + + ValueHost(Widget child) { + this.child = child; + } + + @Override + public Object providedValueFor(Class type) { + return type == String.class ? "model" : null; + } + + @Override + public Widget build(BuildContext context) { + return child; + } + } + + static final class Consumer extends StatelessWidget { + final boolean watch; + int builds; + + Consumer(boolean watch) { + this.watch = watch; + } + + @Override + public Widget build(BuildContext context) { + builds++; + if (watch) { + context.watch(String.class); + } else { + context.read(String.class); + } + return new ProbeBox(1, 1); + } + } + + @Test + void readDoesNotSubscribeAndWatchDoes() { + Consumer reader = new Consumer(false); + Element root = mount(new ValueHost(reader)); + root.rebuildProviderDependents(); + owner.flushSync(); + assertEquals(1, reader.builds, "context.read must not rebuild on a notification"); + + Consumer watcher = new Consumer(true); + root = mount(new ValueHost(watcher)); + root.rebuildProviderDependents(); + owner.flushSync(); + assertEquals(2, watcher.builds, "context.watch does"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/BoxFitGeometryTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/BoxFitGeometryTest.java new file mode 100644 index 00000000000..4368a25f8d0 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/BoxFitGeometryTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BoxFit; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * BoxFit's arithmetic, pinned. The runtime draws artwork into its box at paint + * time rather than keeping a scaled copy of it, so this maths IS the rendering: + * get it wrong and the image is cropped, stretched or centred incorrectly with + * nothing to report. + */ +class BoxFitGeometryTest { + + /** A 200x100 image in a 100x100 box. */ + private static double[] wide(BoxFit fit) { + return ImageRenderElement.fittedSize(fit, 100, 100, 200, 100); + } + + @Test + void fillTakesTheWholeBox() { + double[] r = wide(BoxFit.fill); + assertEquals(100, r[0]); + assertEquals(100, r[1]); + } + + @Test + void containFitsEntirelyInside() { + double[] r = wide(BoxFit.contain); + assertEquals(100, r[0]); + assertEquals(50, r[1]); + } + + @Test + void coverOverflowsTheBox() { + double[] r = wide(BoxFit.cover); + assertEquals(200, r[0]); + assertEquals(100, r[1]); + // The point of cover: it is never SMALLER than the box in either axis, + // which is what lets the component's own clip crop it. + org.junit.jupiter.api.Assertions.assertTrue(r[0] >= 100 && r[1] >= 100); + } + + @Test + void fitWidthAndFitHeightPinOneAxis() { + double[] w = wide(BoxFit.fitWidth); + assertEquals(100, w[0]); + assertEquals(50, w[1]); + double[] h = wide(BoxFit.fitHeight); + assertEquals(200, h[0]); + assertEquals(100, h[1]); + } + + @Test + void noneKeepsTheNaturalSize() { + double[] r = wide(BoxFit.none); + assertEquals(200, r[0]); + assertEquals(100, r[1]); + } + + /** A 40x20 image -- SMALLER than the box -- in that same 100x100 box. */ + private static double[] small(BoxFit fit) { + return ImageRenderElement.fittedSize(fit, 100, 100, 40, 20); + } + + @Test + void scaleDownShrinksAnOversizedPictureLikeContain() { + double[] r = wide(BoxFit.scaleDown); + assertEquals(100, r[0]); + assertEquals(50, r[1]); + } + + @Test + void scaleDownNeverEnlargesAnUndersizedPicture() { + // The difference from `contain`, and the whole point of the fit: + // contain would blow this up to 100x50 to fill the box. + double[] r = small(BoxFit.scaleDown); + assertEquals(40, r[0]); + assertEquals(20, r[1]); + assertEquals(100, small(BoxFit.contain)[0]); + } + + @Test + void aNullFitIsScaleDownNotContain() { + // Flutter's paintImage does `fit ??= BoxFit.scaleDown`, so a widget that + // names no fit never enlarges its artwork. Reading the default as + // `contain` drew the reply study's 200px attachment thumbnails at 432px. + double[] r = small(null); + assertEquals(40, r[0]); + assertEquals(20, r[1]); + } + + @Test + void decodeHintsBoundTheDecodedSizeWithoutEnlargingIt() { + // One hint keeps the aspect ratio... + double[] w = ImageRenderElement.decodeHinted(1000, 500, Long.valueOf(200), null); + assertEquals(200, w[0]); + assertEquals(100, w[1]); + double[] h = ImageRenderElement.decodeHinted(1000, 500, null, Long.valueOf(100)); + assertEquals(200, h[0]); + assertEquals(100, h[1]); + // ...both do not, because dart:ui decodes to exactly the pair it is given. + double[] both = ImageRenderElement.decodeHinted(1000, 500, Long.valueOf(200), Long.valueOf(200)); + assertEquals(200, both[0]); + assertEquals(200, both[1]); + } + + @Test + void aDecodeHintLargerThanTheAssetIsIgnored() { + // ResizeImage passes allowUpscaling: false, so a hint can only shrink. + double[] r = ImageRenderElement.decodeHinted(100, 50, Long.valueOf(4000), null); + assertEquals(100, r[0]); + assertEquals(50, r[1]); + } + + @Test + void noDecodeHintLeavesTheSizeAlone() { + double[] r = ImageRenderElement.decodeHinted(1000, 500, null, null); + assertEquals(1000, r[0]); + assertEquals(500, r[1]); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleChildrenTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleChildrenTest.java new file mode 100644 index 00000000000..f03a1c6a4cb --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleChildrenTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * An element that owns children must let the tree walk see them. + * + *

{@code Element.visitChildren} is empty by default, so owning children and not + * overriding it owns them invisibly -- and the walk that unmounts a subtree is the one + * that suffers. A dismissed row was deactivated while its child and both backgrounds + * stayed mounted with their components still in the scene, so every surviving row drew + * its new content over its old and the lists inside those rows were never unmounted.

+ */ +class DismissibleChildrenTest { + + private static List visited(Dismissible w) { + RenderHost host = new RenderHost(); + RenderElement e = (RenderElement) FlutterUI.mount(w, host, new BuildOwner()); + e.layout(BoxConstraints.loose(200, 200)); + e.position(0, 0); + final List seen = new ArrayList(); + e.visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + seen.add(c); + } + }); + return seen; + } + + @Test + @DisplayName("all three children are visited") + void visitsChildAndBothBackgrounds() { + Dismissible w = new Dismissible(); + w.child(new ProbeBox(60, 40)); + w.background(new ProbeBox(60, 40)); + w.secondaryBackground(new ProbeBox(60, 40)); + assertEquals(3, visited(w).size(), "child, background and secondaryBackground"); + } + + @Test + @DisplayName("only the children that exist are visited") + void visitsOnlyWhatIsThere() { + Dismissible w = new Dismissible(); + w.child(new ProbeBox(60, 40)); + assertEquals(1, visited(w).size(), "just the child"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleConfirmTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleConfirmTest.java new file mode 100644 index 00000000000..c2fe1ea5335 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/DismissibleConfirmTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import dart.async.Future; +import dart.runtime.Funcs; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@code confirmDismiss} answers a {@code Future}, and Flutter dismisses only on + * {@code true}: its Dismissible reads the answer as {@code result ?? false}. A null -- + * a confirmation dialog closed without a choice -- used to count as consent. + */ +class DismissibleConfirmTest { + + private static Boolean decision(final Object answer) { + Dismissible w = new Dismissible(); + w.child(new com.codename1.flutter.testsupport.ProbeBox(10, 10)); + w.confirmDismiss(new Funcs.Func1() { + @Override + public Object call(DismissDirection d) { + return answer; + } + }); + DismissibleRenderElement e = new DismissibleRenderElement(w); + final Boolean[] seen = new Boolean[1]; + e.confirm(null, new Funcs.VoidFunc1() { + @Override + public void call(Boolean ok) { + seen[0] = ok; + } + }); + return seen[0]; + } + + @Test + @DisplayName("a Future completing with null vetoes the dismissal") + void asyncNullVetoes() { + assertEquals(Boolean.FALSE, decision(Future.value(null))); + } + + @Test + @DisplayName("a synchronous null vetoes the dismissal") + void syncNullVetoes() { + assertEquals(Boolean.FALSE, decision(null)); + } + + @Test + @DisplayName("true, sync or async, still dismisses; false still vetoes") + void trueConsentsFalseVetoes() { + assertEquals(Boolean.TRUE, decision(Future.value(Boolean.TRUE))); + assertEquals(Boolean.TRUE, decision(Boolean.TRUE)); + assertEquals(Boolean.FALSE, decision(Future.value(Boolean.FALSE))); + assertEquals(Boolean.FALSE, decision(Boolean.FALSE)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FittedImageLockTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FittedImageLockTest.java new file mode 100644 index 00000000000..74b4fad0fed --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FittedImageLockTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * An {@link com.codename1.ui.EncodedImage} keeps its decoded bitmap behind a soft + * reference and decodes again on demand, so a picture drawn every frame and + * collected between them is decoded every frame. {@link com.codename1.ui.Label} + * avoids that by locking its icon while it is on screen; {@code FittedImage} + * paints its source directly rather than holding a scaled copy as an icon — one + * bitmap in memory instead of two — and so has to do the locking itself. + * + *

What is asserted here is the bookkeeping, which is where the two failures + * live: a lock left behind on a replaced picture is never released, and a second + * lock on a picture already locked is never balanced.

+ */ +class FittedImageLockTest { + + /// Records the calls instead of making them, so the bookkeeping can be + /// exercised without an initialised Display — which constructing any real + /// Image requires. + private static final class Recording extends ImageRenderElement.ImageLock { + + final List calls = new ArrayList(); + + @Override + void lock(Object img) { + calls.add("lock " + img); + } + + @Override + void unlock(Object img) { + calls.add("unlock " + img); + } + } + + @Test + void nothingIsLockedUntilSomethingIsOnScreen() { + Recording r = new Recording(); + r.want(null); + assertEquals(List.of(), r.calls, "nothing on screen means nothing pinned in memory"); + assertNull(r.held()); + } + + @Test + void goingOnScreenLocksAndComingOffUnlocks() { + Recording r = new Recording(); + r.want("picture"); + assertEquals(List.of("lock picture"), r.calls); + assertSame("picture", r.held()); + + r.want(null); + assertEquals(List.of("lock picture", "unlock picture"), r.calls, + "off screen, the decoded bitmap has to be reclaimable again"); + assertNull(r.held()); + } + + @Test + void replacingTheSourceMovesTheLock() { + Recording r = new Recording(); + r.want("first"); + r.want("second"); + assertEquals(List.of("lock first", "unlock first", "lock second"), r.calls, + "the replaced picture must not keep a lock nobody will release"); + assertSame("second", r.held()); + } + + @Test + void settingTheSameSourceAgainDoesNotStackLocks() { + Recording r = new Recording(); + r.want("picture"); + r.want("picture"); + r.want("picture"); + assertEquals(List.of("lock picture"), r.calls, "one lock per component, or the unlock cannot balance it"); + + r.want(null); + assertEquals(List.of("lock picture", "unlock picture"), r.calls); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FractionalTranslationBoxTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FractionalTranslationBoxTest.java new file mode 100644 index 00000000000..dfd0897b34f --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/FractionalTranslationBoxTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A fractional translation's pane has to cover where the subtree ends up. + * + *

Codename One clips every component to its own rectangle before calling its + * paint, so a paint-only shift out of that rectangle is discarded. With the pane + * left at the laid-out box, the gallery's feature-discovery circle -- positioned + * at its centre and pulled back by half its own size -- rendered as the + * bottom-right quadrant of a circle, with two straight edges meeting exactly at + * the centre point.

+ */ +class FractionalTranslationBoxTest { + + /** The real case: a 1618px circle at (657, 216) pulled back by half. */ + @Test + void aNegativeShiftMovesTheOriginAndGrowsTheBox() { + int[] box = FractionalTranslationRenderElement.translatedBox( + 657, 216, 1618, 1618, -809, -809); + assertArrayEquals(new int[] {-152, -593, 2427, 2427, 0, 0}, box); + } + + /** + * A positive shift leaves the origin alone and pays for itself while painting, + * because the box already starts where the subtree is laid out. + */ + @Test + void aPositiveShiftIsPaidAtPaintTime() { + int[] box = FractionalTranslationRenderElement.translatedBox( + 100, 50, 200, 80, 60, 20); + assertArrayEquals(new int[] {100, 50, 260, 100, 60, 20}, box); + } + + /** Whatever the sign, origin plus paint offset lands the subtree exactly dx away. */ + @Test + void theOriginAndThePaintOffsetAlwaysSumToTheShift() { + for (int d = -300; d <= 300; d += 37) { + int[] box = FractionalTranslationRenderElement.translatedBox(500, 500, 40, 40, d, d); + assertEquals(500 + d, box[0] + box[4], "x for shift " + d); + assertEquals(500 + d, box[1] + box[5], "y for shift " + d); + } + } + + @Test + void noShiftIsTheBoxItself() { + assertArrayEquals(new int[] {10, 20, 30, 40, 0, 0}, + FractionalTranslationRenderElement.translatedBox(10, 20, 30, 40, 0, 0)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/GestureHitTestTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/GestureHitTestTest.java new file mode 100644 index 00000000000..912cbef3a59 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/GestureHitTestTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.material.IconButton; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * A GestureDetector must not shadow the controls INSIDE it. + * + *

The overlay that makes a wrapped subtree tappable covers that whole subtree and grabs + * pointer events, so a wrapper around a button used to swallow the button's presses — + * Flutter's hit test gives the innermost target priority. The symptom is a control that + * draws correctly and does nothing, which is why the app bar's overflow button looked + * finished while being inert.

+ * + *

Headless, so no CN1 components exist; what is asserted here is the STRUCTURE the + * dispatch relies on — that the gesture element exposes the content subtree the overlay + * consults, and that the button inside it is a distinct render element with its own + * handler.

+ */ +class GestureHitTestTest { + + private static GestureDetector detectorAround(Widget child, Funcs.VoidFunc0 onTap) { + GestureDetector g = new GestureDetector(); + g.child(child); + g.onTap(onTap); + return g; + } + + @Test + @DisplayName("the detector exposes its content subtree, not just its overlay") + void theContentSubtreeIsReachable() { + GestureDetector g = detectorAround(new ProbeBox(50, 50), null); + GestureRenderElement e = (GestureRenderElement) + FlutterUI.mount(g, new RenderHost(), new BuildOwner()); + + assertNotNull(e.contentElement(), + "the overlay resolves its own subtree through this"); + } + + @Test + @DisplayName("a button inside a detector keeps its own press handler") + void anInnerButtonKeepsItsHandler() { + final int[] pressed = {0}; + IconButton inner = new IconButton(); + inner.icon(new Icon(com.codename1.flutter.Icons.search)); + inner.onPressed(new Funcs.VoidFunc0() { + @Override + public void call() { + pressed[0]++; + } + }); + + GestureRenderElement e = (GestureRenderElement) FlutterUI.mount( + detectorAround(inner, null), new RenderHost(), new BuildOwner()); + e.layout(BoxConstraints.tight(200, 100)); + e.position(0, 0); + + Element content = e.contentElement(); + RenderElement button = RenderElement.findRenderElement(content); + assertNotNull(button, "the button must be a render element of the content subtree"); + assertSame(inner, button.widget(), + "and it must be the button itself, not the detector's overlay"); + } + + @Test + @DisplayName("the overlay is still a separate element, so bare content stays tappable") + void theOverlayStillExists() { + final int[] taps = {0}; + GestureDetector g = detectorAround(new ProbeBox(50, 50), new Funcs.VoidFunc0() { + @Override + public void call() { + taps[0]++; + } + }); + GestureRenderElement e = (GestureRenderElement) + FlutterUI.mount(g, new RenderHost(), new BuildOwner()); + e.layout(BoxConstraints.tight(50, 50)); + + final int[] children = {0}; + e.visitChildren(new Funcs.VoidFunc1() { + @Override + public void call(Element c) { + children[0]++; + } + }); + // content + overlay: losing the overlay would make every wrapped label dead. + org.junit.jupiter.api.Assertions.assertEquals(2, children[0]); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/HiddenChildrenTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/HiddenChildrenTest.java new file mode 100644 index 00000000000..97defbacbf0 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/HiddenChildrenTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.core.DartList; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Widgets whose job is to NOT show something must not show it. + * + *

Both of these drew their child anyway. Crane's back layer is an + * {@code IndexedStack} over three forms, so the study opened with all three + * printed on top of one another — no error, just an unreadable screen.

+ */ +class HiddenChildrenTest { + + private static RenderElement laidOut(Widget root, double w, double h) { + Element e = FlutterUI.mount(root, new RenderHost(), new BuildOwner()); + RenderElement r = RenderElement.findRenderElement(e); + r.layout(BoxConstraints.loose(w, h)); + r.position(0, 0); + return r; + } + + /** Every ProbeBox in the tree, with the size it was laid out at. */ + private static List probes(RenderElement from) { + List out = new ArrayList(); + collect(from, out); + return out; + } + + private static void collect(RenderElement from, List out) { + from.visitChildren(child -> { + RenderElement r = RenderElement.findRenderElement(child); + if (r != null) { + if (r.widget() instanceof ProbeBox) { + out.add(r); + } + collect(r, out); + } + }); + } + + @Test + @DisplayName("an IndexedStack draws only the selected child") + void indexedStackShowsOne() { + IndexedStack stack = new IndexedStack(); + stack.index(1); + stack.children(DartList.of( + new ProbeBox(100, 40), new ProbeBox(200, 60), new ProbeBox(300, 80))); + + RenderElement r = laidOut(stack, 400, 400); + + assertEquals(200, r.size().width(), 1e-9, "sized to the SELECTED child"); + assertEquals(60, r.size().height(), 1e-9); + + List kids = probes(r); + assertEquals(3, kids.size(), "the others stay in the tree, keeping their state"); + assertEquals(0, kids.get(0).size().width(), 1e-9, "but take no space"); + assertEquals(200, kids.get(1).size().width(), 1e-9); + assertEquals(0, kids.get(2).size().width(), 1e-9); + } + + @Test + @DisplayName("visible: false shows nothing") + void invisibleChildIsNotDrawn() { + Visibility v = new Visibility(); + v.visible(false); + v.child(new ProbeBox(120, 40)); + + RenderElement r = laidOut(v, 400, 400); + + assertEquals(0, r.size().width(), 1e-9); + assertEquals(0, r.size().height(), 1e-9); + assertEquals(0, probes(r).size(), "the child is not in the tree at all"); + } + + @Test + @DisplayName("visible: true is unchanged") + void visibleChildIsDrawn() { + Visibility v = new Visibility(); + v.child(new ProbeBox(120, 40)); + + RenderElement r = laidOut(v, 400, 400); + + assertEquals(120, r.size().width(), 1e-9); + assertEquals(1, probes(r).size()); + } + + @Test + @DisplayName("visible: false with a replacement shows the replacement") + void replacementIsDrawn() { + Visibility v = new Visibility(); + v.visible(false); + v.child(new ProbeBox(120, 40)); + v.replacement(new ProbeBox(10, 10)); + + RenderElement r = laidOut(v, 400, 400); + + assertEquals(10, r.size().width(), 1e-9); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/IgnorePointerDeafnessTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/IgnorePointerDeafnessTest.java new file mode 100644 index 00000000000..3dc4e02ca66 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/IgnorePointerDeafnessTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; +import com.codename1.ui.Component; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * An IgnorePointer has to make its subtree DEAF, not merely render it. + * + *

It exists so that something above it receives a touch that lands on top of its + * child, and it used to be a structural pass-through: the child rendered and stayed + * live, so the child answered first and the handler above never ran. The gallery's + * splash screen is built on exactly that -- a strip of the home page wrapped in an + * IgnorePointer inside a detector whose tap dismisses the splash -- so the screen could + * be entered and not left.

+ * + *

Codename One's hit test walks UP from the component it lands on for as long as each + * one ignores pointer events, so the flag has to reach the whole subtree: a touch landing + * on a nested child would otherwise stop there and consume the press.

+ */ +class IgnorePointerDeafnessTest { + + private static RenderElement mount(IgnorePointer w) { + RenderHost host = new RenderHost(); + RenderElement e = (RenderElement) FlutterUI.mount(w, host, new BuildOwner()); + e.layout(BoxConstraints.loose(200, 200)); + e.position(0, 0); + return e; + } + + private static void assertDeaf(RenderElement e, boolean deaf, String where) { + for (RenderElement child : e.renderChildren()) { + Component c = child.component(); + if (c != null) { + assertTrue(deaf == c.isIgnorePointerEvents(), + where + ": " + c.getClass().getSimpleName()); + } else { + assertDeaf(child, deaf, where); + } + } + } + + @Test + @DisplayName("the subtree is deaf to touch by default") + void ignoresByDefault() { + IgnorePointer w = new IgnorePointer(); + w.child(new ProbeBox(50, 50)); + assertDeaf(mount(w), true, "default"); + } + + @Test + @DisplayName("ignoring: false leaves the subtree live") + void honoursIgnoringFalse() { + IgnorePointer w = new IgnorePointer(); + w.ignoring(Boolean.FALSE); + w.child(new ProbeBox(50, 50)); + assertDeaf(mount(w), false, "ignoring false"); + } + + @Test + @DisplayName("deafness reaches a NESTED child, not just the top one") + void reachesNestedChildren() { + Column inner = new Column(); + inner.children(dart.core.DartList.of(new ProbeBox(20, 20), new ProbeBox(20, 20))); + Padding pad = new Padding(); + pad.child(inner); + IgnorePointer w = new IgnorePointer(); + w.child(pad); + RenderElement e = mount(w); + assertDeaf(e, true, "nested"); + assertFalse(e.renderChildren().isEmpty(), "the child must actually be mounted"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImageProviderSourceTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImageProviderSourceTest.java new file mode 100644 index 00000000000..cca67837019 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImageProviderSourceTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.MemoryImage; +import com.codename1.flutter.NetworkImage; +import dart.core.DartMap; +import dart.typed_data.Uint8List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/// Image(image: provider) keeps everything the render element needs to load +/// what the provider describes. +public class ImageProviderSourceTest { + + @Test + public void networkImageHeadersReachTheWidget() { + DartMap headers = new DartMap(); + headers.put("Authorization", "Bearer abc"); + NetworkImage p = new NetworkImage("https://example.com/a.png"); + p.headers(headers); + Image img = new Image(); + img.image(p); + assertEquals("https://example.com/a.png", img.getUrl()); + assertNotNull(img.getHeaders(), "the headers must not be dropped"); + assertEquals("Bearer abc", img.getHeaders().get("Authorization")); + } + + @Test + public void aChangedTokenIsADifferentSource() { + // The render element reloads only when the source key changes, so a + // refreshed token has to change it. + Image a = withHeader("Bearer one"); + Image b = withHeader("Bearer two"); + assertNotEquals(a.sourceKey(), b.sourceKey()); + } + + @Test + public void noHeadersKeepsTheBareUrlSource() { + Image img = new Image(); + img.image(new NetworkImage("https://example.com/a.png")); + assertNull(img.getHeaders()); + assertEquals("url:https://example.com/a.png", img.sourceKey()); + } + + @Test + public void memoryImageCarriesItsBytes() { + Uint8List bytes = new Uint8List(4); + Image img = new Image(); + img.image(new MemoryImage(bytes)); + assertSame(bytes.toBytes(), img.getMemoryBytes(), "the buffer to decode"); + assertNull(img.getUrl()); + assertNull(img.getAssetName()); + } + + @Test + public void memoryImageKeepsItsScale() { + MemoryImage provider = new MemoryImage(new Uint8List(4)); + provider.scale(2.0); + Image img = new Image(); + img.image(provider); + assertEquals(2.0, img.getMemoryScale(), 0.0, + "two encoded pixels per logical pixel, not one"); + Image plain = new Image(); + plain.image(new MemoryImage(new Uint8List(4))); + assertEquals(1.0, plain.getMemoryScale(), 0.0); + } + + @Test + public void aDifferentBufferIsADifferentSource() { + Image a = new Image(); + a.image(new MemoryImage(new Uint8List(4))); + Image b = new Image(); + b.image(new MemoryImage(new Uint8List(4))); + assertNotEquals(a.sourceKey(), b.sourceKey()); + } + + private static Image withHeader(String token) { + DartMap headers = new DartMap(); + headers.put("Authorization", token); + NetworkImage p = new NetworkImage("https://example.com/a.png"); + p.headers(headers); + Image img = new Image(); + img.image(p); + return img; + } + + @Test + public void networkImageKeepsItsScaleAndKeysItsHeadersByContent() { + com.codename1.flutter.NetworkImage provider = new com.codename1.flutter.NetworkImage("https://x/a.png"); + provider.scale(2.0); + java.util.Map headers = new dart.core.DartMap(); + headers.put("Authorization", "Bearer one"); + provider.headers(headers); + Image img = new Image(); + img.image(provider); + assertEquals(2.0, img.getMemoryScale(), 0.0, "NetworkImage's scale is kept"); + String before = img.sourceKey(); + headers.put("Authorization", "Bearer two"); // mutated in place, as a Dart map is + Image again = new Image(); + again.image(provider); + org.junit.jupiter.api.Assertions.assertNotEquals(before, again.sourceKey(), + "new credentials are a new source, even in the same map object"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImplementedWidgetsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImplementedWidgetsTest.java new file mode 100644 index 00000000000..ff55b14eb82 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ImplementedWidgetsTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Offset; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Widgets that used to declare themselves unimplemented and render the child (or nothing) + * now do the thing they describe. + * + *

Each of these was a silent hole: the app drew a plausible screen with a piece missing + * and reported it only to a diagnostic channel nobody reads during normal use.

+ */ +class ImplementedWidgetsTest { + + private static RenderElement mount(Widget w) { + return (RenderElement) FlutterUI.mount(w, new RenderHost(), new BuildOwner()); + } + + // ---------------------------------------------------------------- RotatedBox + + @Test + @DisplayName("an odd quarter turn swaps the box's width and height") + void rotatedBoxSwapsAxes() { + RotatedBox box = new RotatedBox(); + box.quarterTurns(1); + box.child(new ProbeBox(40, 10)); + + RenderElement e = mount(box); + e.layout(BoxConstraints.loose(200, 200)); + + assertEquals(10, e.size().width(), 1e-9, "a quarter turn stands the box on end"); + assertEquals(40, e.size().height(), 1e-9); + } + + @Test + @DisplayName("an even quarter turn leaves the footprint alone") + void rotatedBoxKeepsAxesOnHalfTurn() { + RotatedBox box = new RotatedBox(); + box.quarterTurns(2); + box.child(new ProbeBox(40, 10)); + + RenderElement e = mount(box); + e.layout(BoxConstraints.loose(200, 200)); + + assertEquals(40, e.size().width(), 1e-9); + assertEquals(10, e.size().height(), 1e-9); + } + + @Test + @DisplayName("negative turns normalise rather than misbehaving") + void rotatedBoxNormalisesNegativeTurns() { + RotatedBox box = new RotatedBox(); + box.quarterTurns(-1); + box.child(new ProbeBox(40, 10)); + + RenderElement e = mount(box); + e.layout(BoxConstraints.loose(200, 200)); + + assertEquals(10, e.size().width(), 1e-9); + } + + // ------------------------------------------------- FractionalTranslation + + @Test + @DisplayName("a fractional translation does not move the layout") + void fractionalTranslationIsPaintOnly() { + FractionalTranslation t = new FractionalTranslation(); + t.translation(new Offset(0.5, 0.5)); + t.child(new ProbeBox(40, 20)); + + RenderElement e = mount(t); + e.layout(BoxConstraints.loose(200, 200)); + + assertEquals(40, e.size().width(), 1e-9); + assertEquals(20, e.size().height(), 1e-9); + assertEquals(0.5, t.fraction().dx(), 1e-9); + } + + // ------------------------------------------------------------------ GridTile + + @Test + @DisplayName("a tile with a footer stacks it over the child") + void gridTileStacksItsBands() { + GridTile tile = new GridTile(); + tile.child(new ProbeBox(50, 50)); + tile.footer(new ProbeBox(50, 10)); + + Widget built = tile.build(null); + assertTrue(built instanceof Stack, "header/footer need a Stack, got " + built); + } + + @Test + @DisplayName("a bare tile is still just its child") + void gridTileWithoutBandsIsTheChild() { + GridTile tile = new GridTile(); + ProbeBox child = new ProbeBox(50, 50); + tile.child(child); + + assertEquals(child, tile.build(null), "no bands means no wrapper"); + } + + // ------------------------------------------------------------------ ClipRect + + @Test + @DisplayName("Clip.none really does not clip") + void clipNoneDoesNotClip() { + ClipRect c = new ClipRect(); + c.clipBehavior(com.codename1.flutter.Clip.none); + c.child(new ProbeBox(10, 10)); + + Element e = c.createElement(); + assertFalse(e instanceof ClipRectRenderElement, + "Clip.none must not get the clipping element - its pane clips regardless"); + } + + @Test + @DisplayName("the default behaviour still clips") + void clipHardEdgeStillClips() { + ClipRect c = new ClipRect(); + c.child(new ProbeBox(10, 10)); + + assertTrue(c.createElement() instanceof ClipRectRenderElement); + } + + // ---------------------------------------------------------------- FlutterLogo + + @Test + @DisplayName("the logo paints something, at the size asked for") + void flutterLogoRenders() { + FlutterLogo logo = new FlutterLogo(); + logo.size(48); + + Widget built = logo.build(null); + assertTrue(built instanceof CustomPaint, "the logo is drawn, got " + built); + CustomPaint paint = (CustomPaint) built; + assertNotNull(paint.getPainter(), "with a painter that actually draws"); + assertEquals(48, paint.getSize().width(), 1e-9); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/LetterSpacingTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/LetterSpacingTest.java new file mode 100644 index 00000000000..6dd946c0be5 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/LetterSpacingTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Flutter adds letterSpacing BETWEEN glyphs - n-1 gaps for n characters, with nothing + * trailing the last one. Being one gap out is a whole space of drift on a short label, + * which is exactly where the gallery uses it (headers and button captions). + */ +class LetterSpacingTest { + + @Test + void spacingAddsOneGapFewerThanCharacters() { + assertEquals(58.0, TextRenderElement.spacedWidth(50.0, 5, 2.0)); + } + + @Test + void aSingleCharacterGetsNoSpacing() { + assertEquals(10.0, TextRenderElement.spacedWidth(10.0, 1, 7.0)); + } + + @Test + void zeroSpacingIsTheBareStringWidth() { + assertEquals(50.0, TextRenderElement.spacedWidth(50.0, 5, 0.0)); + } + + @Test + void theEmptyStringHasNoWidth() { + assertEquals(0.0, TextRenderElement.spacedWidth(0.0, 0, 4.0)); + } + + @Test + void negativeSpacingTightens() { + assertEquals(42.0, TextRenderElement.spacedWidth(50.0, 5, -2.0)); + } + + /// The invariant the paint path has to hold: laying a run out glyph by glyph must + /// end exactly where spacedWidth said it would. + /// + /// It did not. The paint path advanced by charWidth, which returns an INT, so every + /// glyph's advance was rounded up and the error accumulated -- the same sentence + /// measured 589px in the reference and drew 607px here, 3.1% wide, about 0.6px per + /// character. Text that is systematically wide ellipsises strings that fit and + /// clips the ones that do not, which is what Reply's sender lines did on iOS. + private static double drawnWidth(double[] charWidths, double runWidth, double spacing) { + double sum = 0; + for (double w : charWidths) { + sum += w; + } + double scale = TextRenderElement.trackingScale(runWidth, sum); + double cursor = 0; + for (int i = 0; i < charWidths.length - 1; i++) { + cursor += charWidths[i] * scale + spacing; + } + return cursor + charWidths[charWidths.length - 1] * scale; + } + + @Test + void aTrackedRunEndsExactlyWhereItWasMeasured() { + // charWidths rounded up from a true 12.4px advance, as an int-returning + // charWidth does; the run itself measures 62, not 5 * 13 = 65. + double[] widths = {13, 13, 13, 13, 13}; + double measured = TextRenderElement.spacedWidth(62.0, 5, 2.0); + assertEquals(measured, drawnWidth(widths, 62.0, 2.0), 1e-9); + } + + @Test + void unevenGlyphsKeepTheirProportions() { + double[] widths = {20, 5, 11, 4}; + double measured = TextRenderElement.spacedWidth(36.0, 4, 1.5); + assertEquals(measured, drawnWidth(widths, 36.0, 1.5), 1e-9); + // and the widest glyph is still the widest + double scale = TextRenderElement.trackingScale(36.0, 40.0); + assertEquals(0.9, scale, 1e-9); + } + + /// Without the scale the run overruns, which is the defect stated numerically. + @Test + void theUnscaledRunOverrunsItsMeasurement() { + double[] widths = {13, 13, 13, 13, 13}; + double measured = TextRenderElement.spacedWidth(62.0, 5, 2.0); + double unscaled = 4 * (13 + 2.0) + 13; // what the old paint path advanced + assertTrue(unscaled > measured + 2, + "expected the unscaled run to overrun; got " + unscaled + " vs " + measured); + } + + /// A font that reports nothing must not divide by zero. + @Test + void aZeroWidthRunScalesByOne() { + assertEquals(1.0, TextRenderElement.trackingScale(0.0, 0.0)); + assertEquals(1.0, TextRenderElement.trackingScale(10.0, 0.0)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/OverlayInitialEntriesTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/OverlayInitialEntriesTest.java new file mode 100644 index 00000000000..ef3f6a0b1e4 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/OverlayInitialEntriesTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import dart.core.DartList; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Entries given to Overlay.initialEntries have the same lifecycle as inserted ones. +public class OverlayInitialEntriesTest { + + @Test + public void anInitialEntryCanBeRemoved() { + OverlayEntry entry = new OverlayEntry(); + DartList initial = new DartList(); + initial.add(entry); + Overlay overlay = new Overlay(); + overlay.initialEntries(initial); + Overlay.OverlayElement element = (Overlay.OverlayElement) overlay.createElement(); + OverlayState state = element.state(); + assertTrue(state.entries().contains(entry)); + + entry.remove(); + + // Unattached, remove() only marked the entry unmounted: it stayed in the + // overlay's list, and no rebuild was asked for to take it off screen. + assertFalse(state.entries().contains(entry), "removed from the overlay it belongs to"); + } + + @Test + public void aRepeatedInitialEntryIsAddedOnce() { + OverlayEntry entry = new OverlayEntry(); + DartList initial = new DartList(); + initial.add(entry); + initial.add(entry); + Overlay overlay = new Overlay(); + overlay.initialEntries(initial); + OverlayState state = ((Overlay.OverlayElement) overlay.createElement()).state(); + assertEquals(1, state.entries().size()); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageControllerTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageControllerTest.java new file mode 100644 index 00000000000..83640efde88 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageControllerTest.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.Element; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.RenderElement; +import com.codename1.flutter.Widget; +import com.codename1.flutter.animation.AnimatedBuilder; +import com.codename1.flutter.rendering.BoxConstraints; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.core.DartList; +import dart.runtime.Funcs; +import dart.runtime.RefLong; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The PageController is a LIVE scroll model, not a stored page number. + * + *

This pins the contract the gallery's home carousel is built on. Every card is + * wrapped in {@code AnimatedBuilder(animation: controller)} and scaled by + * {@code controller.page - index}; when the controller never notified and always + * reported its initial page, every card rendered at the scale it had on the first + * frame — measured on the running app as a centred card drawn at 0.868 of its size + * instead of 1.0, because the builder kept taking the {@code haveDimensions == false} + * branch.

+ */ +class PageControllerTest { + + private BuildOwner owner; + private RenderHost host; + + private PageView carousel(PageController controller, int pages, Boolean snapping, + Funcs.VoidFunc1 onPageChanged) { + DartList children = new DartList(); + for (int i = 0; i < pages; i++) { + children.add(new ProbeBox(10, 10)); + } + PageView v = new PageView(); + v.controller(controller); + v.children(children); + if (snapping != null) { + v.pageSnapping(snapping); + } + if (onPageChanged != null) { + v.onPageChanged(onPageChanged); + } + return v; + } + + private RenderElement mountAndLayout(Widget root, double w, double h) { + owner = new BuildOwner(); + host = new RenderHost(); + FlutterUI.mount(root, host, owner); + RenderElement r = host.rootRenderElement(); + r.layout(BoxConstraints.tight(w, h)); + return r; + } + + @Test + void anUnattachedControllerReportsItsInitialPage() { + PageController c = new PageController(); + c.initialPage(2); + assertEquals(2.0, c.page(), 0.0001); + assertFalse(c.hasClients()); + assertFalse(c.position().haveDimensions(), + "no view has reported a viewport yet"); + } + + @Test + void layoutAttachesTheControllerAndPublishesDimensions() { + PageController c = new PageController(); + c.viewportFraction(0.8); + mountAndLayout(carousel(c, 6, null, null), 500, 200); + + assertTrue(c.hasClients(), "the mounted PageView must attach itself"); + assertTrue(c.position().haveDimensions(), + "layout is the only chance an unscrolled PageView has to publish geometry"); + assertEquals(500, c.position().viewportDimension(), 0.0001); + // five gaps of one page extent (0.8 * 500) between six pages + assertEquals(5 * 400, c.position().maxScrollExtent(), 0.0001); + assertEquals(0.0, c.page(), 0.0001); + } + + @Test + void scrollingMakesThePageFractional() { + PageController c = new PageController(); + c.viewportFraction(0.8); + mountAndLayout(carousel(c, 6, null, null), 500, 200); + + // half a page extent along: exactly the state a mid-drag frame is in + c.applyMetrics(200, 400, 500, 0, 2000); + assertEquals(0.5, c.page(), 0.0001); + + c.applyMetrics(400, 400, 500, 0, 2000); + assertEquals(1.0, c.page(), 0.0001); + } + + @Test + void listenersFireOnlyWhenThePageActuallyMoves() { + PageController c = new PageController(); + final int[] notifications = {0}; + c.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + notifications[0]++; + } + }); + + c.applyMetrics(0, 400, 500, 0, 2000); + assertEquals(1, notifications[0]); + + // a scroll event on a frame where nothing moved must not dirty the builders + c.applyMetrics(0, 400, 500, 0, 2000); + assertEquals(1, notifications[0], "an unchanged page must not notify"); + + c.applyMetrics(40, 400, 500, 0, 2000); + assertEquals(2, notifications[0]); + } + + @Test + void anAnimatedBuilderRebuildsAgainstTheController() { + final PageController c = new PageController(); + final List pagesSeen = new ArrayList(); + + AnimatedBuilder b = new AnimatedBuilder(); + b.animation(c); + b.builder(new Funcs.Func2() { + @Override + public Widget call(com.codename1.flutter.BuildContext ctx, Widget child) { + pagesSeen.add(c.page()); + return new ProbeBox(10, 10); + } + }); + + owner = new BuildOwner(); + host = new RenderHost(); + Element root = FlutterUI.mount(b, host, owner); + assertEquals(1, pagesSeen.size()); + assertEquals(0.0, pagesSeen.get(0), 0.0001); + + c.applyMetrics(200, 400, 500, 0, 2000); + owner.flushSync(); + + assertEquals(2, pagesSeen.size(), "a controller notification must rebuild the builder"); + assertEquals(0.5, pagesSeen.get(1), 0.0001); + assertTrue(root.isMounted()); + } + + @Test + void pageSnappingDefaultsToOnAndIsOptOut() { + assertTrue(new PageView().isPageSnapping(), "Flutter's default is to snap"); + + PageView off = carousel(new PageController(), 3, Boolean.FALSE, null); + assertFalse(off.isPageSnapping(), + "the gallery carousel passes pageSnapping: false and scrolls freely"); + } + + @Test + void onPageChangedFiresOncePerWholePage() { + final List reported = new ArrayList(); + PageController c = new PageController(); + c.viewportFraction(0.8); + mountAndLayout(carousel(c, 6, null, new Funcs.VoidFunc1() { + @Override + public void call(RefLong v) { + reported.add(v.v); + } + }), 500, 200); + + // page 0 is the settled page at mount; it is not a change + assertEquals(0, reported.size()); + } + + @Test + void anInitialPageIsNotReportedAsAChange() { + final List reported = new ArrayList(); + PageController c = new PageController(); + c.initialPage(3); + c.viewportFraction(0.8); + mountAndLayout(carousel(c, 6, null, new Funcs.VoidFunc1() { + @Override + public void call(RefLong v) { + reported.add(v.v); + } + }), 500, 200); + + assertEquals(0, reported.size(), + "starting on page 3 is not a page CHANGE"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageSettleTargetTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageSettleTargetTest.java new file mode 100644 index 00000000000..331c63d64e7 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PageSettleTargetTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Which page a released drag settles on — Flutter's {@code PageScrollPhysics}. + * + *

The rule is not "nearest page": past the velocity tolerance the release carries you to + * the NEXT page even from a barely-moved carousel, which is what makes a flick feel like a + * flick rather than a nudge that springs back.

+ */ +class PageSettleTargetTest { + + private static final double EXTENT = 900; + private static final int MAX = 4500; // six pages + private static final float FLICK = 5f; // comfortably past the tolerance + private static final float STILL = 0f; + + @Test + @DisplayName("a release with no velocity falls to the nearest page") + void noVelocitySettlesToTheNearest() { + assertEquals(0, PageViewRenderElement.settleTarget(100, EXTENT, STILL, MAX)); + assertEquals(900, PageViewRenderElement.settleTarget(800, EXTENT, STILL, MAX)); + assertEquals(900, PageViewRenderElement.settleTarget(1300, EXTENT, STILL, MAX)); + assertEquals(1800, PageViewRenderElement.settleTarget(1360, EXTENT, STILL, MAX)); + } + + @Test + @DisplayName("a flick advances a whole page even from a barely-moved carousel") + void aFlickAdvancesAPage() { + // 40px in: nearest is page 0, but the flick means page 1. + assertEquals(900, PageViewRenderElement.settleTarget(40, EXTENT, FLICK, MAX)); + // ...and a backward flick from page 1 goes back to page 0. Note the offset is + // BELOW the boundary: a flick is always preceded by some drag, so by the time the + // finger lifts the carousel has already moved off the page it started on. That is + // why the half-page bias is applied to where the release actually happened and not + // to the page it began from. + assertEquals(0, PageViewRenderElement.settleTarget(880, EXTENT, -FLICK, MAX)); + } + + @Test + @DisplayName("a flick never skips more than one page") + void aFlickIsOnePageAtATime() { + // Even a very fast flick: Flutter's page physics is one page per gesture. + assertEquals(900, PageViewRenderElement.settleTarget(0, EXTENT, 500f, MAX)); + assertEquals(1800, PageViewRenderElement.settleTarget(900, EXTENT, 500f, MAX)); + } + + @Test + @DisplayName("the settle stays inside the scrollable range") + void targetIsClampedToTheEnds() { + assertEquals(MAX, PageViewRenderElement.settleTarget(MAX, EXTENT, FLICK, MAX), + "a flick past the last page cannot leave the range"); + assertEquals(0, PageViewRenderElement.settleTarget(0, EXTENT, -FLICK, MAX), + "a flick before the first page cannot go negative"); + } + + @Test + @DisplayName("a slow drag-and-hold release is not treated as a flick") + void belowToleranceIsNotAFlick() { + // Just under the tolerance: this is someone stopping, not flicking, so the + // carousel returns to the page it is nearest rather than advancing. + float justUnder = 1e-4f; + assertEquals(0, PageViewRenderElement.settleTarget(100, EXTENT, justUnder, MAX)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PainterClipTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PainterClipTest.java new file mode 100644 index 00000000000..7a53054b95e --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PainterClipTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * A CustomPainter keeps the ground an ancestor Transform shifts it onto. + * + *

Flutter does not clip a CustomPainter to the box it was handed -- an + * ancestor ClipRect does that -- so a painter may draw well outside its own + * size. Codename One clips every component to its bounds, and when an ancestor + * Transform has moved the origin those bounds move with it, so the part of the + * drawing the shift brings into view is cut off instead. The 2D-transformations + * demo centres a board wider than the screen by translating it, and lost a strip + * down the right-hand side exactly as wide as the shift.

+ */ +class PainterClipTest { + + /** The demo's real numbers: a 1029-wide surface at x=48, shifted 192 left. */ + @Test + void aShiftedSurfaceClipsToWhereItIsOnScreen() { + int[] box = CustomPaintRenderElement.onScreenClip( + -144, 468, 0, 0, 48, 468, 1029, 1651); + // Translated space, so screen x 48..1077 is 192..1221 here. + assertArrayEquals(new int[] {192, 0, 1029, 1651}, box); + } + + @Test + void anUnshiftedSurfaceIsLeftAlone() { + // translate + parent-relative == absolute, so no transform is in play + // and the clip already in force is the right one. + assertNull(CustomPaintRenderElement.onScreenClip( + 48, 468, 0, 0, 48, 468, 1029, 1651)); + } + + @Test + void aVerticalShiftIsUndoneToo() { + int[] box = CustomPaintRenderElement.onScreenClip( + 48, 300, 0, 0, 48, 468, 100, 200); + assertArrayEquals(new int[] {0, 168, 100, 200}, box); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PngSizeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PngSizeTest.java new file mode 100644 index 00000000000..b2b7163e800 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/PngSizeTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * A PNG's size comes from its IHDR chunk, wherever that chunk is. + * + *

IHDR is first in a standards-conforming PNG, and this used to read its + * payload at a fixed offset. It is NOT first in the PNGs an iOS app ships: + * Xcode rewrites every bundled PNG into Apple's CgBI form, which puts a + * four-byte CgBI chunk in front. The fixed offset then read that chunk's + * payload as the width and its checksum as the height, and the picture was + * drawn to a garbage aspect -- the gallery's category icons were squashed to + * half their height on device and pixel-exact in every desktop sweep.

+ */ +class PngSizeTest { + + private static void be32(byte[] d, int off, int v) { + d[off] = (byte) (v >>> 24); + d[off + 1] = (byte) (v >>> 16); + d[off + 2] = (byte) (v >>> 8); + d[off + 3] = (byte) v; + } + + private static void type(byte[] d, int off, String t) { + for (int i = 0; i < 4; i++) { + d[off + i] = (byte) t.charAt(i); + } + } + + /** A PNG signature followed by the given chunks, each header-only. */ + private static byte[] png(boolean cgbiFirst, int w, int h) { + int size = 8 + (cgbiFirst ? 16 : 0) + 25; + byte[] d = new byte[size]; + d[0] = (byte) 0x89; + d[1] = 'P'; + d[2] = 'N'; + d[3] = 'G'; + int off = 8; + if (cgbiFirst) { + // Apple's chunk: 4 bytes of payload, then the 4-byte CRC. + be32(d, off, 4); + type(d, off + 4, "CgBI"); + be32(d, off + 8, 0x50000200); + off += 16; + } + be32(d, off, 13); + type(d, off + 4, "IHDR"); + be32(d, off + 8, w); + be32(d, off + 12, h); + return d; + } + + @Test + void anOrdinaryPngReadsItsSize() { + assertArrayEquals(new int[] {64, 64}, + ImageRenderElement.pngSize(png(false, 64, 64))); + } + + @Test + void aCgbiPngReadsItsSizeToo() { + // The layout an iOS bundle actually contains. Read at the old fixed + // offsets 16 and 20 this answers with the CgBI chunk's payload and + // whatever follows it -- never the picture's size. + assertArrayEquals(new int[] {64, 64}, + ImageRenderElement.pngSize(png(true, 64, 64))); + } + + @Test + void aFileWithNoIhdrReportsUnknown() { + byte[] d = new byte[40]; + d[0] = (byte) 0x89; + d[1] = 'P'; + d[2] = 'N'; + d[3] = 'G'; + be32(d, 8, 4); + type(d, 12, "CgBI"); + assertArrayEquals(new int[] {-1, -1}, ImageRenderElement.pngSize(d)); + } + + @Test + void aTruncatedChunkLengthDoesNotRunAway() { + byte[] d = new byte[40]; + d[0] = (byte) 0x89; + d[1] = 'P'; + d[2] = 'N'; + d[3] = 'G'; + be32(d, 8, Integer.MAX_VALUE); + type(d, 12, "junk"); + assertArrayEquals(new int[] {-1, -1}, ImageRenderElement.pngSize(d)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RichTextSpanTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RichTextSpanTest.java new file mode 100644 index 00000000000..1b206e8a392 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RichTextSpanTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.Color; +import com.codename1.flutter.Colors; +import com.codename1.flutter.FontWeight; +import com.codename1.flutter.TextStyle; +import dart.core.DartList; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * TextSpan flattening + style resolution — the pure half of RichText, testable + * without a Display. Wrapping and painting are delegated to + * {@link com.codename1.ui.RichTextComponent} and covered in the core unit tests. + */ +public class RichTextSpanTest { + + private TextSpan span(String text, TextStyle style, TextSpan... children) { + TextSpan s = new TextSpan(); + if (text != null) { + s.text(text); + } + if (style != null) { + s.style(style); + } + if (children.length > 0) { + DartList kids = new DartList(); + for (TextSpan c : children) { + kids.add(c); + } + s.children(kids); + } + return s; + } + + private TextStyle style(Double size, FontWeight weight, Color color) { + TextStyle t = new TextStyle(); + if (size != null) { + t.fontSize(size); + } + if (weight != null) { + t.fontWeight(weight); + } + if (color != null) { + t.color(color); + } + return t; + } + + // ------------------------------------------------------------------ + // Flattening + // ------------------------------------------------------------------ + + @Test + public void ownTextPrecedesChildrenDepthFirst() { + TextSpan root = span("a", null, + span("b", null, span("c", null)), + span("d", null)); + List runs = RichTextRenderElement.flatten(root); + StringBuilder sb = new StringBuilder(); + for (RichTextRenderElement.Run r : runs) { + sb.append(r.text); + } + assertEquals("abcd", sb.toString()); + } + + @Test + public void emptyOrNullTextContributesNoRunButChildrenSurvive() { + TextSpan root = span(null, null, span("only", null)); + List runs = RichTextRenderElement.flatten(root); + assertEquals(1, runs.size()); + assertEquals("only", runs.get(0).text); + } + + @Test + public void childInheritsParentStyleProperties() { + TextStyle parent = style(20.0, FontWeight.bold, Colors.red); + // child overrides only the color; size and weight must inherit + TextSpan root = span("p", parent, span("c", style(null, null, Colors.blue))); + List runs = RichTextRenderElement.flatten(root); + assertEquals(2, runs.size()); + + RichTextRenderElement.Run child = runs.get(1); + assertEquals("c", child.text); + assertEquals(20.0, child.style.getFontSize(), 0.001, "fontSize inherits"); + assertSame(FontWeight.bold, child.style.getFontWeight(), "fontWeight inherits"); + assertEquals(Colors.blue.value(), child.style.getColor().value(), "own color wins"); + } + + @Test + public void deepInheritanceChains() { + TextSpan root = span("a", style(30.0, null, null), + span("b", null, + span("c", style(null, FontWeight.bold, null)))); + List runs = RichTextRenderElement.flatten(root); + RichTextRenderElement.Run deepest = runs.get(2); + assertEquals("c", deepest.text); + assertEquals(30.0, deepest.style.getFontSize(), 0.001, + "size inherits through an intermediate span with no style"); + assertSame(FontWeight.bold, deepest.style.getFontWeight()); + } + + // ------------------------------------------------------------------ + // Style mapping to the editor model consumed by RichTextComponent + // ------------------------------------------------------------------ + + @Test + public void resolvedStyleMapsToEditorStyle() { + TextStyle flutter = style(24.0, FontWeight.bold, Colors.red); + com.codename1.ui.editor.TextStyle editor = + RichTextRenderElement.toEditorStyle(flutter); + assertTrue(editor.isBold(), "bold weight maps to editor bold"); + assertTrue(editor.getFontSizePx() > 0, "font size maps to an absolute pixel size"); + assertEquals(Colors.red.rgb(), editor.getForeColor(), "color maps to foreground color"); + } + + @Test + public void nullStyleMapsToDefault() { + assertSame(com.codename1.ui.editor.TextStyle.DEFAULT, + RichTextRenderElement.toEditorStyle(null)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round11ScrollTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round11ScrollTest.java new file mode 100644 index 00000000000..1268a592d2e --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round11ScrollTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.animation.Curves; + +import dart.async.Future; +import dart.core.Duration; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// animateTo drives the attached list through a motion and completes when it ends. +/// Headless, a run collapses to its end state, so what is checked here is the +/// plumbing -- the list is driven through the controller's run and reaches the target, +/// the future completes, a jump interrupts -- not the frames in between. +class Round11ScrollTest { + + private static boolean done(Future f) { + final boolean[] completed = {false}; + f.then(new dart.runtime.Funcs.VoidFunc1() { + @Override + public void call(Object v) { + completed[0] = true; + } + }); + return completed[0]; + } + + @Test + void animateToDrivesTheListToTheTargetAndCompletes() { + ScrollController c = new ScrollController(); + final List moves = new ArrayList(); + c.attach(new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + moves.add(offset); + } + }); + c.userScrolled(0, 1000, 100); // the list reports its extents, as a mounted one does + Future f = c.animateTo(200, Duration.of(0, 0, 0, 0, 300, 0), Curves.easeInOut); + assertTrue(done(f)); + assertEquals(200.0, moves.get(moves.size() - 1), 0.0); + assertEquals(200.0, c.offset(), 0.0); + } + + @Test + void aZeroDurationOrNoListJumps() { + ScrollController c = new ScrollController(); + c.userScrolled(0, 1000, 100); + assertTrue(done(c.animateTo(50, Duration.of(0, 0, 0, 0, 300, 0), null)), "no list: nothing to animate"); + assertEquals(50.0, c.offset(), 0.0); + final List moves = new ArrayList(); + c.attach(new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + moves.add(offset); + } + }); + moves.clear(); + assertTrue(done(c.animateTo(80, Duration.of(0, 0, 0, 0, 0, 0), null))); + assertEquals("[80.0]", moves.toString()); + } + + @Test + void aJumpDuringAnAnimationEndsItAndTheJumpWins() { + ScrollController c = new ScrollController(); + c.attach(new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + } + }); + c.userScrolled(0, 1000, 100); + c.animateTo(100, Duration.of(0, 0, 0, 0, 300, 0), null); + c.jumpTo(10); + assertEquals(10.0, c.offset(), 0.0); + c.dispose(); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round5WidgetsTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round5WidgetsTest.java new file mode 100644 index 00000000000..156c41c7016 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round5WidgetsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The parts of this round's widget fixes that run without a display: which drag a +/// detector claims, the controller's client plumbing, and the image cache key. +class Round5WidgetsTest { + + @Test + void aDetectorClaimsTheDragItHasCallbacksFor() { + assertEquals(GestureOverlayRenderElement.DRAG_HORIZONTAL, + GestureOverlayRenderElement.claimAxis(30, 5, false, true, false)); + assertEquals(GestureOverlayRenderElement.DRAG_VERTICAL, + GestureOverlayRenderElement.claimAxis(5, 30, true, true, false)); + assertEquals(GestureOverlayRenderElement.DRAG_NONE, + GestureOverlayRenderElement.claimAxis(5, 30, false, true, false), + "a horizontal detector leaves a vertical drag to the enclosing scrollable"); + assertEquals(GestureOverlayRenderElement.DRAG_PAN, + GestureOverlayRenderElement.claimAxis(5, 30, false, false, true)); + } + + @Test + void aControllerDrivesItsAttachedListAndHearsItsScrolls() { + ScrollController c = new ScrollController(); + assertFalse(c.hasClients()); + final double[] movedTo = {-1}; + ScrollController.Client list = new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + movedTo[0] = offset; + } + }; + c.attach(list); + assertTrue(c.hasClients()); + c.jumpTo(120); + assertEquals(120.0, movedTo[0], 0.0, "jumpTo moves the list, not only the model"); + final int[] notified = {0}; + c.addListener(new Funcs.VoidFunc0() { + @Override + public void call() { + notified[0]++; + } + }); + c.userScrolled(300, 1000, 500); + assertEquals(300.0, c.offset(), 0.0, "a user scroll reaches offset"); + assertEquals(1, notified[0]); + c.detach(list); + assertFalse(c.hasClients()); + } + + @Test + void theImageCacheKeyDoesNotCollideOnStringHashCodes() { + assertEquals("Aa".hashCode(), "BB".hashCode()); + assertNotEquals(ImageRenderElement.storageKey("Aa", null), ImageRenderElement.storageKey("BB", null)); + java.util.Map h1 = new java.util.LinkedHashMap(); + h1.put("a", "1"); + h1.put("b", "2"); + java.util.Map h2 = new java.util.LinkedHashMap(); + h2.put("b", "2"); + h2.put("a", "1"); + assertEquals(ImageRenderElement.storageKey("u", h1), ImageRenderElement.storageKey("u", h2), + "header order does not matter"); + h2.put("a", "other-user"); + assertNotEquals(ImageRenderElement.storageKey("u", h1), ImageRenderElement.storageKey("u", h2), + "different credentials are a different entry"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round9RuntimeTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round9RuntimeTest.java new file mode 100644 index 00000000000..a4113304d4b --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/Round9RuntimeTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.FocusNode; +import com.codename1.flutter.ValueKey; +import com.codename1.flutter.material.Checkbox; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// This round's runtime fixes that run without a display: a FocusNode drives the +/// component it is attached to, a tristate checkbox reaches null, ValueKey follows +/// Dart's numeric equality, and a controller's initial offset reaches its list. +class Round9RuntimeTest { + + /** A component stand-in that records what the node asked of it. */ + private static final class RecordingHost implements FocusNode.Host { + final List calls = new ArrayList(); + + @Override + public void focusHost() { + calls.add("focus"); + } + + @Override + public void blurHost() { + calls.add("blur"); + } + } + + @Test + void requestFocusAndUnfocusMoveTheAttachedComponent() { + FocusNode node = new FocusNode(); + RecordingHost host = new RecordingHost(); + node.attachHost(host); + node.requestFocus(); + assertTrue(node.hasFocus()); + assertEquals("[focus]", host.calls.toString()); + node.unfocus(); + assertFalse(node.hasFocus()); + assertEquals("[focus, blur]", host.calls.toString()); + node.dispose(); + } + + @Test + void aFocusRequestedBeforeTheComponentExistsReachesItOnAttach() { + FocusNode node = new FocusNode(); + node.requestFocus(); + RecordingHost host = new RecordingHost(); + node.attachHost(host); + assertEquals("[focus]", host.calls.toString()); + node.dispose(); + } + + @Test + void oneNodeHoldsThePrimaryFocusAndTheScopeUnfocusesIt() { + FocusNode a = new FocusNode(); + FocusNode b = new FocusNode(); + final int[] aNotified = {0}; + a.addListener(() -> aNotified[0]++); + a.requestFocus(); + b.requestFocus(); + assertFalse(a.hasFocus(), "the previous node loses the focus"); + assertEquals(2, aNotified[0]); + assertSame(b, FocusNode.primaryFocus()); + FocusScopeNode scope = new FocusScopeNode(); + assertTrue(scope.hasFocus()); + scope.unfocus(); + assertFalse(b.hasFocus()); + assertNull(FocusNode.primaryFocus()); + scope.requestFocus(a); + assertTrue(a.hasFocus()); + a.dispose(); + b.dispose(); + } + + @Test + void theComponentReportsFocusItGainsAndLosesByItself() { + FocusNode node = new FocusNode(); + node.attachHost(new RecordingHost()); + node.hostFocusChanged(true); + assertTrue(node.hasFocus()); + node.hostFocusChanged(false); + assertFalse(node.hasFocus()); + node.dispose(); + } + + @Test + void aTristateCheckboxCyclesThroughNull() { + Checkbox c = new Checkbox(); + c.tristate(true); + c.value(Boolean.FALSE); + assertEquals(Boolean.TRUE, c.nextValue()); + c.value(Boolean.TRUE); + assertNull(c.nextValue(), "true advances to the indeterminate state"); + c.value(null); + assertEquals(Boolean.FALSE, c.nextValue(), "indeterminate advances to false"); + c.tristate(false); + c.value(Boolean.TRUE); + assertEquals(Boolean.FALSE, c.nextValue()); + } + + @Test + void valueKeysFollowDartNumericEquality() { + ValueKey i = new ValueKey(Long.valueOf(1)); + ValueKey d = new ValueKey(Double.valueOf(1.0)); + assertEquals(i, d); + assertEquals(i.hashCode(), d.hashCode()); + assertNotEquals(new ValueKey(Double.valueOf(Double.NaN)), + new ValueKey(Double.valueOf(Double.NaN))); + assertEquals(new ValueKey(Double.valueOf(0.0)), new ValueKey(Double.valueOf(-0.0))); + assertEquals(new ValueKey(Double.valueOf(0.0)).hashCode(), + new ValueKey(Double.valueOf(-0.0)).hashCode()); + assertNotEquals(new ValueKey(Long.valueOf(1)), new ValueKey(Double.valueOf(1.5))); + } + + @Test + void aControllersInitialOffsetReachesTheListItAttachesTo() { + ScrollController c = new ScrollController(); + c.initialScrollOffset(120); + final double[] movedTo = {-1}; + c.attach(new ScrollController.Client() { + @Override + public void scrollToOffset(double offset) { + movedTo[0] = offset; + } + }); + assertEquals(120.0, movedTo[0], 0.0); + assertEquals(120.0, c.offset(), 0.0); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RoundedImageCornersTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RoundedImageCornersTest.java new file mode 100644 index 00000000000..f6d5600f755 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/RoundedImageCornersTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +/** + * The corner cut applied to an image that fills a rounded Material. + * + *

Flutter expresses card rounding as a clip, and so does this runtime — but a clip is + * only as good as the port: on Codename One's iOS Metal backend a polygon clip masks + * geometry and does NOT mask a textured quad, so a card's artwork paints over its own + * rounded corners. An image that IS the surface therefore rounds its own bitmap, once, + * when it is scaled.

+ */ +class RoundedImageCornersTest { + + /// Runs the private corner cut over a plain opaque ARGB block. + private static int[] cut(int w, int h, int radius) throws Exception { + int[] argb = new int[w * h]; + java.util.Arrays.fill(argb, 0xFF204060); + Method m = ImageRenderElement.class.getDeclaredMethod( + "roundCornersInPlace", int[].class, int.class, int.class, int.class); + m.setAccessible(true); + m.invoke(null, argb, w, h, radius); + return argb; + } + + @Test + @DisplayName("the extreme corner pixels are cleared, the centre is untouched") + void cornersAreCutAndTheBodyIsNot() throws Exception { + int w = 40; + int h = 30; + int[] p = cut(w, h, 8); + + assertEquals(0, p[0], "top left corner must be transparent"); + assertEquals(0, p[w - 1], "top right corner must be transparent"); + assertEquals(0, p[(h - 1) * w], "bottom left corner must be transparent"); + assertEquals(0, p[(h - 1) * w + w - 1], "bottom right corner must be transparent"); + + assertEquals(0xFF204060, p[(h / 2) * w + w / 2], "the centre must be untouched"); + // Mid-edge pixels are inside the shape on every side. + assertEquals(0xFF204060, p[(h / 2) * w], "the left edge midpoint is inside the shape"); + assertEquals(0xFF204060, p[w / 2], "the top edge midpoint is inside the shape"); + } + + @Test + @DisplayName("a pixel just inside the arc survives") + void theArcIsACircleNotABox() throws Exception { + int w = 40; + int h = 30; + int r = 10; + int[] p = cut(w, h, r); + // The diagonal of the corner box is outside the arc; the point next to the arc's + // own centre line is inside it. A box-shaped cut would clear both. + assertEquals(0, p[0], "the extreme diagonal is outside the arc"); + assertNotEquals(0, p[(r - 1) * w + (r - 1)], + "just inside the arc must survive - the cut is a circle, not a square"); + } + + @Test + @DisplayName("zero radius leaves every pixel alone") + void zeroRadiusIsANoop() throws Exception { + int[] p = cut(10, 10, 0); + for (int i = 0; i < p.length; i++) { + assertEquals(0xFF204060, p[i], "index " + i); + } + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/TextClampTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/TextClampTest.java new file mode 100644 index 00000000000..cce380bde75 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/TextClampTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dart.runtime.Funcs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/// `Text.maxLines` and `TextOverflow.ellipsis`, pinned on the pure arithmetic. +/// +/// Both were parsed and dropped, so a one-line preview rendered its whole body. +/// That reads as a layout choice rather than a defect, which is why it survived +/// so long: the Reply study's inbox showed every message in full. +/// +/// A fixed-width measure stands in for a Font, so this needs no display. +class TextClampTest { + + /// Every character is 10 wide, so a line's width is its length times ten. + private static final Funcs.Func1 TEN_PER_CHAR = + new Funcs.Func1() { + @Override + public Double call(String s) { + return Double.valueOf(s.length() * 10.0); + } + }; + + private static List lines(String... l) { + return new ArrayList(Arrays.asList(l)); + } + + @Test + @DisplayName("no maxLines leaves the paragraph alone") + void unclamped() { + List in = lines("one", "two", "three"); + assertEquals(in, TextRenderElement.clamp(in, null, true, TEN_PER_CHAR, 100)); + } + + @Test + @DisplayName("fewer lines than the limit are left alone") + void underTheLimit() { + List in = lines("one", "two"); + assertEquals(in, TextRenderElement.clamp(in, Long.valueOf(3), true, TEN_PER_CHAR, 100)); + } + + @Test + @DisplayName("clip keeps the first maxLines lines verbatim") + void clips() { + List out = TextRenderElement.clamp( + lines("one", "two", "three"), Long.valueOf(2), false, TEN_PER_CHAR, 100); + assertEquals(lines("one", "two"), out); + } + + @Test + @DisplayName("ellipsis marks the last kept line") + void ellipsisMarksTheCut() { + List out = TextRenderElement.clamp( + lines("one", "two", "three"), Long.valueOf(2), true, TEN_PER_CHAR, 100); + assertEquals(2, out.size()); + assertEquals("one", out.get(0)); + assertTrue(out.get(1).endsWith("…"), "the cut is marked: " + out.get(1)); + } + + @Test + @DisplayName("the ellipsis has to fit, so the line gives up characters for it") + void ellipsisFitsWithinTheWidth() { + // Width 50 = five characters. "abcdefgh" plus the marker must come back + // no wider than that, which means dropping characters for it. + List out = TextRenderElement.clamp( + lines("abcdefgh", "next"), Long.valueOf(1), true, TEN_PER_CHAR, 50); + assertEquals(1, out.size()); + assertTrue(TEN_PER_CHAR.call(out.get(0)).doubleValue() <= 50, + "the clamped line must fit: '" + out.get(0) + "'"); + assertTrue(out.get(0).endsWith("…")); + } + + @Test + @DisplayName("no trailing space is left before the ellipsis") + void trailingSpaceIsTrimmed() { + List out = TextRenderElement.clamp( + lines("hello ", "world"), Long.valueOf(1), true, TEN_PER_CHAR, + Double.POSITIVE_INFINITY); + assertEquals("hello…", out.get(0)); + } + + @Test + @DisplayName("an unbounded width still ellipsises") + void unboundedWidth() { + List out = TextRenderElement.clamp( + lines("a", "b"), Long.valueOf(1), true, TEN_PER_CHAR, + Double.POSITIVE_INFINITY); + assertEquals(lines("a…"), out); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ValueListenableBuilderTest.java b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ValueListenableBuilderTest.java new file mode 100644 index 00000000000..10c75cb4809 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/flutter/widgets/ValueListenableBuilderTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.flutter.widgets; + +import com.codename1.flutter.BuildContext; +import com.codename1.flutter.BuildOwner; +import com.codename1.flutter.FlutterUI; +import com.codename1.flutter.Widget; +import com.codename1.flutter.foundation.ValueNotifier; +import com.codename1.flutter.rendering.RenderHost; +import com.codename1.flutter.testsupport.ProbeBox; + +import dart.runtime.Funcs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A ValueListenableBuilder has to LISTEN. Reading the value once at build time produces a + * correct first frame and then freezes, which is the failure mode that hides best: the + * widget renders, so it looks wired up, and only never updates again. + * + *

In the gallery that froze the whole settings panel — the button flipped its + * ValueNotifier and nothing on screen reacted, so the menu appeared to do nothing.

+ */ +class ValueListenableBuilderTest { + + private BuildOwner owner; + + private ValueListenableBuilder builderOn(final ValueNotifier notifier, + final int[] builds) { + ValueListenableBuilder b = new ValueListenableBuilder(); + b.valueListenable(notifier); + b.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, Object value, Widget child) { + builds[0]++; + // Size the box from the value, so the rebuild is observable as geometry + // and not merely as a counter. + int v = value instanceof Number ? ((Number) value).intValue() : 0; + return new ProbeBox(v, v); + } + }); + return b; + } + + private com.codename1.flutter.Element mount(Widget root) { + owner = new BuildOwner(); + return FlutterUI.mount(root, new RenderHost(), owner); + } + + @Test + void theBuilderRunsOnceForTheFirstFrame() { + int[] builds = {0}; + ValueNotifier n = new ValueNotifier(Integer.valueOf(1)); + mount(builderOn(n, builds)); + + assertEquals(1, builds[0]); + } + + @Test + void changingTheValueRebuilds() { + int[] builds = {0}; + ValueNotifier n = new ValueNotifier(Integer.valueOf(1)); + mount(builderOn(n, builds)); + + n.value(Integer.valueOf(2)); + owner.flushSync(); + + assertEquals(2, builds[0], "a value change must re-invoke the builder"); + } + + @Test + void theBuilderSeesTheNewValue() { + final Object[] seen = new Object[1]; + ValueNotifier n = new ValueNotifier(Integer.valueOf(1)); + ValueListenableBuilder b = new ValueListenableBuilder(); + b.valueListenable(n); + b.builder(new Funcs.Func3() { + @Override + public Widget call(BuildContext context, Object value, Widget child) { + seen[0] = value; + return new ProbeBox(1, 1); + } + }); + mount(b); + + n.value(Integer.valueOf(42)); + owner.flushSync(); + + assertEquals(Integer.valueOf(42), seen[0]); + } + + @Test + void anUnmountedBuilderStopsListening() { + int[] builds = {0}; + ValueNotifier n = new ValueNotifier(Integer.valueOf(1)); + com.codename1.flutter.Element root = mount(builderOn(n, builds)); + + FlutterUI.unmountTree(root); + int atUnmount = builds[0]; + + n.value(Integer.valueOf(2)); + owner.flushSync(); + + assertEquals(atUnmount, builds[0], + "an unmounted builder must have removed its listener"); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M2Showcase.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M2Showcase.java new file mode 100644 index 00000000000..c371cb84b13 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M2Showcase.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.*; +import com.codename1.flutter.widgets.*; +import com.codename1.flutter.material.*; +import com.codename1.flutter.rendering.BoxConstraints; +import dart.core.DartList; + +/** + * Compile-contract fixture for the M2 widget set, written in the exact + * transpiled style the Dart transpiler emits: allocate + named-parameter + * setters, and canonical-positional static factories for Dart named + * constructors (positional params first, then every named param in declared + * order, missing args passed as null / boxed). + */ +public class M2Showcase extends StatelessWidget { + public M2Showcase(Key key) { this.key(key); } + + private Widget _buildItem(BuildContext context, long index) { + var $t0 = new Card(); + $t0.child(new Text("Item " + index)); + return $t0; + } + + private void _onPressed() { + } + + @Override + public Widget build(BuildContext context) { + // ListView.builder(itemCount: 20, itemBuilder: (c, i) -> ...) + var $t0 = ListView.builder(null, 20L, (c, i) -> this._buildItem(c, i), + null, null, null, null, null, null, null, null, null); + + // GridView.count(crossAxisCount: 2, children: ...) + var $t1 = GridView.count(null, null, null, null, 2L, null, null, null, null, + DartList.of(new Text("a"), new Text("b"), new Text("c"), new Text("d"))); + + // GridView.count with every named argument + var $t2 = GridView.count(null, null, null, null, 3L, 1.5, 4.0, 4.0, EdgeInsets.all(8.0), + DartList.of(new Icon(Icons.home), new Icon(Icons.settings))); + + // Image.asset('logo.png', width: 100) + var $t3 = Image.asset("logo.png", null, 100.0, null, null, + null, null, null, null, null, null, null, null, null); + var $t4 = Image.network("https://example.com/x.png", null, 64.0, 64.0, BoxFit.cover); + + // Stack + Positioned + Align + var $t5 = new Stack(); + $t5.alignment(Alignment.bottomRight); + var $t6 = new Positioned(); + $t6.left(8.0); + $t6.top(8.0); + $t6.width(40.0); + $t6.height(40.0); + $t6.child($t3); + var $t7 = new Positioned(); + $t7.right(0.0); + $t7.bottom(0.0); + $t7.child(new Icon(Icons.add)); + var $t8 = new Align(); + $t8.alignment(Alignment.topCenter); + $t8.child(new Text("aligned")); + $t5.children(DartList.of($t4, $t6, $t7, $t8)); + + // ConstrainedBox + BoxConstraints + var $t9 = new BoxConstraints(); + $t9.minWidth(100.0); + $t9.maxWidth(200.0); + $t9.minHeight(0.0); + $t9.maxHeight(80.0); + var $t10 = new ConstrainedBox(); + $t10.constraints($t9); + $t10.child($t5); + + // Card / Divider + var $t11 = new Card(); + $t11.color(Colors.white); + $t11.elevation(2.0); + $t11.margin(EdgeInsets.all(6.0)); + $t11.child($t10); + var $t12 = new Divider(); + $t12.height(24.0); + $t12.thickness(2.0); + $t12.color(Colors.grey); + + // all four buttons + var $t13 = new ElevatedButton(); + $t13.onPressed(() -> { + }); + $t13.child(new Text("Elevated")); + var $t14 = new TextButton(); + $t14.onPressed(null); + $t14.child(new Text("Disabled")); + var $t15 = new OutlinedButton(); + $t15.onPressed(this::_onPressed); + $t15.child(new Text("Outlined")); + var $t16 = new IconButton(); + $t16.onPressed(this::_onPressed); + $t16.icon(new Icon(Icons.share)); + $t16.iconSize(32.0); + $t16.color(Colors.red); + + // GestureDetector / InkWell + var $t17 = new GestureDetector(); + $t17.onTap(this::_onPressed); + $t17.onLongPress(() -> { + }); + $t17.child($t11); + var $t18 = new InkWell(); + $t18.onTap(this::_onPressed); + $t18.child(new Text("ink")); + + // ListView children mode + SingleChildScrollView + var $t19 = new ListView(); + $t19.shrinkWrap(true); + $t19.padding(EdgeInsets.symmetric(8.0, 4.0)); + $t19.children(DartList.of($t12, $t13, $t14, $t15, $t16, $t17, $t18, $t1, $t2, $t0)); + var $t20 = new SingleChildScrollView(); + $t20.padding(EdgeInsets.all(16.0)); + $t20.child($t19); + + var $t21 = new Scaffold(); + var $t22 = new AppBar(); + $t22.title(new Text("M2 Showcase")); + $t21.appBar($t22); + $t21.body($t20); + return $t21; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M3Showcase.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M3Showcase.java new file mode 100644 index 00000000000..9ae9223763d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/M3Showcase.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.*; +import com.codename1.flutter.widgets.*; +import com.codename1.flutter.material.*; +import com.codename1.flutter.navigation.MaterialPageRoute; +import com.codename1.flutter.navigation.Navigator; +import dart.core.DartList; +import dart.core.Duration; + +/** + * Compile-contract fixture for the M3 widget set (input widgets, Navigator/ + * routes, dialogs) in the exact transpiled style the Dart transpiler emits: + * allocate + named-parameter setters, canonical positional statics, Funcs + * SAM callbacks. Compilation of this class IS the contract check — it is + * never instantiated by the tests. + */ +public class M3Showcase extends StatelessWidget { + + private TextEditingController _nameCtl = new TextEditingController(); + private boolean _agreed = false; + private String _flavor = "vanilla"; + private double _volume = 0.5; + private long _tab = 0L; + + public M3Showcase(Key key) { + this.key(key); + } + + // The exact statements from the M3 API contract. + private void _contract(BuildContext context, SnackBar $someSnack) { + var $t0 = new TextField(); + $t0.controller(this._nameCtl); + var $t1 = new InputDecoration(); + $t1.labelText("Name"); + $t0.decoration($t1); + $t0.onChanged((s) -> { /* ... */ }); + var $t2 = new Checkbox(); + $t2.value(this._agreed); + $t2.onChanged((v) -> { /* ... */ }); + var $t3 = new MaterialPageRoute(); + $t3.builder((context1) -> new MyApp(null)); + Navigator.push(context, $t3); + Navigator.pop(context); + Dialogs.showDialog(context, (context2) -> { + var $t4 = new AlertDialog(); + $t4.title(new Text("Hi")); + $t4.actions(DartList.of(new TextButton())); + return $t4; + }); + ScaffoldMessenger.of(context).showSnackBar($someSnack); + } + + @Override + public Widget build(BuildContext context) { + // TextField with the full parameter surface + var $t0 = new TextField(); + $t0.controller(this._nameCtl); + var $t1 = new InputDecoration(); + $t1.labelText("Name"); + $t1.hintText("Your name"); + $t0.decoration($t1); + $t0.obscureText(false); + $t0.enabled(true); + $t0.onChanged((s) -> this._nameCtl.text()); + $t0.onSubmitted((s) -> { /* ... */ }); + this._nameCtl.addListener(() -> { /* ... */ }); + this._nameCtl.setText("preset"); + this._nameCtl.clear(); + + // Checkbox / Switch / Radio / Slider (controlled) + var $t2 = new Checkbox(); + $t2.value(this._agreed); + $t2.onChanged((v) -> { /* ... */ }); + var $t3 = new Switch(); + $t3.value(this._agreed); + $t3.onChanged((v) -> { /* ... */ }); + var $t4 = new Radio(); + $t4.value("vanilla"); + $t4.groupValue(this._flavor); + $t4.onChanged((v) -> { /* ... */ }); + var $t5 = new Slider(); + $t5.value(this._volume); + $t5.min(0.0); + $t5.max(10.0); + $t5.divisions(20L); + $t5.onChanged((v) -> { /* ... */ }); + + // ListTile + var $t6 = new ListTile(); + $t6.leading(new Icon(Icons.home)); + $t6.title(new Text("Home")); + $t6.subtitle(new Text("Front page")); + $t6.trailing(new Icon(Icons.add)); + $t6.onTap(() -> { /* ... */ }); + + // BottomNavigationBar + var $t7 = new BottomNavigationBarItem(); + $t7.icon(new Icon(Icons.home)); + $t7.label("Home"); + var $t8 = new BottomNavigationBarItem(); + $t8.icon(new Icon(Icons.settings)); + $t8.label("Settings"); + var $t9 = new BottomNavigationBar(); + $t9.items(DartList.of($t7, $t8)); + $t9.currentIndex(this._tab); + $t9.onTap((i) -> { /* ... */ }); + + // SnackBar with a Duration + var $t10 = new SnackBar(); + $t10.content(new Text("Saved")); + $t10.duration(Duration.of(0L, 0L, 0L, 2L, 0L, 0L)); + + // AlertDialog with the full parameter surface + var $t11 = new AlertDialog(); + $t11.title(new Text("Hi")); + $t11.content(new Text("Body")); + var $t12 = new TextButton(); + $t12.onPressed(() -> Navigator.pop(null)); + $t12.child(new Text("OK")); + $t11.actions(DartList.of($t12)); + + // Drawer + Scaffold with the new named parameters + var $t13 = new Drawer(); + $t13.child(new Text("menu")); + var $t14 = new Scaffold(); + var $t15 = new AppBar(); + $t15.title(new Text("M3 Showcase")); + $t14.appBar($t15); + var $t16 = new ListView(); + $t16.children(DartList.of($t0, $t2, $t3, $t4, $t5, $t6)); + $t14.body($t16); + $t14.drawer($t13); + $t14.bottomNavigationBar($t9); + return $t14; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MainLib.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MainLib.java new file mode 100644 index 00000000000..23acdff7144 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MainLib.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.FlutterUI; + +public final class MainLib { + private MainLib() {} + public static void main$() { + FlutterUI.runApp(new MyApp(null)); + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyApp.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyApp.java new file mode 100644 index 00000000000..d7075ff5971 --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyApp.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.*; +import com.codename1.flutter.widgets.*; +import com.codename1.flutter.material.*; +import dart.core.DartList; + +public class MyApp extends StatelessWidget { + public MyApp(Key key) { this.key(key); } + @Override + public Widget build(BuildContext context) { + var $t0 = new MaterialApp(); + $t0.title("Flutter Demo"); + var $t1 = new ThemeData(); + $t1.colorScheme(ColorScheme.fromSeed(Colors.deepPurple)); + $t1.useMaterial3(true); + $t0.theme($t1); + $t0.home(new MyHomePage(null, "Flutter Demo Home Page")); + return $t0; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyHomePage.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyHomePage.java new file mode 100644 index 00000000000..ca2483d3a4c --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/MyHomePage.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.*; + +public class MyHomePage extends StatefulWidget { + private final String title; + public MyHomePage(Key key, String title) { this.key(key); this.title = title; } + public String get$title() { return title; } + @Override + public State createState() { return new _MyHomePageState(); } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/_MyHomePageState.java b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/_MyHomePageState.java new file mode 100644 index 00000000000..17080fee67d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/generated/flutter/_MyHomePageState.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.generated.flutter; + +import com.codename1.flutter.*; +import com.codename1.flutter.widgets.*; +import com.codename1.flutter.material.*; +import dart.core.DartList; +import dart.runtime.DartRuntime; + +public class _MyHomePageState extends State { + private long _counter = 0L; + + private void _incrementCounter() { + this.setState(() -> { + this._counter = this._counter + 1L; + }); + } + + @Override + public Widget build(BuildContext context) { + var $t0 = new Scaffold(); + var $t1 = new AppBar(); + $t1.backgroundColor(Theme.of(context).colorScheme().inversePrimary()); + $t1.title(new Text(this.widget().get$title())); + $t0.appBar($t1); + var $t2 = new Center(); + var $t3 = new Column(); + $t3.mainAxisAlignment(MainAxisAlignment.center); + var $t4 = new Text("You have pushed the button this many times:"); + var $t5 = new Text(DartRuntime.str(this._counter)); + $t5.style(Theme.of(context).textTheme().headlineMedium()); + $t3.children(DartList.of($t4, $t5)); + $t2.child($t3); + $t0.body($t2); + var $t6 = new FloatingActionButton(); + $t6.onPressed(this::_incrementCounter); + $t6.tooltip("Increment"); + $t6.child(new Icon(Icons.add)); + $t0.floatingActionButton($t6); + return $t0; + } +} diff --git a/maven/flutter-runtime/src/test/java/com/codename1/ui/RubberBandParityTest.java b/maven/flutter-runtime/src/test/java/com/codename1/ui/RubberBandParityTest.java new file mode 100644 index 00000000000..e9a2311579d --- /dev/null +++ b/maven/flutter-runtime/src/test/java/com/codename1/ui/RubberBandParityTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.codename1.flutter.widgets.BouncingScrollPhysics; +import com.codename1.flutter.widgets.FixedScrollMetrics; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Codename One's overscroll and Flutter's are the same curve. + * + *

They do not look alike. Codename One compresses an over-edge drag in closed form, + * {@code c*x*D/(c*x + D)} for a finger distance {@code x} and viewport {@code D}; + * {@link BouncingScrollPhysics} damps each individual drag delta by + * {@code 0.52*(1 - overscroll/D)^2} and never mentions total distance. Integrating the + * second gives the first with {@code c = 0.52}, so the only real difference is the + * coefficient - Codename One ships UIScrollView's 0.55, Flutter uses 0.52 - and matching + * the overscroll is one theme constant rather than a reimplementation.

+ * + *

That is a derivation, and a derivation is exactly the kind of thing that is quietly + * wrong, so these tests check it numerically: integrate the per-delta friction and see + * whether the closed form falls out.

+ * + *

The other half of the claim - that Codename One's own {@code rubberBandCompress} + * really does compute that closed form once the coefficient is 52 - is checked by + * {@code RubberBandCoefficientTest} in core-unittests, which is where the headless + * Display those framework calls need already exists.

+ */ +class RubberBandParityTest { + + private static final double D = 800; + private static final double FLUTTER_C = 0.52; + + private static FixedScrollMetrics atOverscroll(double over) { + FixedScrollMetrics m = new FixedScrollMetrics(); + m.minScrollExtent(0); + m.maxScrollExtent(1000); + m.viewportDimension(D); + m.pixels(-over); // past the leading edge + return m; + } + + /** + * Walks a finger {@code totalFinger} pixels past the edge in small steps, applying the + * physics to each, and returns how far the content actually moved. This is what a real + * drag is: many small deltas, not one big one. + */ + private static double integrate(double totalFinger, double step) { + BouncingScrollPhysics physics = new BouncingScrollPhysics(); + double overscroll = 0; + for (double moved = 0; moved < totalFinger; moved += step) { + // Flutter applies the result as `pixels -= offset`, so a POSITIVE offset is + // the one that pushes further past the leading edge. + overscroll += physics.applyPhysicsToUserOffset(atOverscroll(overscroll), step); + } + return overscroll; + } + + private static double closedForm(double finger, double c) { + return c * finger * D / (c * finger + D); + } + + @Test + @DisplayName("integrating Flutter's per-delta friction gives Codename One's closed form") + void theTwoModelsAreTheSameCurve() { + for (double finger : new double[] {10, 50, 100, 200, 400, 800, 1600}) { + double integrated = integrate(finger, 0.05); + double closed = closedForm(finger, FLUTTER_C); + // 1% of the closed form, which at these distances is well under a pixel; the + // residue is the step size, not a disagreement between the models. + assertEquals(closed, integrated, Math.max(0.5, closed * 0.01), + "finger=" + finger + " integrated=" + integrated + " closed=" + closed); + } + } + + @Test + @DisplayName("a finer step converges on the closed form, confirming it is the integral") + void refiningTheStepConverges() { + double finger = 200; + double closed = closedForm(finger, FLUTTER_C); + double coarse = Math.abs(integrate(finger, 1.0) - closed); + double fine = Math.abs(integrate(finger, 0.05) - closed); + assertTrue(fine < coarse, + "halving the step should move towards the closed form: coarse=" + coarse + + " fine=" + fine); + } + + /** Both edges compress identically — an asymmetric rubber band reads as a broken list. */ + @Test + @DisplayName("the leading and trailing edges resist the same") + void bothEdgesAgree() { + BouncingScrollPhysics physics = new BouncingScrollPhysics(); + for (double over : new double[] {15, 60, 150, 300}) { + FixedScrollMetrics top = atOverscroll(over); + FixedScrollMetrics bottom = new FixedScrollMetrics(); + bottom.minScrollExtent(0); + bottom.maxScrollExtent(1000); + bottom.viewportDimension(D); + bottom.pixels(1000 + over); + assertEquals(physics.applyPhysicsToUserOffset(top, 10.0), + -physics.applyPhysicsToUserOffset(bottom, -10.0), 1e-9, + "overscroll=" + over); + } + } + +} diff --git a/maven/integration-tests/android-native-interface-test.sh b/maven/integration-tests/android-native-interface-test.sh index 741cfc0d013..62c468b17ed 100644 --- a/maven/integration-tests/android-native-interface-test.sh +++ b/maven/integration-tests/android-native-interface-test.sh @@ -59,6 +59,7 @@ mvn archetype:generate \ -DgroupId=com.codename1.androidnativetest1 \ -Dversion=1.0-SNAPSHOT \ -DmainName=AndroidNativeTest \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cp ../androidnativetest/common/codenameone_settings.properties androidnativetest/common/codenameone_settings.properties diff --git a/maven/integration-tests/bare-bones-kotlin-test.sh b/maven/integration-tests/bare-bones-kotlin-test.sh index 46e07e388d2..4918f5d0ffc 100644 --- a/maven/integration-tests/bare-bones-kotlin-test.sh +++ b/maven/integration-tests/bare-bones-kotlin-test.sh @@ -17,6 +17,7 @@ mvn com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-project -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DinteractiveMode=false \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DmainName=MyApp2 \ -DsourceProject=cn1app-archetype-kotlin-template-master diff --git a/maven/integration-tests/build-hint-annotations-test.sh b/maven/integration-tests/build-hint-annotations-test.sh index 0a3337b3d1a..60ab21e6952 100755 --- a/maven/integration-tests/build-hint-annotations-test.sh +++ b/maven/integration-tests/build-hint-annotations-test.sh @@ -23,6 +23,7 @@ mvn archetype:generate \ -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DmainName=MyApp \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cd myapphints diff --git a/maven/integration-tests/cn1app-archetype-test.sh b/maven/integration-tests/cn1app-archetype-test.sh index f269d41b126..c0791e84838 100644 --- a/maven/integration-tests/cn1app-archetype-test.sh +++ b/maven/integration-tests/cn1app-archetype-test.sh @@ -23,10 +23,12 @@ mvn archetype:generate \ -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DmainName=MyApp \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cd myapp1 chmod 755 build.sh + ./build.sh jar if [ -d /Applications/Xcode.app ]; then "mvn" "package" "-DskipTests" "-Dcodename1.platform=ios" "-Dcodename1.buildTarget=ios-source" -Dopen=false diff --git a/maven/integration-tests/cn1app-desktop-build-test.sh b/maven/integration-tests/cn1app-desktop-build-test.sh index 0552ef5f8ca..c07f54a4753 100644 --- a/maven/integration-tests/cn1app-desktop-build-test.sh +++ b/maven/integration-tests/cn1app-desktop-build-test.sh @@ -53,6 +53,7 @@ mvn archetype:generate \ -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DmainName=MyApp \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cd myappdesktop diff --git a/maven/integration-tests/cn1app-staged-jar-test.sh b/maven/integration-tests/cn1app-staged-jar-test.sh index a2d5b56c5e3..bae00ef4ee6 100755 --- a/maven/integration-tests/cn1app-staged-jar-test.sh +++ b/maven/integration-tests/cn1app-staged-jar-test.sh @@ -60,6 +60,7 @@ mvn -B -ntp archetype:generate \ -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DmainName=MyApp \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cd $APP diff --git a/maven/integration-tests/cssfonts.sh b/maven/integration-tests/cssfonts.sh index f839f6d073f..8ea11c57223 100644 --- a/maven/integration-tests/cssfonts.sh +++ b/maven/integration-tests/cssfonts.sh @@ -18,6 +18,7 @@ mvn com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-project -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DinteractiveMode=false \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DsourceProject=cn1-css-fonts-test-1.0 rm -rf cn1-css-fonts-test-1.0 diff --git a/maven/integration-tests/googlemaps-demo.sh b/maven/integration-tests/googlemaps-demo.sh index 41f8a98664a..67d7713a560 100644 --- a/maven/integration-tests/googlemaps-demo.sh +++ b/maven/integration-tests/googlemaps-demo.sh @@ -18,6 +18,7 @@ mvn -e com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-proj -DgroupId=com.example \ -Dversion=1.0-SNAPSHOT \ -DinteractiveMode=false \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DsourceProject=googlemaps-maven-demo-1.0.2 rm -rf googlemaps-maven-demo-1.0.2 diff --git a/maven/integration-tests/inc/env.sh b/maven/integration-tests/inc/env.sh index fe285c9fa12..ff5d6f8a4b3 100644 --- a/maven/integration-tests/inc/env.sh +++ b/maven/integration-tests/inc/env.sh @@ -8,3 +8,11 @@ if [ ! -z ${SCRIPTPATH} ]; then mkdir $SCRIPTPATH/build fi fi + +# The javaVersion to generate archetype projects with, derived from the running +# JDK so each leg tests a pairing that can actually exist. See the helper for +# why this is not simply the archetype's own default. +if [ -z "${CN1_ARCHETYPE_JAVA_VERSION:-}" ]; then + CN1_ARCHETYPE_JAVA_VERSION="$(bash "$SCRIPTPATH/../../scripts/ci/archetype-java-version.sh")" +fi +export CN1_ARCHETYPE_JAVA_VERSION diff --git a/maven/integration-tests/initializr-roundtrip-test.sh b/maven/integration-tests/initializr-roundtrip-test.sh index cc1bd31f6fb..f6c0d111b29 100644 --- a/maven/integration-tests/initializr-roundtrip-test.sh +++ b/maven/integration-tests/initializr-roundtrip-test.sh @@ -27,6 +27,7 @@ mvn archetype:generate \ -DgroupId="$PACKAGE_NAME" \ -Dversion=1.0-SNAPSHOT \ -DmainName="$APP_NAME" \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DinteractiveMode=false cat > "$SOURCE_PROJECT/generate-app-project.rpf" < + + diff --git a/maven/pom.xml b/maven/pom.xml index c74b37a72bd..024576fb199 100644 --- a/maven/pom.xml +++ b/maven/pom.xml @@ -90,6 +90,19 @@ css-compiler svg-transcoder lottie-transcoder + + dart-transpiler sqlite-jdbc backend @@ -673,6 +686,24 @@ + + + flutter-modules + + [17,) + + + dart-runtime + flutter-runtime + + + tests diff --git a/scripts/ci/archetype-java-version.sh b/scripts/ci/archetype-java-version.sh new file mode 100755 index 00000000000..08e29aecdbf --- /dev/null +++ b/scripts/ci/archetype-java-version.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Prints the archetype javaVersion that suits the JDK running this build. +# +# The archetype defaults javaVersion to 17 (Flutter support needs it, and it +# matches start.codenameone.com), while several CI legs deliberately run the +# test suites on JDK 8 so the framework is built the way it has to be. A suite +# that generates a project and then builds it has to ask for a version that the +# JDK in its hands can actually produce, or it fails with "invalid target +# release: 17" -- a failure about the generated project that reads like a +# broken archetype. +# +# Shared by tests/env.sh and maven/integration-tests/inc/env.sh so the two +# suites cannot drift apart on it. Override with CN1_ARCHETYPE_JAVA_VERSION. +set -u + +if [ -n "${CN1_ARCHETYPE_JAVA_VERSION:-}" ]; then + echo "$CN1_ARCHETYPE_JAVA_VERSION" + exit 0 +fi + +java_bin="java" +[ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ] && java_bin="$JAVA_HOME/bin/java" + +# "1.8.0_452" -> 8, "17.0.20" -> 17. The 1.x form is the only reason this is +# not a single field split. +version_line="$("$java_bin" -version 2>&1 | head -1)" +major="$(echo "$version_line" | sed -E 's/[^"]*"([0-9]+)\.?([0-9]+)?.*/\1 \2/')" +set -- $major +if [ "${1:-}" = "1" ]; then + major="${2:-8}" +else + major="${1:-}" +fi + +if [ -n "$major" ] && [ "$major" -ge 17 ] 2>/dev/null; then + echo 17 +else + echo 8 +fi diff --git a/scripts/ci/install-linux-native-deps.sh b/scripts/ci/install-linux-native-deps.sh new file mode 100755 index 00000000000..c296555538e --- /dev/null +++ b/scripts/ci/install-linux-native-deps.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# Installs the native Linux port's build dependencies. +# +# The port's CMakeLists resolves all of these through pkg_check_modules, so a +# missing one stops configuration before a single source file is compiled -- +# "Package 'libcurl', required by 'virtual:world', not found" is what an +# incomplete list looks like, and it names only the first miss, so trimming by +# trial and error costs one CI round trip per package. +# +# One definition because there is more than one consumer: the native desktop +# workflow and the Flutter benchmark build the same local-linux-device target +# and need the same stack. A caller that needs extra packages passes them as +# arguments rather than keeping a second copy of this list. +# +# GTK3 + Cairo + Pango + GdkPixbuf + GLib/GIO (render/widgets), +# FontConfig/FreeType (bundled-font registration), libcurl (HTTP), GStreamer +# (media/camera/audio), WebKitGTK (browser), libsecret (secure storage), +# libnotify (notifications), GeoClue (location), libepoxy + EGL/GLES + the Mesa +# software driver (the offscreen 3D backend). Plus CMake/Ninja, Xvfb and a font +# for Pango to lay out. +# +# The GStreamer plugin set matters as much as the library: base + good carry +# appsrc/appsink, videoconvert and mp4mux but NO codec. VideoIO's encoder is +# x264enc/x265enc (plugins-ugly) parsed by h264parse/h265parse (plugins-bad) +# with avenc_aac for audio and avdec_* for playback (libav), so without those +# three packages the port has no encoder and no H.264 decoder at all. +# +# Usage: scripts/ci/install-linux-native-deps.sh [extra packages...] +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd -P)" + +# Through the shared helper: it refreshes the index once when a .deb 404s, +# which is what a point release landing mid-job looks like -- three openssl +# packages took the desktop job out on 2026-08-25. +bash "$HERE/apt-get-install.sh" \ + cmake ninja-build pkg-config unzip xvfb fonts-dejavu-core \ + libgtk-3-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libglib2.0-dev \ + libfontconfig1-dev libfreetype-dev \ + libcurl4-openssl-dev libssl-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav \ + libwebkit2gtk-4.1-dev libsecret-1-dev libnotify-dev libgeoclue-2-dev \ + libepoxy-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri \ + "$@" diff --git a/scripts/copyright-header-exclusions.txt b/scripts/copyright-header-exclusions.txt index df66bc4af45..83688a141dc 100644 --- a/scripts/copyright-header-exclusions.txt +++ b/scripts/copyright-header-exclusions.txt @@ -245,3 +245,14 @@ vm/JavaAPI/src/java/text/Format.java | Codename One contribution whose copyright vm/JavaAPI/src/java/text/ParseException.java | Codename One contribution whose copyright line names its contributor; the notice is kept as written vm/JavaAPI/src/java/text/SimpleDateFormat.java | Codename One contribution whose copyright line names its contributor; the notice is kept as written maven/build-engine/src/main/java/com/codename1/builders/util/JSONSanitizer.java | Adapted from the OWASP JSON Sanitizer (Apache-2.0), whose documentation it keeps verbatim + +maven/dart-transpiler/src/test/resources/golden/counter/expected/FlutterRegistry.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/counter/expected/MainLib.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/counter/expected/MyApp.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/counter/expected/MyHomePage.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/counter/expected/_MyHomePageState.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoApp.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/m2demo/expected/DemoPage.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/m2demo/expected/FlutterRegistry.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/m2demo/expected/MainLib.java | Transpiler golden fixture: expected generated output, compared byte for byte +maven/dart-transpiler/src/test/resources/golden/m2demo/expected/_DemoPageState.java | Transpiler golden fixture: expected generated output, compared byte for byte diff --git a/scripts/developer-guide/validate-guide-snippets.py b/scripts/developer-guide/validate-guide-snippets.py index 0bc3dcb4c40..04bcdc9bf24 100755 --- a/scripts/developer-guide/validate-guide-snippets.py +++ b/scripts/developer-guide/validate-guide-snippets.py @@ -289,8 +289,14 @@ def java_compile_classpath(): candidates = [ Path("docs/demos/common/target/classes"), Path("maven/core/target/classes"), + # The Flutter runtime, so the guide's FlutterUI examples are compiled + # rather than taken on trust. Absent entries are dropped below, so a + # checkout that has not built it simply cannot use those types -- which + # surfaces as the snippet failing to compile, not as it being skipped. + Path("maven/flutter-runtime/target/classes"), Path.home() / ".m2/repository/com/codenameone/codenameone-core/8.0-SNAPSHOT/codenameone-core-8.0-SNAPSHOT.jar", Path.home() / ".m2/repository/com/codenameone/java-runtime/8.0-SNAPSHOT/java-runtime-8.0-SNAPSHOT.jar", + Path.home() / ".m2/repository/com/codenameone/codenameone-flutter-runtime/8.0-SNAPSHOT/codenameone-flutter-runtime-8.0-SNAPSHOT.jar", ] return os.pathsep.join(str(p) for p in candidates if p.exists()) diff --git a/scripts/fidelity-app/common/src/main/java/com/codenameone/fidelity/FidelityDeviceRunner.java b/scripts/fidelity-app/common/src/main/java/com/codenameone/fidelity/FidelityDeviceRunner.java index 7c2e759dd12..29c6bcc1d8c 100644 --- a/scripts/fidelity-app/common/src/main/java/com/codenameone/fidelity/FidelityDeviceRunner.java +++ b/scripts/fidelity-app/common/src/main/java/com/codenameone/fidelity/FidelityDeviceRunner.java @@ -373,7 +373,7 @@ public void run() { // it passed while the running app showed no glass -- a false green. Capturing the live // screen makes the suite tell the truth: glass widgets go red until the live-screen // glass actually works. (emitTiles, the old offscreen path, is kept for reference.) - cropAndEmit(captureScreen(), wrappers, names, w, h); + cropAndEmit(captureSettled(wrappers), wrappers, names, w, h); } // ---- animation-frame render ---- @@ -455,7 +455,7 @@ public void run() { } }); settle(); - cropAndEmit(captureScreen(), wrappers, names, w, h); + cropAndEmit(captureSettled(wrappers), wrappers, names, w, h); } } @@ -643,6 +643,174 @@ private void cropAndEmit(Image screen, List wrappers, List names, int w, int h) } } + /// A tile that records where it was last painted. + /// + /// The crop rectangle comes from the tile's CURRENT layout, and a screen capture + /// that stops changing is not proof the screen shows that layout: a device that + /// has not presented the new form yet is just as still. Android's disabled text + /// field was captured 50px right of where it sits twice (dark once, light once, + /// 81% against a 96-97% baseline) with the settle-until-stable capture already in + /// place. The capture now waits until every tile has been painted at the position + /// it will be cropped from. + private static final class PaintedTile extends Container { + /// The geometry the last paint drew, or null before the first paint. + private String paintedGeometry; + + PaintedTile(com.codename1.ui.layouts.Layout layout) { + super(layout); + } + + @Override + public void paint(com.codename1.ui.Graphics g) { + super.paint(g); + paintedGeometry = geometry(); + } + + /// Position AND size of the tile and of each child. Position alone was not + /// enough: a centring layout (FlowLayout CENTER) moves the child inside a + /// tile whose own x/y never change when the tile's WIDTH settles after a + /// paint, and the crop then cut the old pixels with the new geometry -- + /// TextField_normal_light 50px right of its golden on Android. + private String geometry() { + StringBuilder b = new StringBuilder(); + b.append(getAbsoluteX()).append(',').append(getAbsoluteY()).append(',') + .append(getWidth()).append(',').append(getHeight()); + int n = getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = getComponentAt(i); + b.append(';').append(c.getX()).append(',').append(c.getY()).append(',') + .append(c.getWidth()).append(',').append(c.getHeight()); + } + return b.toString(); + } + + boolean paintedWhereItIs() { + return paintedGeometry != null && paintedGeometry.equals(geometry()); + } + } + + private static final int PAINT_WAIT_ATTEMPTS = 30; + + /// Blocks until every tile has painted at its laid-out position, repainting the + /// form between checks; see PaintedTile. Gives up after ~3s and says so rather + /// than hanging the suite. + private void waitForTilesPainted(final List wrappers) { + for (int attempt = 0; attempt < PAINT_WAIT_ATTEMPTS; attempt++) { + final boolean[] ready = new boolean[] {true}; + runOnEdtSync(new Runnable() { + @Override + public void run() { + for (int i = 0; i < wrappers.size(); i++) { + Object t = wrappers.get(i); + if (t instanceof PaintedTile && !((PaintedTile) t).paintedWhereItIs()) { + ready[0] = false; + } + } + if (!ready[0]) { + Form current = Display.getInstance().getCurrent(); + if (current != null) { + current.repaint(); + } + } + } + }); + if (ready[0]) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + println("CN1SS:INFO:fidelity tiles not painted at their layout after " + PAINT_WAIT_ATTEMPTS + " checks"); + } + + /// How many extra captures to wait for the screen to stop changing, and how far apart. + private static final int STABLE_ATTEMPTS = 6; + private static final long STABLE_GAP_MS = 200; + + /// A screenshot of a screen that has stopped changing: captures are repeated until + /// two consecutive ones are identical. + /// + /// A single capture after a fixed settle raced the device. The crop rectangles are + /// read from the laid-out tiles AFTER the capture, so a frame the screen had not yet + /// caught up with -- a slow emulator still presenting the previous layout -- was cut + /// with the final geometry and its content landed shifted: Android's disabled dark + /// text field was once captured 50px right of where it sits (81% against a 96% + /// baseline), and the next run on the same code measured exactly the baseline. + /// Content that never settles (a blinking caret) falls back to the last capture, + /// which is what a single capture gave before. + private static final int SETTLED_CAPTURE_ATTEMPTS = 5; + + /// A screen capture taken while every tile still sits where it was painted. + /// + /// waitForTilesPainted alone left a window: a layout that lands after the check + /// and before the capture still pairs new geometry with old pixels. The tiles are + /// re-checked after the capture, and a capture that raced a layout is retaken. + private Image captureSettled(final List wrappers) { + Image shot = null; + for (int attempt = 0; attempt < SETTLED_CAPTURE_ATTEMPTS; attempt++) { + waitForTilesPainted(wrappers); + shot = captureStableScreen(); + final boolean[] still = new boolean[] {true}; + runOnEdtSync(new Runnable() { + @Override + public void run() { + for (int i = 0; i < wrappers.size(); i++) { + Object t = wrappers.get(i); + if (t instanceof PaintedTile && !((PaintedTile) t).paintedWhereItIs()) { + still[0] = false; + } + } + } + }); + if (still[0]) { + return shot; + } + Log.p("FidelityDeviceRunner: layout moved during capture; retaking (attempt " + + (attempt + 1) + ")"); + } + return shot; + } + + private Image captureStableScreen() { + Image last = captureScreen(); + for (int attempt = 0; attempt < STABLE_ATTEMPTS && last != null; attempt++) { + try { + Thread.sleep(STABLE_GAP_MS); + } catch (InterruptedException ignored) { + } + Image next = captureScreen(); + if (next == null) { + return last; + } + if (samePixels(last, next)) { + return next; + } + last = next; + } + println("CN1SS:INFO:fidelity screen still changing after " + STABLE_ATTEMPTS + + " extra captures; using the last"); + return last; + } + + private static boolean samePixels(Image a, Image b) { + if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) { + return false; + } + int[] pa = a.getRGB(); + int[] pb = b.getRGB(); + if (pa.length != pb.length) { + return false; + } + for (int i = 0; i < pa.length; i++) { + if (pa[i] != pb[i]) { + return false; + } + } + return true; + } + private Image captureScreen() { final Image[] out = new Image[1]; final Object lock = new Object(); @@ -718,19 +886,19 @@ private Container newTile(Component comp, String compId, int w, int h, String ap // bottom-anchored and the slack costs nothing; this tile anchors the bar // at the TOP like the native reference, so raise it by the slack to put // the pill where the native one is. - tile = new Container(new RaisedCenterLayout(glassSlackY)); + tile = new PaintedTile(new RaisedCenterLayout(glassSlackY)); } else if (centered) { - tile = new Container(new FlowLayout(Component.CENTER, Component.TOP)); + tile = new PaintedTile(new FlowLayout(Component.CENTER, Component.TOP)); } else if (fullWidth) { - tile = new Container(new BorderLayout()); + tile = new PaintedTile(new BorderLayout()); } else if (widthCenter) { // Full-width but thin. The slider track floats vertically centred; the // progress bar sits at the TOP of the tile (the native linear bar is a // top-anchored hairline), so progress is top-aligned, slider centred. int valign = "ProgressBar".equals(compId) ? Component.TOP : Component.CENTER; - tile = new Container(new FlowLayout(Component.LEFT, valign)); + tile = new PaintedTile(new FlowLayout(Component.LEFT, valign)); } else { - tile = new Container(new FlowLayout(Component.LEFT, Component.TOP)); + tile = new PaintedTile(new FlowLayout(Component.LEFT, Component.TOP)); } applyBackdrop(tile, backdropSpec, appearance); tile.getAllStyles().setPadding(0, 0, 0, 0); diff --git a/scripts/flutter-bench/.gitignore b/scripts/flutter-bench/.gitignore new file mode 100644 index 00000000000..7a60b85e148 --- /dev/null +++ b/scripts/flutter-bench/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/scripts/flutter-bench/README.md b/scripts/flutter-bench/README.md new file mode 100644 index 00000000000..59cc892c7c7 --- /dev/null +++ b/scripts/flutter-bench/README.md @@ -0,0 +1,335 @@ +# Flutter vs Codename One benchmark + +Measures **one application built two ways**: the Flutter toolchain's own +release build of the gallery, and the identical Dart source transpiled to Java +and built by Codename One. Same source, same screens, same machine, so a +difference in size, start-up or memory is a difference between the two +runtimes and nothing else. + +``` +app/prepare.sh materialises both projects from the installed Flutter SDK +app/build_apps.sh builds both, release, for one platform +run_bench.py measures, renders, and gates +flutter_baseline.py the regression gate's baselines (baseline/): check, fold, calibrate +port_status.py folds the results into the website's data +``` + +## Where the application comes from + +Neither side is vendored. `prepare.sh` copies the gallery out of the Flutter +SDK (`$FLUTTER_ROOT/dev/integration_tests/new_gallery`) into **both** project +trees, so there is no second copy for an edit to land on. The Codename One +project is generated from the shipping `cn1app-archetype` with the +`codenameone-flutter-runtime` dependency enabled -- the same two steps the +developer guide tells a user to take -- which means a change that breaks the +documented Flutter wiring breaks this benchmark too. + +What *is* committed here is the two entry points, which are ours: +`app/cn1/Bench.java` and `app/flutter/main_bench.dart`. Each prints the +start-up marker the harness times against. + +The SDK version is **pinned** in the workflow (`FLUTTER_REF`), not tracked from +`stable`. A benchmark whose reference moves on its own is not one -- the +gallery, its dependency resolution and Flutter's own code generation all change +between releases, so a number from last week and a number from today would +differ for reasons unrelated to this repository. It is not hypothetical: +tracking `stable` broke outright when 3.47.5's tree resolved `google_fonts` to +a version without `robotoCondensed` and three studies stopped compiling. +Bumping the pin re-baselines the comparison: expect every start-up and memory +ratio to move, and rebaseline them in the same change's own overlay (see +"Baselines" below). + +Lifting the gallery out of the SDK's pub workspace takes two things with it. +Its **lockfile**, because the gallery pins almost nothing (`google_fonts: any`) +and the workspace root is the only thing holding its dependencies at tested +versions. And its **asset packages** -- `flutter_gallery_assets`, +`rally_assets`, `shrine_images` -- which the workspace root declares once for +every member, so a lifted package loses them entirely. Those are derived from +the pubspec's own asset paths rather than listed, so a fourth would be picked +up on its own. + +## Running it locally + +```bash +scripts/flutter-bench/app/prepare.sh --work /tmp/fbench \ + --maven-repo "$PWD/.m2-repo" # per-checkout repo; see CLAUDE.md + +scripts/flutter-bench/app/build_apps.sh --work /tmp/fbench --platform macos + +# the cn1= and flutter= paths build_apps.sh printed +scripts/flutter-bench/app/measure.sh macos /tmp/fbench-out +``` + +`measure.sh` is what every CI leg runs, so a local run measures exactly what CI +does. It writes `result-.json` and `baseline-.json` (the +candidate `flutter_baseline.py calibrate` reads). + +`run_bench.py --list` reports which platform adapters have been **exercised +end to end**, which is deliberately not the same question as which ones exist. + +```bash +python3 scripts/flutter-bench/test_benchlib.py # the arithmetic and the gate +python3 scripts/flutter-bench/test_platforms.py # marker timing, artifacts, installs +python3 scripts/flutter-bench/test_flutter_baseline.py # the regression gate's baselines +``` + +## Why the numbers are shaped the way they are + +Each of these exists because the obvious alternative produced a flattering +result, and several were caught only after being measured the wrong way first. + +- **Interleaved rounds, load recorded.** Nine rounds, each one run of each + side, back to back. A machine that gets busier halfway through then penalizes + both sides equally. On this project two walkthrough recordings desynchronised + and looked like a timing regression; the cause was a stray simulator holding + the machine at load 8. + +- **Start-up and memory are judged round by round.** The ratio is the median + of the per-round ratios, and the figures shown are each side's median. Each + side's best run let one lucky round decide: on the macOS runner Codename One + was faster in three of five rounds (1787, 707, 468, 584, 668 ms against + Flutter's 2512, 617, 522, 356, 769), and best-of reported a Flutter win, 356 + against 468, on the strength of a single round. The median of the per-round + ratios is 1.11x. Memory is paired for the same reason: the emulator's first + round read both apps half loaded (33 MB and 61 MB against 45 MB and 89 MB in + every later round), and best-of compared those. The regression gate reads + the same paired statistic (see "Baselines"). + +- **Start-up compares the same event on both sides: the first content frame + on screen.** Codename One's `FIRSTFRAME` fires once its first form has been + drawn. On the desktop, Flutter's `RASTERDONE` says how long ago its + first content frame finished rasterizing, read from the engine's own frame + timings, and the harness subtracts that from when the line appeared. Neither + `FIRSTCONTENT` nor the line's own arrival is that event: + - `FIRSTCONTENT` is a UI-thread callback that runs two frames after the + content frame, because it waits for the element tree to stay the same size + across two more frames. + - A release engine batches timing reports for about a second, so the line + itself arrives about a second late. + + On the web there is no shared clock to read, so Flutter's figure stays a range + (`FIRSTCONTENT`..`RASTERDONE`), and the ratio is taken from the end least + favorable to Codename One. + +- **Executable code is every Mach-O in the bundle**, not the main executable. + On iOS a Flutter application's own code is not in the executable at all -- + `Runner` is a thin launcher and the Dart image lives in + `Frameworks/App.framework`. Sizing the executable alone compared our whole + runtime against their stub and reported a loss that did not exist. + +- **Assets are staged from Flutter's own bundle.** Staging the whole + `flutter_gallery_assets` package ships files Flutter tree-shakes away, which + inflates our installed size; staging only the 1x images ships fewer than + Flutter does, which flatters us by tens of megabytes. Reading the built + bundle removes the judgement call entirely. + +- **iOS reports sizes only.** `flutter build ios --simulator --release` is + refused by the Flutter tool, because Dart cannot AOT-compile for the + simulator. A simulator run would time Flutter's JIT debug engine against a + Codename One release build, so start-up and memory are reported as not + measured rather than substituted. Sizes come from release device bundles, + which need no signing. + +- **Only our own numbers are gated.** Flutter's are recorded for the ratio and + are outside our control, so a Flutter SDK upgrade that grows their build + must not turn our build red. + +- **Deadlines are enforced by the clock.** Output is read on a thread with a + timeout, so a launch that hangs before its marker is abandoned at the launch + timeout and a healthy application that goes quiet after its last marker is + let go at the settle time. A blocking `readline()` waited for one more line + in both cases, and the job sat until the workflow's own timeout. + +## Baselines: two gates, and only one of them is a baseline + +Every leg run with `--gate` answers two questions, and fails on either. + +**Is Codename One at least level with Flutter?** `benchlib.check_behind`: any +measured metric with a ratio under 1.00x, a compute geometric mean under 1.00x, +or a compute workload Codename One got wrong fails the leg. It needs no baseline +and no tolerance: both sides come from the same Dart source and are measured +interleaved on the same runner. + +**Did Codename One move against itself?** The regression gate, against +`baseline/`. It uses the ParparVM performance gate's model and its code +(`vm/selfhost/perf_baseline.py`, run by `flutter_baseline.py` with its own +layout), so the rules are the same and are written down once: + +- `baseline/policy.json`: the global tolerance and floor per metric, and the + metrics a platform records without gating, each with its reason (macOS + start-up: one hosted-runner run spread from 465 to 2588 ms). +- `baseline/base/[@].json`: the consolidated rows. Only the + nightly fold (`.github/workflows/perf-baseline.yml`) writes them; a pull + request that edits them fails `flutter_baseline.py check --base`. +- `baseline/pr/.json`: one pull request's calibrations and rebaselines. + No other branch writes that file, so two pull requests never conflict on a + baseline; two that moved the same row are reported by name. + +What is judged, per platform: + +| Metric | Value | Keyed by | Policy tolerance | +| --- | --- | --- | --- | +| `code_bytes`, `install_bytes`, `wire_bytes` | Codename One's own size, absolute | platform | 0 | +| `cold_start_ratio` | Codename One / Flutter, median of the per-round ratios | platform, or platform@CPU model | 25% | +| `idle_memory_ratio` | the same, for memory at rest | platform, or platform@CPU model | 15% | + +- **Sizes are absolute** because they are deterministic for a source tree, so + any move is a real change. A row may carry its own tolerance: Android's code + size has 0.5%, because at 0 every change that added bytes failed the leg + while Android code stays 4.5x under Flutter's; Android's install and + download sizes have 0.1%, because two runs of identical sources differ by 4 + bytes of APK packaging. Learned size tolerances round up in 0.1% steps, not + the timing gate's 5%. +- **Start-up and memory are ratios to Flutter from the same run.** The pinned + Flutter build is the unit of measure, as JDK 25 is for the ParparVM gate. An + absolute figure moved with the runner: measured over the branch's seven + complete runs, Codename One's median start-up ranged -40% to +10% around its + median on Windows and -24% to +5% on Linux, while the ratio ranged -12% to +4% + and -11% to +9%. The ratio replaces the one-sided `runner_slowdown_discount` + and its `flutter_reference` yardstick, which corrected start-up only, only on + Android, and only when the runner was slower. One cost, measured: on the + Android emulator Flutter's own resting memory swings 83-93 MB, so the memory + ratio is noisier there (+12%) than Codename One's absolute memory (within 4%). +- **Ratios fail in either direction; sizes only when they grow.** A ratio past + its tolerance below the row fails as well as one above it, as in the ParparVM + gate: an improvement nobody wrote down lets a later change give it back + unseen. A size that SHRANK past its tolerance is reported in the job and the + comment as "improved -- rebaseline to tighten", with the calibrate command, + but does not fail. The reason is the paths filter: core pull requests that + shrink the app mostly never run this benchmark, so a two-sided size row would + turn the nightly run on master red for a good change that no pull request can + rebaseline in its own overlay. Growth still fails, and so does a measured + metric with no row and a row whose metric the run did not produce. +- **Only our own numbers are gated.** Flutter's enter only as the denominator + of a ratio; a Flutter SDK that grows its build cannot fail a size row. +- **JavaScript reports its losses to Flutter without failing on them.** It is the + one platform where a loss is currently accepted (`HEAD_TO_HEAD_REPORT_ONLY` in + `benchlib.py`): the comment still shows the comparison and marks it "reported, + not gated". A workload Codename One computes wrongly or does not run still + fails there, and so does a move outside JavaScript's own baseline rows. + +When the gate fails, the job prints (and the comment shows) the command that +accepts the move. Download the leg's `baseline-.json` artifact and: + +```bash +# a new row (a platform or CPU model with no row yet) +python3 scripts/flutter-bench/flutter_baseline.py calibrate --pr 5931 baseline-android.json +# a row that moved on purpose +python3 scripts/flutter-bench/flutter_baseline.py calibrate --pr 5931 \ + --reason "why it moved" baseline-*.json +``` + +`--all` re-measures every row the files cover (for learning a row's noise from +several runs of unchanged code), `--metric code_bytes,...` limits it, and +`--per-cpu` calibrates ratio rows for the runners' CPU models. Commit the +overlay it writes with the pull request. + +The one-file-per-platform `baselines/` this replaced is retired, and `check` +fails a pull request that changes it. A branch with edits to it converts them +with `flutter_baseline.py import-legacy --pr N --legacy +--original `. + +## Compute: the VM workloads on every platform + +Beside the application metrics, each platform also compares raw compute: the +eleven workloads of `vm/benchmarks` (`CommonWorkloads.java` and its Dart port, +`vm/benchmarks/dart/common_workloads.dart`), which the VM suite already holds to +bit-identical checksums between the two languages. + +- **Same binaries.** Both benchmark apps carry the workloads and switch to them + when asked, so no second pair of apps is built. `prepare.sh` copies the sources + in from `vm/benchmarks`, their only home. +- **How each app is asked:** a marker file `/tmp/nat/BENCH_COMPUTE` on the + desktop (ParparVM has no `getenv`), the launch intent on Android (a data URI + for Codename One, the `dart_entrypoint_args` extra for Flutter), and + `?benchCompute=1` on the web. +- **Scoring.** Each app runs every workload twice to warm up and five times + timed, and prints the best. The ratio is Flutter's time over ours, and the + headline is the geometric mean. A workload whose checksums differ is shown and + left out of the mean -- two different computations have no ratio. +- **Attributed.** Each leg also runs the same `CommonWorkloads.java` on the host + JVM. When the two apps disagree, or one prints nothing for a workload, that + reference says which side failed, and the table shows "wrong result" or "did + not run" against that side instead of dropping the row. On the web, Flutter + gets `intArithmetic` and `arrayRandom` wrong (Dart compiled to JavaScript does + integer arithmetic in doubles) and produces nothing for the 64-bit workloads. + Two apps that agree are compared whatever the JVM says, since a transcendental + workload may differ from a desktop libm in the last bit on both. +- **Gated.** A geometric mean under 1.00x fails the platform's leg, like any + other metric Codename One loses, and so does any workload Codename One gets + wrong or does not run. +- **iOS is not measured**: a device build needs signed hardware, and on the + simulator Flutter runs its JIT debug engine. The macOS row runs the same two + compilers on the same Apple silicon. + +## Nothing here uses the cloud builder + +Every recipe in `app/build_apps.sh` builds on the machine it runs on. The +generated `build.sh` offers targets that send the build to the Codename One +build server -- `ios-device`, `android-device`, `mac-os-x-desktop` -- and those +are credentialed, billed, and apply a 100MB artifact limit that this +application exceeds. The `*-source` targets generate an Xcode or Gradle +project locally instead, and the script compiles those. The 100MB limit is a +property of the cloud build, not of a local one. + +## Desktop means the NATIVE ports, not JavaSE + +Codename One has two desktop stories and only one of them is a fair comparison +here. The `javase` targets bundle a JVM and ship an executable jar beside its +dependencies; the native targets compile through ParparVM to a real binary -- +AppKit on macOS (`local-mac-device`), clang-cl on Windows +(`local-windows-device`), CMake/Ninja against GTK3/Cairo on Linux +(`local-linux-device`). Flutter's desktop builds are native binaries, so the +native ports are the like-for-like artifact and the javase ones are not +comparable on size at all. + +Those three targets need a real toolchain on the runner -- Xcode, clang-cl, +and the GTK3 development packages respectively. Where one is missing the +Codename One half fails loudly rather than falling back to a JVM build that +would quietly produce an incomparable number. + +## Per-platform mechanics + +- **Desktop artifacts are directories.** Flutter's Linux bundle and Windows + Release folder, and the result folder of Codename One's native builders, are + handed over whole; the adapter launches the one executable at the top level + and refuses to guess if there is more than one. The Codename One path comes + from what the native builder logs ("Built native Linux executable: ..."), + not from a guessed directory name. +- **Code size is every native image**, as on Apple platforms: all ELF files on + Linux and all PE images on Windows. Flutter's Dart image is lib/libapp.so and + its engine a separate library, so the launcher alone would be a fraction of + its code. +- **Linux measures under Xvfb.** Both applications are GTK programs. The + xvfb-run around the Maven build ends with that build, so the measurement + starts its own. +- **Android runs on an emulator** (the same API level and image as the Android + port's instrumentation leg), and each APK is installed fresh -- uninstalled + first -- so the build that was sized is the build that is launched. Codename + One's `assembleRelease` output is unsigned, which `adb` refuses to install; + it is signed with the debug key, as Flutter's release template already is, + so both sides carry a signature. Its launcher activity is `.BenchStub`, not + `.MainActivity`. + +## Where the numbers go + +Every run posts the comment on the pull request. A nightly or dispatched run +on master also publishes the folded results to the `port-status-data` branch +as `benchmarks/flutter.json`, and the website build resolves that into +`data/port_status_flutter_benchmark.json`, which the Port Status page renders +as its own section. That section is the only place the page may name another +framework: `scripts/website/validate_port_status.mjs` still fails any Flutter +mention outside it, and fails the section if it reuses the compliance matrix's +counted attributes. + +## Known constraints + +- **No adapter has been exercised end to end yet.** The Codename One side of + the `macos` recipe has been built from a prepared tree, and `prepare.sh` + itself is verified; the native compile steps and every other platform are + not. `run_bench.py --list` reports this rather than implying otherwise. +- **Ratio rows are not keyed per runner CPU yet.** No run before the CPU was + recorded (every result now carries `cpu`), so each platform has one plain row + that judges every runner, with a tolerance learned across the mixed pool. + Android start-up needs 55% for that. Once runs show a CPU model's ratio sits + apart, `calibrate --per-cpu` gives it its own row. diff --git a/scripts/flutter-bench/app/build_apps.sh b/scripts/flutter-bench/app/build_apps.sh new file mode 100755 index 00000000000..97316f4aa30 --- /dev/null +++ b/scripts/flutter-bench/app/build_apps.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +# Builds both sides of the benchmark for one platform, from a tree that +# prepare.sh has already materialised. +# +# build_apps.sh --work /tmp/fbench --platform macos +# +# Prints the two artifact paths as `cn1=` and `flutter=` so the +# caller does not have to know each platform's output layout. +# +# RELEASE ON BOTH SIDES, always. A debug build of either is not a measurement: +# Flutter's debug engine carries the whole JIT, and a Codename One debug build +# keeps its assertions. Where a platform cannot produce a release build for +# both, this script fails rather than quietly measuring mismatched ones -- see +# the ios case, where Dart cannot compile ahead of time for the simulator at +# all. +# +# EVERY recipe here builds LOCALLY. The generated build.sh offers targets that +# send the build to the cloud builder -- `ios-device`, `android-device`, +# `mac-os-x-desktop` and friends -- which is credentialed, billed, and applies +# a 100MB artifact limit this application exceeds. The *-source targets +# generate an Xcode or Gradle project on the runner instead, and this script +# compiles those. Nothing here contacts the build server. +# +# Desktop is measured against the NATIVE ports, never against JavaSE. Codename +# One has two different desktop stories and only one of them is a fair +# comparison here: the javase targets bundle a JVM and ship an executable jar +# beside its dependencies, while the native targets compile through ParparVM to +# a real binary -- AppKit on macOS, clang-cl on Windows, CMake/Ninja with +# GTK3/Cairo on Linux. Flutter's desktop builds are native binaries, so the +# native ports are the like-for-like artifact and the javase ones are not. +# +# EXERCISED: none end to end yet. The Codename One side of `macos` has been +# built from a prepared tree; the native compile steps and the other platforms +# have not. Treat their first CI run as the thing under review. +set -Eeuo pipefail +# Report WHERE a failure happened. This script drives Maven, Flutter, pub and +# python, several of which can fail with no output at all -- the Windows leg +# twice reported an exit code and nothing else, and the first guess about +# which command produced it was wrong. -E so the trap is inherited by the +# subshells the build steps run in. +trap 'rc=$?; echo "build_apps.sh: FAILED at line $LINENO (exit $rc)" >&2' ERR + +HERE="$(cd "$(dirname "$0")" && pwd -P)" +WORK="" +PLATFORM="" +MAVEN_REPO_LOCAL="${MAVEN_REPO_LOCAL:-}" + +while [ $# -gt 0 ]; do + case "$1" in + --work) WORK="$2"; shift 2 ;; + --platform) PLATFORM="$2"; shift 2 ;; + --maven-repo) MAVEN_REPO_LOCAL="$2"; shift 2 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +[ -n "$WORK" ] && [ -n "$PLATFORM" ] || { + echo "usage: build_apps.sh --work --platform " >&2; exit 2; } + +FL="$WORK/flutter" +CN1="$WORK/cn1" +MVN_REPO_ARG="" +[ -n "$MAVEN_REPO_LOCAL" ] && MVN_REPO_ARG="-Dmaven.repo.local=$MAVEN_REPO_LOCAL" + +# The Codename One build needs a display. Its CSS compiler (CN1CSSCLI) opens a +# JFrame, so on a headless runner `cn1:css` dies with HeadlessException after +# the transpile has already succeeded -- which reads like a Flutter problem and +# is not one. -Djava.awt.headless=true is NOT the fix: the toolchain genuinely +# uses AWT windowing, and asking for headless turns the crash into a different +# crash. On Linux that means xvfb, the way every other Linux leg in this +# repository runs Maven; the macOS and Windows runners have a display already. +XVFB="" +if [ "$(uname -s)" = "Linux" ] && [ -z "${DISPLAY:-}" ]; then + command -v xvfb-run >/dev/null 2>&1 || { + echo "xvfb-run is needed to build on a headless Linux host, and is absent." >&2 + echo "Install it (apt-get install xvfb) or run with a DISPLAY set." >&2 + exit 2; } + XVFB="xvfb-run -a" +fi + +# The Flutter entry point, on every platform. prepare.sh copies our +# main_bench.dart into the project beside the gallery's own main.dart, and it is +# the one that prints the start-up markers the harness times against -- but a +# bare `flutter build` builds lib/main.dart, so the Flutter side of every +# marker-timed platform printed nothing and was never measured. Android hid it: +# `am start -W` times the launch without markers. +FLUTTER_ENTRY="lib/main_bench.dart" + +# Kept so a recipe can read what the builder REPORTS rather than guess at its +# layout; see cn1_reported. +CN1_LOG="$WORK/cn1-build.log" + +# nativeTheme=custom: the application's own theme is the only one (see the +# includeNativeBool edit in prepare.sh), so no build packages a platform theme. +# The per-platform hints are passed too because they outrank nativeTheme, and +# the archetype's settings file sets ios.themeMode and and.themeMode to modern. +cn1_build() { # cn1_build [extra maven args...] + local plat="$1" target="$2"; shift 2 + # retry.sh for Central's transient 403/429 only; a build failure is not retried. + ( cd "$CN1" && RETRY_ONLY_MATCHING=transient $XVFB bash "$HERE/../../ci/retry.sh" \ + mvn -B $MVN_REPO_ARG package -DskipTests \ + -DskipComplianceCheck=true \ + -Dcodename1.arg.nativeTheme=custom \ + -Dcodename1.arg.ios.themeMode=custom -Dcodename1.arg.and.themeMode=custom \ + -Dcodename1.platform="$plat" -Dcodename1.buildTarget="$target" "$@" ) 2>&1 | tee "$CN1_LOG" +} + + +emit() { # emit : hand an artifact path to the workflow, or fail + # A missing artifact FAILS the build. An empty path used to reach the harness, + # which reported the platform "not measured" and let the job pass -- macOS and + # JavaScript were both green that way without measuring anything. + [ -n "$2" ] && [ -e "$2" ] || { + echo "no $1 artifact to measure (got '${2}')" >&2; exit 2; } + printf '%s=%s\n' "$1" "$2" +} + +cn1_reported() { # cn1_reported : the path a builder logged after + # The native Linux and Windows builders print where they put the binary + # ("Built native Linux executable: "). The recipes used to look for a + # "*-" directory instead, a layout those builders never produce -- + # they write to a "result" directory -- so the artifact path came back empty. + local path + path="$(sed -n "s/.*$1 //p" "$CN1_LOG" | tail -1 | tr -d '\r')" + # On Windows the builder logs D:\a\...\Bench.exe. Git Bash's dirname does not + # split on a backslash, so it is normalized to D:/a/.../Bench.exe first, a + # form both this shell and the native Windows Python downstream accept. + if [ -n "$path" ] && command -v cygpath >/dev/null 2>&1; then + path="$(cygpath -m "$path")" + fi + [ -n "$path" ] || { echo "the Codename One build did not report: $1" >&2; exit 2; } + [ -e "$path" ] || { + echo "the Codename One build reported '$1 $path', which no longer exists" >&2; exit 2; } + printf '%s\n' "$path" +} + +sign_for_install() { # sign_for_install : prints an installable apk path + # assembleRelease with no signing configuration produces + # app-release-unsigned.apk, which adb refuses to install at all. Flutter's + # release template signs with the debug key, so signing ours with the same + # key is the like-for-like choice, and it keeps the sizes comparable: both + # sides then carry a signature block. + local apk="$1" signer keystore out + signer="$(ls -d "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1)" + [ -n "$signer" ] || { echo "apksigner not found under the Android SDK" >&2; exit 2; } + if "$signer" verify "$apk" >/dev/null 2>&1; then + printf '%s\n' "$apk"; return + fi + keystore="$HOME/.android/debug.keystore" + if [ ! -f "$keystore" ]; then + mkdir -p "$(dirname "$keystore")" + keytool -genkeypair -keystore "$keystore" -storepass android -keypass android \ + -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Android Debug,O=Android,C=US" >/dev/null + fi + out="${apk%.apk}-debugsigned.apk" + "$signer" sign --ks "$keystore" --ks-pass pass:android --key-pass pass:android \ + --ks-key-alias androiddebugkey --out "$out" "$apk" + printf '%s\n' "$out" +} +first() { # first existing match, or empty + find "$@" 2>/dev/null | head -1 +} + +case "$PLATFORM" in + macos) + # local-mac-device is the NATIVE AppKit app built on this machine, as + # opposed to mac-os-x-native (the same build, submitted to the cloud) and + # mac-os-x-desktop (the javase build, which bundles a JVM). + ( cd "$FL" && flutter build macos --release -t "$FLUTTER_ENTRY" ) + # UNSIGNED, on purpose and explicitly. MacOSBuildHints defaults both signing + # identities to a real certificate rather than leaving them null, so a build + # that names none still tries to sign and stops at "Signing for Bench + # requires selecting a development team" -- after translating and compiling + # the whole application. `none` is the sentinel the builder documents for + # this, and it makes it pass CODE_SIGNING_ALLOWED=NO to xcodebuild, which is + # what the ios recipe below does by hand. Both channels, because the two + # default independently. + # + # Nothing here is distributed, so there is nothing to sign FOR: the binary + # is measured and thrown away. Flutter's side is unsigned too. + # The SAME SURFACE SIZE as Flutter (benchlib's first rule): the Flutter template's + # MainMenu.xib opens an 800x600 content area, and Codename One's native macOS + # default is 1024x685 -- 1.46x the pixels, in every window-sized buffer that + # memory at rest is mostly made of. fixedWindowSize is the one size hint this + # port reads. + cn1_build ios local-mac-device \ + -Dcodename1.arg.macos.signingIdentity.appStore=none \ + -Dcodename1.arg.macos.signingIdentity.developerID=none \ + -Dcodename1.arg.macos.fixedWindowSize=800x600 + emit flutter "$FL/build/macos/Build/Products/Release/gallery.app" + # What the builder reports, not a search: the app is six levels down, and + # a four-level search found nothing and left the platform "not measured". + emit cn1 "$(cn1_reported 'Built native macOS application:')" + ;; + + ios) + # Device release on both sides, unsigned. NOT the simulator: `flutter build + # ios --simulator --release` is refused outright by the Flutter tool, + # because Dart cannot AOT-compile for the simulator, so a simulator run + # would compare Flutter's JIT debug engine against a release build. + ( cd "$FL" && flutter build ios --release -t "$FLUTTER_ENTRY" --no-codesign ) + # ios-source generates the Xcode project locally rather than sending the + # build to the cloud builder. + cn1_build ios ios-source + XCPROJ="$(first "$CN1/ios/target" -maxdepth 2 -type d -name '*-ios-source')" + [ -n "$XCPROJ" ] || { echo "no generated Xcode project" >&2; exit 2; } + # BY TARGET, and with an explicit output directory. + # + # -derivedDataPath is refused without -scheme ("The flag -scheme, + # -testProductsPath, or -xctestrun is required when specifying + # -derivedDataPath"), and a Codename One project does not ship a shared + # scheme -- scripts/ios/create-shared-scheme.py exists precisely because + # one has to be created. That helper also wires a UI test bundle, which a + # size measurement has no use for. Targets are always present, and + # CONFIGURATION_BUILD_DIR puts the .app somewhere known instead of a hashed + # DerivedData directory, which is all -derivedDataPath was for. + TARGET="$( cd "$XCPROJ" && xcodebuild -list -project *.xcodeproj -json 2>/dev/null \ + | python3 -c 'import json,sys +try: + project = json.load(sys.stdin).get("project", {}) +except ValueError: + project = {} +targets = project.get("targets") or [] +name = project.get("name") +# The application target, which is the one named after the project; the first +# target otherwise. Picking a test or extension target would measure the wrong +# binary rather than fail, so the preference is explicit. +print(name if name in targets else (targets[0] if targets else ""))' )" + [ -n "$TARGET" ] || { + echo "the generated Xcode project declares no target to build" >&2; exit 2; } + # Stripped, because that is what ships: the cloud build runs `archive`, which + # strips the product, and a plain `build` leaves the whole symbol table in -- + # about a quarter of the binary, measured against a stripped Flutter. + ( cd "$XCPROJ" && xcodebuild -project *.xcodeproj -target "$TARGET" \ + -configuration Release -sdk iphoneos \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \ + DEPLOYMENT_POSTPROCESSING=YES STRIP_INSTALLED_PRODUCT=YES STRIP_STYLE=all \ + CONFIGURATION_BUILD_DIR="$XCPROJ/build" build ) + emit flutter "$FL/build/ios/iphoneos/Runner.app" + # Directly under the build directory, because CONFIGURATION_BUILD_DIR put + # it there; the Build/Products/... nesting above it is DerivedData's layout, + # which this no longer uses. + CN1_APP="$(first "$XCPROJ/build" -maxdepth 1 -name '*.app')" + [ -n "$CN1_APP" ] || { + echo "xcodebuild reported success but produced no .app under" >&2 + echo "$XCPROJ/build" >&2; exit 2; } + emit cn1 "$CN1_APP" + ;; + + android) + # Retried ONLY for a failed SDK download. Flutter's Gradle build installs + # the NDK it needs on first use, and one run got a corrupt archive from the + # SDK server ("Archive is not a ZIP archive", "Failed to install the + # following SDK components") after earlier runs had fetched the same NDK + # cleanly. Any other failure is a real one and is not retried. + ( cd "$FL" && RETRY_ATTEMPTS=3 \ + RETRY_ONLY_MATCHING='Failed to install the following SDK components|Archive is not a ZIP archive|Error on ZipFile unknown archive' \ + bash "$HERE/../../ci/retry.sh" flutter build apk --release -t "$FLUTTER_ENTRY" ) + cn1_build android android-source + GRADLE_PROJECT="$(first "$CN1/android/target" -maxdepth 2 -type d -name '*-android-source')" + [ -n "$GRADLE_PROJECT" ] || { echo "no generated Gradle project" >&2; exit 2; } + ( cd "$GRADLE_PROJECT" && ./gradlew assembleRelease ) + emit flutter "$FL/build/app/outputs/flutter-apk/app-release.apk" + CN1_APK="$(first "$GRADLE_PROJECT" -name '*-release*.apk' -not -name '*-debugsigned.apk')" + [ -n "$CN1_APK" ] || { echo "assembleRelease produced no apk" >&2; exit 2; } + emit cn1 "$(sign_for_install "$CN1_APK")" + ;; + + linux) + # ParparVM to a native ELF against GTK3/Cairo, built here rather than + # submitted. Needs the GTK3 development packages and a C toolchain on the + # runner; without them the Codename One half fails loudly rather than + # falling back to a JVM build that would not be comparable. + ( cd "$FL" && flutter build linux --release -t "$FLUTTER_ENTRY" ) + # Flutter's Linux template opens 1280x720 (my_application.cc); Codename One's + # native Linux default is 800x600. Same surface size on both sides -- see the + # macOS case. + cn1_build linux local-linux-device \ + -Dcodename1.arg.desktop.width=1280 -Dcodename1.arg.desktop.height=720 + emit flutter "$FL/build/linux/x64/release/bundle" + # The result DIRECTORY: the executable plus the libraries it ships beside + # itself, which count toward both installed and code size. + emit cn1 "$(dirname "$(cn1_reported 'Built native Linux executable:')")" + ;; + + windows) + # ParparVM to a native binary through clang-cl, built here rather than + # submitted. + ( cd "$FL" && flutter build windows --release -t "$FLUTTER_ENTRY" ) + # Flutter's Windows template opens 1280x720 (runner/main.cpp); Codename One's + # native Windows default is 800x600. Same surface size -- see the macOS case. + cn1_build win local-windows-device -Dcodename1.arg.desktop.width=1280 -Dcodename1.arg.desktop.height=720 + emit flutter "$FL/build/windows/x64/runner/Release" + emit cn1 "$(dirname "$(cn1_reported 'Built native Windows executable:')")" + ;; + + javascript) + ( cd "$FL" && flutter build web --release -t "$FLUTTER_ENTRY" ) + # local-javascript, not the `javascript` target, which is the cloud build. + cn1_build javascript local-javascript + emit flutter "$FL/build/web" + # The deployable war, because it is the one that SURVIVES the build: the + # "browser bundle" zip is written inside target/codenameone/antProject, + # a scratch project removed when the build ends, and only the war is + # copied out to target/. A war's root is the static site a browser loads; + # WEB-INF and META-INF are the server side, which no browser downloads, so + # they are left out of what is sized. + WEB_ZIP="$(cn1_reported 'JavaScript deployable bundle written to')" + rm -rf "$WORK/cn1-web"; mkdir -p "$WORK/cn1-web" + # Unpacked whole and pruned, rather than `unzip -x`: an exclusion pattern + # that matches nothing is exit 11 for some unzip builds, which would end a + # build over a folder a particular war happens not to have. + ( cd "$WORK/cn1-web" && unzip -q "$WEB_ZIP" && rm -rf WEB-INF META-INF ) + WEB_INDEX="$(find "$WORK/cn1-web" -name index.html | awk '{ print length, $0 }' | sort -n | head -1 | cut -d' ' -f2-)" + [ -n "$WEB_INDEX" ] || { echo "the browser bundle has no index.html" >&2; exit 2; } + emit cn1 "$(dirname "$WEB_INDEX")" + ;; + + *) + echo "no build recipe for platform: $PLATFORM" >&2 + exit 2 + ;; +esac diff --git a/scripts/flutter-bench/app/cn1/Bench.java b/scripts/flutter-bench/app/cn1/Bench.java new file mode 100644 index 00000000000..65fdaccad04 --- /dev/null +++ b/scripts/flutter-bench/app/cn1/Bench.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.example.bench; + +import com.codename1.system.Lifecycle; + +/** + * Boots the transpiled Flutter Gallery. The transcode-flutter goal emits + * com.codename1.generated.flutter.FlutterRegistry whose invokeMain() runs the + * Dart app's main() through the Codename One Flutter runtime. + * + *

This is the MEASURED build, so it carries nothing the Flutter side's + * lib/main_bench.dart does not: the gallery, the first-frame marker and the + * phase timings. It used to also open an MCP driving channel with a dozen + * harness tools, which put the MCP server, its JSON and socket code and every + * tool into the binary being sized against Flutter -- scaffolding counted as + * Codename One's code. Driving tools belong in a build that is not measured. + */ +public class Bench extends Lifecycle { + + /** See runApp: attribution costs real time, so it is not on by default. */ + private static final boolean TRACE_STARTUP = false; + + /// Skips the Codename One theme that Lifecycle.init would load. + /// + /// FlutterUI.runApp installs its own Material base theme, and + /// UIManager.setThemeProps RESETS the property table before merging -- so the + /// theme loaded here is discarded a moment later, along with the native theme + /// its @includeNativeBool pulls in and every style built from both. A Flutter + /// application draws its own visuals and never consults the Codename One + /// theme, so loading one is pure start-up cost. + /// + /// Everything else Lifecycle.init does is kept. + @Override + public void init(Object context) { + // The device this build stands in for. defaultTargetPlatform decides the back + // chevron against the arrow, page transitions, switches and scrollbars, and the + // JavaSE simulator reports "SE", which falls through to android -- while the skin + // it wears and the reference frames it is measured against are both an iPhone. + // Set before anything reads it: FoundationLib resolves it once, on first use. + // One-arg getProperty and a null check, NOT the two-arg overload: ParparVM's + // java.lang.System declares only the former, and the translator resolves a + // missing method to an undeclared C function rather than to an error, so the + // two-arg form builds here and fails to compile in the generated project. + String platform = System.getProperty( + com.codename1.flutter.foundation.FoundationLib.PLATFORM_PROPERTY); + com.codename1.ui.Display.getInstance().setProperty( + com.codename1.flutter.foundation.FoundationLib.PLATFORM_PROPERTY, + platform == null ? "ios" : platform); + com.codename1.ui.CN.updateNetworkThreadCount(2); + com.codename1.ui.Toolbar.setGlobalToolbar(true); + com.codename1.io.Log.bindCrashProtection(true); + } + + /// Compute mode: the VM workloads (vm/benchmarks' CommonWorkloads, copied in + /// by prepare.sh) instead of the gallery, in the same binary. The Flutter app + /// runs the Dart port of the same workloads with the identical repetition + /// scheme, and both print one `BENCH:COMPUTE` line per workload. + private static final int COMPUTE_WARMUP = 2; + private static final int COMPUTE_MEASURED = 5; + + /// Asked for by a marker file on the desktop, the launch intent's data URI on + /// Android (which the port publishes as AppArg), or the page's query string on + /// the web -- the channels the Flutter app reads the same request from. + /// + /// AppArg is read on Android ONLY. On iOS and macOS, asking for it from + /// start() parks the event thread until the application becomes active (see + /// IOSImplementation.getAppArg), which is 70-90ms into a macOS cold start -- + /// and every timed launch asked, so the check that selects compute mode was + /// itself the largest single item in the start-up figure, a cost the Flutter + /// build does not pay and no application of ours pays unless it asks. + private static boolean computeRequested() { + if (markerFile("BENCH_COMPUTE")) { + return true; + } + com.codename1.ui.Display d = com.codename1.ui.Display.getInstance(); + if ("and".equals(d.getPlatformName())) { + String arg = d.getProperty("AppArg", null); + return arg != null && arg.indexOf("benchcompute") >= 0; + } + String search = d.getProperty("browser.window.location.search", null); + return search != null && search.indexOf("benchCompute") >= 0; + } + + private interface Workload { + long run(); + } + + private static void computeOne(String name, Workload w) { + long checksum = 0; + for (int i = 0; i < COMPUTE_WARMUP; i++) { + checksum = w.run(); + } + long best = -1; + for (int i = 0; i < COMPUTE_MEASURED; i++) { + long start = System.currentTimeMillis(); + checksum = w.run(); + long ms = System.currentTimeMillis() - start; + if (best < 0 || ms < best) { + best = ms; + } + } + System.out.println("BENCH:COMPUTE name=" + name + " checksum=" + checksum + " ms=" + best); + System.out.flush(); + } + + /// Off the event dispatch thread, as the Flutter side runs off the platform's + /// main thread: a long computation on the EDT would stall the port's own loop. + private static void runCompute() { + new Thread(new Runnable() { + @Override + public void run() { + computeOne("intArithmetic", new Workload() { + public long run() { return com.bench.CommonWorkloads.intArithmetic(); } + }); + computeOne("longArithmetic", new Workload() { + public long run() { return com.bench.CommonWorkloads.longArithmetic(); } + }); + computeOne("mathTranscendental", new Workload() { + public long run() { return com.bench.CommonWorkloads.mathTranscendental(); } + }); + computeOne("arraySequential", new Workload() { + public long run() { return com.bench.CommonWorkloads.arraySequential(); } + }); + computeOne("arrayRandom", new Workload() { + public long run() { return com.bench.CommonWorkloads.arrayRandom(); } + }); + computeOne("objectAllocation", new Workload() { + public long run() { return com.bench.CommonWorkloads.objectAllocation(); } + }); + computeOne("valueEscape", new Workload() { + public long run() { return com.bench.CommonWorkloads.valueEscape(); } + }); + computeOne("hashMapChurn", new Workload() { + public long run() { return com.bench.CommonWorkloads.hashMapChurn(); } + }); + computeOne("stringBuilding", new Workload() { + public long run() { return com.bench.CommonWorkloads.stringBuilding(); } + }); + computeOne("recursion", new Workload() { + public long run() { return com.bench.CommonWorkloads.recursion(); } + }); + computeOne("quicksortBench", new Workload() { + public long run() { return com.bench.CommonWorkloads.quicksortBench(); } + }); + System.out.println("BENCH:COMPUTE-DONE"); + System.out.flush(); + } + }).start(); + } + + @Override + public void runApp() { + if (computeRequested()) { + runCompute(); + return; + } + // The runtime's startup attribution is OFF here, because a benchmark has + // to measure the build a user would ship. The counters it turns on are + // per-build and per-layout -- a nanoTime pair, a class-name string and a + // map lookup on every box in the tree -- so leaving them on measured the + // instrumentation as well as the app. + // + // The simulator switches them on with + // -Dcn1.flutter.startupTrace=true (JavaSE resolves an unknown Codename + // One property from the system properties); a native build has no such + // channel, so flip TRACE_STARTUP and rebuild when attributing one. + if (TRACE_STARTUP) { + try { + com.codename1.ui.Display.getInstance() + .setProperty("cn1.flutter.startupTrace", "true"); + } catch (Throwable ignored) { + } + } + // Error capture stays on the startup path: it is cheap, and a failure + // during the first frame is exactly the kind the sweep must not miss. + // Baseline mode: the same binary, booted to an EMPTY form instead of the + // gallery. Subtracting this from the full run separates what every + // Codename One app pays -- dyld, the VM's constant pool, NSApplication, + // the first NSWindow, the first present -- from what THIS app's UI + // costs, and the counterpart minimal Flutter app allows the same split + // on the other side. Without it a start-up figure cannot say whether a + // gap is framework overhead or application work. + // + // The switch is a file rather than an environment variable because + // ParparVM has no System.getenv: neither vm/JavaAPI nor Ports/CLDC11 + // declares it, so a build using it would fail to link on the device. + if (baselineMode()) { + markFirstFrame(); + new com.codename1.ui.Form().show(); + return; + } + long t0 = System.currentTimeMillis(); + com.codename1.flutter.FlutterErrorReport.install(); + long t1 = System.currentTimeMillis(); + markFirstFrame(); + com.codename1.generated.flutter.FlutterRegistry.invokeMain(); + long t2 = System.currentTimeMillis(); + // Phase timings, so a slow start can be attributed instead of guessed + // at. The harness's own clock still owns the headline number; these say + // which part of the app owns the milliseconds. + System.out.println("BENCH:PHASE errorReport=" + (t1 - t0) + "ms invokeMain=" + + (t2 - t1) + "ms"); + System.out.flush(); + } + + /** + * Prints one line the moment the app has painted its first frame. + * + *

The benchmark's cold-start figure is the wall time from launching the + * process to this line appearing, measured by the harness on the outside so + * that neither runtime has to be trusted for its own clock. The Flutter + * build prints the identical marker from its own wrapper, so the two + * numbers measure the same thing. + * + *

Deliberately hooked to a PAINT rather than to the end of runApp: the + * question is when the user sees the app, not when its widget tree exists. + */ + private void markFirstFrame() { + final long start = System.currentTimeMillis(); + com.codename1.ui.Display.getInstance().callSerially(new Runnable() { + private boolean done; + private int passes; + + @Override + public void run() { + if (done) { + return; + } + passes++; + com.codename1.ui.Form f = com.codename1.ui.Display.getInstance().getCurrent(); + if (f == null || f.getComponentCount() == 0) { + // Not painted yet -- look again on the next pass. + com.codename1.ui.Display.getInstance().callSerially(this); + return; + } + done = true; + // The pass count separates WORK from WAITING: several passes + // means the event thread went round the loop with nothing to + // show, which is a scheduling problem, not a slow app. + // A frame that threw is not a frame. Without this the marker + // certified a startup in which the widget tree had failed + // half-way through layout -- and a broken screen paints + // FASTER than a complete one, so the crash read as a good + // benchmark result rather than as a bug. + String failures = com.codename1.flutter.FlutterErrorReport.summary(); + if (failures != null && !failures.startsWith("0 ")) { + System.out.println("BENCH:FIRSTFRAME-INVALID after=" + + (System.currentTimeMillis() - start) + "ms edtPasses=" + passes + + " -- the first frame reported errors:"); + System.out.println(failures); + System.out.flush(); + return; + } + System.out.println("BENCH:FIRSTFRAME after=" + + (System.currentTimeMillis() - start) + "ms edtPasses=" + passes); + System.out.flush(); + } + }); + } + + /// True when the baseline-mode marker file is present. One stat() on the + /// start-up path, and only in this benchmark harness -- never in the runtime. + private static boolean baselineMode() { + return markerFile("CN1_BASELINE"); + } + + /// True when the named switch file exists. Used instead of an environment + /// variable because ParparVM has no System.getenv -- neither vm/JavaAPI nor + /// Ports/CLDC11 declares it, so a build using it would fail to link. + private static boolean markerFile(String name) { + try { + com.codename1.io.FileSystemStorage fs = + com.codename1.io.FileSystemStorage.getInstance(); + return fs.exists("file:///tmp/nat/" + name) + || fs.exists("/tmp/nat/" + name); + } catch (Throwable t) { + return false; + } + } +} diff --git a/scripts/flutter-bench/app/flutter/bench_compute.dart b/scripts/flutter-bench/app/flutter/bench_compute.dart new file mode 100644 index 00000000000..033b4e21101 --- /dev/null +++ b/scripts/flutter-bench/app/flutter/bench_compute.dart @@ -0,0 +1,62 @@ +// Compute mode: the VM workloads from vm/benchmarks, instead of the gallery. +// +// common_workloads.dart is vm/benchmarks/dart/common_workloads.dart, copied in +// by prepare.sh: a port of com.bench.CommonWorkloads that reproduces Java's +// 32-bit wrapping, unsigned shift and String.hashCode, so both apps do the same +// work and print the same checksum. The Codename One app runs the Java source +// with the identical repetition scheme (Bench.java). +import 'common_workloads.dart' if (dart.library.js_interop) 'common_workloads_web.dart'; + +typedef _Work = int Function(); + +/// Warm-up passes before timing, then the best of the timed passes. +const int _warmup = 2; +const int _measured = 5; + +final Map _workloads = { + 'intArithmetic': intArithmetic, + 'longArithmetic': longArithmetic, + 'mathTranscendental': mathTranscendental, + 'arraySequential': arraySequential, + 'arrayRandom': arrayRandom, + 'objectAllocation': objectAllocation, + 'valueEscape': valueEscape, + 'hashMapChurn': hashMapChurn, + 'stringBuilding': stringBuilding, + 'recursion': recursion, + 'quicksortBench': quicksortBench, +}; + +/// Runs every workload and prints one `BENCH:COMPUTE` line each, then +/// `BENCH:COMPUTE-DONE`. Yields between workloads so the platform's own event +/// loop keeps running. +Future runCompute() async { + for (final MapEntry w in _workloads.entries) { + int checksum = 0; + try { + checksum = w.value(); + } on UnsupportedError { + // A workload this platform cannot run (64-bit integers on the web) is left + // out, and the report shows it as not measured. + continue; + } + for (int i = 1; i < _warmup; i++) { + checksum = w.value(); + } + int best = -1; + for (int i = 0; i < _measured; i++) { + final Stopwatch sw = Stopwatch()..start(); + checksum = w.value(); + sw.stop(); + final int ms = sw.elapsedMilliseconds; + if (best < 0 || ms < best) { + best = ms; + } + } + // ignore: avoid_print + print('BENCH:COMPUTE name=${w.key} checksum=$checksum ms=$best'); + await Future.delayed(Duration.zero); + } + // ignore: avoid_print + print('BENCH:COMPUTE-DONE'); +} diff --git a/scripts/flutter-bench/app/flutter/compute_flag_io.dart b/scripts/flutter-bench/app/flutter/compute_flag_io.dart new file mode 100644 index 00000000000..76207c40396 --- /dev/null +++ b/scripts/flutter-bench/app/flutter/compute_flag_io.dart @@ -0,0 +1,15 @@ +// Native builds: the harness asks for compute mode with an argument (Android's +// dart_entrypoint_args intent extra) or a marker file (desktop), the same file +// the Codename One app checks. +import 'dart:io'; + +bool computeRequested(List args) { + if (args.contains('compute')) { + return true; + } + try { + return File('/tmp/nat/BENCH_COMPUTE').existsSync(); + } catch (_) { + return false; + } +} diff --git a/scripts/flutter-bench/app/flutter/compute_flag_web.dart b/scripts/flutter-bench/app/flutter/compute_flag_web.dart new file mode 100644 index 00000000000..2c41cdb1e70 --- /dev/null +++ b/scripts/flutter-bench/app/flutter/compute_flag_web.dart @@ -0,0 +1,4 @@ +// Web builds: the harness opens the page with ?benchCompute=1, the same query +// the Codename One app reads through browser.window.location.search. +bool computeRequested(List args) => + Uri.base.queryParameters.containsKey('benchCompute'); diff --git a/scripts/flutter-bench/app/flutter/main_bench.dart b/scripts/flutter-bench/app/flutter/main_bench.dart new file mode 100644 index 00000000000..d6a0835e6de --- /dev/null +++ b/scripts/flutter-bench/app/flutter/main_bench.dart @@ -0,0 +1,217 @@ +// Benchmark entry point: the gallery, plus one printed marker. +// +// Kept OUT of lib/main.dart so the app the goldens render and the app the +// Codename One side transpiles stay byte-identical to the Flutter SDK's own +// copy. This file only wraps it. +// +// The marker is printed on the first painted frame. The benchmark's cold-start +// figure is the wall time from launching the process to this line appearing, +// measured from outside so neither runtime is trusted for its own clock; the +// Codename One build prints the identical marker from its own wrapper. +import 'dart:async'; +import 'dart:ui' show FramePhase; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:gallery/main.dart'; + +import 'bench_compute.dart'; +import 'compute_flag_io.dart' if (dart.library.js_interop) 'compute_flag_web.dart'; + +int _frames = 0; +// Set when FIRSTCONTENT is announced; the next FrameTiming to arrive belongs to +// a frame at or after the content frame, and carries its raster timings. +bool _contentAnnounced = false; +// When the callback that first saw the final tree ran, on the WALL clock, so +// the content frame's FrameTiming can be picked out. +// +// The content frame is the FIRST frame that built the final tree, not the one +// that announces it: FIRSTCONTENT waits for two more frames with the same +// element count to prove the tree has stopped growing, and those two repaint +// nothing -- their raster measured 0-1ms against 10-11ms for the frame that drew +// the gallery. Timing to the announcing frame charged Flutter for two frames of +// waiting that a user does not see. +// +// Found by time, not by number: PlatformDispatcher.frameData.frameNumber is not +// kept current through the start-up frames (the warm-up, the 59-element and the +// gallery frame all read 1 on macOS). And by WALL time, the one clock FrameTiming +// shares with Dart everywhere (FramePhase.rasterFinishWallTime against +// DateTime.now). Its monotonic phases matched dart:developer's Timeline on macOS +// but not on Windows, where no frame could be matched and Flutter's start-up +// went unmeasured. +int _contentSeenWallUs = -1; +// rasterFinishWallTime and raster duration of every frame reported, in +// microseconds. Kept because the content frame's timing can arrive in a batch +// BEFORE the frame that announces content has run. +final List> _frameTimes = >[]; +bool _rasterAnnounced = false; +int _buildUs = 0; +int _rasterUs = 0; + +late final Stopwatch _benchClock; + +void main(List args) { + // Compute mode runs the VM workloads instead of the gallery; see + // bench_compute.dart. The same binary: both apps carry the workloads. + if (computeRequested(args)) { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const SizedBox.shrink()); + runCompute(); + return; + } + final Stopwatch clock = Stopwatch()..start(); + _benchClock = clock; + GoogleFonts.config.allowRuntimeFetching = false; + WidgetsFlutterBinding.ensureInitialized(); + // Flutter's FIRST frame is a warm-up frame: runApp schedules it before the + // root widget is attached, so it paints an essentially empty tree (26 + // elements, 3 render objects) in ~10ms. Reporting that as "time to first + // frame" and comparing it against a runtime that paints its built UI is not + // a comparison at all. + // + // So both are reported: FIRSTFRAME for the warm-up, and FIRSTCONTENT for the + // first frame after the element tree stops growing — the point at which the + // user is actually looking at the app. + bool announcedFirst = false; + int previous = -1; + int stable = 0; + void watch(Duration _) { + final int n = _countElements(); + if (!announcedFirst) { + announcedFirst = true; + // ignore: avoid_print + print('BENCH:FIRSTFRAME after=${clock.elapsedMilliseconds}ms ' + 'elements=$n renderObjects=${_countRenderObjects()}'); + } + if (n == previous) { + stable++; + } else { + stable = 0; + previous = n; + _contentSeenWallUs = DateTime.now().microsecondsSinceEpoch; + } + if (stable >= 2) { + // ignore: avoid_print + print('BENCH:FIRSTCONTENT after=${clock.elapsedMilliseconds}ms ' + 'elements=$n renderObjects=${_countRenderObjects()}'); + _contentAnnounced = true; + _announcePresented(); + // Stage split, so the comparison is not just a single number. build is + // widget build + layout on the UI thread; raster is paint and GPU + // submission on the raster thread. Codename One reports the same split as + // mount/show and paint. + // addTimingsCallback delivers a frame's timing AFTER the frame, and + // asynchronously, so the frames that built the tree have not been + // reported yet at this point. Wait before reporting the split. + Timer(const Duration(milliseconds: 600), () { + // ignore: avoid_print + print('BENCH:STAGES frames=$_frames buildMs=${_buildUs ~/ 1000} ' + 'rasterMs=${_rasterUs ~/ 1000} ' + 'buildUs=$_buildUs rasterUs=$_rasterUs'); + }); + return; + } + SchedulerBinding.instance.addPostFrameCallback(watch); + SchedulerBinding.instance.scheduleFrame(); + } + + SchedulerBinding.instance.addTimingsCallback((List timings) { + for (final FrameTiming t in timings) { + _frames++; + _buildUs += t.buildDuration.inMicroseconds; + _rasterUs += t.rasterDuration.inMicroseconds; + // Codename One's own marker fires when the GPU has finished presenting, + // so FIRSTCONTENT -- a UI-thread callback that runs BEFORE raster -- is + // not the same event and comparing the two charges one runtime for + // rasterising its first screen and not the other. + // + // This reports the moment the content frame's timings are in hand, which + // is after its raster completed. addTimingsCallback is delivered + // asynchronously, so it is an UPPER bound: the true present-complete time + // lies between FIRSTCONTENT and this. + // + // Off the web it is EXACT instead. The timings carry the wall-clock moment + // the content frame's raster finished, so + // the line says how long ago that was and the harness subtracts it: the + // result is the content frame on screen -- the event Codename One's marker + // reports -- without the ~1s the engine batches timing reports for in a + // release build, which is what made the old upper bound useless. + if (kIsWeb) { + if (_contentAnnounced && !_rasterAnnounced) { + _rasterAnnounced = true; + // ignore: avoid_print + print('BENCH:RASTERDONE after=${_benchClock.elapsedMilliseconds}ms ' + 'rasterMs=${t.rasterDuration.inMicroseconds ~/ 1000}'); + } + } else { + _frameTimes.add([ + t.timestampInMicroseconds(FramePhase.rasterFinishWallTime), + t.rasterDuration.inMicroseconds, + ]); + } + } + _announcePresented(); + }); + SchedulerBinding.instance.addPostFrameCallback(watch); + runApp(const GalleryApp()); +} + +/// Prints RASTERDONE for the content frame once both halves are known: that the +/// tree has settled, and the timing of the frame that first drew it. +void _announcePresented() { + if (kIsWeb || _rasterAnnounced || !_contentAnnounced) { + return; + } + // The content frame's raster finishes after the callback that saw its tree, + // which runs at the end of its build; the previous frame's has already + // finished. So it is the earliest raster finish at or after that callback. + // Timings arrive in order, so the first batch holding such a frame holds it. + List? raster; + for (final List f in _frameTimes) { + if (f[0] >= _contentSeenWallUs && (raster == null || f[0] < raster[0])) { + raster = f; + } + } + if (raster == null) { + return; + } + _rasterAnnounced = true; + final int agoUs = DateTime.now().microsecondsSinceEpoch - raster[0]; + // ignore: avoid_print + print('BENCH:RASTERDONE after=${_benchClock.elapsedMilliseconds}ms ' + 'rasterMs=${raster[1] ~/ 1000} presentedAgoUs=$agoUs'); +} + +/// How many elements exist once the first frame is on screen. +/// +/// The point of comparison is not "how fast is each runtime" but "is each one +/// doing the same work". A first frame that built a tenth of the tree is not a +/// faster first frame, and comparing against it would be measuring nothing. +int _countElements() { + int n = 0; + void visit(Element e) { + n++; + e.visitChildren(visit); + } + + final Element? root = WidgetsBinding.instance.rootElement; + if (root != null) { + visit(root); + } + return n; +} + +int _countRenderObjects() { + int n = 0; + void visit(RenderObject r) { + n++; + r.visitChildren(visit); + } + + final RenderObject? root = WidgetsBinding.instance.rootElement?.renderObject; + if (root != null) { + visit(root); + } + return n; +} diff --git a/scripts/flutter-bench/app/measure.sh b/scripts/flutter-bench/app/measure.sh new file mode 100755 index 00000000000..4b4c3f388cc --- /dev/null +++ b/scripts/flutter-bench/app/measure.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Measures one platform from the two artifacts build_apps.sh reported. +# +# measure.sh +# +# Writes /result-.json and, when the platform was measured, +# /baseline-.json -- a baseline recorded from this run, +# which is what gets committed to arm or re-arm the regression gate. +# +# One script for every CI leg, because the legs cannot share a workflow step: +# Android has to run inside the emulator action, whose `script:` runs each line +# in a separate shell, and Linux has to run under Xvfb. Keeping the invocation +# here means those wrappers only ever differ in what they wrap. +set -Eeuo pipefail +trap 'rc=$?; echo "measure.sh: FAILED at line $LINENO (exit $rc)" >&2' ERR + +[ $# -eq 4 ] || { echo "usage: measure.sh " >&2; exit 2; } +PLATFORM="$1"; CN1_APP="$2"; FLUTTER_APP="$3"; OUT="$4" +HERE="$(cd "$(dirname "$0")" && pwd -P)" +mkdir -p "$OUT" + +PYTHON=python3 +command -v python3 >/dev/null 2>&1 || PYTHON=python + +# The launcher activities differ between the sides. A Codename One Android +# app's launcher is Stub (AndroidGradleBuilder writes it into the +# manifest), not the MainActivity Flutter's template uses; launching +# .MainActivity there fails before anything is timed. +exec "$PYTHON" "$HERE/../run_bench.py" \ + --platform "$PLATFORM" \ + --cn1-app "$CN1_APP" \ + --flutter-app "$FLUTTER_APP" \ + --cn1-bundle com.example.bench \ + --flutter-bundle com.codenameone.bench.gallery \ + --cn1-activity .BenchStub \ + --flutter-activity .MainActivity \ + --json "$OUT/result-$PLATFORM.json" \ + --baseline-out "$OUT/baseline-$PLATFORM.json" \ + --gate diff --git a/scripts/flutter-bench/app/prepare.sh b/scripts/flutter-bench/app/prepare.sh new file mode 100755 index 00000000000..83d3c63a111 --- /dev/null +++ b/scripts/flutter-bench/app/prepare.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# Materialises BOTH sides of the Flutter benchmark into a work directory. +# +# scripts/flutter-bench/app/prepare.sh --work /tmp/fbench +# +# The point of this script is that the two sides are built from ONE source +# tree. The gallery is not vendored into this repository; it is taken from the +# Flutter SDK that is already installed, at +# $FLUTTER_ROOT/dev/integration_tests/new_gallery. That has three consequences +# worth stating, because each is a property the benchmark depends on: +# +# * The comparison cannot drift. Both sides copy the same 159 files, so no +# edit can land on one side and not the other. An earlier round of this +# work compared two DIFFERENT galleries -- the SDK one against the +# standalone GitHub one, 237 files with materially different widget code -- +# and every visual score quoted from it was meaningless. +# * The reference is pinned to the SDK version, which is recorded in the +# result so a number can be traced to what produced it. +# * Nothing third-party enters this repository. What IS committed here is the +# two entry points, which are ours: cn1/Bench.java and +# flutter/main_bench.dart. +# +# Outputs: +# /flutter/ a Flutter project, gallery + platform scaffolding +# /cn1/ a Codename One project generated from the archetype +# /prepared.json +set -Eeuo pipefail +# Report WHERE a failure happened. This script drives Maven, Flutter, pub and +# python, several of which can fail with no output at all -- the Windows leg +# twice reported an exit code and nothing else, and the first guess about +# which command produced it was wrong. -E so the trap is inherited by the +# subshells the build steps run in. +trap 'rc=$?; echo "prepare.sh: FAILED at line $LINENO (exit $rc)" >&2' ERR + +WORK="" +FLUTTER_ROOT_ARG="" +CN1_VERSION="${CN1_VERSION:-}" +# Concurrent checkouts of this repository share ~/.m2 and overwrite each +# other's snapshots, so a developer machine must point this at a per-checkout +# repository. CI has one checkout and can leave it unset. +MAVEN_REPO_LOCAL="${MAVEN_REPO_LOCAL:-}" + +while [ $# -gt 0 ]; do + case "$1" in + --work) WORK="$2"; shift 2 ;; + --flutter-root) FLUTTER_ROOT_ARG="$2"; shift 2 ;; + --cn1-version) CN1_VERSION="$2"; shift 2 ;; + --maven-repo) MAVEN_REPO_LOCAL="$2"; shift 2 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +[ -n "$WORK" ] || { echo "usage: prepare.sh --work

" >&2; exit 2; } +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HERE/../../.." && pwd)" +# Maven Central throttles CI runners with 403/429, and Maven treats either as a +# permanent resolution failure. retry.sh backs off and retries those -- only +# those: RETRY_ONLY_MATCHING=transient returns a real failure on the first try. +RETRY="$REPO/scripts/ci/retry.sh" + +# --- the Flutter SDK, and the gallery inside it ----------------------------- + +if [ -n "$FLUTTER_ROOT_ARG" ]; then + FLUTTER_ROOT="$FLUTTER_ROOT_ARG" +elif [ -n "${FLUTTER_ROOT:-}" ]; then + FLUTTER_ROOT="$FLUTTER_ROOT" +else + command -v flutter >/dev/null 2>&1 || { + echo "flutter is not on PATH; pass --flutter-root" >&2; exit 2; } + # Resolve symlinks first. A package-manager install puts a link in + # /opt/homebrew/bin, and taking its parent's parent lands on /opt/homebrew + # rather than on the SDK -- which then fails further down complaining that + # the SDK has no gallery in it, pointing at a directory that was never one. + FLUTTER_BIN="$(python3 -c 'import os,shutil,sys; print(os.path.realpath(shutil.which("flutter")))')" + FLUTTER_ROOT="$(cd "$(dirname "$FLUTTER_BIN")/.." && pwd)" +fi + +GALLERY="$FLUTTER_ROOT/dev/integration_tests/new_gallery" +[ -d "$GALLERY/lib" ] || { + echo "the gallery is not in this Flutter SDK: $GALLERY" >&2 + echo "the benchmark needs a full SDK checkout, not a stripped release" >&2 + exit 2; } + +FLUTTER_VERSION="$(cd "$FLUTTER_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo unknown)" +DART_FILES="$(find "$GALLERY/lib" -name '*.dart' | wc -l | tr -d ' ')" +echo "==> gallery: $DART_FILES Dart files from Flutter $FLUTTER_VERSION" + +MVN_REPO_ARG="" +[ -n "$MAVEN_REPO_LOCAL" ] && MVN_REPO_ARG="-Dmaven.repo.local=$MAVEN_REPO_LOCAL" + +mkdir -p "$WORK" + +# --- the Flutter side ------------------------------------------------------- + +echo "==> preparing the Flutter build" +rm -rf "$WORK/flutter" +mkdir -p "$WORK/flutter" +cp -R "$GALLERY/lib" "$WORK/flutter/lib" +cp "$GALLERY/pubspec.yaml" "$WORK/flutter/pubspec.yaml" +cp "$FLUTTER_ROOT/pubspec.lock" "$WORK/flutter/pubspec.lock" + +# The gallery is a MEMBER of the Flutter SDK's pub workspace, so its pubspec +# carries `resolution: workspace` and refuses to resolve anywhere else: +# "found no workspace root including it in parent directories". Lifting the +# package out of the SDK means dropping that line. +# +# And then taking the workspace's LOCKFILE with it, which is not optional. The +# gallery pins almost nothing -- `google_fonts: any` -- so the only thing +# holding its dependencies at the versions the Flutter team tests is the +# workspace root's pubspec.lock. Without it pub resolved google_fonts 8.1.0 +# instead of 6.2.1, and the build failed on +# "Member not found: 'GoogleFonts.robotoCondensed'" in three of the studies: +# the benchmark would have been comparing against a gallery that does not +# build, on a dependency nobody chose. +python3 - "$WORK/flutter/pubspec.yaml" "$WORK/asset-packages.txt" <<'PY_INNER' +import io, re, sys +path = sys.argv[1] +lines = [line for line in io.open(path, encoding="utf-8").read().splitlines() + if line.strip() != "resolution: workspace"] + +# The gallery's ASSET packages are not in its own pubspec: the workspace root +# declares them once for every member, so a package lifted out of the workspace +# loses them and the build stops at "Could not resolve package for asset +# packages/rally_assets/logo.png" -- an error about an asset, for a dependency +# that is simply absent. +# +# Derived from the asset paths rather than hard-coded. The gallery references +# three today (flutter_gallery_assets, rally_assets, shrine_images); a list +# written out here would be wrong the first time it gained a fourth, and would +# fail in the same indirect way. +referenced = set() +for line in lines: + match = re.match(r"\s*-\s+packages/([A-Za-z0-9_]+)/", line) + if match: + referenced.add(match.group(1)) + +declared = set() +in_deps = False +for line in lines: + if re.match(r"^[a-z_]+:", line): + in_deps = line.startswith("dependencies:") + continue + if in_deps: + match = re.match(r"\s{2}([A-Za-z0-9_]+):", line) + if match: + declared.add(match.group(1)) + +missing = sorted(referenced - declared) +if missing: + out = [] + for line in lines: + out.append(line) + if line.rstrip() == "dependencies:": + # Unpinned: the workspace lockfile copied beside this file is what + # decides the versions. + for name in missing: + out.append(" %s: any" % name) + lines = out + sys.stderr.write(" declared asset packages: %s\n" % ", ".join(missing)) + +io.open(path, "w", encoding="utf-8").write("\n".join(lines) + "\n") +# Every package the asset paths reference, for the staging check further down. +# newline="\n": Windows Python would otherwise write CRLF, and the shell below +# would read each name with a trailing carriage return. +io.open(sys.argv[2], "w", encoding="utf-8", newline="\n").write("\n".join(sorted(referenced)) + "\n") +PY_INNER +# The asset packages the gallery references; staging checks each one landed. +# CRs stripped as well: on the Windows leg a package name read with a trailing +# \r named a directory that does not exist, and the staging check below failed +# for a package that was in the bundle all along. +DECLARED_ASSET_PACKAGES="$(tr -d '\r' < "$WORK/asset-packages.txt" | tr '\n' ' ')" + +# Platform scaffolding is GENERATED rather than committed: `flutter create` on +# an existing project adds the ios/android/macos/linux/windows/web directories +# and leaves lib/ alone. Committing them would pin us to the scaffolding of +# whichever SDK generated them once. +( cd "$WORK/flutter" && flutter create --project-name gallery --org com.codenameone.bench . >/dev/null ) + +# Our own entry point, which prints the start-up markers the harness times +# against. The gallery's own main.dart is left in place and unused. +cp "$HERE/flutter/main_bench.dart" "$WORK/flutter/lib/main_bench.dart" +# Compute mode (the VM workloads instead of the gallery). The workloads come +# from their one home in vm/benchmarks, so the Flutter app and the Codename One +# app below run the same source the VM suite checks checksums against. +cp "$HERE/flutter/bench_compute.dart" "$HERE/flutter/compute_flag_io.dart" \ + "$HERE/flutter/compute_flag_web.dart" "$WORK/flutter/lib/" +cp "$REPO/vm/benchmarks/dart/common_workloads.dart" "$WORK/flutter/lib/common_workloads.dart" +# The web build cannot compile longArithmetic at all: dart2js rejects its 64-bit +# literals ("can't be represented exactly in JavaScript"), and a JavaScript +# number could not hold its results anyway. The web variant replaces that one +# function with a stub the runner skips; everything else is the same source. +python3 - "$WORK/flutter/lib/common_workloads.dart" "$WORK/flutter/lib/common_workloads_web.dart" <<'PY' +import sys +src = open(sys.argv[1]).read() +start = src.index("int longArithmetic() {") +depth = 0 +for i in range(src.index("{", start), len(src)): + if src[i] == "{": + depth += 1 + elif src[i] == "}": + depth -= 1 + if depth == 0: + end = i + 1 + break +stub = ("// Not on the web: dart2js cannot represent this workload's 64-bit constants.\n" + "int longArithmetic() => throw UnsupportedError('64-bit integers on the web');") +open(sys.argv[2], "w").write(src[:start] + stub + src[end:]) +PY + +( cd "$WORK/flutter" && flutter pub get >/dev/null ) + +# --- the Codename One side -------------------------------------------------- + +echo "==> preparing the Codename One build" +rm -rf "$WORK/cn1" +if [ -z "$CN1_VERSION" ]; then + # help:evaluate prints the value itself. The exec:exec it replaces ran + # `echo` as a process, and Windows has no echo executable -- it is a cmd + # builtin -- so the Windows leg failed right here, and because stderr was + # discarded it said nothing but an exit code. Maven's stderr stays visible. + CN1_VERSION="$(cd "$REPO/maven" && RETRY_ONLY_MATCHING=transient bash "$RETRY" \ + mvn -q -B $MVN_REPO_ARG help:evaluate \ + -Dexpression=project.version -DforceStdout --non-recursive | tr -d '\r' | tail -1)" +fi +[ -n "$CN1_VERSION" ] || { echo "could not determine the Codename One version" >&2; exit 2; } +echo " archetype version $CN1_VERSION" + +# Generated from the SHIPPING archetype rather than from a project skeleton +# kept here. That is deliberate: it means the benchmark builds the same way a +# user's project does, and a change that breaks the documented Flutter wiring +# breaks the benchmark too instead of passing unnoticed. +# NOT -q. A generation that fails under -q prints nothing at all, which is how +# the Windows leg reported an exit code and no reason for it; a build step that +# cannot say why it failed is worse than a noisy one. +echo " maven repository ${MAVEN_REPO_LOCAL:-}" +( cd "$WORK" && RETRY_ONLY_MATCHING=transient bash "$RETRY" mvn -B $MVN_REPO_ARG archetype:generate \ + -DarchetypeArtifactId=cn1app-archetype \ + -DarchetypeGroupId=com.codenameone \ + -DarchetypeVersion="$CN1_VERSION" \ + -DartifactId=cn1 \ + -DgroupId=com.example.bench \ + -Dversion=1.0-SNAPSHOT \ + -DmainName=Bench \ + -DinteractiveMode=false ) + +COMMON="$WORK/cn1/common" + +# The archetype ships the runtime dependency commented out; this is the one +# edit that turns a stock project into a Flutter-capable one, and it is exactly +# what the developer guide tells a user to do. +python3 - "$COMMON/pom.xml" <<'PY' +import io, re, sys +path = sys.argv[1] +text = io.open(path, encoding="utf-8").read() +block = re.search( + r"", + text, re.S) +if block: + text = text[:block.start()] + block.group(1) + text[block.end():] +elif "codenameone-flutter-runtime" not in text: + raise SystemExit("the archetype no longer offers the flutter runtime dependency") +io.open(path, "w", encoding="utf-8").write(text) +PY +grep -q "codenameone-flutter-runtime" "$COMMON/pom.xml" || { + echo "failed to enable the flutter runtime dependency" >&2; exit 2; } + +# The SAME 159 files the Flutter side is building. +mkdir -p "$COMMON/src/main/flutter" +cp -R "$GALLERY/lib/." "$COMMON/src/main/flutter/" + +# Our entry point REPLACES the generated one in place, rather than being added +# beside it in a package of its own. The archetype also generates a desktop +# stub (javase/src/desktop/java/.../BenchStub.java) that names the main class, +# so moving the class breaks a file the archetype owns. That is why the project +# is generated into com.example.bench above: the generated layout then already +# matches the package our Bench.java declares. +GENERATED_MAIN="$COMMON/src/main/java/com/example/bench/Bench.java" +[ -f "$GENERATED_MAIN" ] || { + echo "the archetype did not generate a main class at $GENERATED_MAIN" >&2 + exit 2; } +cp "$HERE/cn1/Bench.java" "$GENERATED_MAIN" + +# The Flutter widgets draw every pixel themselves, so the application needs no +# platform theme under them. includeNativeBool:false stops the theme asking for +# one at run time; build_apps.sh passes nativeTheme=custom, which keeps the +# platform theme files out of the bundle. Both are ordinary settings any +# application whose own theme is complete can use -- nothing Flutter-specific. +THEME_CSS="$COMMON/src/main/css/theme.css" +python3 - "$THEME_CSS" <<'PY' +import io, re, sys +path = sys.argv[1] +text = io.open(path, encoding="utf-8").read() +text, n = re.subn(r"includeNativeBool\s*:\s*true", "includeNativeBool: false", text) +if n != 1: + raise SystemExit("expected one includeNativeBool: true in %s, found %d" % (path, n)) +io.open(path, "w", encoding="utf-8").write(text) +PY +# The Java side of compute mode, from the same vm/benchmarks source. +mkdir -p "$COMMON/src/main/java/com/bench" +cp "$REPO/vm/benchmarks/common/src/main/java/com/bench/CommonWorkloads.java" \ + "$COMMON/src/main/java/com/bench/CommonWorkloads.java" + +# Assets: exactly what Flutter's own build bundles, for EVERY asset package. +# +# `flutter build bundle` writes the asset bundle to build/flutter_assets, the +# same on every platform, and stage_from_flutter_bundle copies what is under +# its packages/ -- so the Codename One build ships the artwork Flutter ships, +# 2x and 3x variants included, and installed size compares like with like. +# +# This used to stage the flutter_gallery_assets package from the pub cache and +# nothing else. The gallery also draws from rally_assets and shrine_images, so +# those screens rendered blank on the Codename One side and the app it measured +# was smaller than Flutter's by that artwork -- a flattering size comparison of +# two different applications. It also contradicted the README, which says the +# assets come from Flutter's bundle. +( cd "$WORK/flutter" && flutter build bundle --release -t lib/main_bench.dart >/dev/null ) +BUNDLE="$WORK/flutter/build/flutter_assets" +[ -d "$BUNDLE/packages" ] || { + echo "flutter build bundle produced no packages/ under $BUNDLE; the Codename One" >&2 + echo "build would render without artwork and its size would not be comparable." >&2 + exit 2; } +# Fatal, not a warning, for any package the gallery declares that did not land: +# a build missing one is a smaller build, and would be measured as a win. +for pkg in $DECLARED_ASSET_PACKAGES; do + [ -d "$BUNDLE/packages/$pkg" ] || { + echo "the Flutter bundle is missing the declared asset package $pkg" >&2; exit 2; } +done +python3 "$HERE/stage_assets.py" --from-flutter-bundle "$BUNDLE" "$COMMON/src/main/resources" + +cat > "$WORK/prepared.json" < prepared" +echo " flutter: $WORK/flutter" +echo " cn1: $WORK/cn1" diff --git a/scripts/flutter-bench/app/stage_assets.py b/scripts/flutter-bench/app/stage_assets.py new file mode 100755 index 00000000000..c4dc37ce998 --- /dev/null +++ b/scripts/flutter-bench/app/stage_assets.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Stages the gallery's asset package into a Codename One resource directory. + + stage_assets.py + +Codename One resources are FLAT on every port -- there are no directories +inside the bundle -- while Flutter addresses assets by path. The runtime +(`com.codename1.flutter.FlutterAssets`) bridges that by mangling the Flutter +key into a single flat name, and this script has to produce exactly the same +name or the asset is simply not found at runtime: + + packages/flutter_gallery_assets/places/india.png + -> cn1f_packages_sflutter__gallery__assets_splaces_sindia.png + +The rule is: prefix `cn1f_`, then `_` is an escape always followed by one +character -- `__` for an underscore, `_s` for a `/`. That is prefix-free, so no +two keys share a name. (Doubling underscores and using a single `_` for `/` was +not: `a_/b` and `a/_b` both became `a___b`.) + +Hard links where the filesystem allows, so the asset pack costs no extra disk; +a stamp file makes a repeat run a no-op, which matters because this runs on +every build of the benchmark project. +""" + +import os +import shutil +import sys + +STAMP = ".flutter-assets-staged" +PREFIX = "packages/flutter_gallery_assets/" + + +def flat_name(key): + """The flat Codename One resource name for a Flutter asset key.""" + out = [] + for ch in key: + if ch == "_": + out.append("__") + elif ch == "/": + out.append("_s") + else: + out.append(ch) + return "cn1f_" + "".join(out) + + +def stage_from_flutter_bundle(bundle, out_dir): + """Stages exactly the assets the Flutter build itself bundled. + + This is the fair set, and neither of the obvious alternatives is. + + Staging the whole `flutter_gallery_assets` package ships files Flutter + never bundles, which inflates our installed size against a build that + tree-shook them -- and it overflowed the desktop packaging limit outright. + Staging only the 1x images ships FEWER: Flutter bundles the 2x and 3x + resolution variants too, so dropping them would have flattered us by tens + of megabytes on exactly the metric the benchmark leads with. + + Reading the built bundle removes the judgement call. Whatever Flutter + decided to carry is what we carry, so "installed size" compares two + applications shipping the same artwork. + """ + if not os.path.isdir(bundle): + raise SystemExit("flutter_assets not found: %s" % bundle) + os.makedirs(out_dir, exist_ok=True) + staged = 0 + for root, _dirs, files in os.walk(bundle): + rel = os.path.relpath(root, bundle).replace(os.sep, "/") + if not (rel == "packages" or rel.startswith("packages/")): + continue + for name in files: + key = name if rel == "." else rel + "/" + name + target = os.path.join(out_dir, flat_name(key)) + if os.path.exists(target): + staged += 1 + continue + src = os.path.join(root, name) + try: + os.link(src, target) + except OSError: + shutil.copyfile(src, target) + staged += 1 + with open(os.path.join(out_dir, STAMP), "w") as handle: + handle.write("%d\n" % staged) + return staged + + +def stage(source, out_dir): + if not os.path.isdir(source): + raise SystemExit("asset package not found: %s" % source) + os.makedirs(out_dir, exist_ok=True) + if os.path.exists(os.path.join(out_dir, STAMP)): + return 0 + staged = 0 + for root, _dirs, files in os.walk(source): + rel = os.path.relpath(root, source) + for name in files: + key = name if rel == "." else rel.replace(os.sep, "/") + "/" + name + target = os.path.join(out_dir, flat_name(PREFIX + key)) + if os.path.exists(target): + staged += 1 + continue + src = os.path.join(root, name) + try: + os.link(src, target) + except OSError: + shutil.copyfile(src, target) + staged += 1 + with open(os.path.join(out_dir, STAMP), "w") as handle: + handle.write("%d\n" % staged) + return staged + + +def main(argv): + args = list(argv[1:]) + from_bundle = False + if args and args[0] == "--from-flutter-bundle": + from_bundle = True + args = args[1:] + if len(args) != 2: + raise SystemExit( + "usage: stage_assets.py [--from-flutter-bundle] ") + if from_bundle: + staged = stage_from_flutter_bundle(args[0], args[1]) + else: + staged = stage(args[0], args[1]) + print("staged %d asset(s) into %s" % (staged, args[1])) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/flutter-bench/baseline/README.md b/scripts/flutter-bench/baseline/README.md new file mode 100644 index 00000000000..9f0bec4bf23 --- /dev/null +++ b/scripts/flutter-bench/baseline/README.md @@ -0,0 +1,17 @@ +# Flutter benchmark regression baselines + +The rows `scripts/flutter-bench/flutter_baseline.py` holds every platform's +benchmark leg to: Codename One's own sizes (absolute) and its start-up and +memory as ratios to Flutter from the same run. + +- `policy.json`: the global tolerance and floor per metric, and the metrics a + platform records without gating, each with its reason. +- `base/[@].json`: the consolidated rows. Written only by + the nightly fold (`.github/workflows/perf-baseline.yml`); a pull request that + edits them fails. +- `pr/.json`: one pull request's calibrations and rebaselines, written + by `flutter_baseline.py calibrate --pr `. It is folded into `base/` + after the merge. + +The rules are the ParparVM performance gate's (`vm/selfhost/perf_baseline.py`); +what this layout adds is in `../flutter_baseline.py` and `../README.md`. diff --git a/scripts/flutter-bench/baseline/base/android.json b/scripts/flutter-bench/baseline/base/android.json new file mode 100644 index 00000000000..51bc99f14e5 --- /dev/null +++ b/scripts/flutter-bench/baseline/base/android.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 4720336 + }, + "install_bytes": { + "runs": 1, + "value": 91589516 + }, + "wire_bytes": { + "runs": 1, + "value": 91589516 + } + } +} diff --git a/scripts/flutter-bench/baseline/base/ios.json b/scripts/flutter-bench/baseline/base/ios.json new file mode 100644 index 00000000000..77ca675c444 --- /dev/null +++ b/scripts/flutter-bench/baseline/base/ios.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 38015576 + }, + "install_bytes": { + "runs": 1, + "value": 145431213 + }, + "wire_bytes": { + "runs": 1, + "value": 110425140 + } + } +} diff --git a/scripts/flutter-bench/baseline/base/javascript.json b/scripts/flutter-bench/baseline/base/javascript.json new file mode 100644 index 00000000000..b13673b6afc --- /dev/null +++ b/scripts/flutter-bench/baseline/base/javascript.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 12910326 + }, + "install_bytes": { + "runs": 1, + "value": 110000963 + }, + "wire_bytes": { + "runs": 1, + "value": 80610118 + } + } +} diff --git a/scripts/flutter-bench/baseline/base/linux.json b/scripts/flutter-bench/baseline/base/linux.json new file mode 100644 index 00000000000..5256eac3f55 --- /dev/null +++ b/scripts/flutter-bench/baseline/base/linux.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 18958610 + }, + "install_bytes": { + "runs": 1, + "value": 119594984 + }, + "wire_bytes": { + "runs": 1, + "value": 95706942 + } + } +} diff --git a/scripts/flutter-bench/baseline/base/macos.json b/scripts/flutter-bench/baseline/base/macos.json new file mode 100644 index 00000000000..44227c366a3 --- /dev/null +++ b/scripts/flutter-bench/baseline/base/macos.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 73212360 + }, + "install_bytes": { + "runs": 1, + "value": 168810517 + }, + "wire_bytes": { + "runs": 1, + "value": 109700753 + } + } +} diff --git a/scripts/flutter-bench/baseline/base/windows.json b/scripts/flutter-bench/baseline/base/windows.json new file mode 100644 index 00000000000..e333312625f --- /dev/null +++ b/scripts/flutter-bench/baseline/base/windows.json @@ -0,0 +1,16 @@ +{ + "gallery": { + "code_bytes": { + "runs": 1, + "value": 17088000 + }, + "install_bytes": { + "runs": 1, + "value": 116684800 + }, + "wire_bytes": { + "runs": 1, + "value": 94685578 + } + } +} diff --git a/scripts/flutter-bench/baseline/policy.json b/scripts/flutter-bench/baseline/policy.json new file mode 100644 index 00000000000..fcee94a5645 --- /dev/null +++ b/scripts/flutter-bench/baseline/policy.json @@ -0,0 +1,21 @@ +{ + "floor": { + "code_bytes": 0.0, + "cold_start_ratio": 0.0, + "idle_memory_ratio": 0.0, + "install_bytes": 0.0, + "wire_bytes": 0.0 + }, + "tolerance": { + "code_bytes": 0.0, + "cold_start_ratio": 0.25, + "idle_memory_ratio": 0.15, + "install_bytes": 0.0, + "wire_bytes": 0.0 + }, + "ungated": { + "macos": { + "cold_start_ratio": "Recorded, not gated, carried over from the retired baselines/macos.json, which gave macOS start-up no tolerance: On the hosted macOS runner one run spread Codename One's start-up from 465 to 2588 ms at a host load of 12-32, so any band wide enough not to fire on that noise would catch nothing. Sizes and memory at rest (101.3-111.3 MB in the same run) are stable and gated." + } + } +} diff --git a/scripts/flutter-bench/baseline/pr/5883.json b/scripts/flutter-bench/baseline/pr/5883.json new file mode 100644 index 00000000000..65413ed066a --- /dev/null +++ b/scripts/flutter-bench/baseline/pr/5883.json @@ -0,0 +1,237 @@ +{ + "calibrate": { + "android": { + "gallery": { + "cold_start_ratio": { + "runs": 8, + "tolerance": { + "value": 0.55 + }, + "value": 0.265 + }, + "idle_memory_ratio": { + "runs": 8, + "tolerance": { + "value": 0.2 + }, + "value": 0.523 + } + } + }, + "linux": { + "gallery": { + "cold_start_ratio": { + "runs": 7, + "value": 0.67 + }, + "idle_memory_ratio": { + "runs": 7, + "value": 0.387 + } + } + }, + "macos": { + "gallery": { + "idle_memory_ratio": { + "runs": 7, + "value": 0.682 + } + } + }, + "windows": { + "gallery": { + "cold_start_ratio": { + "runs": 8, + "value": 0.632 + }, + "idle_memory_ratio": { + "runs": 8, + "value": 0.616 + } + } + } + }, + "pr": 5883, + "rebaseline": { + "android": { + "gallery": { + "code_bytes": { + "from": { + "value": 4720336 + }, + "reason": "Android code grew with features added on this branch after the gate was armed: 4,720,336 -> 4,772,660 (d50f587217, +1.1%), +656 bytes for the custom theme mode (0bcf456eab), +188 bytes for the runtime mount/styling change (2fc4148b80); still 4.5x under Flutter. The tolerance went from 0 to 0.5% (d070d57bc1) because at 0 every change that added bytes failed the leg; growth past ~24 KB still fails it.", + "runs": 1, + "tolerance": { + "value": 0.005 + }, + "value": 4773504 + }, + "install_bytes": { + "from": { + "value": 91589516 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "tolerance": { + "value": 0.001 + }, + "value": 81949943 + }, + "wire_bytes": { + "from": { + "value": 91589516 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "tolerance": { + "value": 0.001 + }, + "value": 81949943 + } + } + }, + "ios": { + "gallery": { + "code_bytes": { + "from": { + "value": 38015576 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 19288088 + }, + "install_bytes": { + "from": { + "value": 145431213 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 114736933 + }, + "wire_bytes": { + "from": { + "value": 110425140 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 84339699 + } + } + }, + "javascript": { + "gallery": { + "code_bytes": { + "from": { + "value": 12910326 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 12749524 + }, + "install_bytes": { + "from": { + "value": 110000963 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 108035821 + }, + "wire_bytes": { + "from": { + "value": 80610118 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 79699243 + } + } + }, + "linux": { + "gallery": { + "code_bytes": { + "from": { + "value": 18958610 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 1, + "value": 14375154 + }, + "install_bytes": { + "from": { + "value": 119594984 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 1, + "value": 116189664 + }, + "wire_bytes": { + "from": { + "value": 95706942 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 1, + "value": 84650942 + } + } + }, + "macos": { + "gallery": { + "code_bytes": { + "from": { + "value": 73212360 + }, + "reason": "Re-measured at 604b8ee2e5 (flutter-bench run 37015988276). The armed value predates the native ports' size work on this branch; the latest +224 bytes of code are the macOS first-frame and menu-bar commits (bebca11b6f, 604b8ee2e5).", + "runs": 1, + "value": 36743096 + }, + "install_bytes": { + "from": { + "value": 168810517 + }, + "reason": "Re-measured at 604b8ee2e5 (flutter-bench run 37015988276). The armed value predates the native ports' size work on this branch; the latest +224 bytes of code are the macOS first-frame and menu-bar commits (bebca11b6f, 604b8ee2e5).", + "runs": 1, + "value": 132268573 + }, + "wire_bytes": { + "from": { + "value": 109700753 + }, + "reason": "Re-measured at 604b8ee2e5 (flutter-bench run 37015988276). The armed value predates the native ports' size work on this branch; the latest +224 bytes of code are the macOS first-frame and menu-bar commits (bebca11b6f, 604b8ee2e5).", + "runs": 1, + "value": 90622729 + } + } + }, + "windows": { + "gallery": { + "code_bytes": { + "from": { + "value": 17088000 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 13228544 + }, + "install_bytes": { + "from": { + "value": 116684800 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686 and 36965347627, commits 9717cb0603 and 1b36753c18, identical Codename One sources). The armed values predate the native ports' size work on this branch, and the old gate failed only growth, so a shrink never asked for a re-record; this gate fails a move either way. Android's install and download sizes differ by 4 bytes between those two runs of identical sources (APK packaging), hence their 0.1% tolerance.", + "runs": 2, + "value": 114205184 + }, + "wire_bytes": { + "from": { + "value": 94685578 + }, + "reason": "Re-measured on this branch (flutter-bench runs 36896512686, 36965347627 and 37015988276, identical Codename One sources for Windows). The armed value predates the native ports' size work on this branch. The download size, a zip of the build, differs by 1 byte between those runs, hence its 0.1% tolerance.", + "runs": 3, + "tolerance": { + "value": 0.001 + }, + "value": 82273541 + } + } + } + } +} diff --git a/scripts/flutter-bench/benchlib.py b/scripts/flutter-bench/benchlib.py new file mode 100644 index 00000000000..d2d9d645e6f --- /dev/null +++ b/scripts/flutter-bench/benchlib.py @@ -0,0 +1,1066 @@ +#!/usr/bin/env python3 +"""Shared machinery for the Flutter-vs-Codename One benchmark. + +The benchmark measures ONE application built two ways: Flutter's own AOT +compile of the gallery, and the identical Dart source transpiled to Java and +run on Codename One. Same source, same screens, same device, so a difference +in size, in start-up time or in memory is a difference between the two runtimes +and nothing else. + +This module holds everything that is not platform specific: the metric list, +the sizing helpers, the per-round statistics, the rendering of the report, and +the regression gate. Platform specific work -- how an +application is launched, and how its memory is read -- lives behind the adapter +interface in `platforms.py`. + +Rules the numbers have to obey, because it is easy to produce flattering ones: + + * Release/AOT on both sides. A debug build of either is meaningless. + * The SAME SURFACE SIZE. Window or screen area drives the size of the GPU + surfaces that dominate a UI application's resident memory, so two builds + measured at different sizes cannot be compared on memory at all. + * INTERLEAVED rounds, and the machine's load average recorded. A ratio + taken under different load twice is not a ratio -- a walkthrough recording + on this project desynchronised twice and looked like a timing defect, and + the cause was a stray simulator holding the machine at load 8. + * Head to head, start-up and memory at rest are judged PER ROUND: the ratio + is the median of the per-round ratios, each from two launches made back to + back, and the figures shown are each side's median. Not each side's best: + one undisturbed outlier on either side decided the verdict on its own (see + paired_rounds). The regression gate (flutter_baseline.py) reads the same + statistic, Codename One over Flutter, so the pinned Flutter build is its + unit of measure and a slow runner cancels out. + * The start-up clock runs OUTSIDE both processes: each application prints one + marker on its first painted frame and the harness times from launch to that + line, so neither runtime is trusted to time itself. + * Every metric is reported, including the ones Codename One loses. +""" + +import json +import os +import re +import subprocess +import time +import zipfile + +# Lower is better for every metric here; that is what makes "wins" well +# defined and lets the regression gate use a single comparison. + +# Every platform the benchmark measures, in the order the report lists them. +# The workflow's plan job carries its own copy (it runs before any checkout); +# test_benchlib.PlatformIdsMatchTheWorkflow holds the two together. +PLATFORM_IDS = ("macos", "ios", "android", "linux", "windows", "javascript") + +METRICS = [ + ("install_bytes", "Installed size", "bytes"), + ("code_bytes", "Executable code", "bytes"), + ("wire_bytes", "Download size (zipped)", "bytes"), + ("cold_start_ms", "Cold start to first frame on screen", "ms"), + ("idle_memory_bytes", "Memory at rest", "bytes"), +] + +# Start-up is a point wherever Flutter can report the moment its content frame +# was on screen (see platforms.MARKERS), and then `cold_start_lower_ms` is None. +# Where it cannot -- the web -- it is reported as a BRACKET, because the two +# runtimes do not expose the same event. Flutter's `cold_start_ms` is RASTERDONE, after its +# first frame is rasterised, and `cold_start_lower_ms` is FIRSTCONTENT, a +# UI-thread callback before it; Codename One has one marker. The RATIO is taken +# from Flutter's LOWER end -- the one least favourable to Codename One -- because +# the bracket exists precisely because the events may not line up, and a ratio +# of Flutter's time over ours only grows, and flatters us, as Flutter's figure +# grows. Both ends are carried so a reader sees the width of the uncertainty. +# The gate is Codename One's alone, so it is unaffected. +BRACKET_METRIC = "cold_start_ms" +BRACKET_LOWER = "cold_start_lower_ms" + +METRIC_UNITS = dict((key, unit) for key, _label, unit in METRICS) +METRIC_LABELS = dict((key, label) for key, label, _unit in METRICS) + +SIDES = ("codenameone", "flutter") + +# ---------------------------------------------------------------------- +# Sizing. Portable: every platform ships either a directory tree or a +# single archive, and both reduce to "how many bytes does the user get". +# ---------------------------------------------------------------------- + +def tree_size(path): + """Apparent size of `path`: what the artifact actually occupies. + + Deliberately not `du`: that reports ALLOCATED blocks, which rounds every + file up to the filesystem's block size and counts hard-linked framework + copies once. An application's size as the user experiences it is the sum + of its file lengths, and that is what both stores quote. + """ + if os.path.isfile(path): + return os.path.getsize(path) + total = 0 + for root, _dirs, files in os.walk(path): + for name in files: + full = os.path.join(root, name) + if os.path.islink(full): + continue + try: + total += os.path.getsize(full) + except OSError: + # Deliberately ignored. A build tree is live while this walks + # it -- a compiler or packaging step can remove or replace a + # file between the listing and the stat -- and a file that has + # gone is a file the artifact does not ship. Failing here would + # turn a harmless race into a lost measurement. + continue + return total + + +def wire_size(path, workdir): + """What the artifact compresses to -- the number a download actually costs. + + Stores compress before shipping, so an uncompressed comparison flatters + whichever side ships more compressible bytes. Deflate at a fixed level so + the number is reproducible across runners rather than dependent on + whichever zip binary is installed. + """ + out = os.path.join(workdir, os.path.basename(str(path).rstrip("/")) + ".benchzip") + if os.path.exists(out): + os.remove(out) + with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive: + if os.path.isfile(path): + archive.write(path, os.path.basename(path)) + else: + for root, _dirs, files in os.walk(path): + for name in files: + full = os.path.join(root, name) + if os.path.islink(full): + continue + archive.write(full, os.path.relpath(full, path)) + size = os.path.getsize(out) + os.remove(out) + return size + + +# Mach-O magic numbers, both endiannesses plus the fat/universal wrapper. +_MACHO_MAGIC = ( + b"\xfe\xed\xfa\xce", b"\xce\xfa\xed\xfe", # 32 bit + b"\xfe\xed\xfa\xcf", b"\xcf\xfa\xed\xfe", # 64 bit + b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca", # universal +) + + +def macho_code_size(bundle): + """Every Mach-O binary in an Apple bundle, summed. + + NOT just the main executable, and the difference is not small. On iOS a + Flutter application's own code is not in the executable at all: `Runner` is + a thin launcher of about a tenth of a megabyte, and the Dart AOT image and + the engine live in Frameworks/App.framework and Frameworks/Flutter.framework. + Sizing only the executable therefore compared our entire runtime against + Flutter's stub and reported a 700x loss that did not exist. + + Summing every Mach-O in the bundle needs no per-side special casing: it + asks "how many bytes of compiled code does this application ship", which + is the same question on both sides however each chooses to lay them out. + """ + return _sum_by_magic(bundle, lambda head: head[:4] in _MACHO_MAGIC) + + +def elf_code_size(tree): + """Machine code in every ELF under `tree`: the bytes of its EXECUTABLE sections. + + Not whole files, for two reasons that pull the same way. Flutter's bundle + puts the Dart AOT image in lib/libapp.so beside the engine library, so the + `gallery` launcher alone would repeat the iOS mistake; every ELF is walked. + And Codename One's native Linux port links the application's resources INTO + the executable (an .incbin'd blob in .rodata), so whole-file bytes counted + the gallery's artwork as Codename One code -- 114 MB against Flutter's + 51 MB, a comparison of pictures. Executable sections (SHF_EXECINSTR) are + compiled code on both sides and nothing else; read-only data is excluded + symmetrically, Flutter's Dart snapshot data included. + """ + return _sum_by_magic(tree, lambda head: head[:4] == b"\x7fELF", _elf_exec_bytes) + + +def pe_code_size(tree): + """Machine code in every PE image under `tree`; see elf_code_size. + + The same argument: Flutter's code is flutter_windows.dll plus the Dart image + beside gallery.exe, and Codename One's Windows build embeds its resources in + the executable. Sections marked IMAGE_SCN_MEM_EXECUTE are counted. + """ + return _sum_by_magic(tree, lambda head: head[:2] == b"MZ", _pe_exec_bytes) + + +def _elf_exec_bytes(data): + """Total size of SHF_EXECINSTR sections that occupy file bytes, or None.""" + import struct + if len(data) < 64 or data[:4] != b"\x7fELF": + return None + wide = data[4] == 2 + order = "<" if data[5] == 1 else ">" + try: + if wide: + shoff, = struct.unpack_from(order + "Q", data, 0x28) + shentsize, shnum = struct.unpack_from(order + "HH", data, 0x3A) + else: + shoff, = struct.unpack_from(order + "I", data, 0x20) + shentsize, shnum = struct.unpack_from(order + "HH", data, 0x2E) + total = 0 + for index in range(shnum): + base = shoff + index * shentsize + if wide: + sh_type, sh_flags = struct.unpack_from(order + "IQ", data, base + 4) + sh_size, = struct.unpack_from(order + "Q", data, base + 0x20) + else: + sh_type, sh_flags = struct.unpack_from(order + "II", data, base + 4) + sh_size, = struct.unpack_from(order + "I", data, base + 0x14) + if sh_flags & 0x4 and sh_type != 8: # SHF_EXECINSTR, not SHT_NOBITS + total += sh_size + return total + except struct.error: + return None + + +def _pe_exec_bytes(data): + """Total raw size of IMAGE_SCN_MEM_EXECUTE sections, or None.""" + import struct + try: + pe, = struct.unpack_from("_runs` holds the values and, when the +# harness recorded it, `_rounds` the round each value came from. +# +# Memory at rest is here too, not left at best-of. The case for the minimum is +# that it is the run least disturbed by the machine, which holds for a clock +# and not for resident memory: a low reading is more often an app that had not +# finished loading than an undisturbed one. Measured on the Android emulator: +# the first round read 33.0 MB for Codename One against 45.0-45.7 MB in every +# other round, and 60.7 MB for Flutter against 88.1-90.6 MB, so best-of compared +# two half-loaded apps. The median of the per-round ratios ignores such a round +# on either side, for the same reason it ignores a lucky start-up. +PAIRED_METRICS = { + "cold_start_ms": "cold_start", + "idle_memory_bytes": "idle_memory", +} +BRACKET_LOWER_BASE = "cold_start_lower" + + +def _samples(side, base): + """(values, rounds) for one sample list; rounds is None when not recorded.""" + values = list(side.get(base + "_runs") or []) + rounds = side.get(base + "_rounds") + if rounds is not None and len(rounds) != len(values): + rounds = None + return values, (list(rounds) if rounds is not None else None) + + +def paired_rounds(ours_side, theirs_side, key): + """[(ours, theirs)] for every round both sides measured, or (None, reason). + + Why per round. The runs are INTERLEAVED, so the two launches of one round + are adjacent in time and share whatever the machine was doing; two + different rounds do not. Best-of-N compared each side's luckiest round + with the other's, so a single undisturbed launch on either side decided + the verdict alone. Real numbers from the macOS runner (ms, five rounds): + Codename One 1787, 707, 468, 584, 668 against Flutter 2512, 617, 522, + 356, 769. Codename One was faster in three of the five rounds, yet + best-of reported a Flutter win (356 against 468, 0.76x), on the strength + of one Flutter round; each side's median flips the same way (668 against + 617), because it too compares different rounds. The median of the five + per-round ratios is 522/468 = 1.115, and no single round can move it past the + middle one. + + Rounds are matched by the round number the harness recorded with each + sample, so a launch that failed on one side drops that round only, rather + than shifting every later pair. Reports without round numbers are paired + by position, which is only sound when both lists are the same length; + otherwise this declines and the caller falls back to best-of. + + For the bracketed start-up (BRACKET_METRIC), Flutter's figure in each + round is the end of ITS bracket least favourable to Codename One -- + min(upper, lower) -- so the bracket rule holds round by round. A round + with no lower end is dropped rather than judged by the upper end alone, + which would flatter us. + """ + base = PAIRED_METRICS[key] + ours, ours_rounds = _samples(ours_side, base) + theirs, theirs_rounds = _samples(theirs_side, base) + lower, lower_rounds = ([], None) + if key == BRACKET_METRIC: + lower, lower_rounds = _samples(theirs_side, BRACKET_LOWER_BASE) + if not ours or not theirs: + return None, "no per-round samples on both sides" + if ours_rounds is not None and theirs_rounds is not None \ + and (not lower or lower_rounds is not None): + theirs_by_round = dict(zip(theirs_rounds, theirs)) + lower_by_round = dict(zip(lower_rounds or [], lower)) + pairs = [] + for number, value in zip(ours_rounds, ours): + if number not in theirs_by_round: + continue + other = theirs_by_round[number] + if lower: + if number not in lower_by_round: + continue + pairs.append((value, other, lower_by_round[number])) + else: + pairs.append((value, other, None)) + else: + if len(ours) != len(theirs): + return None, ("the sides measured %d and %d rounds and carry no round " + "numbers to match them by" % (len(ours), len(theirs))) + if lower and len(lower) != len(theirs): + return None, ("Flutter's bracket has %d lower ends for %d rounds and " + "no round numbers to match them by" % (len(lower), len(theirs))) + pairs = [(a, b, lower[i] if lower else None) + for i, (a, b) in enumerate(zip(ours, theirs))] + # A zero is a failed sample, not an infinitely fast one; verdict treats a + # zero summary the same way. + pairs = [p for p in pairs if p[0] and p[1] and (p[2] is None or p[2])] + if not pairs: + return None, "no round was measured by both sides" + return pairs, None + + +def percentile(values, pct): + """The `pct` percentile by nearest rank, with no interpolation. + + Frame times are a sample of real frames, not a continuous distribution, + so an interpolated p95 invents a frame that never rendered. + """ + if not values: + return None + ordered = sorted(values) + rank = max(1, int(round(pct / 100.0 * len(ordered)))) + return ordered[min(rank, len(ordered)) - 1] + + +def load_average(): + """The 1/5/15 minute load, or None where the platform has no such notion. + + Recorded with every report because a ratio measured under different load + twice is not a ratio, and because a surprising result is usually the + machine rather than the code. + """ + try: + one, five, fifteen = os.getloadavg() + return [round(one, 2), round(five, 2), round(fifteen, 2)] + except (OSError, AttributeError): + return None + + +# ---------------------------------------------------------------------- +# Report assembly +# ---------------------------------------------------------------------- + +def summarise(side): + """Collapses a side's per-run samples into one figure per metric: the best run. + + Read only where rounds cannot be paired (verdict's fallback, which says so in + the report). Neither the head-to-head verdict nor the regression gate uses it + otherwise: both judge the median of the per-round ratios (paired_rounds). + """ + out = dict(side) + out["cold_start_ms"] = best_of(side.get("cold_start_runs") or []) + out["cold_start_lower_ms"] = best_of(side.get("cold_start_lower_runs") or []) + out["idle_memory_bytes"] = best_of(side.get("idle_memory_runs") or []) + return out + + +# ---------------------------------------------------------------------- +# Compute: the VM workloads, run inside each app +# ---------------------------------------------------------------------- + +# vm/benchmarks' CommonWorkloads, in its own order. The Java source runs in the +# Codename One app and vm/benchmarks/dart/common_workloads.dart -- a port that +# reproduces Java's 32-bit wrapping, unsigned shift and String.hashCode -- in the +# Flutter one, so the two do the same work and must produce the same checksum. +COMPUTE_WORKLOADS = ( + "intArithmetic", "longArithmetic", "mathTranscendental", "arraySequential", + "arrayRandom", "objectAllocation", "valueEscape", "hashMapChurn", + "stringBuilding", "recursion", "quicksortBench", +) + +COMPUTE_METRIC = "compute_geomean" +COMPUTE_LABEL = "Compute (geomean of the VM workloads)" + +_COMPUTE_LINE = re.compile( + r"BENCH:COMPUTE name=(\w+) checksum=(-?\d+) ms=(\d+)") +COMPUTE_DONE = "BENCH:COMPUTE-DONE" + + +def parse_compute_line(line): + """(name, checksum, ms) from one app's result line, or None.""" + match = _COMPUTE_LINE.search(line) + if not match: + return None + return match.group(1), match.group(2), int(match.group(3)) + + +# What a side's result on one workload was, when it was not a comparable timing. +# "wrong result" and "did not run" name the side that failed; the geometric mean +# is still taken over the workloads both sides got right, so a failure is shown, +# never scored as a timing. +FAILED_WRONG = "wrong result" +FAILED_NOT_RUN = "did not run" + + +def compute_verdict(ours, theirs, reference=None): + """Per-workload ratios and their geometric mean. + + `ours` and `theirs` map a workload to (checksum, ms). The ratio is + flutter_ms / codenameone_ms, so above 1.00x Codename One is faster. + + A workload is only compared when both checksums are identical: a different + checksum means the two sides did not do the same computation -- on the web, + for one, JavaScript numbers cannot hold a 64-bit integer -- and a ratio + between two different computations is not a measurement. + + `reference` maps a workload to the checksum a host JVM computes from the same + source (reference_checksums). With it, a disagreement or a missing result is + attributed: the row says WHICH side got a wrong result or did not run, and + `failures` lists them, instead of both collapsing into "checksum mismatch" or + "not measured". Before this, Flutter web returning a wrong intArithmetic and + no longArithmetic at all simply removed those workloads from the web leg -- + including ones Codename One wins -- and a Codename One failure would have + vanished the same way. The reference only arbitrates a DISAGREEMENT: two sides + that agree are compared whatever the JVM says, because a transcendental + workload may legitimately differ from a desktop JVM's libm in the last bit on + both. + """ + rows = [] + ratios = [] + failures = [] + reference = reference or {} + for name in COMPUTE_WORKLOADS: + a = (ours or {}).get(name) + b = (theirs or {}).get(name) + expected = reference.get(name) + if a is None or b is None: + row = {"name": name, "status": "not measured"} + failed = [] + if a is None and b is not None: + failed = [("codenameone", FAILED_NOT_RUN)] + elif b is None and a is not None: + failed = [("flutter", FAILED_NOT_RUN)] + if expected is not None: + # The side that did run can still be wrong. + if a is not None and a[0] != expected: + failed.append(("codenameone", FAILED_WRONG)) + if b is not None and b[0] != expected: + failed.append(("flutter", FAILED_WRONG)) + if failed: + row["failed"] = [{"side": side, "how": how} for side, how in failed] + failures.extend({"name": name, "side": side, "how": how} for side, how in failed) + if a is not None: + row["codenameone"] = a[1] + if b is not None: + row["flutter"] = b[1] + rows.append(row) + continue + if a[0] != b[0]: + row = {"name": name, "status": "checksum mismatch", + "codenameone": a[1], "flutter": b[1], + "checksums": [a[0], b[0]]} + if expected is not None: + failed = [] + if a[0] != expected: + failed.append("codenameone") + if b[0] != expected: + failed.append("flutter") + if len(failed) == 1: + row["failed"] = [{"side": failed[0], "how": FAILED_WRONG}] + failures.append({"name": name, "side": failed[0], "how": FAILED_WRONG}) + rows.append(row) + continue + if a[1] <= 0 or b[1] <= 0: + rows.append({"name": name, "status": "too fast to time", + "codenameone": a[1], "flutter": b[1]}) + continue + ratio = float(b[1]) / float(a[1]) + ratios.append(ratio) + rows.append({"name": name, "status": "measured", "codenameone": a[1], + "flutter": b[1], "ratio": round(ratio, 3)}) + out = {"workloads": rows, "compared": len(ratios), "failures": failures} + if ratios: + product = 1.0 + for r in ratios: + product *= r + out["geomean"] = round(product ** (1.0 / len(ratios)), 3) + return out + + +_REFERENCE_DRIVER = """ +public class RefDriver { + public static void main(String[] a) { + String[] names = {%s}; + for (String n : names) { + long c; + try { + c = (Long) com.bench.CommonWorkloads.class.getMethod(n).invoke(null); + } catch (Exception e) { + throw new RuntimeException(n, e); + } + System.out.println("REF " + n + " " + c); + } + } +} +""" + + +def reference_checksums(repo_root, workdir, java_home=None): + """The checksum each compute workload produces on the host JVM, or None. + + Compiled and run from vm/benchmarks' CommonWorkloads.java -- the very file + prepare.sh copies into the Codename One app -- so it cannot drift from what + the apps run. None (and the verdict falls back to unattributed mismatches) + when no JDK is on hand; that is reported, never guessed. + """ + source = os.path.join(repo_root, "vm", "benchmarks", "common", "src", "main", + "java", "com", "bench", "CommonWorkloads.java") + if not os.path.isfile(source): + return None + java_home = java_home or os.environ.get("JAVA_HOME") + def tool(name): + if java_home: + for candidate in (name, name + ".exe"): + path = os.path.join(java_home, "bin", candidate) + if os.path.isfile(path): + return path + return name + out_dir = os.path.join(workdir, "compute-reference") + src_dir = os.path.join(out_dir, "src") + os.makedirs(os.path.join(src_dir, "com", "bench"), exist_ok=True) + with open(source, "rb") as fin, open(os.path.join(src_dir, "com", "bench", + "CommonWorkloads.java"), "wb") as fout: + fout.write(fin.read()) + names = ", ".join('"%s"' % n for n in COMPUTE_WORKLOADS) + with open(os.path.join(src_dir, "RefDriver.java"), "w") as f: + f.write(_REFERENCE_DRIVER % names) + classes = os.path.join(out_dir, "classes") + os.makedirs(classes, exist_ok=True) + try: + subprocess.run([tool("javac"), "-d", classes, + os.path.join(src_dir, "com", "bench", "CommonWorkloads.java"), + os.path.join(src_dir, "RefDriver.java")], + check=True, capture_output=True, timeout=300) + result = subprocess.run([tool("java"), "-cp", classes, "RefDriver"], + check=True, capture_output=True, text=True, timeout=900) + except (OSError, subprocess.SubprocessError): + return None + refs = {} + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) == 3 and parts[0] == "REF": + refs[parts[1]] = parts[2] + return refs or None + + +def render_compute(report): + """The compute table for one platform, or None when nothing ran.""" + compute = report.get("compute") + if not compute: + return None + intro = ("**Compute** -- the VM workloads (`vm/benchmarks`), run inside each app; " + + "time is the best of the app's own repetitions, higher ratio means " + + "Codename One is faster.") + lines = [intro, ""] + if compute.get("status") != "measured": + reason = compute.get("reason", "no reason given").rstrip(".") + lines.append("_Not measured: %s._" % reason) + return "\n".join(lines) + verdict = compute["verdict"] + lines.append("| Workload | Codename One | Flutter | Ratio |") + lines.append("| --- | ---: | ---: | ---: |") + for row in verdict["workloads"]: + if row.get("failed"): + # Attributed: say which side failed, and show whatever timing the + # other side has. Not part of the mean. + def cell(side): + for f in row["failed"]: + if f["side"] == side: + return f["how"] + return "%d ms" % row[side] if side in row else "--" + lines.append("| %s | %s | %s | %s |" % ( + row["name"], cell("codenameone"), cell("flutter"), + "; ".join("%s: %s" % ("Codename One" if f["side"] == "codenameone" + else "Flutter", f["how"]) for f in row["failed"]))) + elif row["status"] == "measured": + lines.append("| %s | %d ms | %d ms | %.2fx |" % ( + row["name"], row["codenameone"], row["flutter"], row["ratio"])) + elif row["status"] == "not measured": + lines.append("| %s | -- | -- | not measured |" % row["name"]) + else: + lines.append("| %s | %s | %s | %s |" % ( + row["name"], + "%d ms" % row["codenameone"], "%d ms" % row["flutter"], row["status"])) + if "geomean" in verdict: + lines.append("| **Geometric mean** | | | **%.2fx** |" % verdict["geomean"]) + return "\n".join(lines) + + +def verdict(report): + """Who wins each metric, and by how much. + + `ratio` is flutter/codenameone with lower-is-better throughout, so a ratio + above 1 means Codename One is ahead by that factor. A metric either side + failed to produce is reported as not measured rather than as a win: a + missing number is not a result. + """ + out = {} + for key, _label, _unit in METRICS: + if key in PAIRED_METRICS: + pairs, reason = paired_rounds(report["codenameone"], report["flutter"], key) + if pairs: + out[key] = _paired_entry(pairs) + continue + ours = report["codenameone"].get(key) + theirs = report["flutter"].get(key) + if key == BRACKET_METRIC: + lower = report["flutter"].get(BRACKET_LOWER) + if lower and theirs: + # The least favourable end: see BRACKET_METRIC. This used the + # upper end, which the report's own note said it did not. + theirs = min(theirs, lower) + if ours is None or theirs is None or not ours or not theirs: + out[key] = {"status": "not measured"} + continue + out[key] = { + "status": "measured", + "codenameone": ours, + "flutter": theirs, + "ratio": round(float(theirs) / float(ours), 3), + "winner": "codenameone" if ours < theirs else "flutter", + } + if key in PAIRED_METRICS: + # Said in the report: a reader must not take this for the paired + # statistic. Only reached with per-side figures but no pairable + # rounds -- a report rendered from older JSON, or rounds lost. + out[key]["statistic"] = "best_of" + out[key]["fallback"] = reason + return out + + +def _paired_entry(pairs): + """The verdict entry for a metric judged round by round (paired_rounds).""" + ratios = [] + theirs = [] + for ours, upper, lower in pairs: + # The bracket's least favourable end, per round: see BRACKET_METRIC. + other = min(upper, lower) if lower is not None else upper + theirs.append(other) + ratios.append(float(other) / float(ours)) + ratio = round(median(ratios), 3) + entry = { + "status": "measured", + "statistic": "paired_median", + # Each side's own median, for the magnitudes. The ratio is NOT their + # quotient: it is the median of the per-round ratios, and the two can + # disagree -- that disagreement is the whole reason for pairing. + "codenameone": median([p[0] for p in pairs]), + "flutter": median(theirs), + "ratio": ratio, + # By the ratio, so the winner, check_behind and the tally all read the + # same statistic. A tie goes to Flutter, as it does for a summary. + "winner": "codenameone" if ratio > 1.0 else "flutter", + "rounds": len(pairs), + "rounds_won": sum(1 for r in ratios if r > 1.0), + } + if any(p[2] is not None for p in pairs): + entry["flutter_bracket"] = [median([p[2] for p in pairs]), + median([p[1] for p in pairs])] + return entry + + +def build_report(platform_id, sides, runs, notes=None): + report = { + "schema_version": 1, + "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "platform": platform_id, + "load_average": load_average(), + "runs": runs, + "app": "flutter gallery (new_gallery), same Dart source on both sides", + "codenameone": summarise(sides["codenameone"]), + "flutter": summarise(sides["flutter"]), + } + if notes: + report["notes"] = notes + report["verdict"] = verdict(report) + return report + + +def format_value(key, value): + if value is None: + return "--" + unit = METRIC_UNITS.get(key) + if unit == "bytes": + return "%.1f MB" % (value / 1024.0 / 1024.0) + if unit == "ms": + return "%.0f ms" % value + return str(value) + + +def render_markdown(reports, title="Flutter vs Codename One"): + """The PR comment body: one table per platform, plus a roll-up. + + Written for a reader who will not open the artifact, so every row states + both absolute numbers rather than only the ratio -- a 2x on a metric + nobody cares about reads the same as a 2x on one that matters, unless the + magnitudes are visible. + """ + lines = ["## %s" % title, ""] + if not reports: + lines.append("_No benchmark results were produced._") + return "\n".join(lines) + "\n" + + lines.append("Same application, built both ways: Flutter's AOT compile of " + "the gallery, and the identical Dart source transpiled to Java " + "on Codename One. Lower is better for every metric, so a ratio " + "above 1.00x means Codename One is ahead by that factor.") + lines.append("") + + for report in reports: + lines.append("### %s" % report["platform"]) + lines.append("") + load = report.get("load_average") + lines.append("_%d interleaved rounds%s_" % ( + report.get("runs", 0), + ("; host load %s" % ", ".join(str(x) for x in load)) if load else "")) + lines.append("") + lines.append("| Metric | Codename One | Flutter | Ratio | Winner |") + lines.append("| --- | ---: | ---: | ---: | --- |") + for key, label, _unit in METRICS: + entry = report["verdict"].get(key, {}) + if entry.get("status") != "measured": + lines.append("| %s | -- | -- | -- | not measured |" % label) + continue + flutter_cell = format_value(key, entry["flutter"]) + if entry.get("flutter_bracket"): + # Medians of each end over the paired rounds. + flutter_cell = "%s (%s-%s)" % ( + flutter_cell, format_value(key, entry["flutter_bracket"][0]), + format_value(key, entry["flutter_bracket"][1])) + elif key == BRACKET_METRIC and entry.get("statistic") != "paired_median": + lower = report["flutter"].get(BRACKET_LOWER) + if lower is not None: + # Both ends of the bracket, so the reader can see how much + # of the gap is measurement uncertainty rather than runtime. + flutter_cell = "%s (%s-%s)" % ( + flutter_cell, format_value(key, lower), + format_value(key, report["flutter"].get(BRACKET_METRIC))) + lines.append("| %s | %s | %s | %.2fx | %s |" % ( + label, + format_value(key, entry["codenameone"]), + flutter_cell, + entry["ratio"], + "Codename One" if entry["winner"] == "codenameone" else "Flutter")) + lines.append("") + for note in statistics_notes(report): + lines.append("> %s" % note) + lines.append("") + compute_block = render_compute(report) + if compute_block: + lines.append(compute_block) + lines.append("") + gate_line = render_gate(report) + if gate_line: + lines.append(gate_line) + lines.append("") + for note in report.get("notes", []) or []: + lines.append("> %s" % note) + if report.get("notes"): + lines.append("") + + wins, measured = tally(reports) + lines.append("**Codename One wins %d of %d measured metrics across %d platform(s).**" + % (wins, measured, len(reports))) + lines.append("") + return "\n".join(lines) + "\n" + + +def statistics_notes(report): + """What the start-up and memory rows' numbers are, stated under the table. + + Needed because the paired ratio is not the quotient of the two medians + shown beside it, and a reader who checks the arithmetic deserves to be + told why it does not divide out. + """ + notes = [] + verdict_ = report.get("verdict") or {} + paired = [] + for key, label, _unit in METRICS: + entry = verdict_.get(key, {}) + if entry.get("status") != "measured": + continue + if entry.get("statistic") == "paired_median": + paired.append("%d of %d rounds on %s" % (entry["rounds_won"], entry["rounds"], label.lower())) + elif entry.get("statistic") == "best_of": + notes.append("%s could not be paired round by round (%s), so its " + "figures and ratio are each side's best run, and one " + "undisturbed round can decide it." % (label, entry.get("fallback"))) + if paired: + notes.append("Start-up and memory at rest are judged round by round: the " + "ratio is the median of the per-round ratios, each from two " + "launches made back to back, and the times and sizes shown " + "are each side's median, so the ratio need not equal their " + "quotient. Codename One was ahead in %s." + % "; ".join(paired)) + entry = verdict_.get(BRACKET_METRIC, {}) + if entry.get("status") == "measured" and ( + entry.get("flutter_bracket") + or (entry.get("statistic") != "paired_median" + and report["flutter"].get(BRACKET_LOWER) is not None)): + notes.append("Start-up is bracketed: the two runtimes do not expose the " + "same event, so Flutter's figure is given as a range and " + "the ratio uses the end least favourable to Codename One%s." + % (", round by round" if entry.get("flutter_bracket") else "")) + return notes + + +def tally(reports): + wins = 0 + measured = 0 + for report in reports: + for key, _label, _unit in METRICS: + entry = report["verdict"].get(key, {}) + if entry.get("status") != "measured": + continue + measured += 1 + if entry["winner"] == "codenameone": + wins += 1 + return wins, measured + + +# ---------------------------------------------------------------------- +# The regression gate +# ---------------------------------------------------------------------- + +# Platforms where losing to Flutter is reported but does not fail the gate, each with +# the reason the comment prints. Only the head-to-head comparison is relaxed: a +# workload Codename One gets wrong or does not run still fails, and so does a move +# outside this platform's regression baselines. +# +# JavaScript is the one platform where a loss to Flutter is currently accepted: the +# browser runs Flutter's AOT-compiled Dart through dart2js/Wasm against our +# bytecode-to-JavaScript translation, and closing the compute gap there is not this +# benchmark's priority. It stays measured and shown so the gap remains visible. +HEAD_TO_HEAD_REPORT_ONLY = { + "javascript": "a loss to Flutter on the web is currently accepted", +} + + +def gating_behind(platform_id, behind): + """The part of `behind` that fails the gate on `platform_id`. + + Everywhere but a report-only platform that is all of it. On a report-only + platform it is only Codename One's own compute failures (a wrong or missing + result), which are a correctness defect rather than a loss on speed or size. + """ + if platform_id not in HEAD_TO_HEAD_REPORT_ONLY: + return list(behind) + return [item for item in behind if item.get("failed")] + + +def check_behind(report): + """Every measured metric on which Codename One is BEHIND Flutter. + + The benchmark is a gate, not a scoreboard: a ratio under 1.00 on any metric + fails the job, exactly as a regression against the baseline does. It needs + no baseline and no tolerance -- both sides are built from the same Dart + source and measured interleaved on the same runner, so a slow runner slows + both and cannot turn a win into a loss or a loss into a win. + + A metric that was not measured is not judged here; the regression gate fails + the ones a baseline gates, and the report says which were not measured. + """ + findings = [] + compute = report.get("compute") or {} + # A workload Codename One got wrong or did not run, where the reference says + # which side failed, fails the gate on its own: attributing Flutter's failures + # is only honest if ours are held to the same standard. + for failure in (compute.get("verdict") or {}).get("failures", []): + if failure["side"] == "codenameone": + findings.append({ + "metric": COMPUTE_METRIC, + "label": "%s: %s (%s)" % (COMPUTE_LABEL, failure["name"], failure["how"]), + "codenameone": None, + "flutter": None, + "ratio": 0.0, + "behind": True, + "failed": failure, + }) + geomean = (compute.get("verdict") or {}).get("geomean") + if compute.get("status") == "measured" and geomean is not None and geomean < 1.0: + findings.append({ + "metric": COMPUTE_METRIC, + "label": COMPUTE_LABEL, + "codenameone": None, + "flutter": None, + "ratio": geomean, + "behind": True, + }) + for key, label, _unit in METRICS: + entry = (report.get("verdict") or {}).get(key, {}) + if entry.get("status") != "measured": + continue + if entry["ratio"] < 1.0: + findings.append({ + "metric": key, + "label": label, + "codenameone": entry["codenameone"], + "flutter": entry["flutter"], + "ratio": entry["ratio"], + "behind": True, + }) + return findings + + +def render_gate(report): + """One line saying whether this platform's numbers were actually gated.""" + gate = report.get("gate") + if not gate: + return None + behind = report.get("behind") or [] + lost = ("; **BEHIND FLUTTER** on %s" % ", ".join(item["label"] for item in behind) + if behind else "") + accepted = report.get("behind_accepted") + if behind and accepted: + lost += " (reported, not gated: %s)" % accepted + if gate.get("status") == "armed": + findings = report.get("regressions") or [] + line = ("**Gate:** %s against `scripts/flutter-bench/baseline`%s." + % ("OUTSIDE THE BASELINE" if findings else "within tolerance", lost)) + if gate.get("stale_overlay"): + line += (" This pull request's own overlay is stale (%s); the run was judged " + "without it, and fails until it is re-measured." % gate["stale_overlay"]) + advisories = report.get("advisories") or [] + if advisories: + line += (" Smaller than its baseline, which does not fail the gate: %s -- " + "rebaseline to tighten." % "; ".join( + "%s %+.2f%%" % (a["label"], a["moved_by"]) for a in advisories)) + if findings or gate.get("stale_overlay") or advisories: + line += (" To accept the move, download this platform's `baseline-.json` " + "artifact and run `%s`, then commit the overlay it writes." + % gate.get("fix", "flutter_baseline.py calibrate")) + return line + return ("**Gate: NOT JUDGED** -- %s%s." + % (gate.get("reason", "no baseline could be read"), lost)) + + +def render_regressions(platform_id, findings): + lines = [] + for item in findings: + if item.get("failed"): + lines.append("%s: Codename One %s on compute workload %s (checked against the host JVM)" + % (platform_id, + "gave a wrong result" if item["failed"]["how"] == FAILED_WRONG + else "did not run", + item["failed"]["name"])) + continue + if item.get("behind") and item["metric"] == COMPUTE_METRIC: + lines.append("%s: %s is %.2fx (the gate requires 1.00x or better)" + % (platform_id, item["label"], item["ratio"])) + continue + if item.get("behind"): + lines.append("%s: %s is %s against Flutter's %s (ratio %.2fx; the gate requires 1.00x or better)" + % (platform_id, item["label"], + format_value(item["metric"], item["codenameone"]), + format_value(item["metric"], item["flutter"]), + item["ratio"])) + continue + if item.get("verdict"): + # A regression-gate finding (flutter_baseline.judge). Imported here, not at + # the top: flutter_baseline imports this module. + import flutter_baseline + lines.append("%s: %s" % (platform_id, flutter_baseline.describe(item))) + continue + lines.append("%s: %s" % (platform_id, item.get("label", item.get("metric")))) + return lines + + +def run(cmd, **kwargs): + """subprocess.run with the arguments this harness always wants.""" + kwargs.setdefault("capture_output", True) + kwargs.setdefault("text", True) + return subprocess.run(cmd, **kwargs) diff --git a/scripts/flutter-bench/flutter_baseline.py b/scripts/flutter-bench/flutter_baseline.py new file mode 100644 index 00000000000..c0ce528dbb7 --- /dev/null +++ b/scripts/flutter-bench/flutter_baseline.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +"""The Flutter benchmark's regression baselines, on the ParparVM performance gate's model. + + python3 scripts/flutter-bench/flutter_baseline.py check [--base REF] [--pr N] + python3 scripts/flutter-bench/flutter_baseline.py fold + python3 scripts/flutter-bench/flutter_baseline.py calibrate [--pr N] [--reason TEXT] + [--all] [--metric M,...] [--per-cpu] RESULT-OR-CANDIDATE.json ... + python3 scripts/flutter-bench/flutter_baseline.py import-legacy --pr N --legacy DIR + [--original DIR] [--reason TEXT] + +Two different questions are asked of every run, and this file is only about the second: + + 1. Is Codename One at least level with Flutter on every metric? benchlib.check_behind, + head to head, no baseline. That is the benchmark's purpose and is untouched here. + 2. Did Codename One move against ITSELF? A regression gate against checked-in rows. + +THE LAYOUT (scripts/flutter-bench/baseline/), exactly vm/selfhost/perf-baseline's: + policy.json the global tolerance and floor per metric, and the metrics a + platform records without gating ("ungated", each with its reason) + base/.json the consolidated rows of one platform, = platform, or + platform@cpu-model for a ratio calibrated per runner CPU: + {"gallery": {metric: {"value": v, "runs": n, "tolerance"?: {"value": t}}}} + pr/.json ONE pull request's calibrations and rebaselines; folded nightly + +The overlay rules (calibrate / rebaseline with "from" and a per-row "reason", chaining, +combining, the fold that provably changes no verdict, `check --base` refusing base/ and +other pull requests' overlays) are vm/selfhost/perf_baseline.py's, run with the FLUTTER +layout below rather than copied. + +THE METRICS + code_bytes, install_bytes, wire_bytes + Codename One's own sizes, ABSOLUTE. They are deterministic for a source tree and + toolchain, so the global tolerance is 0 and any move -- either way -- fails until the + pull request that caused it rebaselines the row in its own overlay, saying why. + Keyed by platform alone: a size does not depend on the runner's CPU. + cold_start_ratio, idle_memory_ratio + Codename One OVER Flutter, from the same run: the median of the per-round ratios of + the interleaved launches (benchlib.paired_rounds), lower is better. The pinned + Flutter build measured beside it on the same runner is the unit of measure, as JDK 25 + is for the ParparVM gate, so a slow runner slows both halves of a round and cancels. + That is what runner_slowdown_discount and flutter_reference approximated one-sidedly + for start-up alone; a ratio does it for both directions and both metrics, so both are + gone. Keyed per runner CPU model once a platform has such rows (--per-cpu); until + then one plain-platform row judges every CPU, as macos-arm64 does in the ParparVM + gate. + +A run is judged per metric. A RATIO more than its tolerance away from its row in EITHER +direction fails (an improvement nobody wrote down can be given back unseen). A SIZE fails +only by growing past its tolerance; a shrink past it is reported as "improved -- +rebaseline to tighten" and does not fail (see judge() for why). A measured metric with no +row (uncalibrated) fails, and so does a row whose metric the run did not produce. +""" +import argparse +import importlib.util +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +import benchlib # noqa: E402 + +REPO = HERE.parents[1] +ROOT = HERE / 'baseline' +LEGACY_DIR = 'scripts/flutter-bench/baselines' # the retired one-file-per-platform layout +APP = 'gallery' + + +def _load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +perf_baseline = _load('perf_baseline', REPO / 'vm' / 'selfhost' / 'perf_baseline.py') +perf_gate = _load('perf_gate', REPO / 'vm' / 'selfhost' / 'perf-gate.py') +BaselineError = perf_baseline.BaselineError + +SIZE_METRICS = ('code_bytes', 'install_bytes', 'wire_bytes') +# The report's sample list each ratio is paired from (benchlib.PAIRED_METRICS key). +RATIO_METRICS = {'cold_start_ratio': 'cold_start_ms', 'idle_memory_ratio': 'idle_memory_bytes'} +METRIC_IDS = SIZE_METRICS + tuple(RATIO_METRICS) +LABELS = {'code_bytes': 'Executable code', 'install_bytes': 'Installed size', + 'wire_bytes': 'Download size (zipped)', + 'cold_start_ratio': 'Cold start (Codename One / Flutter)', + 'idle_memory_ratio': 'Memory at rest (Codename One / Flutter)'} + + +def _row_default(policy_map, key, bench, cores): + if policy_map is None: + return None + return {'value': policy_map.get(cores, 0.0)} + + +def _round_value(value): + return int(value) if float(value).is_integer() else round(value, 3) + + +FLUTTER = perf_baseline.Layout( + ROOT, ('value',), re.compile(r'^[a-z]+(@[a-z0-9-]+)?$'), LEGACY_DIR, + # policy.json, not base/: base/ may legitimately be empty between folds, and its + # absence on master must not let a later pull request pass as "the migration". + migration_marker='policy.json', + policy_metrics=METRIC_IDS, zero_tolerance=True, value_word='number', + row_default=_row_default, + widest_key=lambda bench, cores, metric: (bench, cores, metric), + round_value=_round_value, + # A size is deterministic up to a few bytes of packaging jitter; 5% steps would turn + # that into megabytes of slack. + tolerance_step=lambda key, bench, cores: 0.001 if cores in SIZE_METRICS else 0.05, + calibrate_command='flutter_baseline.py calibrate --pr N', + legacy_message='%s/ changed: those files are retired and nothing reads them. Express ' + 'the change as rows in scripts/flutter-bench/baseline/pr/.json ' + '(flutter_baseline.py calibrate --pr N, or import-legacy for a branch ' + 'that edited the old files) and leave the old files deleted.' % LEGACY_DIR) + + +# ---------------------------------------------------------------------- +# Loading, with the checks the shared engine cannot know about +# ---------------------------------------------------------------------- + +def _check_names(where, tree): + for key, bench, cores, _row in perf_baseline._rows(where, tree, FLUTTER): + if bench != APP: + raise BaselineError('%s: %s/%s: the only application is %r' % (where, key, bench, APP)) + if cores not in METRIC_IDS: + raise BaselineError('%s: %s: unknown metric %r (one of %s)' + % (where, key, cores, ', '.join(METRIC_IDS))) + if cores in SIZE_METRICS and '@' in key: + raise BaselineError('%s: %s %s: a size does not depend on the runner CPU; it ' + 'belongs under the plain platform key' % (where, key, cores)) + + +def _check_policy(policy): + ungated = policy.get('ungated', {}) + if not isinstance(ungated, dict): + raise BaselineError('policy.json: ungated must map platform to {metric: reason}') + for platform, metrics in ungated.items(): + if not isinstance(metrics, dict): + raise BaselineError('policy.json: ungated %s must map metric to its reason' % platform) + for metric, reason in metrics.items(): + if metric not in METRIC_IDS: + raise BaselineError('policy.json: ungated %s: unknown metric %r' % (platform, metric)) + if not isinstance(reason, str) or not reason.strip(): + raise BaselineError('policy.json: ungated %s %s needs the reason it is not ' + 'gated' % (platform, metric)) + unknown = set(policy) - {'tolerance', 'floor', 'ungated', 'notes'} + if unknown: + raise BaselineError('policy.json: unknown field(s) %s' % ', '.join(sorted(unknown))) + + +def _check_tree(root): + root = Path(root) + for path in sorted((root / 'base').glob('*.json')): + _check_names(str(path), {path.stem: perf_baseline._read_json(path)}) + for number, overlay in perf_baseline.load_overlays(root, FLUTTER): + for kind in ('calibrate', 'rebaseline'): + _check_names('pr/%d.json %s' % (number, kind), overlay.get(kind, {})) + + +def load(root=ROOT, exclude=None): + """The policy and the resolved rows, as perf_baseline.load gives them.""" + _check_tree(root) + data = perf_baseline.load(root, exclude, FLUTTER) + _check_policy(data['policy']) + data['ungated'] = data['policy'].get('ungated', {}) + return data + + +def check(root=ROOT, base_ref=None, number=None): + problems = perf_baseline.check(root, base_ref, number, FLUTTER) + try: + _check_tree(root) + _check_policy(perf_baseline.load_policy(root, FLUTTER)) + except BaselineError as error: + problems.append(str(error)) + return problems + + +# ---------------------------------------------------------------------- +# What a run measured, and which row judges it +# ---------------------------------------------------------------------- + +def ratio(report, metric): + """Codename One over Flutter for a ratio metric: the median of the per-round ratios, + or None when no round was measured by both sides. Flutter's figure in a round is the + end of its bracket least favourable to Codename One, as in the head-to-head verdict.""" + pairs, _reason = benchlib.paired_rounds(report.get('codenameone') or {}, + report.get('flutter') or {}, RATIO_METRICS[metric]) + if not pairs: + return None + return benchlib.median([ours / float(min(upper, lower) if lower is not None else upper) + for ours, upper, lower in pairs]) + + +def measurements(report): + """{metric: value} for every metric this run produced. Accepts a run's result JSON or + the candidate run_bench.py writes beside it (which carries them precomputed).""" + if 'values' in report: + return {k: v for k, v in report['values'].items() if v is not None} + out = {} + for metric in SIZE_METRICS: + value = (report.get('codenameone') or {}).get(metric) + if value: + out[metric] = value + for metric in RATIO_METRICS: + value = ratio(report, metric) + if value: + out[metric] = value + return out + + +def candidate(report): + """What run_bench.py --baseline-out writes: this run's measurements in the form the + calibrator reads, so re-baselining is `calibrate --pr N `.""" + return {'schema_version': 2, 'platform': report.get('platform'), + 'cpu': report.get('cpu'), 'generated_at': report.get('generated_at'), + 'values': {m: (round(v, 4) if m in RATIO_METRICS else v) + for m, v in measurements(report).items()}} + + +def row_key(rows, platform, cpu, metric): + """The key whose row judges `metric` for this runner, or None. + + A size: the plain platform. A ratio: platform@model when that model has a row; None + when the platform has per-model rows for the metric but not this model (another + microarchitecture's ratio must not judge it -- it fails as uncalibrated); otherwise + the plain platform.""" + has = lambda key: metric in rows.get(key, {}).get(APP, {}) + if metric in SIZE_METRICS: + return platform if has(platform) else None + cls = perf_gate.cpu_class(cpu) + if cls and has('%s@%s' % (platform, cls)): + return '%s@%s' % (platform, cls) + if any(k.startswith(platform + '@') and has(k) for k in rows): + return None + return platform if has(platform) else None + + +def judge(report, data): + """Every finding of the regression gate for one platform's run, as a list of dicts: + verdict 'regression', 'improved', 'uncalibrated' or 'missing'. Empty means within + tolerance. Ungated metrics are never findings.""" + platform = report.get('platform') + rows = data['platforms'] + ungated = data['ungated'].get(platform, {}) + values = measurements(report) + findings = [] + for metric in METRIC_IDS: + if metric in ungated: + continue + key = row_key(rows, platform, report.get('cpu'), metric) + row = rows.get(key, {}).get(APP, {}).get(metric) if key else None + value = values.get(metric) + if row is None: + if value is not None: + try: + key = key or _calibration_key(rows, platform, report.get('cpu'), metric) + except SystemExit: + key = '%s@' % platform + findings.append({'metric': metric, 'label': LABELS[metric], 'actual': value, + 'verdict': 'uncalibrated', 'key': key}) + continue + tolerance = row.get('tolerance', {}).get('value', data['tolerance'][metric]) + if value is None: + findings.append({'metric': metric, 'label': LABELS[metric], 'baseline': row['value'], + 'actual': None, 'tolerance': tolerance, 'verdict': 'missing', + 'key': key}) + continue + outcome = perf_gate.verdict(value, row['value'], tolerance, data['floor'][metric]) + if outcome != 'ok': + finding = {'metric': metric, 'label': LABELS[metric], 'baseline': row['value'], + 'actual': value, 'tolerance': tolerance, 'verdict': outcome, + 'key': key, + 'moved_by': round((value / float(row['value']) - 1.0) * 100.0, 2)} + if outcome == 'improved' and metric in SIZE_METRICS: + # A SIZE judged one-sided, unlike the ParparVM gate and unlike the ratio + # rows here: a shrink is reported ("rebaseline to tighten") but does not + # fail. flutter-bench.yml's paths filter means the pull requests that + # shrink the app -- core changes -- mostly never run this benchmark, so a + # two-sided size row would turn the NIGHTLY run on master red for a good + # change nobody can rebaseline in their own pull request. Growth still + # fails. Ratios stay two-sided: they move with the runtimes being + # compared, not with every core edit, and an unrecorded speed-up there is + # what a later change could silently give back. + finding['advisory'] = True + findings.append(finding) + return findings + + +def failing(findings): + """The findings that fail the leg: all but an advisory (a shrunk size).""" + return [f for f in findings if not f.get('advisory')] + + +def _calibration_key(rows, platform, cpu, metric, per_cpu=False): + if metric in SIZE_METRICS: + return platform + cls = perf_gate.cpu_class(cpu) + per_model = any(k.startswith(platform + '@') and metric in rows.get(k, {}).get(APP, {}) + for k in rows) + if cls and (per_cpu or per_model): + return '%s@%s' % (platform, cls) + if per_model or per_cpu: + raise SystemExit('%s: the runner reported no decodable CPU model (%r), and %s %s is ' + 'calibrated per CPU model; fix perf-gate.cpu_model() for this runner' + % (platform, cpu, platform, metric)) + return platform + + +def describe(item): + """One line for a gate finding.""" + metric = item['metric'] + + def fmt(value): + if value is None: + return '--' + if metric in SIZE_METRICS: + return '{:,} bytes'.format(int(value)) + return '%.3fx' % value + + if item['verdict'] == 'uncalibrated': + return ('%s has no baseline row (%s); measured %s' + % (item['label'], item['key'], fmt(item['actual']))) + if item['verdict'] == 'missing': + return ('%s was not measured, but the baseline gates it at %s' + % (item['label'], fmt(item['baseline']))) + return ('%s %s: %s against a baseline of %s (%+.2f%%, tolerance %s%%)' + % (item['label'], 'REGRESSED' if item['verdict'] == 'regression' + else 'improved -- rebaseline to tighten (not a failure)' if item.get('advisory') + else 'IMPROVED (rebaseline it)', fmt(item['actual']), fmt(item['baseline']), + item['moved_by'], ('%g' % (item['tolerance'] * 100)))) + + +def fix_command(number, findings, artifact): + moved = any(f['verdict'] in ('regression', 'improved') for f in findings) + # (an advisory shrink counts: tightening the row rebaselines it, which needs a reason) + return ('python3 scripts/flutter-bench/flutter_baseline.py calibrate --pr %s%s %s' + % (number or '', ' --reason ""' if moved else '', + artifact)) + + +# ---------------------------------------------------------------------- +# Calibration: the shared arithmetic, fed this benchmark's runs +# ---------------------------------------------------------------------- + +def collect(paths, rows, ungated, only=None, per_cpu=False): + """{key: {(app, metric): {'value': [one value per run]}}}, each run feeding the row + that judged it (or the row it would calibrate).""" + runs = {} + for path in paths: + report = json.loads(Path(path).read_text()) + if report.get('status') not in (None, 'measured'): + raise SystemExit('%s was not measured (%s); calibrate only from complete runs' + % (path, report.get('reason'))) + platform = report.get('platform') + if not platform: + raise SystemExit('%s names no platform' % path) + for metric, value in measurements(report).items(): + if metric not in METRIC_IDS or (only and metric not in only) \ + or metric in ungated.get(platform, {}): + continue + key = None if per_cpu and metric in RATIO_METRICS else \ + row_key(rows, platform, report.get('cpu'), metric) + if key is None: + key = _calibration_key(rows, platform, report.get('cpu'), metric, per_cpu) + runs.setdefault(key, {}).setdefault((APP, metric), {'value': []})['value'] \ + .append(value) + return runs + + +def calibrate(root, number, paths, reason=None, everything=False, only=None, per_cpu=False): + context = perf_baseline.calibration_context(root, number, FLUTTER) + policy = context['policy'] + _check_policy(policy) + runs = collect(paths, context['judged'], policy.get('ungated', {}), only, per_cpu) + plan_calibrate, plan_rebaseline = perf_baseline.plan_calibration( + context, runs, perf_gate.verdict, everything=everything) + return perf_baseline.finish_calibration(root, context, plan_calibrate, plan_rebaseline, + reason) + + +# ---------------------------------------------------------------------- +# The retired layout: baselines/.json +# ---------------------------------------------------------------------- + +def legacy_rows(files, policy): + """The rows a set of old baselines/.json files expresses, and what it cannot. + + Sizes carry over exactly: value, and a tolerance where it differs from the policy's. + Absolute start-up and memory figures have no equivalent -- the gate now judges a + ratio against Flutter -- so they are returned separately, as the metrics the old file + GATED (a tolerance present) and the ones it recorded without gating.""" + rows, gated, recorded = {}, {}, {} + for platform, legacy in sorted(files.items()): + values = legacy.get('codenameone', {}) + tolerances = legacy.get('tolerances', {}) + for metric in SIZE_METRICS: + if values.get(metric) is None: + continue + if tolerances.get(metric) is None: + raise BaselineError('%s %s is recorded but not gated in the old file; the ' + 'new layout gates every size' % (platform, metric)) + row = {'value': values[metric], 'runs': 1} + if tolerances[metric] > policy['tolerance'][metric]: + row['tolerance'] = {'value': tolerances[metric]} + rows.setdefault(platform, {}).setdefault(APP, {})[metric] = row + for old, new in (('cold_start_ms', 'cold_start_ratio'), + ('idle_memory_bytes', 'idle_memory_ratio')): + if values.get(old) is not None: + (gated if tolerances.get(old) is not None else recorded) \ + .setdefault(platform, []).append(new) + return rows, gated, recorded + + +def read_legacy_dir(directory): + return {p.stem: json.loads(p.read_text()) for p in sorted(Path(directory).glob('*.json'))} + + +def import_legacy(root, number, legacy, original=None, reason=None): + """A branch's edits to the old files, as its overlay. Only rows that differ between the + branch's copy (`legacy`) and the copy it started from (`original`) are its own; a size + the baseline lacks becomes a calibration, one it has a rebaseline from its current + value. Returns the overlay's path, or None if the branch changed no size.""" + current = load(root)['platforms'] + policy = perf_baseline.load_policy(root, FLUTTER) + new, _, _ = legacy_rows(legacy, policy) + old, _, _ = legacy_rows(original or {}, policy) + calibrate_, rebaseline = {}, {} + for key, bench, metric, row in perf_baseline._rows('legacy', new, FLUTTER): + if old.get(key, {}).get(bench, {}).get(metric) == row: + continue + existing = current.get(key, {}).get(bench, {}).get(metric) + if existing is None: + calibrate_.setdefault(key, {}).setdefault(bench, {})[metric] = row + elif existing != row: + moved = dict(row, **{'from': perf_baseline.from_row(existing, FLUTTER)}) + rebaseline.setdefault(key, {}).setdefault(bench, {})[metric] = moved + if not calibrate_ and not rebaseline: + return None + return perf_baseline.write_overlay(root, number, calibrate_, rebaseline, reason, FLUTTER) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split('\n')[0]) + parser.add_argument('--root', default=str(ROOT)) + sub = parser.add_subparsers(dest='command', required=True) + p = sub.add_parser('check', help='validate the baselines (and, with --base, a PR diff)') + p.add_argument('--base') + p.add_argument('--pr', type=int) + sub.add_parser('fold', help='fold every overlay into base/ (nightly, on master)') + p = sub.add_parser('calibrate', help="write a pull request's overlay from runs") + p.add_argument('--pr', type=int) + p.add_argument('--reason') + p.add_argument('--all', action='store_true', + help='rebaseline every measured row, not only those out of tolerance') + p.add_argument('--metric', help='comma-separated metric ids to take from the runs') + p.add_argument('--per-cpu', action='store_true', + help='calibrate ratio rows per runner CPU model (platform@model)') + p.add_argument('results', nargs='+') + p = sub.add_parser('import-legacy', help="convert a branch's baselines/*.json edits") + p.add_argument('--pr', type=int) + p.add_argument('--reason') + p.add_argument('--legacy', required=True, help="the branch's baselines/ directory") + p.add_argument('--original', help='the baselines/ directory the branch started from') + args = parser.parse_args(argv) + root = Path(args.root) + try: + if args.command == 'check': + problems = check(root, args.base, args.pr) + for problem in problems: + print('flutter-baseline: ' + problem) + if problems: + return 1 + print('flutter-baseline: OK') + elif args.command == 'fold': + _check_tree(root) + folded = perf_baseline.fold(root, FLUTTER) + print('flutter-baseline: folded %s' % (', '.join('pr/%d.json' % n for n in folded) + if folded else 'nothing')) + elif args.command == 'calibrate': + number = args.pr or perf_baseline.pr_number() + if number is None: + raise SystemExit('No pull request number: pass --pr N') + only = set(args.metric.split(',')) if args.metric else None + if only and only - set(METRIC_IDS): + raise SystemExit('unknown metric(s): %s' % ', '.join(sorted(only - set(METRIC_IDS)))) + calibrate(root, number, args.results, args.reason, args.all, only, args.per_cpu) + elif args.command == 'import-legacy': + number = args.pr or perf_baseline.pr_number() + if number is None: + raise SystemExit('No pull request number: pass --pr N') + path = import_legacy(root, number, read_legacy_dir(args.legacy), + read_legacy_dir(args.original) if args.original else None, + args.reason) + print('flutter-baseline: %s' % (path or 'the branch changed no size row')) + except BaselineError as error: + print('flutter-baseline: ' + str(error)) + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/flutter-bench/platforms.py b/scripts/flutter-bench/platforms.py new file mode 100644 index 00000000000..143b4a5bf1d --- /dev/null +++ b/scripts/flutter-bench/platforms.py @@ -0,0 +1,1067 @@ +#!/usr/bin/env python3 +"""Per-platform halves of the Flutter-vs-Codename One benchmark. + +Everything that differs between platforms is here, behind one small interface: +how an artifact is located and sized, how the application is launched, and how +its memory is read while it sits idle. The orchestration, statistics and +reporting are platform independent and live in `benchlib.py`. + +An adapter that cannot run reports WHY through `available()`, and the harness +records that reason instead of silently producing a partial comparison. A +benchmark that quietly drops a platform reads exactly like one that measured it +and found no difference. + +Which adapters have actually been exercised is recorded per class in +`EXERCISED`, and printed by `run_bench.py --list`. That distinction is worth +keeping honest: an unexercised adapter is a proposal, not a measurement. +""" + +import glob +import os +import platform as _host +import re +import shutil +import queue +import subprocess +import threading +import time + +import benchlib + +# Which line ends the start-up clock on each side. +# +# Getting this pairing right is the whole difficulty of the start-up metric, +# and it is easy to get wrong in our own favour. +# +# Flutter's FIRST frame is a warm-up frame: runApp schedules it before the root +# widget is attached, so it paints a couple of dozen elements in ~10ms. Timing +# to that measures nothing. The gallery therefore also prints FIRSTCONTENT, the +# first frame after its element tree stops growing. +# +# But FIRSTCONTENT is still not the event to compare against. It is a UI-thread +# post-frame callback that runs BEFORE that frame is rasterised, whereas +# Codename One's FIRSTFRAME fires once the form is on screen. Comparing the two +# charges one runtime for rasterising its first screen and not the other -- +# main_bench.dart says so in as many words, and the harness this replaces +# compared exactly that pair, which flattered Codename One. +# +# So the comparison is BRACKETED. RASTERDONE is printed when the content +# frame's timings arrive, which is after its raster completed and is therefore +# an upper bound; FIRSTCONTENT is the lower bound. Flutter's true +# present-complete lies between them, and both ends are reported rather than +# one being picked. +# +# Except where the app can say exactly where. Off the web RASTERDONE carries +# presentedAgoUs, how long before the line was printed the content frame's +# raster finished, read from the engine's own FrameTiming. The time the line +# appears minus that is the moment Flutter's first content frame was on screen +# -- the same event Codename One's marker reports -- so the figure is a point, +# not a bracket. The bracket's upper end was ~1s late on every desktop run, +# because a release engine batches timing reports, and its lower end is two +# frames late, because FIRSTCONTENT waits for the tree to stay the same size +# across two more frames; neither was the event being compared. +# +# BENCH:FIRSTFRAME-INVALID is deliberately NOT matched: the application prints +# it instead of the marker when its first frame reported errors, so a run whose +# widget tree failed half way through layout times out here rather than +# recording the much faster time it took to paint a broken screen. +MARKERS = { + "codenameone": re.compile(r"BENCH:FIRSTFRAME(?!-INVALID)"), + "flutter": re.compile(r"BENCH:RASTERDONE"), +} + +# The optimistic end of the bracket, for the side that has two. +LOWER_BOUND_MARKERS = { + "flutter": re.compile(r"BENCH:FIRSTCONTENT"), +} + +# How long before its RASTERDONE line Flutter's content frame finished +# rasterising; see MARKERS. +_PRESENTED_AGO = re.compile(r"presentedAgoUs=(\d+)") + +ANDROID_WARMUP_LAUNCHES = int(os.environ.get("BENCH_ANDROID_WARMUP", "3")) +LAUNCH_TIMEOUT_S = float(os.environ.get("BENCH_LAUNCH_TIMEOUT", "90")) +SETTLE_S = float(os.environ.get("BENCH_SETTLE", "12")) + + +# Compute mode (the VM workloads) runs every workload with warm-ups and repeats +# inside the app, so it takes minutes, not seconds -- longest on the emulator. +COMPUTE_TIMEOUT_S = float(os.environ.get("BENCH_COMPUTE_TIMEOUT", "1800")) +# No single workload takes minutes -- a whole native run is seconds -- so a run +# that reports nothing new for this long has stalled, and waiting out the full +# timeout only delays saying which workload it stalled on. +COMPUTE_IDLE_S = float(os.environ.get("BENCH_COMPUTE_IDLE", "300")) +# The desktop request for compute mode: a file both apps check. The Codename One +# port has no System.getenv to read instead. On Windows the path resolves against +# the current drive, which the harness and both apps share. +COMPUTE_MARKER = os.path.join(os.path.abspath(os.path.join(os.sep, "tmp", "nat")), + "BENCH_COMPUTE") + + +class Unavailable(Exception): + """Raised by an adapter that cannot measure on this host.""" + + +class Adapter(object): + """One platform's half of the benchmark.""" + + id = None + label = None + # Whether this adapter has been run end to end against real builds. + exercised = False + + def available(self): + """(ok, reason). `reason` is shown in the report when ok is False.""" + raise NotImplementedError + + def artifact(self, side): + """The built application for `side`, as a path.""" + raise NotImplementedError + + def sizes(self, side, workdir): + """install/code/wire bytes for `side`.""" + path = self.artifact(side) + return { + "install_bytes": benchlib.tree_size(path), + "code_bytes": self.code_size(side), + "wire_bytes": benchlib.wire_size(path, workdir), + } + + def code_size(self, side): + """Executable bytes only, or None where the platform cannot say. + + Reported separately from installed size because the two answer + different questions: installed size includes assets, which are the + same file on both sides and therefore dilute the comparison, while + executable size is the runtime and the compiled application. + """ + return None + + def launch_and_time(self, side): + """(upper_ms, lower_ms, idle_memory_bytes) for one run.""" + raise NotImplementedError + + def run_compute(self, side): + """{workload: (checksum, ms)} from one compute-mode launch of `side`.""" + raise Unavailable("compute is not measured on this platform") + + @staticmethod + def _read_compute(reader, deadline, side): + """Collects BENCH:COMPUTE lines until BENCH:COMPUTE-DONE. + + A run that never says DONE -- a crash, a hang, a timeout -- is not a + result: a partial set would drop exactly the workloads that failed and + report a mean over the rest. + """ + results = {} + last = time.time() + while True: + now = time.time() + if now >= deadline: + raise Unavailable("the %s app did not finish its compute run within %ds " + "(%d workloads reported)" + % (side, COMPUTE_TIMEOUT_S, len(results))) + if now - last >= COMPUTE_IDLE_S: + raise Unavailable("the %s app stalled in its compute run: nothing for %ds " + "after %s (%d workloads reported)" + % (side, COMPUTE_IDLE_S, _last_workload(results), + len(results))) + line = reader.get(max(0.0, min(deadline, last + COMPUTE_IDLE_S) - now)) + if line is _LineReader.EOF: + raise Unavailable("the %s app exited before finishing its compute run " + "(%d workloads reported)" % (side, len(results))) + if line is None: + continue + if benchlib.COMPUTE_DONE in line: + return results + parsed = benchlib.parse_compute_line(line) + if parsed: + results[parsed[0]] = (parsed[1], parsed[2]) + last = time.time() + # Echoed, so the job log shows how far each app got and when. + print(" %s %s checksum=%s %dms" % (side, parsed[0], parsed[1], parsed[2]), + flush=True) + + def notes(self): + """Caveats that belong beside this platform's numbers. + + Carried into the report rather than left in this source file: a reader + looking at a table has no way to know that one of its rows means + something different from the others unless the table says so. + """ + return [] + + # -- shared helpers ------------------------------------------------- + + def _time_stream(self, side, reader, started): + """Times from `started` to the side's markers, reading from `reader`. + + The clock is started by the caller immediately before the process is + spawned, so it spans process creation too -- which is part of what a + user waits for and is not something either runtime can measure about + itself. + + Returns (upper_ms, lower_ms). `lower_ms` is None for a side that emits + only one marker, and the report then treats the bracket as a point. + + Both limits are enforced by the CLOCK, never by the child's output. + `reader` is a _LineReader, whose get() gives up after a timeout, so a + launch that hangs before its marker is abandoned at LAUNCH_TIMEOUT_S, + and an application that goes quiet after its last marker -- which is + what a healthy one does -- is let go SETTLE_S after its marker. With a + blocking readline both waited for one more line that might never come, + and the job sat until the workflow's own two-hour timeout. + + The idle period is counted from the MARKER, not from the launch: the + caller samples memory as soon as this returns, so a deadline measured + from launch gave a side that started in 10 s only 2 s to settle and one + that started in 13 s none at all -- the two sides were sampled at + different points in their lifecycle. The Android adapter sleeps SETTLE_S + after its launch returns for the same reason. LAUNCH_TIMEOUT_S bounds + only the wait for the marker, so it cannot cut a settle short either. + """ + marker = MARKERS[side] + lower_marker = LOWER_BOUND_MARKERS.get(side) + upper = None + lower = None + exact = False + deadline = started + LAUNCH_TIMEOUT_S + settle_until = None + while True: + now = time.time() + limit = deadline if settle_until is None else settle_until + if now >= limit: + break + line = reader.get(max(0.0, limit - now)) + if line is _LineReader.EOF: + break + if line is None: + continue # timed out; the checks above decide whether to stop + if lower is None and lower_marker and lower_marker.search(line): + lower = (time.time() - started) * 1000.0 + if upper is None and marker.search(line): + seen = time.time() + upper = (seen - started) * 1000.0 + settle_until = seen + SETTLE_S + ago = _PRESENTED_AGO.search(line) + if ago is not None: + presented = upper - int(ago.group(1)) / 1000.0 + # Only a time inside the launch is believed: a clock the + # engine and the timeline did not share would say so here. + if 0 < presented <= upper: + upper, exact = presented, True + if exact: + # The exact moment, so there is no bracket to report. + lower = None + return upper, lower + + +class _LineReader(object): + """A child's output as lines that can be waited for WITH A TIMEOUT. + + A pipe's readline() blocks until the child writes or exits, and nothing in + the standard library puts a timeout on it portably -- select() does not + work on pipes on Windows. So one daemon thread per stream does the blocking + read and hands lines over through a queue, and the reader waits on the + queue instead. + """ + + EOF = object() + + def __init__(self, stream, on_line=None): + self._lines = queue.Queue() + self._on_line = on_line + thread = threading.Thread(target=self._pump, args=(stream,)) + thread.daemon = True + thread.start() + + def _pump(self, stream): + try: + for line in iter(stream.readline, ""): + if self._on_line is not None: + self._on_line(line) + self._lines.put(line) + except (OSError, ValueError): + pass # the stream was closed under us; that is an end too + finally: + self._lines.put(_LineReader.EOF) + + def get(self, timeout): + """The next line, _LineReader.EOF at the end, or None on timeout.""" + try: + return self._lines.get(timeout=timeout) + except queue.Empty: + return None + + +# ---------------------------------------------------------------------- +# Desktop: a plain child process whose stdout the harness can read. +# ---------------------------------------------------------------------- + +class _ProcessAdapter(Adapter): + """Common shape for platforms where the app is a child process.""" + + def _executable(self, side): + raise NotImplementedError + + def _memory(self, pid): + raise NotImplementedError + + def run_compute(self, side): + exe = self._executable(side) + os.makedirs(os.path.dirname(COMPUTE_MARKER), exist_ok=True) + with open(COMPUTE_MARKER, "w") as handle: + handle.write("1") + try: + proc = subprocess.Popen( + [exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, env=dict(os.environ, BENCH_MARKERS="1")) + try: + return self._read_compute(_LineReader(proc.stdout), + time.time() + COMPUTE_TIMEOUT_S, side) + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + finally: + # Removed however the run ended: a marker left behind would turn every + # later timed launch into a compute run. + os.remove(COMPUTE_MARKER) + + def launch_and_time(self, side): + exe = self._executable(side) + started = time.time() + proc = subprocess.Popen( + [exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, env=dict(os.environ, BENCH_MARKERS="1")) + try: + upper, lower = self._time_stream(side, _LineReader(proc.stdout), started) + memory = self._memory(proc.pid) if upper is not None else None + return upper, lower, memory + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + +class MacOSAdapter(_ProcessAdapter): + """macOS / Mac Catalyst, both sides as .app bundles. + + This is the shape `benchcn1/tools/bench_compare.py` measured by hand, and + the only pairing whose numbers have been quoted so far. + """ + + id = "macos" + label = "macOS (Mac Catalyst)" + exercised = False + + def __init__(self, cn1_app, flutter_app): + self.apps = {"codenameone": cn1_app, "flutter": flutter_app} + + def available(self): + if _host.system() != "Darwin": + return False, "not a macOS host" + for side, path in self.apps.items(): + if not os.path.isdir(path): + return False, "missing %s build at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.apps[side] + + def _executable(self, side): + app = self.apps[side] + macos = os.path.join(app, "Contents", "MacOS") + entries = [e for e in glob.glob(os.path.join(macos, "*")) + if os.access(e, os.X_OK)] + if not entries: + raise Unavailable("no executable in %s" % macos) + return entries[0] + + def code_size(self, side): + return benchlib.macho_code_size(self.apps[side]) + + def _memory(self, pid): + """Physical footprint, not RSS. + + `ps -o rss` counts every resident page including clean, file-backed + ones the kernel is free to evict, so it moves with whatever else the + machine is doing. Measured on one unchanged binary in one state it + reported 151MB, 207MB and 219MB within a single afternoon, while + phys_footprint for that state was stable -- and phys_footprint is the + number Apple's own memory limits are enforced against. + """ + out = benchlib.run(["vmmap", "--summary", str(pid)]) + for line in out.stdout.splitlines(): + if line.startswith("Physical footprint:"): + return _parse_bytes(line.split(":", 1)[1].strip()) + return None + + +class LinuxAdapter(_ProcessAdapter): + """Linux desktop: Flutter's `linux` bundle against Codename One's NATIVE + Linux port (ParparVM to an ELF against GTK3/Cairo), not the JVM build. + + NOT YET EXERCISED. The mechanics are straightforward -- both sides are + ELF executables that print to stdout, and /proc reports memory exactly -- + but no run has produced numbers, so treat this adapter as a proposal + until CI has published one. + """ + + id = "linux" + label = "Linux" + exercised = False + + def __init__(self, cn1_app, flutter_app): + self.apps = {"codenameone": cn1_app, "flutter": flutter_app} + + def available(self): + if _host.system() != "Linux": + return False, "not a Linux host" + if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): + # Measured on this project: the JavaSE simulator needs a real + # display and hangs in Display.init(null) without one, so a + # headless runner has to bring up Xvfb rather than pass a + # headless flag. + return False, "no display; start Xvfb before measuring" + for side, path in self.apps.items(): + if not os.path.exists(path): + return False, "missing %s build at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.apps[side] + + def _executable(self, side): + return _launchable(self.apps[side], _is_linux_executable, "ELF executable") + + def code_size(self, side): + """Every ELF in the artifact: see benchlib.elf_code_size for why.""" + return benchlib.elf_code_size(self.apps[side]) + + def _memory(self, pid): + """VmRSS from /proc, which is what Linux memory limits act on.""" + try: + with open("/proc/%d/status" % pid) as handle: + for line in handle: + if line.startswith("VmRSS:"): + return _parse_bytes(line.split(":", 1)[1].strip()) + except OSError: + return None + return None + + +def _launchable(artifact, is_executable, kind): + """The one binary to launch in a desktop artifact. + + The recipes hand over what each toolchain produces, which is a DIRECTORY: + Flutter's Linux bundle and Windows Release folder, and the result folder + of Codename One's native builders. Popen cannot launch a directory, so + accepting only an executable file reported every desktop side as having no + executable at all. A file is still taken as-is. + + Top level only, deliberately: the libraries each runtime ships beside its + launcher live in subfolders or carry library names, and more than one + candidate is reported rather than guessed at -- launching the wrong one + would time something that is not the application. + """ + if os.path.isfile(artifact): + if is_executable(artifact): + return artifact + raise Unavailable("%s is not an %s" % (artifact, kind)) + if not os.path.isdir(artifact): + raise Unavailable("no artifact at %s" % artifact) + found = sorted(os.path.join(artifact, name) for name in os.listdir(artifact) + if is_executable(os.path.join(artifact, name))) + if len(found) == 1: + return found[0] + if not found: + raise Unavailable("no %s in %s" % (kind, artifact)) + raise Unavailable("more than one %s in %s, refusing to guess: %s" + % (kind, artifact, ", ".join(os.path.basename(f) for f in found))) + + +def _is_linux_executable(path): + """An ELF regular file with the execute bit that is not a shared library. + + The name decides the library case, because the ELF type cannot: a modern + position-independent executable is ET_DYN exactly like a .so, and builds + commonly leave the execute bit set on libraries too. + """ + name = os.path.basename(path) + if not os.path.isfile(path) or os.path.islink(path) or not os.access(path, os.X_OK): + return False + if name.endswith(".so") or ".so." in name: + return False + try: + with open(path, "rb") as handle: + return handle.read(4) == b"\x7fELF" + except OSError: + return False + + +def _is_windows_executable(path): + return os.path.isfile(path) and path.lower().endswith(".exe") + + +class WindowsAdapter(_ProcessAdapter): + """Windows desktop. + + NOT YET EXERCISED. + """ + + id = "windows" + label = "Windows" + exercised = False + + def __init__(self, cn1_app, flutter_app): + self.apps = {"codenameone": cn1_app, "flutter": flutter_app} + + def available(self): + if os.name != "nt": + return False, "not a Windows host" + for side, path in self.apps.items(): + if not os.path.exists(path): + return False, "missing %s build at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.apps[side] + + def _executable(self, side): + return _launchable(self.apps[side], _is_windows_executable, ".exe") + + def code_size(self, side): + """Every PE image in the artifact: see benchlib.pe_code_size for why.""" + return benchlib.pe_code_size(self.apps[side]) + + def _memory(self, pid): + out = benchlib.run([ + "powershell", "-NoProfile", "-Command", + "(Get-Process -Id %d).WorkingSet64" % pid]) + try: + return int(out.stdout.strip()) + except (TypeError, ValueError): + return None + + +# ---------------------------------------------------------------------- +# iOS simulator: launched through simctl, which hands back a host pid. +# ---------------------------------------------------------------------- + +class IOSAdapter(Adapter): + """iOS, from RELEASE device bundles. + + Sizes only, and the reason is not a limitation of this harness. + + `flutter build ios --simulator --release` is refused outright by the + Flutter tool -- "Release mode is not supported for simulators" -- because + Dart cannot AOT-compile for the simulator, which runs a JIT debug engine. + So a simulator comparison puts Flutter's debug build against a Codename One + release build, and every number it produces is meaningless: the debug + engine carries the whole JIT, is several times larger, and starts on a + completely different path. + + What CI can do without provisioning is build BOTH sides for a device in + release and size them. `--no-codesign` leaves an unsigned .app that is + byte-accurate for sizing. Start-up and memory need the application to + actually run, which needs a signed build on real hardware, so this adapter + reports them as not measured rather than substituting a simulator figure. + + Pass `--ios-udid` for a real attached device to get timings too. + """ + + id = "ios" + label = "iOS (release, device bundle)" + exercised = False + + def __init__(self, udid, bundles, apps, renderer=None, device=False): + self.udid = udid + self.bundles = bundles + self.apps = apps + self.renderer = renderer + self.device = device + if renderer: + self.id = "ios-%s" % renderer + self.label = "iOS (release, %s)" % renderer + + def available(self): + if not shutil.which("xcrun"): + return False, "xcrun not on PATH" + for side, path in self.apps.items(): + if not os.path.isdir(path): + return False, "missing %s build at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.apps[side] + + def code_size(self, side): + return benchlib.macho_code_size(self.apps[side]) + + def run_compute(self, side): + raise Unavailable( + "iOS compute is not measured: a device build needs signed hardware, and on the " + "simulator Flutter runs its JIT debug engine rather than AOT code. The macOS row " + "runs the same ParparVM and Dart AOT compilers on the same Apple silicon.") + + def launch_and_time(self, side): + if not self.device: + raise Unavailable( + "iOS start-up and memory are not measured here: Flutter cannot " + "AOT-compile for the simulator, so a simulator run would time a " + "JIT debug build against a release build. Sizes come from " + "release device bundles; timings need signed hardware.") + bundle = self.bundles[side] + benchlib.run(["xcrun", "simctl", "terminate", self.udid, bundle]) + started = time.time() + proc = subprocess.Popen( + ["xcrun", "simctl", "launch", "--console-pty", self.udid, bundle], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) + # simctl prints ": " on its own line. Reading the pid from + # there beats searching the process table: the table lookup has to guess + # at the executable's name, and both sides of this benchmark are + # deliberately similar applications. + pid_line = re.compile(r"^%s:\s*([0-9]+)\s*$" % re.escape(bundle)) + seen = {} + + def note_pid(line): + match = pid_line.match(line.strip()) + if match: + seen["pid"] = int(match.group(1)) + + try: + upper, lower = self._time_stream( + side, _LineReader(proc.stdout, on_line=note_pid), started) + memory = None + if upper is not None: + memory = self._memory(bundle, seen.get("pid")) + return upper, lower, memory + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + benchlib.run(["xcrun", "simctl", "terminate", self.udid, bundle]) + + def _memory(self, bundle, pid=None): + if pid is None: + pid = self._pid_of(bundle) + if pid is None: + return None + out = benchlib.run(["vmmap", "--summary", str(pid)]) + for line in out.stdout.splitlines(): + if line.startswith("Physical footprint:"): + return _parse_bytes(line.split(":", 1)[1].strip()) + return None + + def _pid_of(self, bundle): + """Fallback pid lookup, for a launch whose pid line was not seen. + + The executable name is the last path component of the installed + bundle. This is a guess and the stream-parsed pid is preferred. + """ + name = self.apps["codenameone"] if bundle == self.bundles["codenameone"] \ + else self.apps["flutter"] + exe = os.path.splitext(os.path.basename(name))[0] + out = benchlib.run(["pgrep", "-f", "%s.app/%s" % (exe, exe)]) + for token in out.stdout.split(): + try: + return int(token) + except ValueError: + continue + return None + + +# ---------------------------------------------------------------------- +# Android: adb does the launching, and reports start-up itself. +# ---------------------------------------------------------------------- + +class AndroidAdapter(Adapter): + """Android, both sides installed on one emulator or device. + + NOT YET EXERCISED. + + Start-up comes from `am start -W`, which reports TotalTime: the interval + the platform itself considers "until the activity was drawn". That is a + different clock from the desktop adapters' marker timing and the two are + not interchangeable -- but it is the clock Android's own tooling and Play + vitals use, and inventing a second one would make our number unreviewable. + + Memory is PSS from `dumpsys meminfo`, not RSS: on Android the runtime and + its shared libraries are mapped into every process, and only PSS divides + that shared cost correctly between them. + """ + + id = "android" + label = "Android" + exercised = False + + def __init__(self, serial, packages, activities, apks): + self.serial = serial + self.packages = packages + self.activities = activities + self.apks = apks + self._installed = set() + + def _adb(self, *args): + cmd = ["adb"] + if self.serial: + cmd += ["-s", self.serial] + return benchlib.run(cmd + list(args)) + + def available(self): + if not shutil.which("adb"): + return False, "adb not on PATH" + out = self._adb("get-state") + if out.returncode != 0 or "device" not in out.stdout: + return False, "no attached device or emulator" + for side, path in self.apks.items(): + if not os.path.exists(path): + return False, "missing %s apk at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.apks[side] + + # The ABI every size is reported for: what a phone is actually delivered. + SIZE_ABI = "arm64-v8a" + + def _entries(self, side): + import zipfile as _zip + try: + with _zip.ZipFile(self.apks[side]) as archive: + return archive.infolist() + except (OSError, _zip.BadZipFile): + return None + + @classmethod + def _other_abi(cls, name): + """Whether a zip entry is a native library for an ABI other than SIZE_ABI.""" + parts = name.split("/") + return len(parts) > 2 and parts[0] == "lib" and parts[1] != cls.SIZE_ABI + + def code_size(self, side): + """The dex and the SIZE_ABI native libraries: the runtime plus the app. + + One ABI, not every ABI in the file. `flutter build apk` produces a FAT + apk with the engine and the Dart image for arm64, armv7 and x86_64, and + a phone installs one of them -- Play delivers per-ABI splits. Summing all + three counted Flutter's code about three times over and reported a 13x + difference that no user would see. The rule is the same for both sides. + """ + entries = self._entries(side) + if entries is None: + return None + total = sum(i.file_size for i in entries + if not self._other_abi(i.filename) + and (i.filename.endswith(".dex") or i.filename.endswith(".so"))) + return total or None + + def delivered_size(self, side): + """The apk as delivered for SIZE_ABI: the file minus other ABIs' libraries. + + Each excluded entry costs its compressed bytes plus its local and central + directory headers (30 and 46 bytes, each carrying the name), so an apk + with no other ABIs comes out at exactly its file size. + """ + entries = self._entries(side) + if entries is None: + return None + excluded = sum(i.compress_size + 30 + 46 + 2 * len(i.filename.encode("utf-8")) + for i in entries if self._other_abi(i.filename)) + return os.path.getsize(self.apks[side]) - excluded + + def sizes(self, side, workdir): + # An apk is already a zip, so its size as delivered IS the download size. + delivered = self.delivered_size(side) + return { + "install_bytes": delivered, + "code_bytes": self.code_size(side), + "wire_bytes": delivered, + } + + def notes(self): + # Each note parenthesized: a list of bare adjacent literals is where a + # missing comma silently merges two items, so every boundary is explicit. + return [ + ("Android sizes are for the arm64-v8a slice a phone is delivered, on both " + "sides; Flutter's release apk carries three ABIs and would otherwise count " + "its engine about three times."), + ("Android start-up and memory come from an x86_64 emulator with a software " + "GPU. They compare the two runtimes on one machine; they are not phone " + "timings, and a renderer that leans on the GPU is penalised more there."), + ] + + def _install(self, side): + """Installs the APK that was sized, once per run, replacing any other. + + Launching without installing measured nothing on a fresh emulator -- + the package was simply not there -- and on a reused device it could + measure whatever older build happened to be installed under the same + package, beside the size of the one that was built. Uninstalling first + rather than `install -r` also clears a copy signed with another key, + which `-r` refuses to replace. + """ + if side in self._installed: + return + package = self.packages[side] + self._adb("uninstall", package) # absent is fine; that is the usual case + out = self._adb("install", "-t", self.apks[side]) + if out.returncode != 0 or "Success" not in (out.stdout or ""): + detail = ((out.stdout or "") + (out.stderr or "")).strip().splitlines() + raise Unavailable("could not install the %s apk: %s" + % (side, detail[-1] if detail else "adb returned %d" % out.returncode)) + # Compiled to its steady state BEFORE anything is timed. A fresh install + # is compiled in the background as it runs, so start-up fell across + # every run -- 2853, 888, 1209, 543, 418 ms in one of them -- and + # best-of-five picked whichever run the compiler had reached. The + # "best" moved 282 to 418 ms between runs of unchanged code and tripped + # a 25% gate. `speed` is the state an idle device optimizes an app to, + # applied to both sides (Flutter's own dex is small, its code is AOT). + compiled = self._adb("shell", "cmd", "package", "compile", "-m", "speed", "-f", package) + if compiled.returncode != 0 or "Success" not in (compiled.stdout or ""): + raise Unavailable("could not compile the %s app ahead of time: %s" + % (side, ((compiled.stdout or "") + (compiled.stderr or "")).strip())) + # Launches that are not timed: first-run work -- creating the app's + # data directory, first-launch caches -- belongs to installation, not + # to start-up. Three, not one: with a single warm-up both sides still + # fell across all five timed runs (Codename One 1065, 1362, 478, 406, + # 367 ms; Flutter 1993 to 1130), because the emulator's own caches + # keep warming after the app's code is compiled. + for _ in range(ANDROID_WARMUP_LAUNCHES): + self._adb("shell", "am", "start", "-W", "-n", "%s/%s" % (package, self.activities[side])) + self._adb("shell", "am", "force-stop", package) + self._installed.add(side) + + def launch_and_time(self, side): + self._install(side) + package = self.packages[side] + self._adb("shell", "am", "force-stop", package) + self._adb("shell", "logcat", "-c") + out = self._adb("shell", "am", "start", "-W", "-n", + "%s/%s" % (package, self.activities[side])) + cold = None + for line in out.stdout.splitlines(): + if line.startswith("TotalTime:"): + try: + cold = float(line.split(":", 1)[1].strip()) + except ValueError: + cold = None + time.sleep(SETTLE_S) + memory = self._memory(package) + self._adb("shell", "am", "force-stop", package) + # `am start -W` reports one number, so there is no bracket here. + return cold, None, memory + + def run_compute(self, side): + """One compute-mode launch, read back from logcat. + + Codename One publishes the launch intent's data URI as AppArg; Flutter's + FlutterActivity passes the dart_entrypoint_args extra to main(). Both + print to logcat (System.out / flutter), which is polled rather than + streamed so a stalled logcat pipe cannot hang the job. + """ + self._install(side) + package = self.packages[side] + self._adb("shell", "am", "force-stop", package) + self._adb("logcat", "-c") + component = "%s/%s" % (package, self.activities[side]) + if side == "codenameone": + self._adb("shell", "am", "start", "-n", component, "-d", "benchcompute://run") + else: + # --esal, not --esa: FlutterActivity reads the extra with + # getSerializableExtra and casts it to a List, which a String[] is not. + self._adb("shell", "am", "start", "-n", component, + "--esal", "dart_entrypoint_args", "compute") + started = time.time() + deadline = started + COMPUTE_TIMEOUT_S + results = {} + last = started + try: + while time.time() < deadline: + out = self._adb("logcat", "-d") + done = False + before = len(results) + for line in out.stdout.splitlines(): + if benchlib.COMPUTE_DONE in line: + done = True + parsed = benchlib.parse_compute_line(line) + if parsed and parsed[0] not in results: + results[parsed[0]] = (parsed[1], parsed[2]) + print(" %s %s checksum=%s %dms" + % (side, parsed[0], parsed[1], parsed[2]), flush=True) + if done: + return results + if len(results) > before: + last = time.time() + elif time.time() - last >= COMPUTE_IDLE_S: + raise Unavailable("the %s app stalled in its compute run: nothing for %ds " + "after %s (%d workloads reported)" + % (side, COMPUTE_IDLE_S, _last_workload(results), + len(results))) + # An app that is no longer running will never finish; say so now + # rather than waiting out the whole timeout. + if time.time() - started > 30 and not self._adb( + "shell", "pidof", package).stdout.strip(): + raise Unavailable("the %s app exited before finishing its compute " + "run (%d workloads reported)" % (side, len(results))) + time.sleep(5) + finally: + self._adb("shell", "am", "force-stop", package) + raise Unavailable("the %s app did not finish its compute run within %ds " + "(%d workloads reported)" % (side, COMPUTE_TIMEOUT_S, len(results))) + + def _memory(self, package): + out = self._adb("shell", "dumpsys", "meminfo", package) + for line in out.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("TOTAL PSS:"): + try: + return int(stripped.split(":")[1].split()[0]) * 1024 + except (IndexError, ValueError): + return None + if stripped.startswith("TOTAL"): + parts = stripped.split() + if len(parts) > 1 and parts[1].isdigit(): + return int(parts[1]) * 1024 + return None + + +# ---------------------------------------------------------------------- +# JavaScript: both sides are static bundles served over http. +# ---------------------------------------------------------------------- + +class JavaScriptAdapter(Adapter): + """The web build of each side, measured in headless Chrome. + + NOT YET EXERCISED. + + Size is the served bundle rather than an installed tree, which is the + honest analogue: nothing is installed, and what the user pays for is the + transfer. Memory is the JS heap, which is the only figure a page can + report about itself -- it excludes the renderer's own allocations, so it + understates both sides and must not be compared against a native number. + """ + + id = "javascript" + label = "JavaScript" + exercised = False + + def __init__(self, bundles): + self.bundles = bundles + + def available(self): + chrome = _find_chrome() + if not chrome: + return False, "no Chrome or Chromium on PATH" + for side, path in self.bundles.items(): + if not os.path.isdir(path): + return False, "missing %s bundle at %s" % (side, path) + return True, None + + def artifact(self, side): + return self.bundles[side] + + def code_size(self, side): + """Script bytes only: the runtime plus the compiled application.""" + total = 0 + for root, _dirs, files in os.walk(self.bundles[side]): + for name in files: + if name.endswith(".js") or name.endswith(".wasm"): + total += os.path.getsize(os.path.join(root, name)) + return total or None + + def launch_and_time(self, side): + raise Unavailable( + "the JavaScript adapter needs a Chrome DevTools driver; " + "sizes are reported and timings are not") + + def run_compute(self, side): + """The bundle served locally and opened in headless Chrome with + ?benchCompute=1; the page's console is read from Chrome's log on stderr.""" + import functools + import http.server + import tempfile + chrome = _find_chrome() + if not chrome: + raise Unavailable("no Chrome or Chromium on PATH") + if not os.path.isfile(os.path.join(self.bundles[side], "index.html")): + # A missing page would 404 and leave the run waiting out the timeout. + raise Unavailable("the %s bundle has no index.html to open" % side) + handler = functools.partial(_QuietHandler, directory=self.bundles[side]) + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + profile = tempfile.mkdtemp(prefix="bench-chrome-") + url = "http://127.0.0.1:%d/index.html?benchCompute=1" % server.server_address[1] + proc = subprocess.Popen( + [chrome, "--headless=new", "--no-sandbox", "--disable-gpu", + "--no-first-run", "--enable-logging=stderr", "--v=0", + "--user-data-dir=" + profile, url], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) + try: + return self._read_compute(_LineReader(proc.stdout), + time.time() + COMPUTE_TIMEOUT_S, side) + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + server.shutdown() + shutil.rmtree(profile, ignore_errors=True) + + +def _quiet_handler_base(): + import http.server + return http.server.SimpleHTTPRequestHandler + + +class _QuietHandler(_quiet_handler_base()): + """Serves a bundle without logging every request into the job log.""" + + def log_message(self, *args): + pass + + +def _last_workload(results): + """The workload a stalled run was on: the one after the last it reported.""" + order = benchlib.COMPUTE_WORKLOADS + done = [name for name in order if name in results] + if not done: + return "starting (before %s)" % order[0] + index = order.index(done[-1]) + return "%s (it was running %s)" % (done[-1], order[index + 1]) \ + if index + 1 < len(order) else done[-1] + + +def _find_chrome(): + for name in ("google-chrome", "chromium", "chromium-browser"): + found = shutil.which(name) + if found: + return found + mac = ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") + return mac if os.path.exists(mac) else None + + +def _parse_bytes(text): + """Parses the human sizes vmmap and /proc print: '123.4M', '5016 kB'.""" + text = text.strip() + match = re.match(r"([0-9]+(?:\.[0-9]+)?)\s*([KMGkmg]?)B?", text) + if not match: + return None + value = float(match.group(1)) + scale = {"": 1, "k": 1024, "K": 1024, + "m": 1024 ** 2, "M": 1024 ** 2, + "g": 1024 ** 3, "G": 1024 ** 3}[match.group(2)] + # /proc reports kB with no suffix letter in the number itself. + if match.group(2) == "" and text.lower().endswith("kb"): + scale = 1024 + return int(value * scale) diff --git a/scripts/flutter-bench/port_status.py b/scripts/flutter-bench/port_status.py new file mode 100755 index 00000000000..a0db3c70417 --- /dev/null +++ b/scripts/flutter-bench/port_status.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Folds benchmark results into one document for the website's data. + + scripts/flutter-bench/port_status.py --results 'out/result-*.json' \ + --out + +Writes ONE file, `/flutter_benchmark.json`. The nightly workflow publishes +it to the port-status-data branch as benchmarks/flutter.json (publish_benchmark.py), +and the website build resolves it into data/port_status_flutter_benchmark.json +(scripts/website/sync_port_status_reports.sh), the same road every port's own +status report travels. It is a separate document rather than a key on each port +report because it is not a port report: the per-port acceptance check would +rightly reject it, and the Port Status page's contract check asserts that page's +exact shape. + +The file records, per platform, both sides' figures and the ratio, plus the +provenance needed to know what produced them: the commit, the Flutter SDK +revision, and the host load at the time. +""" + +import argparse +import glob +import json +import os +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import benchlib # noqa: E402 + + +def collect(patterns): + reports = [] + for pattern in patterns: + for path in sorted(glob.glob(pattern)): + with open(path) as handle: + reports.append(json.load(handle)) + return reports + + +def to_port_status(reports, commit): + platforms = {} + for report in reports: + platform = report.get("platform") + if not platform: + continue + if report.get("status") != "measured": + # Recorded, not dropped. A platform missing from the page reads as + # "no difference found" when it means "never ran". + platforms[platform] = { + "status": "not measured", + "reason": report.get("reason", "no reason given"), + } + continue + metrics = {} + for key, label, unit in benchlib.METRICS: + entry = report["verdict"].get(key, {}) + if entry.get("status") != "measured": + metrics[key] = {"status": "not measured"} + continue + metrics[key] = { + "label": label, + "unit": unit, + "codenameone": entry["codenameone"], + "flutter": entry["flutter"], + "ratio": entry["ratio"], + "winner": entry["winner"], + } + platforms[platform] = { + "status": "measured", + "runs": report.get("runs"), + "load_average": report.get("load_average"), + "generated_at": report.get("generated_at"), + "metrics": metrics, + "regressions": report.get("regressions", []), + } + + wins, measured = benchlib.tally( + [r for r in reports if r.get("status") == "measured"]) + return { + "schema_version": 1, + "commit": commit, + "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "app": "flutter gallery (new_gallery), same Dart source on both sides", + "summary": {"codenameone_wins": wins, "metrics_measured": measured}, + "platforms": platforms, + } + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--results", nargs="+", required=True) + parser.add_argument("--out", required=True) + parser.add_argument("--commit", default=os.environ.get("GITHUB_SHA", "unknown")) + args = parser.parse_args(argv) + + reports = collect(args.results) + if not reports: + print("no results matched %s; nothing written" % args.results) + return 0 + + payload = to_port_status(reports, args.commit) + os.makedirs(args.out, exist_ok=True) + path = os.path.join(args.out, "flutter_benchmark.json") + with open(path, "w") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + handle.write("\n") + print("wrote %s (%d platform(s))" % (path, len(payload["platforms"]))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/flutter-bench/publish_benchmark.py b/scripts/flutter-bench/publish_benchmark.py new file mode 100755 index 00000000000..487a1f44f91 --- /dev/null +++ b/scripts/flutter-bench/publish_benchmark.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Codename One designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Codename One through http://www.codenameone.com/ if you +# need additional information or have any questions. +"""Publishes the Flutter benchmark to the port-status-data branch. + + scripts/flutter-bench/publish_benchmark.py + +The same branch, and the same compare-and-swap, that every port's status report +goes through: the website build fetches it before Hugo renders the Port Status +page (scripts/website/sync_port_status_reports.sh). Stored as +benchmarks/flutter.json, beside ports/, because it is not a port report and the +per-port acceptance check would rightly reject it. +""" + +import json +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "hellocodenameone", "conformance")) + +import publish_port_status # noqa: E402 + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import benchlib # noqa: E402 + +TARGET = "benchmarks/flutter.json" + + +def refusal(report): + """Why `report` must not replace the published one, or None if it may. + + Publishing REPLACES benchmarks/flutter.json, so the document has to be a + complete run. At least one platform must have been MEASURED: unmeasured + platforms are recorded in the document too, so "has platforms" is not + enough, and a night where every leg failed would otherwise publish "not + measured" everywhere over the last good numbers. And every platform must be + PRESENT: a matrix leg that dies before writing its result leaves no file, + the upload only warns about that, and port_status.py builds the document + from the files that exist -- so the failed platform would simply vanish + from the public table instead of keeping its last numbers. + """ + if report.get("schema_version") != 1: + return "unsupported schema_version %r" % report.get("schema_version") + present = report.get("platforms") or {} + missing = [p for p in benchlib.PLATFORM_IDS if p not in present] + if missing: + return "no result for %s; publishing would drop it from the page" % ", ".join(missing) + measured = [p for p in present.values() + if isinstance(p, dict) and p.get("status") == "measured"] + if not measured: + return "no measured platforms" + return None + + +def main(argv): + if len(argv) != 3: + print("usage: publish_benchmark.py ", file=sys.stderr) + return 2 + with open(argv[1], encoding="utf-8") as handle: + report = json.load(handle) + reason = refusal(report) + if reason: + print("Not publishing %s: %s." % (argv[1], reason), file=sys.stderr) + return 1 + try: + publish_port_status.publish(report, argv[2], target=TARGET, + message="Update the Flutter benchmark") + except (publish_port_status.ApiError, ValueError, KeyError, OSError) as error: + print("Flutter benchmark publication failed: %s" % error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/flutter-bench/run_bench.py b/scripts/flutter-bench/run_bench.py new file mode 100755 index 00000000000..77583e9b408 --- /dev/null +++ b/scripts/flutter-bench/run_bench.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Runs the Flutter-vs-Codename One benchmark for one platform. + + scripts/flutter-bench/run_bench.py --platform ios \ + --cn1-app /path/to/Bench.app --cn1-bundle com.example.bench \ + --flutter-app /path/to/Runner.app --flutter-bundle com.example.gallery \ + --json out/ios.json --markdown out/ios.md + + scripts/flutter-bench/run_bench.py --list # adapters and their state + scripts/flutter-bench/run_bench.py --render out/*.json --markdown all.md + +The runs are INTERLEAVED -- one Codename One run, one Flutter run, repeated -- +so a machine that gets busier partway through penalises both sides equally +instead of whichever one happened to run second. That is not a theoretical +concern on this project: two walkthrough recordings desynchronised and looked +like a timing regression, and the cause was a stray simulator holding the +machine at load 8. + +Exit status is 0 unless `--gate` was asked for and either Codename One is behind +Flutter on a metric (except on a platform in benchlib.HEAD_TO_HEAD_REPORT_ONLY, +where the loss is reported but only a wrong or missing compute result fails), or a +metric moved past its row in scripts/flutter-bench/baseline (flutter_baseline.py) +-- or has no row to be judged by. +""" + +import argparse +import glob +import json +import os +import sys +import tempfile + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import benchlib # noqa: E402 +import flutter_baseline # noqa: E402 +import platforms # noqa: E402 + +HERE = os.path.dirname(os.path.abspath(__file__)) +BASELINE_ROOT = str(flutter_baseline.ROOT) + + +def build_adapter(args): + """Constructs the adapter named by --platform from the supplied paths.""" + kind = args.platform + if kind in ("macos", "catalyst"): + return platforms.MacOSAdapter(args.cn1_app, args.flutter_app) + if kind == "linux": + return platforms.LinuxAdapter(args.cn1_app, args.flutter_app) + if kind == "windows": + return platforms.WindowsAdapter(args.cn1_app, args.flutter_app) + if kind.startswith("ios"): + renderer = kind.split("-", 1)[1] if "-" in kind else None + return platforms.IOSAdapter( + udid=args.ios_udid, + bundles={"codenameone": args.cn1_bundle, + "flutter": args.flutter_bundle}, + apps={"codenameone": args.cn1_app, "flutter": args.flutter_app}, + renderer=renderer, + device=bool(args.ios_udid)) + if kind == "android": + return platforms.AndroidAdapter( + serial=args.android_serial, + packages={"codenameone": args.cn1_bundle, + "flutter": args.flutter_bundle}, + activities={"codenameone": args.cn1_activity, + "flutter": args.flutter_activity}, + apks={"codenameone": args.cn1_app, "flutter": args.flutter_app}) + if kind == "javascript": + return platforms.JavaScriptAdapter( + {"codenameone": args.cn1_app, "flutter": args.flutter_app}) + raise SystemExit("unknown platform: %s" % kind) + + +def _booted_simulator(): + out = benchlib.run(["xcrun", "simctl", "list", "devices", "booted"]) + import re + match = re.search(r"([0-9A-F]{8}-[0-9A-F-]{27})", out.stdout or "") + return match.group(1) if match else None + + +def measure(adapter, runs, workdir): + """Sizes both sides, then times them in interleaved rounds.""" + sides = {} + for side in benchlib.SIDES: + entry = {"artifact": adapter.artifact(side)} + entry.update(adapter.sizes(side, workdir)) + # Each sample list carries the round it came from, so benchlib pairs + # the two sides' launches of the same round even when a launch failed + # on one side and the lists drifted out of step (paired_rounds). + for base in ("cold_start", "cold_start_lower", "idle_memory"): + entry[base + "_runs"] = [] + entry[base + "_rounds"] = [] + sides[side] = entry + + notes = [] + for index in range(runs): + for side in benchlib.SIDES: + try: + upper, lower, memory = adapter.launch_and_time(side) + except platforms.Unavailable as err: + if str(err) not in notes: + notes.append(str(err)) + continue + for base, value in (("cold_start", upper), ("cold_start_lower", lower), + ("idle_memory", memory)): + if value is not None: + sides[side][base + "_runs"].append(value) + sides[side][base + "_rounds"].append(index) + print(" run %d/%d %-12s cold=%s memory=%s" + % (index + 1, runs, side, + "--" if upper is None else "%7.0fms" % upper, + "--" if memory is None else + "%6.1fMB" % (memory / 1024.0 / 1024.0)), flush=True) + return sides, notes + + +def measure_compute(adapter, workdir=None): + """One compute-mode launch per side, scored by benchlib.compute_verdict. + + Each app repeats every workload itself (warm-ups, then the best of five), + so a single launch per side is a complete result. A side that cannot run + makes the whole section "not measured" with its reason: half a comparison + is not a comparison. + """ + results = {} + for side in benchlib.SIDES: + try: + results[side] = adapter.run_compute(side) + except platforms.Unavailable as err: + return {"status": "not measured", "reason": str(err)} + print(" compute %-12s %d workloads" % (side, len(results[side])), flush=True) + # Which side is right when they disagree, or when one produced nothing: + # the same source on the host JVM. See benchlib.compute_verdict. + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + reference = benchlib.reference_checksums( + repo_root, workdir or os.path.join(repo_root, "scripts", "flutter-bench", "out")) + print(" compute reference: %s" % ( + "%d workloads from the host JVM" % len(reference) if reference + else "unavailable (no JDK); disagreements stay unattributed"), flush=True) + return { + "status": "measured", + "codenameone": dict((k, list(v)) for k, v in results["codenameone"].items()), + "flutter": dict((k, list(v)) for k, v in results["flutter"].items()), + "reference": reference, + "verdict": benchlib.compute_verdict(results["codenameone"], results["flutter"], + reference), + } + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--platform") + # Interleaved rounds. Odd, so the per-round median benchlib judges start-up + # and memory by is one real round rather than the mean of two; nine rather + # than five so that two disturbed rounds -- a CI runner's first launch is + # routinely 2-3x the rest -- still leave the median among clean ones. A + # round cost 25-30 s on the hosted runners (two launches, each held + # SETTLE_S for its memory reading), so this adds about two minutes to legs + # that finish in 15-25 of their 120. + parser.add_argument("--runs", type=int, + default=int(os.environ.get("BENCH_RUNS", "9"))) + parser.add_argument("--cn1-app") + parser.add_argument("--flutter-app") + parser.add_argument("--cn1-bundle") + parser.add_argument("--flutter-bundle") + parser.add_argument("--cn1-activity", default=".MainActivity") + parser.add_argument("--flutter-activity", default=".MainActivity") + parser.add_argument("--ios-udid") + parser.add_argument("--android-serial") + parser.add_argument("--json") + parser.add_argument("--markdown") + # The platform's temp directory, not "/tmp": native Windows Python has no + # /tmp, and the zip wire_size writes there failed the Windows leg after both + # applications had built. + parser.add_argument("--workdir", default=tempfile.gettempdir()) + parser.add_argument("--gate", action="store_true", + help="fail when a metric regresses past its baseline, " + "or when there is no baseline to compare against") + parser.add_argument("--baseline-out", + help="write a baseline recorded from this run here") + parser.add_argument("--list", action="store_true", + help="list adapters and whether each has been exercised") + parser.add_argument("--render", nargs="*", + help="render markdown from existing result json files") + args = parser.parse_args(argv) + + if args.list: + return _list_adapters() + + if args.render is not None: + return _render(args) + + if not args.platform: + parser.error("--platform is required unless --list or --render is used") + + adapter = build_adapter(args) + ok, reason = adapter.available() + if not ok: + # A platform that cannot be measured is recorded as such rather than + # dropped. A benchmark that silently omits a platform reads exactly + # like one that measured it and found nothing to report. + report = { + "schema_version": 1, + "platform": adapter.id, + "status": "unavailable", + "reason": reason, + } + _write(args, report, [report]) + print("%s: not measured -- %s" % (adapter.id, reason)) + # Under --gate an unmeasured platform FAILS. Returning success let a + # platform escape its gate just by not being measured -- the emulator + # dropping off adb, Chrome missing -- with the leg green and a regression + # riding through unseen. The result is still written above, so the + # comment says why. + return 1 if args.gate else 0 + + print("%s: %d interleaved rounds" % (adapter.label, args.runs)) + sides, notes = measure(adapter, args.runs, args.workdir) + notes.extend(adapter.notes()) + if not adapter.exercised: + notes.append( + "This platform's adapter has not been exercised end to end before; " + "treat the first published run as the thing under review.") + report = benchlib.build_report(adapter.id, sides, args.runs, notes) + report["status"] = "measured" + # The runner's CPU model, recorded with every result: a ratio row can be keyed by it + # (flutter_baseline.row_key), as the ParparVM gate's are. On Android this is the host + # the emulator runs on, which is the CPU both apps actually execute on. + report["cpu"] = flutter_baseline.perf_gate.cpu_model() + print("%s: compute workloads" % adapter.label) + report["compute"] = measure_compute(adapter, args.workdir) + + if args.baseline_out: + _ensure_dir(args.baseline_out) + with open(args.baseline_out, "w") as handle: + json.dump(flutter_baseline.candidate(report), handle, indent=2, sort_keys=True) + print("wrote baseline candidate %s" % args.baseline_out) + + findings = [] + stale = False + if args.gate: + findings, stale = _judge(report, adapter.id) + # Losing to Flutter fails the gate on its own, baseline or not: the + # benchmark exists to show Codename One ahead on every metric, and a + # loss that matched the baseline would otherwise pass as "no change". + behind = benchlib.check_behind(report) + report["behind"] = behind + accepted = benchlib.HEAD_TO_HEAD_REPORT_ONLY.get(adapter.id) + if accepted: + report["behind_accepted"] = accepted + findings = findings + benchlib.gating_behind(adapter.id, behind) + + _write(args, report, [report]) + # Printed AFTER the gate is decided, so the job log carries the gate line + # ("within tolerance", "REGRESSED", "NOT ARMED"); printed before, a green + # job could not show whether it had compared against anything. + print(benchlib.render_markdown([report])) + + for line in benchlib.render_regressions(adapter.id, report.get("advisories") or []): + print("note: " + line) + if report.get("advisories"): + print("note: to tighten: %s" % report["gate"]["fix"]) + if findings or stale: + print("\nGATE FAILED") + for line in benchlib.render_regressions(adapter.id, findings): + print(" " + line) + if stale: + print(" this pull request's own overlay is stale: %s" % report["gate"]["stale_overlay"]) + if report.get("regressions") or stale: + print(" to accept the move: %s" % report["gate"]["fix"]) + return 1 + return 0 + + +def _judge(report, platform_id): + """The regression gate: (findings, stale). Reads scripts/flutter-bench/baseline the + way the ParparVM gate reads its own (vm/selfhost/perf-gate.py), including what it does + when this pull request's own overlay went stale: judge without it and fail, rather than + refuse to judge and leave nothing to re-measure from.""" + number = flutter_baseline.perf_baseline.pr_number() + artifact = "baseline-%s.json" % platform_id + stale = None + try: + data = flutter_baseline.load(BASELINE_ROOT) + except flutter_baseline.BaselineError as error: + data = None + own = number and os.path.isfile(os.path.join(BASELINE_ROOT, "pr", "%d.json" % number)) + if own: + try: + data = flutter_baseline.load(BASELINE_ROOT, exclude=number) + stale = str(error) + except flutter_baseline.BaselineError: + data = None + if data is None: + # Overlays that contradict each other leave nothing to judge against: a + # failure, never a skip, and the comment says which. + report["gate"] = {"status": "refused", "reason": "flutter-baseline: %s" % error} + finding = {"metric": "baseline", "label": "the baselines could not be read: %s" + % error} + report["regressions"] = [finding] + return [finding], False + found = flutter_baseline.judge(report, data) + findings = flutter_baseline.failing(found) + for note in data.get("notes", []): + print("flutter-baseline note: %s" % note) + # Advisory findings (a shrunk size) are recorded and shown, never returned as failures. + report["regressions"] = findings + report["advisories"] = [f for f in found if f.get("advisory")] + report["gate"] = {"status": "armed", "baseline": "scripts/flutter-bench/baseline", + "pr": number, + "fix": flutter_baseline.fix_command(number, found, artifact)} + if stale: + report["gate"]["stale_overlay"] = stale + return findings, bool(stale) + + +def _write(args, report, reports): + if args.json: + _ensure_dir(args.json) + with open(args.json, "w") as handle: + json.dump(report, handle, indent=2, sort_keys=True) + print("wrote %s" % args.json) + if args.markdown: + _ensure_dir(args.markdown) + with open(args.markdown, "w") as handle: + handle.write(benchlib.render_markdown( + [r for r in reports if r.get("status") == "measured"])) + print("wrote %s" % args.markdown) + + +def _ensure_dir(path): + parent = os.path.dirname(os.path.abspath(path)) + if parent and not os.path.isdir(parent): + os.makedirs(parent) + + +def _render(args): + """Merges per-platform result files into one comment body.""" + paths = [] + for pattern in (args.render or []): + paths.extend(sorted(glob.glob(pattern))) + reports = [] + skipped = [] + for path in paths: + with open(path) as handle: + report = json.load(handle) + if report.get("status") == "measured": + reports.append(report) + else: + skipped.append(report) + body = benchlib.render_markdown(reports) + if skipped: + body += "\n_Not measured: %s._\n" % ", ".join( + "%s (%s)" % (r.get("platform"), r.get("reason", "no reason given")) + for r in skipped) + if args.markdown: + _ensure_dir(args.markdown) + with open(args.markdown, "w") as handle: + handle.write(body) + print("wrote %s" % args.markdown) + else: + print(body) + return 0 + + +def _list_adapters(): + rows = [ + ("macos", "macOS (Catalyst)", platforms.MacOSAdapter.exercised), + ("ios", "iOS (release bundles)", platforms.IOSAdapter.exercised), + ("android", "Android", platforms.AndroidAdapter.exercised), + ("linux", "Linux", platforms.LinuxAdapter.exercised), + ("windows", "Windows", platforms.WindowsAdapter.exercised), + ("javascript", "JavaScript", platforms.JavaScriptAdapter.exercised), + ] + print("%-12s %-22s %s" % ("id", "platform", "exercised end to end")) + for ident, label, exercised in rows: + print("%-12s %-22s %s" % (ident, label, "yes" if exercised else "NO")) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/flutter-bench/test_benchlib.py b/scripts/flutter-bench/test_benchlib.py new file mode 100644 index 00000000000..53829a4938b --- /dev/null +++ b/scripts/flutter-bench/test_benchlib.py @@ -0,0 +1,677 @@ +#!/usr/bin/env python3 +"""Tests for the parts of the benchmark that decide what a number MEANS. + + python3 scripts/flutter-bench/test_benchlib.py + +The arithmetic here is what turns two measurements into a published claim, so +it is the part worth pinning: a ratio computed the wrong way round, or a +regression gate that compares against the wrong side, produces a confident +number that is simply false. The platform adapters are not covered -- they need +real builds and real devices, and `run_bench.py --list` reports which of them +have ever been run rather than pretending otherwise. +""" + +import json +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import benchlib # noqa: E402 +import platforms # noqa: E402 + + +def _report(cn1, flutter): + return benchlib.build_report( + "test", + {"codenameone": dict(cn1), "flutter": dict(flutter)}, + runs=3) + + + +class PlatformIdsMatchTheWorkflow(unittest.TestCase): + """The plan job's matrix and benchlib.PLATFORM_IDS name the same platforms.""" + + def test_the_workflow_measures_exactly_the_known_platforms(self): + import re + workflow = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "..", ".github", "workflows", "flutter-bench.yml") + with open(workflow) as handle: + text = handle.read() + ids = re.findall(r'\{"platform": "([a-z]+)", "runner"', text) + self.assertTrue(ids, "the plan job's ALL list was not found") + self.assertEqual(list(benchlib.PLATFORM_IDS), ids) + + +class BehindFlutterGateTest(unittest.TestCase): + """Losing to Flutter on any measured metric fails the gate by itself.""" + + def test_a_loss_on_any_metric_is_a_finding(self): + report = _report({"install_bytes": 200, "code_bytes": 50}, + {"install_bytes": 100, "code_bytes": 100}) + found = benchlib.check_behind(report) + self.assertEqual([f["metric"] for f in found], ["install_bytes"]) + self.assertTrue(found[0]["behind"]) + self.assertIn("requires 1.00x", benchlib.render_regressions("macos", found)[0]) + + def test_winning_everything_passes(self): + report = _report({"install_bytes": 50, "code_bytes": 50}, + {"install_bytes": 100, "code_bytes": 100}) + self.assertEqual(benchlib.check_behind(report), []) + + def test_a_tie_is_not_behind(self): + report = _report({"install_bytes": 100}, {"install_bytes": 100}) + self.assertEqual(benchlib.check_behind(report), []) + + def test_an_unmeasured_metric_is_not_judged_here(self): + report = _report({"install_bytes": 50}, {}) + self.assertEqual(benchlib.check_behind(report), []) + + def test_the_gate_line_names_what_we_lost(self): + report = _report({"install_bytes": 200}, {"install_bytes": 100}) + report["gate"] = {"status": "armed"} + report["behind"] = benchlib.check_behind(report) + self.assertIn("BEHIND FLUTTER", benchlib.render_gate(report)) + +class ReportOnlyHeadToHeadTest(unittest.TestCase): + """JavaScript reports a loss to Flutter without failing on it.""" + + def _losing(self): + report = _report({"install_bytes": 200}, {"install_bytes": 100}) + report["compute"] = {"status": "measured", + "verdict": benchlib.compute_verdict({"recursion": ("1", 300)}, + {"recursion": ("1", 100)})} + return report + + def test_javascript_is_report_only_and_says_why(self): + self.assertIn("javascript", benchlib.HEAD_TO_HEAD_REPORT_ONLY) + self.assertTrue(benchlib.HEAD_TO_HEAD_REPORT_ONLY["javascript"]) + + def test_a_javascript_loss_does_not_gate(self): + behind = benchlib.check_behind(self._losing()) + self.assertEqual(2, len(behind), "the losses are still found and reported") + self.assertEqual([], benchlib.gating_behind("javascript", behind)) + + def test_every_other_platform_still_gates_on_a_loss(self): + behind = benchlib.check_behind(self._losing()) + for platform in benchlib.PLATFORM_IDS: + if platform != "javascript": + self.assertEqual(behind, benchlib.gating_behind(platform, behind), platform) + + def test_a_wrong_result_still_fails_on_javascript(self): + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report["compute"] = {"status": "measured", "verdict": { + "failures": [{"side": "codenameone", "name": "recursion", "how": "wrong result"}]}} + gating = benchlib.gating_behind("javascript", benchlib.check_behind(report)) + self.assertEqual(["recursion"], [f["failed"]["name"] for f in gating]) + + def test_the_gate_line_says_the_loss_is_not_gated(self): + report = self._losing() + report["gate"] = {"status": "armed"} + report["behind"] = benchlib.check_behind(report) + report["behind_accepted"] = benchlib.HEAD_TO_HEAD_REPORT_ONLY["javascript"] + line = benchlib.render_gate(report) + self.assertIn("BEHIND FLUTTER", line) + self.assertIn("reported, not gated", line) + + +class ComputeTest(unittest.TestCase): + """The VM-workload comparison: same checksum or no ratio, geomean gates.""" + + def test_parses_both_apps_lines_whatever_the_log_prefix(self): + self.assertEqual(benchlib.parse_compute_line( + "I/System.out( 123): BENCH:COMPUTE name=recursion checksum=-9 ms=12"), + ("recursion", "-9", 12)) + self.assertEqual(benchlib.parse_compute_line( + 'INFO:CONSOLE(3)] "BENCH:COMPUTE name=intArithmetic checksum=5 ms=40"'), + ("intArithmetic", "5", 40)) + self.assertIsNone(benchlib.parse_compute_line("BENCH:FIRSTFRAME after=3ms")) + + def test_ratio_is_flutter_over_ours_and_the_mean_is_geometric(self): + ours = {"intArithmetic": ("1", 100), "recursion": ("2", 50)} + theirs = {"intArithmetic": ("1", 400), "recursion": ("2", 50)} + v = benchlib.compute_verdict(ours, theirs) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["intArithmetic"]["ratio"], 4.0) + self.assertEqual(rows["recursion"]["ratio"], 1.0) + self.assertEqual(v["geomean"], 2.0) + self.assertEqual(v["compared"], 2) + + def test_a_different_checksum_is_not_a_measurement(self): + v = benchlib.compute_verdict({"longArithmetic": ("1", 10), "recursion": ("2", 10)}, + {"longArithmetic": ("999", 1), "recursion": ("2", 20)}) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["longArithmetic"]["status"], "checksum mismatch") + self.assertEqual(v["compared"], 1) + self.assertEqual(v["geomean"], 2.0, "the mismatch must not reach the mean") + + def test_every_workload_has_a_row_even_when_missing(self): + v = benchlib.compute_verdict({}, {}) + self.assertEqual(len(v["workloads"]), len(benchlib.COMPUTE_WORKLOADS)) + self.assertNotIn("geomean", v) + + def test_a_geomean_behind_flutter_fails_the_gate(self): + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report["compute"] = {"status": "measured", + "verdict": benchlib.compute_verdict({"recursion": ("1", 200)}, + {"recursion": ("1", 100)})} + found = benchlib.check_behind(report) + self.assertEqual([f["metric"] for f in found], [benchlib.COMPUTE_METRIC]) + self.assertIn("0.50x", benchlib.render_regressions("linux", found)[0]) + + def test_the_comment_carries_the_compute_table(self): + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report.update(platform="macos", runs=5) + report["compute"] = {"status": "measured", + "verdict": benchlib.compute_verdict({"recursion": ("1", 50)}, + {"recursion": ("1", 100)})} + body = benchlib.render_markdown([report]) + self.assertIn("| recursion | 50 ms | 100 ms | 2.00x |", body) + self.assertIn("**Geometric mean**", body) + + def test_the_reference_names_the_side_with_the_wrong_result(self): + # Flutter web's intArithmetic: a result, but not Java's. + v = benchlib.compute_verdict({"intArithmetic": ("5", 300), "recursion": ("2", 10)}, + {"intArithmetic": ("7", 500), "recursion": ("2", 20)}, + {"intArithmetic": "5", "recursion": "2"}) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["intArithmetic"]["failed"], + [{"side": "flutter", "how": benchlib.FAILED_WRONG}]) + self.assertEqual(v["failures"], [{"name": "intArithmetic", "side": "flutter", + "how": benchlib.FAILED_WRONG}]) + self.assertEqual(v["geomean"], 2.0, "a failed workload is shown, not averaged") + + def test_a_side_that_produced_nothing_did_not_run(self): + v = benchlib.compute_verdict({"longArithmetic": ("9", 40)}, {}, + {"longArithmetic": "9"}) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["longArithmetic"]["failed"], + [{"side": "flutter", "how": benchlib.FAILED_NOT_RUN}]) + + def test_sides_that_agree_are_compared_whatever_the_jvm_says(self): + # A transcendental workload may differ from the host JVM's libm on both. + v = benchlib.compute_verdict({"mathTranscendental": ("4", 50)}, + {"mathTranscendental": ("4", 100)}, + {"mathTranscendental": "3"}) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["mathTranscendental"]["status"], "measured") + self.assertEqual(v["failures"], []) + + def test_without_a_reference_a_disagreement_stays_unattributed(self): + v = benchlib.compute_verdict({"recursion": ("1", 10)}, {"recursion": ("2", 10)}) + rows = dict((r["name"], r) for r in v["workloads"]) + self.assertEqual(rows["recursion"]["status"], "checksum mismatch") + self.assertNotIn("failed", rows["recursion"]) + + def test_our_wrong_result_fails_the_gate_and_theirs_does_not(self): + ref = {"intArithmetic": "5", "recursion": "2"} + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report["compute"] = {"status": "measured", "verdict": benchlib.compute_verdict( + {"intArithmetic": ("5", 10), "recursion": ("8", 10)}, + {"intArithmetic": ("7", 20), "recursion": ("2", 20)}, ref)} + found = benchlib.check_behind(report) + self.assertEqual(len(found), 1) + self.assertEqual(found[0]["failed"]["name"], "recursion") + self.assertIn("Codename One gave a wrong result on compute workload recursion", + benchlib.render_regressions("javascript", found)[0]) + + def test_the_table_says_who_failed(self): + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report.update(platform="javascript", runs=5) + report["compute"] = {"status": "measured", "verdict": benchlib.compute_verdict( + {"intArithmetic": ("5", 300), "longArithmetic": ("9", 40)}, + {"intArithmetic": ("7", 500)}, {"intArithmetic": "5", "longArithmetic": "9"})} + body = benchlib.render_markdown([report]) + self.assertIn("| intArithmetic | 300 ms | wrong result | Flutter: wrong result |", body) + self.assertIn("| longArithmetic | 40 ms | did not run | Flutter: did not run |", body) + + def test_an_unmeasured_compute_run_says_why(self): + report = _report({"install_bytes": 50}, {"install_bytes": 100}) + report.update(platform="ios", runs=5) + report["compute"] = {"status": "not measured", "reason": "needs signed hardware."} + body = benchlib.render_markdown([report]) + self.assertIn("Not measured: needs signed hardware._", body) + self.assertNotIn(".._", body) + + +class PublishRefusalTest(unittest.TestCase): + """Publishing replaces the public document, so only a complete run may.""" + + def _report(self, statuses): + return {"schema_version": 1, + "platforms": dict((p, {"status": s}) for p, s in statuses.items())} + + def setUp(self): + sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "hellocodenameone", "conformance")) + import publish_benchmark + self.refusal = publish_benchmark.refusal + + def test_a_complete_run_publishes(self): + statuses = dict((p, "measured") for p in benchlib.PLATFORM_IDS) + statuses["javascript"] = "not measured" + self.assertIsNone(self.refusal(self._report(statuses))) + + def test_a_platform_whose_leg_left_no_result_blocks_publication(self): + statuses = dict((p, "measured") for p in benchlib.PLATFORM_IDS if p != "windows") + reason = self.refusal(self._report(statuses)) + self.assertIsNotNone(reason) + self.assertIn("windows", reason) + + def test_every_platform_unmeasured_blocks_publication(self): + statuses = dict((p, "not measured") for p in benchlib.PLATFORM_IDS) + self.assertIn("no measured platforms", self.refusal(self._report(statuses))) + +class VerdictTest(unittest.TestCase): + + def test_lower_is_better_so_a_ratio_above_one_is_our_win(self): + report = _report( + {"install_bytes": 10 * 1024 * 1024, "cold_start_runs": [200]}, + {"install_bytes": 25 * 1024 * 1024, "cold_start_runs": [500]}) + size = report["verdict"]["install_bytes"] + self.assertEqual(size["winner"], "codenameone") + self.assertAlmostEqual(size["ratio"], 2.5, places=3) + + def test_a_loss_is_reported_as_a_loss(self): + """The harness must be able to say we lost, or it is not a measurement.""" + report = _report( + {"install_bytes": 40 * 1024 * 1024}, + {"install_bytes": 20 * 1024 * 1024}) + size = report["verdict"]["install_bytes"] + self.assertEqual(size["winner"], "flutter") + self.assertLess(size["ratio"], 1.0) + + def test_a_missing_side_is_not_measured_rather_than_a_win(self): + report = _report({"install_bytes": 10}, {}) + self.assertEqual(report["verdict"]["install_bytes"]["status"], + "not measured") + + def test_a_zero_is_not_measured_rather_than_an_infinite_ratio(self): + report = _report({"install_bytes": 0}, {"install_bytes": 10}) + self.assertEqual(report["verdict"]["install_bytes"]["status"], + "not measured") + + +class StatisticsTest(unittest.TestCase): + + def test_best_of_takes_the_minimum_not_the_mean(self): + # A shared runner's long right tail measures the runner, not the build. + self.assertEqual(benchlib.best_of([210, 205, 900, 207]), 205) + + def test_best_of_no_samples_is_none(self): + self.assertIsNone(benchlib.best_of([])) + + def test_percentile_is_nearest_rank(self): + values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + self.assertEqual(benchlib.percentile(values, 50), 5) + self.assertEqual(benchlib.percentile(values, 100), 10) + + def test_summarise_brackets_start_up(self): + side = benchlib.summarise({ + "cold_start_runs": [300, 280], + "cold_start_lower_runs": [210, 200], + }) + self.assertEqual(side["cold_start_ms"], 280) + self.assertEqual(side["cold_start_lower_ms"], 200) + + +class SizingTest(unittest.TestCase): + + def test_tree_size_sums_file_lengths(self): + with tempfile.TemporaryDirectory() as tmp: + os.makedirs(os.path.join(tmp, "sub")) + for name, size in (("a", 100), ("sub/b", 250)): + with open(os.path.join(tmp, name), "wb") as handle: + handle.write(b"x" * size) + self.assertEqual(benchlib.tree_size(tmp), 350) + + def test_tree_size_ignores_symlinks(self): + """A symlink is not bytes the user downloads; counting it double-counts.""" + with tempfile.TemporaryDirectory() as tmp: + target = os.path.join(tmp, "real") + with open(target, "wb") as handle: + handle.write(b"x" * 100) + os.symlink(target, os.path.join(tmp, "link")) + self.assertEqual(benchlib.tree_size(tmp), 100) + + def test_wire_size_is_smaller_than_the_tree_for_compressible_input(self): + with tempfile.TemporaryDirectory() as tmp: + with open(os.path.join(tmp, "a"), "wb") as handle: + handle.write(b"x" * 100000) + self.assertLess(benchlib.wire_size(tmp, tmp), + benchlib.tree_size(tmp)) + + +class MarkdownTest(unittest.TestCase): + + def test_an_empty_run_says_so(self): + body = benchlib.render_markdown([]) + self.assertIn("No benchmark results", body) + + def test_the_table_shows_both_magnitudes_not_only_the_ratio(self): + report = _report( + {"install_bytes": 10 * 1024 * 1024}, + {"install_bytes": 20 * 1024 * 1024}) + body = benchlib.render_markdown([report]) + self.assertIn("10.0 MB", body) + self.assertIn("20.0 MB", body) + self.assertIn("2.00x", body) + + def test_the_bracket_is_disclosed_when_present(self): + report = _report( + {"cold_start_runs": [250]}, + {"cold_start_runs": [500], "cold_start_lower_runs": [400]}) + body = benchlib.render_markdown([report]) + self.assertIn("bracketed", body) + self.assertIn("400 ms", body) + + def test_tally_counts_only_measured_metrics(self): + report = _report({"install_bytes": 10}, {"install_bytes": 20}) + wins, measured = benchlib.tally([report]) + self.assertEqual((wins, measured), (1, 1)) + + +class GateArming(unittest.TestCase): + """The gate's result is the exit status: a missing row fails, a judged one passes.""" + + def _report(self, install=1000): + return {"platform": "fake", "generated_at": "2026-01-01T00:00:00Z", + "codenameone": {"install_bytes": install, "code_bytes": None}, + "flutter": {"install_bytes": 2000}} + + def test_the_gate_line_says_what_to_run(self): + report = dict(self._report(), regressions=[{"metric": "install_bytes"}], + gate={"status": "armed", "fix": "flutter_baseline.py calibrate --pr 9 x"}) + self.assertIn("flutter_baseline.py calibrate --pr 9", benchlib.render_gate(report)) + + def test_an_unreadable_baseline_is_rendered_as_not_judged(self): + report = dict(self._report(), gate={"status": "refused", "reason": "two overlays"}) + self.assertIn("NOT JUDGED", benchlib.render_gate(report)) + + def _main(self, rows, install=1000): + import run_bench + import flutter_baseline + work = tempfile.mkdtemp() + root = os.path.join(work, "baseline") + os.makedirs(os.path.join(root, "pr")) + with open(os.path.join(flutter_baseline.ROOT, "policy.json")) as src, \ + open(os.path.join(root, "policy.json"), "w") as dst: + dst.write(src.read()) + flutter_baseline.perf_baseline.write_base(root, rows) + report = self._report(install) + + class Stub(object): + id = "fake" + label = "Fake" + exercised = True + + def available(self): + return True, None + + def notes(self): + return [] + + def run_compute(self, side): + raise platforms.Unavailable("the stub runs no workloads") + + saved = (run_bench.build_adapter, run_bench.measure, run_bench.BASELINE_ROOT, + os.environ.get("CN1_PR_NUMBER")) + run_bench.build_adapter = lambda args: Stub() + run_bench.measure = lambda adapter, runs, workdir: ( + {side: {"artifact": "x", "install_bytes": report[side]["install_bytes"], + "cold_start_runs": [], "cold_start_lower_runs": [], + "idle_memory_runs": []} for side in benchlib.SIDES}, []) + run_bench.BASELINE_ROOT = root + os.environ["CN1_PR_NUMBER"] = "9" + try: + out = os.path.join(work, "result.json") + candidate = os.path.join(work, "candidate.json") + code = run_bench.main(["--platform", "fake", "--gate", "--json", out, + "--baseline-out", candidate, "--runs", "1"]) + with open(out) as handle: + written = json.load(handle) + with open(candidate) as handle: + return code, written, json.load(handle) + finally: + (run_bench.build_adapter, run_bench.measure, run_bench.BASELINE_ROOT, number) = saved + if number is None: + os.environ.pop("CN1_PR_NUMBER", None) + else: + os.environ["CN1_PR_NUMBER"] = number + + def test_a_report_only_platform_passes_a_loss_to_flutter(self): + # Larger than Flutter's 2000 bytes, but exactly on its own baseline row. + rows = {"fake": {"gallery": {"install_bytes": {"value": 3000, "runs": 1}}}} + code, written, _ = self._main(rows, install=3000) + self.assertEqual(1, code, "behind Flutter fails the gate on an ordinary platform") + saved = dict(benchlib.HEAD_TO_HEAD_REPORT_ONLY) + benchlib.HEAD_TO_HEAD_REPORT_ONLY["fake"] = "accepted for this test" + try: + code, written, _ = self._main(rows, install=3000) + finally: + benchlib.HEAD_TO_HEAD_REPORT_ONLY.clear() + benchlib.HEAD_TO_HEAD_REPORT_ONLY.update(saved) + self.assertEqual(0, code, "a report-only platform's loss is shown, not failed") + self.assertEqual(["install_bytes"], [f["metric"] for f in written["behind"]]) + self.assertEqual("accepted for this test", written["behind_accepted"]) + + def test_a_missing_row_fails_the_run_and_leaves_a_candidate(self): + code, written, candidate = self._main({}) + self.assertEqual(1, code, "an uncalibrated metric must not report success") + self.assertEqual(["uncalibrated"], [f["verdict"] for f in written["regressions"]]) + self.assertEqual({"install_bytes": 1000}, candidate["values"]) + self.assertIn("calibrate --pr 9 baseline-fake.json", written["gate"]["fix"]) + + def test_an_unmeasured_platform_fails_the_gate(self): + import run_bench + + class Gone(object): + id = "fake" + label = "Fake" + exercised = True + + def available(self): + return False, "no attached device or emulator" + + saved = run_bench.build_adapter + run_bench.build_adapter = lambda args: Gone() + try: + out = os.path.join(tempfile.mkdtemp(), "r.json") + self.assertEqual(1, run_bench.main(["--platform", "fake", "--gate", "--json", out])) + self.assertEqual(0, run_bench.main(["--platform", "fake", "--json", out]), + "ungated, an unavailable platform is simply reported") + with open(out) as handle: + self.assertEqual("unavailable", json.load(handle)["status"]) + finally: + run_bench.build_adapter = saved + + def test_a_judged_run_within_tolerance_passes(self): + code, written, _ = self._main({"fake": {"gallery": {"install_bytes": { + "value": 1000, "runs": 1}}}}) + self.assertEqual(0, code) + self.assertEqual("armed", written["gate"]["status"]) + self.assertEqual([], written["regressions"]) + + def test_a_size_that_shrank_passes_and_says_to_tighten(self): + code, written, _ = self._main({"fake": {"gallery": {"install_bytes": { + "value": 1200, "runs": 1}}}}) + self.assertEqual(0, code) + self.assertEqual([], written["regressions"]) + self.assertEqual("improved", written["advisories"][0]["verdict"]) + line = benchlib.render_gate(written) + self.assertIn("rebaseline to tighten", line) + self.assertIn("calibrate --pr 9", line) + + def test_a_size_that_grew_fails(self): + code, written, _ = self._main({"fake": {"gallery": {"install_bytes": { + "value": 800, "runs": 1}}}}) + self.assertEqual(1, code) + self.assertEqual("regression", written["regressions"][0]["verdict"]) + + +class StartupBracket(unittest.TestCase): + """The start-up ratio comes from Flutter's end least favourable to us.""" + + def _report(self): + # The first Windows run that measured both sides: Codename One 219 ms, + # Flutter bracketed 116 ms (FIRSTCONTENT) to 260 ms (RASTERDONE). + return {"codenameone": {"cold_start_ms": 219.0}, + "flutter": {"cold_start_ms": 260.0, "cold_start_lower_ms": 116.0}} + + def test_the_ratio_uses_flutters_lower_end(self): + entry = benchlib.verdict(self._report())["cold_start_ms"] + self.assertAlmostEqual(116.0 / 219.0, entry["ratio"], places=3) + self.assertEqual("flutter", entry["winner"], + "judged by the upper end this read as a Codename One win") + + def test_the_table_still_shows_the_whole_bracket(self): + report = self._report() + report.update(platform="windows", runs=5, verdict=benchlib.verdict(report)) + text = benchlib.render_markdown([report]) + self.assertIn("116", text) + self.assertIn("260", text) + + +class PairedRoundsTest(unittest.TestCase): + """Start-up and memory are judged by the median of the per-round ratios.""" + + # A real macOS run, five interleaved rounds. Codename One was faster in + # rounds 1, 3 and 5; best-of called it for Flutter (356 against 468). + CN1 = [1787, 707, 468, 584, 668] + FLUTTER = [2512, 617, 522, 356, 769] + + def test_the_real_macos_rounds_are_a_codename_one_win(self): + report = _report({"cold_start_runs": self.CN1}, {"cold_start_runs": self.FLUTTER}) + entry = report["verdict"]["cold_start_ms"] + self.assertEqual("paired_median", entry["statistic"]) + self.assertEqual("codenameone", entry["winner"]) + self.assertAlmostEqual(522.0 / 468.0, entry["ratio"], places=3) + self.assertEqual((3, 5), (entry["rounds_won"], entry["rounds"])) + self.assertEqual([], [f for f in benchlib.check_behind(report) + if f["metric"] == "cold_start_ms"]) + # The columns are each side's median; the baseline still reads best-of. + self.assertEqual((668, 617), (entry["codenameone"], entry["flutter"])) + self.assertEqual(468, report["codenameone"]["cold_start_ms"]) + self.assertEqual(356, report["flutter"]["cold_start_ms"]) + + def test_one_lucky_flutter_round_does_not_decide(self): + report = _report({"cold_start_runs": [500, 500, 500, 500, 500]}, + {"cold_start_runs": [600, 600, 100, 600, 600]}) + self.assertEqual("codenameone", report["verdict"]["cold_start_ms"]["winner"]) + + def test_one_lucky_codename_one_round_does_not_decide(self): + report = _report({"cold_start_runs": [600, 600, 100, 600, 600]}, + {"cold_start_runs": [500, 500, 500, 500, 500]}) + entry = report["verdict"]["cold_start_ms"] + self.assertEqual("flutter", entry["winner"]) + self.assertTrue(benchlib.check_behind(report)) + + def test_memory_is_paired_too(self): + report = _report({"idle_memory_runs": [33, 45, 46, 45, 46]}, + {"idle_memory_runs": [61, 89, 91, 88, 89]}) + entry = report["verdict"]["idle_memory_bytes"] + self.assertEqual("paired_median", entry["statistic"]) + self.assertEqual(45, entry["codenameone"]) + + def test_unequal_lengths_without_round_numbers_fall_back_to_best_of(self): + report = _report({"cold_start_runs": [500, 400, 450]}, + {"cold_start_runs": [450, 420]}) + entry = report["verdict"]["cold_start_ms"] + self.assertEqual("best_of", entry["statistic"]) + self.assertIn("3 and 2 rounds", entry["fallback"]) + self.assertAlmostEqual(420.0 / 400.0, entry["ratio"], places=3) + body = benchlib.render_markdown([report]) + self.assertIn("could not be paired round by round", body) + + def test_round_numbers_pair_across_a_dropped_launch(self): + # Flutter's round 1 failed. Positionally every later pair would be one + # round out of step; by round number round 1 alone is dropped. + report = _report( + {"cold_start_runs": [500, 300, 500], "cold_start_rounds": [0, 1, 2]}, + {"cold_start_runs": [600, 600], "cold_start_rounds": [0, 2]}) + entry = report["verdict"]["cold_start_ms"] + self.assertEqual("paired_median", entry["statistic"]) + self.assertEqual(2, entry["rounds"]) + self.assertAlmostEqual(1.2, entry["ratio"], places=3) + + def test_the_web_bracket_is_applied_per_round(self): + # Flutter's least favourable end differs by round: the lower end in + # rounds 0 and 2, the upper end in round 1 (a lower end is never above + # the upper in practice, but min() must not care). + cn1 = {"cold_start_runs": [400, 400, 400]} + flutter = {"cold_start_runs": [800, 300, 800], + "cold_start_lower_runs": [500, 900, 300]} + entry = _report(cn1, flutter)["verdict"]["cold_start_ms"] + # Per-round Flutter figures 500, 300, 300 -> ratios 1.25, 0.75, 0.75. + self.assertAlmostEqual(0.75, entry["ratio"], places=3) + self.assertEqual("flutter", entry["winner"]) + self.assertEqual(300, entry["flutter"]) + self.assertEqual([500, 800], entry["flutter_bracket"]) + body = benchlib.render_markdown([dict(_report(cn1, flutter), platform="javascript")]) + self.assertIn("round by round", body) + self.assertIn("300 ms (500 ms-800 ms)", body) + + def test_a_round_without_a_lower_end_is_dropped_not_judged_by_the_upper(self): + report = _report( + {"cold_start_runs": [400, 400], "cold_start_rounds": [0, 1]}, + {"cold_start_runs": [800, 800], "cold_start_rounds": [0, 1], + "cold_start_lower_runs": [300], "cold_start_lower_rounds": [1]}) + entry = report["verdict"]["cold_start_ms"] + self.assertEqual(1, entry["rounds"]) + self.assertAlmostEqual(0.75, entry["ratio"], places=3) + + def test_the_report_explains_the_ratio_is_not_the_medians_quotient(self): + report = dict(_report({"cold_start_runs": self.CN1}, + {"cold_start_runs": self.FLUTTER}), platform="macos") + body = benchlib.render_markdown([report]) + self.assertIn("median of the per-round ratios", body) + # 522 / 468 = 1.115: the middle round's ratio, not 617 / 668. + self.assertIn("1.11x", body) + self.assertIn("3 of 5 rounds", body) + + def test_the_harness_records_the_round_of_every_sample(self): + import run_bench + + class Flaky(object): + def artifact(self, side): + return "x" + + def sizes(self, side, workdir): + return {} + + def launch_and_time(self, side): + self.calls = getattr(self, "calls", 0) + 1 + if side == "flutter" and self.calls == 4: + raise platforms.Unavailable("launch failed") + return (500.0 if side == "codenameone" else 600.0), None, 1000 + + sides, notes = run_bench.measure(Flaky(), 3, tempfile.gettempdir()) + self.assertEqual([0, 1, 2], sides["codenameone"]["cold_start_rounds"]) + self.assertEqual([0, 2], sides["flutter"]["cold_start_rounds"]) + self.assertEqual([0, 2], sides["flutter"]["idle_memory_rounds"]) + self.assertEqual([], sides["flutter"]["cold_start_lower_rounds"]) + entry = _report(sides["codenameone"], sides["flutter"])["verdict"]["cold_start_ms"] + self.assertEqual(("paired_median", 2), (entry["statistic"], entry["rounds"])) + + def test_median_of_an_even_count_averages_the_middle_two(self): + self.assertEqual(2.5, benchlib.median([4, 1, 3, 2])) + self.assertEqual(3, benchlib.median([5, 1, 3])) + self.assertIsNone(benchlib.median([])) + + +class FlatAssetNames(unittest.TestCase): + """Must agree with FlutterAssets and TranscodeFlutterMojo, name for name.""" + + def test_same_vectors_as_the_runtime_and_the_plugin(self): + sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "app")) + import stage_assets + self.assertEqual("cn1f_assets_sstudies_sreply__card.png", + stage_assets.flat_name("assets/studies/reply_card.png")) + self.assertEqual("cn1f_a___sb.png", stage_assets.flat_name("a_/b.png")) + self.assertEqual("cn1f_a_s__b.png", stage_assets.flat_name("a/_b.png")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/scripts/flutter-bench/test_flutter_baseline.py b/scripts/flutter-bench/test_flutter_baseline.py new file mode 100644 index 00000000000..19ecd896bc0 --- /dev/null +++ b/scripts/flutter-bench/test_flutter_baseline.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +"""Tests for the Flutter benchmark's regression baselines (flutter_baseline.py). + + python3 -m unittest test_flutter_baseline (from scripts/flutter-bench) + +The overlay rules themselves are vm/selfhost/perf_baseline.py's and are tested there +(test_perf_gate). These pin what this layout adds: which statistic is judged, which row +judges it, the sizes' fine tolerance step, the ungated policy, the retired files, and the +migration. +""" + +import json +import os +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import flutter_baseline as fb # noqa: E402 + +pb = fb.perf_baseline + +POLICY = { + "tolerance": {"code_bytes": 0.0, "install_bytes": 0.0, "wire_bytes": 0.0, + "cold_start_ratio": 0.25, "idle_memory_ratio": 0.15}, + "floor": {m: 0.0 for m in fb.METRIC_IDS}, + "ungated": {"macos": {"cold_start_ratio": "too noisy on the hosted runner"}}, +} + + +def row(value, runs=1, tolerance=None): + r = {"value": value, "runs": runs} + if tolerance: + r["tolerance"] = {"value": tolerance} + return r + + +def rows(**per_key): + """rows(android={"code_bytes": row(...)}) -> the {key: {app: {metric: row}}} tree.""" + return {key.replace("__", "@"): {fb.APP: metrics} for key, metrics in per_key.items()} + + +class Tree(object): + def __init__(self, base=None, overlays=None, policy=None): + self._tmp = tempfile.mkdtemp() + self.root = Path(self._tmp) / "baseline" + (self.root / "pr").mkdir(parents=True) + (self.root / "policy.json").write_text(json.dumps(policy or POLICY)) + pb.write_base(self.root, base or {}) + for number, overlay in (overlays or {}).items(): + (self.root / "pr" / ("%d.json" % number)).write_text(json.dumps(overlay)) + + def overlay(self, number): + path = self.root / "pr" / ("%d.json" % number) + return json.loads(path.read_text()) if path.exists() else None + + def run_file(self, name, report): + path = Path(self._tmp) / name + path.write_text(json.dumps(report)) + return str(path) + + def close(self): + shutil.rmtree(self._tmp) + + +def result(platform="android", cpu=None, cn1_start=None, fl_start=None, cn1_mem=None, + fl_mem=None, **sizes): + """A run's result JSON, as run_bench.py writes it.""" + def side(start, mem, extra): + out = dict(extra) + if start: + out["cold_start_runs"] = list(start) + out["cold_start_rounds"] = list(range(len(start))) + if mem: + out["idle_memory_runs"] = list(mem) + out["idle_memory_rounds"] = list(range(len(mem))) + return out + report = {"platform": platform, "status": "measured", + "codenameone": side(cn1_start, cn1_mem, sizes), + "flutter": side(fl_start, fl_mem, {})} + if cpu: + report["cpu"] = cpu + return report + + +class MeasurementTest(unittest.TestCase): + def test_a_ratio_is_the_median_of_the_per_round_ratios_ours_over_theirs(self): + # Rounds 1/4, 3/4 and 2/4: median 2/4. The quotient of the medians would agree + # here; the point is the direction (ours on top, lower is better). + report = result(cn1_start=[100, 300, 200], fl_start=[400, 400, 400]) + self.assertAlmostEqual(0.5, fb.ratio(report, "cold_start_ratio")) + + def test_a_slow_runner_slows_both_halves_and_cancels(self): + fast = result(cn1_start=[300, 310, 305], fl_start=[1000, 1000, 1000]) + slow = result(cn1_start=[600, 620, 610], fl_start=[2000, 2000, 2000]) + self.assertAlmostEqual(fb.ratio(fast, "cold_start_ratio"), + fb.ratio(slow, "cold_start_ratio")) + + def test_the_web_bracket_uses_flutters_end_least_favourable_to_us(self): + report = result(cn1_start=[200], fl_start=[400]) + report["flutter"]["cold_start_lower_runs"] = [100] + report["flutter"]["cold_start_lower_rounds"] = [0] + self.assertAlmostEqual(2.0, fb.ratio(report, "cold_start_ratio")) + + def test_sizes_are_absolute_and_unmeasured_metrics_are_absent(self): + values = fb.measurements(result(code_bytes=10, install_bytes=20)) + self.assertEqual({"code_bytes": 10, "install_bytes": 20}, values) + + def test_the_candidate_round_trips_through_measurements(self): + report = result(cpu="AMD EPYC 7763 64-Core Processor", code_bytes=10, + cn1_mem=[50, 50, 50], fl_mem=[100, 100, 100]) + candidate = fb.candidate(report) + self.assertEqual({"code_bytes": 10, "idle_memory_ratio": 0.5}, fb.measurements(candidate)) + self.assertEqual(report["cpu"], candidate["cpu"]) + + +class JudgeTest(unittest.TestCase): + def judge(self, report, base, policy=None): + tree = Tree(base, policy=policy) + try: + return fb.judge(report, fb.load(tree.root)) + finally: + tree.close() + + def test_within_tolerance_passes(self): + base = rows(android={"code_bytes": row(1000), "cold_start_ratio": row(0.5)}) + report = result(code_bytes=1000, cn1_start=[110], fl_start=[200]) + self.assertEqual([], self.judge(report, base)) + + def test_a_grown_size_fails(self): + base = rows(android={"code_bytes": row(1000)}) + grown = self.judge(result(code_bytes=1001), base) + self.assertEqual(["regression"], [f["verdict"] for f in grown]) + self.assertEqual(grown, fb.failing(grown)) + + def test_a_shrunk_size_is_reported_but_does_not_fail(self): + """One-sided on purpose: core pull requests that shrink the app mostly never run + this benchmark (paths filter), so a failing shrink would turn the nightly master + run red for a good change.""" + base = rows(android={"code_bytes": row(1000)}) + shrunk = self.judge(result(code_bytes=999), base) + self.assertEqual(["improved"], [f["verdict"] for f in shrunk]) + self.assertTrue(shrunk[0]["advisory"]) + self.assertEqual([], fb.failing(shrunk)) + self.assertIn("rebaseline to tighten", fb.describe(shrunk[0])) + + def test_an_improved_ratio_still_fails(self): + base = rows(android={"cold_start_ratio": row(0.5)}) + found = self.judge(result(cn1_start=[100], fl_start=[1000]), base) + self.assertEqual(["improved"], [f["verdict"] for f in found]) + self.assertEqual(found, fb.failing(found)) + self.assertIn("IMPROVED (rebaseline it)", fb.describe(found[0])) + + def test_a_row_tolerance_absorbs_small_growth(self): + base = rows(android={"code_bytes": row(1000000, tolerance=0.005)}) + self.assertEqual([], self.judge(result(code_bytes=1004000), base)) + self.assertEqual(1, len(self.judge(result(code_bytes=1006000), base))) + + def test_a_ratio_regression_beyond_tolerance_fails(self): + base = rows(android={"cold_start_ratio": row(0.3)}) + found = self.judge(result(cn1_start=[400], fl_start=[1000]), base) + self.assertEqual("regression", found[0]["verdict"]) + self.assertAlmostEqual(33.33, found[0]["moved_by"], places=1) + + def test_a_measured_metric_without_a_row_is_uncalibrated(self): + found = self.judge(result(code_bytes=5), {}) + self.assertEqual([("code_bytes", "uncalibrated", "android")], + [(f["metric"], f["verdict"], f["key"]) for f in found]) + + def test_a_gated_metric_the_run_did_not_produce_fails(self): + # Sizes come from the build output, so they fill in even when the app never + # reached its first frame; the start-up ratio is simply absent. + base = rows(android={"code_bytes": row(5), "cold_start_ratio": row(0.3)}) + found = self.judge(result(code_bytes=5), base) + self.assertEqual([("cold_start_ratio", "missing")], + [(f["metric"], f["verdict"]) for f in found]) + self.assertIn("was not measured", fb.describe(found[0])) + + def test_an_ungated_metric_is_recorded_not_judged(self): + found = self.judge(result(platform="macos", cn1_start=[900], fl_start=[100]), {}) + self.assertEqual([], found) + + def test_flutters_own_numbers_only_enter_as_the_denominator(self): + # A Flutter SDK that grows its build cannot fail OUR size rows. + base = rows(android={"code_bytes": row(1000)}) + report = result(code_bytes=1000) + report["flutter"]["code_bytes"] = 99999999 + self.assertEqual([], self.judge(report, base)) + + +class RowKeyTest(unittest.TestCase): + CPU = "AMD EPYC 7763 64-Core Processor" + MODEL = "android@amd-epyc-7763-64-core-processor" + + def test_one_plain_row_judges_every_cpu_until_models_have_rows(self): + tree = rows(android={"cold_start_ratio": row(0.3)}) + self.assertEqual("android", fb.row_key(tree, "android", self.CPU, "cold_start_ratio")) + + def test_a_model_row_judges_its_model_and_an_unseen_model_is_uncalibrated(self): + tree = rows(android={"cold_start_ratio": row(0.3)}, + android__amd_x={"cold_start_ratio": row(0.4)}) + tree = {k.replace("_", "-"): v for k, v in tree.items()} + self.assertEqual("android@amd-x", fb.row_key(tree, "android", "AMD X", "cold_start_ratio")) + self.assertIsNone(fb.row_key(tree, "android", self.CPU, "cold_start_ratio")) + + def test_a_size_ignores_the_cpu(self): + tree = rows(android={"code_bytes": row(1)}) + self.assertEqual("android", fb.row_key(tree, "android", self.CPU, "code_bytes")) + + def test_a_size_under_a_cpu_key_is_refused(self): + tree = Tree({"android@x": {fb.APP: {"code_bytes": row(1)}}}) + try: + with self.assertRaises(fb.BaselineError): + fb.load(tree.root) + finally: + tree.close() + + def test_an_unknown_metric_is_refused(self): + tree = Tree(rows(android={"cold_start_ms": row(1)})) + try: + with self.assertRaisesRegex(fb.BaselineError, "unknown metric"): + fb.load(tree.root) + finally: + tree.close() + + def test_an_ungated_entry_needs_a_reason(self): + policy = dict(POLICY, ungated={"macos": {"cold_start_ratio": " "}}) + tree = Tree({}, policy=policy) + try: + with self.assertRaisesRegex(fb.BaselineError, "reason"): + fb.load(tree.root) + finally: + tree.close() + + +class CalibrateTest(unittest.TestCase): + def calibrate(self, runs, base=None, overlays=None, reason=None, per_cpu=False, + everything=False): + tree = Tree(base, overlays) + try: + files = [tree.run_file("r%d.json" % i, r) for i, r in enumerate(runs)] + fb.calibrate(tree.root, 7, files, reason, everything, None, per_cpu) + return tree.overlay(7), fb.load(tree.root)["platforms"] + finally: + tree.close() + + def test_new_ratio_rows_are_calibrated_with_a_spread_learned_both_ways(self): + runs = [result(cn1_start=[c], fl_start=[1000]) for c in (250, 300, 350, 300, 300)] + overlay, resolved = self.calibrate(runs) + new = overlay["calibrate"]["android"][fb.APP]["cold_start_ratio"] + self.assertEqual(0.3, new["value"]) + self.assertEqual(5, new["runs"]) + # (0.35 / 0.3 - 1) x 1.5 = 0.25, the policy's own, so not repeated in the row. + self.assertNotIn("tolerance", new) + self.assertEqual(new, resolved["android"][fb.APP]["cold_start_ratio"]) + + def test_a_moved_size_is_rebaselined_from_its_old_value_and_needs_a_reason(self): + base = rows(android={"code_bytes": row(1000)}) + with self.assertRaises(SystemExit): + self.calibrate([result(code_bytes=1200)], base) + overlay, _ = self.calibrate([result(code_bytes=1200)], base, reason="grew") + moved = overlay["rebaseline"]["android"][fb.APP]["code_bytes"] + self.assertEqual({"value": 1000}, moved["from"]) + self.assertEqual(1200, moved["value"]) + self.assertEqual("grew", moved["reason"]) + + def test_size_jitter_learns_a_fine_tolerance_not_a_five_percent_one(self): + base = rows(android={"install_bytes": row(1000000)}) + overlay, _ = self.calibrate([result(install_bytes=81949941), + result(install_bytes=81949945)], base, reason="r") + moved = overlay["rebaseline"]["android"][fb.APP]["install_bytes"] + self.assertEqual({"value": 0.001}, moved["tolerance"]) + + def test_a_row_inside_its_tolerance_is_left_alone(self): + base = rows(android={"code_bytes": row(1000000, tolerance=0.005)}) + tree = Tree(base) + try: + path = tree.run_file("r.json", result(code_bytes=1001000)) + self.assertIsNone(fb.calibrate(tree.root, 7, [path])) + self.assertIsNone(tree.overlay(7)) + finally: + tree.close() + + def test_per_cpu_calibrates_a_model_row_beside_the_plain_one(self): + base = rows(android={"cold_start_ratio": row(0.3)}) + runs = [result(cpu="AMD EPYC 7763 64-Core Processor", cn1_start=[400], fl_start=[1000])] + overlay, resolved = self.calibrate(runs, base, per_cpu=True) + self.assertIn("android@amd-epyc-7763-64-core-processor", overlay["calibrate"]) + self.assertEqual(0.3, resolved["android"][fb.APP]["cold_start_ratio"]["value"]) + + def test_an_ungated_metric_is_not_calibrated(self): + overlay, _ = self.calibrate([result(platform="macos", code_bytes=5, cn1_start=[1], + fl_start=[2])]) + self.assertEqual(["code_bytes"], list(overlay["calibrate"]["macos"][fb.APP])) + + def test_the_candidate_file_calibrates_like_the_result(self): + report = result(code_bytes=7, cn1_mem=[40], fl_mem=[80]) + a, _ = self.calibrate([report]) + b, _ = self.calibrate([fb.candidate(report)]) + self.assertEqual(a, b) + + +class OverlayRulesTest(unittest.TestCase): + """The shared rules, through this layout.""" + + def test_two_pull_requests_moving_one_size_conflict_by_name(self): + base = rows(android={"code_bytes": row(1000)}) + def moved(number, value): + return {"pr": number, "rebaseline": {"android": {fb.APP: {"code_bytes": dict( + row(value), **{"from": {"value": 1000}, "reason": "r"})}}}} + tree = Tree(base, {11: moved(11, 1100), 12: moved(12, 1200)}) + try: + with self.assertRaisesRegex(fb.BaselineError, "pr/11.json, pr/12.json"): + fb.load(tree.root) + finally: + tree.close() + + def test_fold_moves_overlays_into_base_and_changes_nothing(self): + overlay = {"pr": 3, "calibrate": {"linux": {fb.APP: {"idle_memory_ratio": row(0.4, 6)}}}} + tree = Tree(rows(linux={"code_bytes": row(9)}), {3: overlay}) + try: + before = fb.load(tree.root)["platforms"] + self.assertEqual([3], pb.fold(tree.root, fb.FLUTTER)) + self.assertEqual(before, fb.load(tree.root)["platforms"]) + self.assertIsNone(tree.overlay(3)) + finally: + tree.close() + + def test_the_checked_in_baselines_resolve(self): + data = fb.load() + self.assertIn("android", data["platforms"]) + + +class CheckTest(unittest.TestCase): + def check(self, changed, migrating=False, legacy=False, number=31): + saved = pb.changed_files, pb.exists_at, pb.legacy_touched + seen = {} + + def touched(base_ref, path=None): + seen["path"] = path + return legacy + pb.changed_files = lambda base_ref, root=None: changed + pb.exists_at = lambda ref, path, root=None: ( + not migrating if path == "policy.json" else False) + pb.legacy_touched = touched + try: + return fb.check(fb.ROOT, "base-sha", number), seen + finally: + pb.changed_files, pb.exists_at, pb.legacy_touched = saved + + def test_base_is_the_folds_alone(self): + problems, _ = self.check(["base/android.json"]) + self.assertEqual(1, len(problems)) + self.assertIn("flutter_baseline.py calibrate --pr N", problems[0]) + + def test_a_pull_request_writes_its_own_overlay_and_the_policy(self): + self.assertEqual([], self.check(["pr/31.json", "policy.json"])[0]) + + def test_another_pull_requests_overlay_is_off_limits(self): + self.assertIn("pr/30.json", self.check(["pr/30.json"])[0][0]) + + def test_the_retired_files_are_refused_and_it_asks_about_the_right_path(self): + problems, seen = self.check([], legacy=True) + self.assertEqual(fb.LEGACY_DIR, seen["path"]) + self.assertIn("retired", problems[0]) + + def test_the_migration_may_create_base(self): + self.assertEqual([], self.check(["base/android.json"], migrating=True, legacy=True)[0]) + + +class LegacyTest(unittest.TestCase): + ORIGINAL = { + "android": {"codenameone": {"code_bytes": 4720336, "install_bytes": 91589516, + "cold_start_ms": 418.0}, + "tolerances": {"code_bytes": 0.0, "install_bytes": 0.0, + "cold_start_ms": 0.25}}, + "macos": {"codenameone": {"code_bytes": 7, "cold_start_ms": 465.0}, + "tolerances": {"code_bytes": 0.0}}, + } + + def branch(self): + legacy = json.loads(json.dumps(self.ORIGINAL)) + legacy["android"]["codenameone"]["code_bytes"] = 4773504 + legacy["android"]["tolerances"]["code_bytes"] = 0.005 + return legacy + + def test_sizes_carry_over_exactly_and_gated_absolutes_are_reported(self): + new, gated, recorded = fb.legacy_rows(self.branch(), POLICY) + self.assertEqual(row(4773504, tolerance=0.005), new["android"][fb.APP]["code_bytes"]) + self.assertEqual(row(91589516), new["android"][fb.APP]["install_bytes"]) + self.assertEqual({"android": ["cold_start_ratio"]}, gated) + self.assertEqual({"macos": ["cold_start_ratio"]}, recorded) + + def test_a_branchs_edit_becomes_its_rebaseline_and_resolves_to_the_branch(self): + base, _, _ = fb.legacy_rows(self.ORIGINAL, POLICY) + tree = Tree(base) + try: + fb.import_legacy(tree.root, 7, self.branch(), self.ORIGINAL, "features") + moved = tree.overlay(7)["rebaseline"]["android"][fb.APP]["code_bytes"] + self.assertEqual({"value": 4720336}, moved["from"]) + branch_rows, _, _ = fb.legacy_rows(self.branch(), POLICY) + self.assertEqual(branch_rows, fb.load(tree.root)["platforms"]) + finally: + tree.close() + + def test_an_unchanged_branch_writes_nothing(self): + base, _, _ = fb.legacy_rows(self.ORIGINAL, POLICY) + tree = Tree(base) + try: + self.assertIsNone(fb.import_legacy(tree.root, 7, self.ORIGINAL, self.ORIGINAL)) + finally: + tree.close() + + def test_the_policy_ungates_exactly_what_the_retired_files_did_not_gate(self): + """The migration kept the gating decisions: every absolute start-up or memory + figure the old files recorded without a tolerance is ungated, and nothing else.""" + policy = json.loads((fb.ROOT / "policy.json").read_text()) + ungated = {(p, m) for p, ms in policy.get("ungated", {}).items() for m in ms} + self.assertEqual({("macos", "cold_start_ratio")}, ungated) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/flutter-bench/test_platforms.py b/scripts/flutter-bench/test_platforms.py new file mode 100644 index 00000000000..25b4eef6c1a --- /dev/null +++ b/scripts/flutter-bench/test_platforms.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +"""Tests for the adapter mechanics that do not need a real build or device. + + python3 scripts/flutter-bench/test_platforms.py + +Marker timing against real child processes, and how a desktop artifact +directory is resolved to the binary to launch and the code to size. The +measurements themselves still need real builds; see test_benchlib.py. +""" + +import os +import stat +import subprocess +import sys +import tempfile +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import benchlib # noqa: E402 +import platforms # noqa: E402 + + +def _child(code): + return subprocess.Popen([sys.executable, "-u", "-c", code], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1) + + +class TimeStreamDeadlines(unittest.TestCase): + """The limits hold whatever the child does, because the clock enforces them.""" + + def setUp(self): + self._saved = (platforms.LAUNCH_TIMEOUT_S, platforms.SETTLE_S) + platforms.LAUNCH_TIMEOUT_S = 2.0 + platforms.SETTLE_S = 1.0 + self.adapter = platforms.Adapter() + + def tearDown(self): + platforms.LAUNCH_TIMEOUT_S, platforms.SETTLE_S = self._saved + + def _time(self, side, code): + proc = _child(code) + try: + started = time.time() + result = self.adapter._time_stream(side, platforms._LineReader(proc.stdout), started) + return result, time.time() - started + finally: + proc.kill() + proc.wait() + + def test_a_silent_hang_is_abandoned_at_the_launch_timeout(self): + (upper, lower), elapsed = self._time("codenameone", "import time; time.sleep(60)") + self.assertIsNone(upper) + self.assertLess(elapsed, 5.0, "a blocking read would have waited out the child") + + def test_a_quiet_app_after_its_marker_is_let_go_at_settle(self): + # What a HEALTHY app does: print the marker, then nothing. The loop used + # to wait for another line before it would look at the settle time. + (upper, _), elapsed = self._time( + "codenameone", "import time; print('BENCH:FIRSTFRAME'); time.sleep(60)") + self.assertIsNotNone(upper) + self.assertLess(elapsed, 2.5) + self.assertGreaterEqual(elapsed, 0.9, "the idle period is still waited for") + + def test_the_idle_period_is_counted_from_the_marker_not_the_launch(self): + # A side that reaches its marker late still gets the full SETTLE_S + # before memory is sampled; a deadline from launch cut it to nothing. + # The marker also lands close to LAUNCH_TIMEOUT_S, which bounds only + # the wait FOR the marker. + (upper, _), elapsed = self._time( + "codenameone", "import time; time.sleep(1.5); print('BENCH:FIRSTFRAME');" + "time.sleep(60)") + self.assertIsNotNone(upper) + self.assertGreaterEqual(elapsed, upper / 1000.0 + 0.9, + "the settle must start at the marker") + self.assertLess(elapsed, 4.0) + + def test_the_flutter_bracket_is_still_read(self): + (upper, lower), _ = self._time( + "flutter", "import time; print('BENCH:FIRSTCONTENT'); time.sleep(0.2);" + "print('BENCH:RASTERDONE'); time.sleep(60)") + self.assertIsNotNone(lower) + self.assertIsNotNone(upper) + self.assertLessEqual(lower, upper) + + def test_an_exact_present_time_replaces_the_bracket(self): + # RASTERDONE printed ~500ms after a frame that finished rasterising 400ms + # before it: the figure is when that frame was on screen, and a point. + (upper, lower), _ = self._time( + "flutter", "import time; print('BENCH:FIRSTCONTENT'); time.sleep(0.5);" + "print('BENCH:RASTERDONE after=1ms presentedAgoUs=400000');" + "time.sleep(60)") + self.assertIsNone(lower, "an exact figure is not a bracket") + self.assertGreater(upper, 50.0) + self.assertLess(upper, 350.0, "the reported delay must be subtracted") + + def test_an_impossible_present_time_is_not_believed(self): + # A delay longer than the launch itself can only be two clocks that do + # not agree; the bracket is kept rather than a negative start-up. + (upper, lower), _ = self._time( + "flutter", "import time; print('BENCH:FIRSTCONTENT'); time.sleep(0.2);" + "print('BENCH:RASTERDONE presentedAgoUs=99000000'); time.sleep(60)") + self.assertIsNotNone(lower) + self.assertGreaterEqual(upper, 150.0) + + def test_an_exit_ends_the_wait(self): + (upper, _), elapsed = self._time("codenameone", "print('unrelated')") + self.assertIsNone(upper) + self.assertLess(elapsed, 1.5) + + +class ComputeReading(unittest.TestCase): + """A compute run is complete only when the app says so.""" + + def setUp(self): + self._saved = platforms.COMPUTE_TIMEOUT_S + platforms.COMPUTE_TIMEOUT_S = 3.0 + + def tearDown(self): + platforms.COMPUTE_TIMEOUT_S = self._saved + + def _read(self, code): + proc = _child(code) + try: + return platforms.Adapter._read_compute( + platforms._LineReader(proc.stdout), time.time() + platforms.COMPUTE_TIMEOUT_S, + "codenameone") + finally: + proc.kill() + proc.wait() + + def test_collects_every_workload_until_done(self): + got = self._read("print('noise'); print('BENCH:COMPUTE name=recursion checksum=7 ms=12');" + "print('BENCH:COMPUTE-DONE'); import time; time.sleep(60)") + self.assertEqual(got, {"recursion": ("7", 12)}) + + def test_an_app_that_dies_midway_is_not_a_result(self): + with self.assertRaises(platforms.Unavailable) as ctx: + self._read("print('BENCH:COMPUTE name=recursion checksum=7 ms=12')") + self.assertIn("exited before finishing", str(ctx.exception)) + + def test_a_run_that_stops_reporting_names_where_it_stalled(self): + saved = platforms.COMPUTE_IDLE_S + platforms.COMPUTE_IDLE_S = 1.0 + try: + with self.assertRaises(platforms.Unavailable) as ctx: + self._read("print('BENCH:COMPUTE name=intArithmetic checksum=1 ms=5');" + "import time; time.sleep(60)") + finally: + platforms.COMPUTE_IDLE_S = saved + self.assertIn("stalled", str(ctx.exception)) + self.assertIn("it was running longArithmetic", str(ctx.exception)) + + def test_an_app_that_hangs_is_abandoned_at_the_timeout(self): + started = time.time() + with self.assertRaises(platforms.Unavailable) as ctx: + self._read("import time; time.sleep(60)") + self.assertIn("did not finish", str(ctx.exception)) + self.assertLess(time.time() - started, 6.0) + + +def _elf(text, rodata): + """A minimal ELF64 with an executable .text and a non-executable .rodata.""" + import struct + header = bytearray(64) + header[:4] = b"\x7fELF" + header[4], header[5] = 2, 1 # 64-bit, little-endian + struct.pack_into("> '%s'\n" + "case \"$*\" in *\\ install\\ *|install\\ *) cat '%s';; " + "*'package compile'*) echo Success;; " + "*'am start'*) echo 'TotalTime: 123';; esac\n" + % (self.log, self.result)) + os.chmod(adb, 0o755) + self._path = os.environ["PATH"] + os.environ["PATH"] = self.bin + os.pathsep + self._path + self._settle = platforms.SETTLE_S + platforms.SETTLE_S = 0 + apk = os.path.join(self.bin, "app.apk") + open(apk, "wb").close() + self.adapter = platforms.AndroidAdapter( + None, {"codenameone": "com.example.bench", "flutter": "com.x.gallery"}, + {"codenameone": ".Main", "flutter": ".MainActivity"}, + {"codenameone": apk, "flutter": apk}) + + def tearDown(self): + os.environ["PATH"] = self._path + platforms.SETTLE_S = self._settle + + def _calls(self): + with open(self.log) as handle: + return [line.strip() for line in handle] + + def test_uninstalls_then_installs_before_the_first_launch_only(self): + cold, _, _ = self.adapter.launch_and_time("codenameone") + self.adapter.launch_and_time("codenameone") + calls = self._calls() + installs = [c for c in calls if c.startswith("install")] + self.assertEqual(1, len(installs), "once per run, not per launch: %s" % calls) + self.assertLess(calls.index("uninstall com.example.bench"), calls.index(installs[0])) + first_start = next(i for i, c in enumerate(calls) if "am start" in c) + self.assertLess(calls.index(installs[0]), first_start, "installed before it is launched") + compile_at = calls.index("shell cmd package compile -m speed -f com.example.bench") + self.assertLess(compile_at, first_start, "compiled to steady state before any launch") + starts = [c for c in calls if "am start" in c] + self.assertEqual(platforms.ANDROID_WARMUP_LAUNCHES + 2, len(starts), + "the untimed warm-up launches, then the two timed ones") + self.assertEqual(123.0, cold) + + def test_a_failed_install_is_reported_not_measured(self): + with open(self.result, "w") as handle: + handle.write("Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]") + with self.assertRaises(platforms.Unavailable) as caught: + self.adapter.launch_and_time("flutter") + self.assertIn("NO_CERTIFICATES", str(caught.exception)) + self.assertFalse(any("am start" in c for c in self._calls()), + "nothing may be timed when the install failed") + + +class AndroidSizing(unittest.TestCase): + """Sizes are for the one ABI a phone is delivered, on both sides.""" + + def _apk(self, entries): + import zipfile + path = os.path.join(tempfile.mkdtemp(), "app.apk") + with zipfile.ZipFile(path, "w", zipfile.ZIP_STORED) as archive: + for name, size in entries: + archive.writestr(name, b"x" * size) + return path + + def _adapter(self, apk): + return platforms.AndroidAdapter(None, {"codenameone": "a", "flutter": "b"}, + {"codenameone": ".A", "flutter": ".B"}, + {"codenameone": apk, "flutter": apk}) + + def test_a_fat_apk_counts_one_abi(self): + apk = self._apk([("classes.dex", 1000), ("lib/arm64-v8a/libapp.so", 5000), + ("lib/x86_64/libapp.so", 5000), ("lib/armeabi-v7a/libapp.so", 5000), + ("assets/flutter_assets/a.png", 2000)]) + adapter = self._adapter(apk) + self.assertEqual(6000, adapter.code_size("flutter"), + "the dex and the arm64 libraries, not all three ABIs") + delivered = adapter.delivered_size("flutter") + self.assertLess(delivered, os.path.getsize(apk) - 9900) + self.assertGreater(delivered, 8000, "the arm64 slice, the dex and the assets stay") + + def test_a_single_abi_apk_is_its_own_size(self): + apk = self._apk([("classes.dex", 4000), ("lib/arm64-v8a/libcn1.so", 700), + ("assets/a.png", 2000)]) + adapter = self._adapter(apk) + self.assertEqual(os.path.getsize(apk), adapter.delivered_size("codenameone")) + self.assertEqual(4700, adapter.code_size("codenameone")) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/hellocodenameone/conformance/benchmark_comparison.json b/scripts/hellocodenameone/conformance/benchmark_comparison.json new file mode 100644 index 00000000000..4b4a22965a8 --- /dev/null +++ b/scripts/hellocodenameone/conformance/benchmark_comparison.json @@ -0,0 +1,73 @@ +{ + "schema_version": 1, + "what": "The same application, built by two toolchains, measured on the same machine.", + "why_not_public": "The Port Status page is contractually free of competitor comparisons; scripts/website/validate_port_status.mjs fails the site build on the mention. This dataset is project evidence, gated in CI, and never rendered.", + "method": "Release/AOT on both sides. Runs are interleaved and best-of-N, with the machine's load average recorded, because a ratio taken under different load twice is not a ratio. The start-up clock runs OUTSIDE both processes: each build prints one marker on its first painted frame and the harness times from launch to that line. Every metric is recorded, including the ones Codename One loses.", + "harness": "benchcn1/tools/bench_compare.py", + "metrics": { + "install_bytes": "Bytes the installed application occupies. Lower is better.", + "code_bytes": "Bytes of executable payload — binaries and frameworks, excluding the artwork both builds bundle identically. Lower is better.", + "wire_bytes": "Bytes a user downloads: the application compressed. Lower is better.", + "cold_start_ms": "Milliseconds from launching the process to its first painted frame. Lower is better.", + "idle_rss_bytes": "Physical footprint once the application has settled -- vmmap's `Physical footprint:`, the figure Apple's own memory limits are enforced against. NOT `ps rss`: RSS counts clean, shared, file-backed framework pages, and reported 151MB, 207MB and 219MB for one unchanged binary in one afternoon. Lower is better." + }, + "platforms": { + "macos": { + "status": "measured", + "competitor": "Flutter 3.35.4, AOT, release", + "app": "the Flutter Gallery (dev/integration_tests/new_gallery) — the same 159 Dart files on both sides", + "configuration": "Codename One: ParparVM compiled to a native arm64 Mac Catalyst binary at -O3, no JVM. Flutter: flutter build macos --release. Both windows pinned to 1024x768 — window area drives GPU surface memory, so a memory comparison across different window sizes is meaningless. Codename One is on Mac CATALYST (UIKit for Mac); the Flutter build is native AppKit. A startup profile shows a third of the sampled main-thread start-up inside UIKitCore's _initiateIOSMacConnections -- system appearance bridging, CoreUI theme stores, key-command handler installation -- which an AppKit process never pays. That is a target difference, not a Codename One inefficiency, and it is a reason to treat iOS (both sides on UIKit) as the like-for-like comparison.", + "host": "Darwin 25.5.0 arm64", + "load_average": "{ 11.41 33.61 27.53 }", + "runs": 7, + "measured_at": "2026-08-23T19:21:02Z", + "harness": "benchcn1/tools/bench_compare.py", + "metrics": { + "install_bytes": { + "codenameone": 111599786, + "competitor": 146100418 + }, + "code_bytes": { + "codenameone": 35571432, + "competitor": 50212416 + }, + "wire_bytes": { + "codenameone": 80594389, + "competitor": 97936527 + }, + "cold_start_ms": { + "codenameone": 186, + "competitor": 161 + }, + "idle_rss_bytes": { + "codenameone": 121739673, + "competitor": 91855257 + } + }, + "losses": "Codename One is behind on cold start (272ms vs 186ms) and level on memory (144.3MB vs 139.5MB, a 3% gap). Both were far worse before a Codename One core fix landed: caching the prefixed (pressed/disabled) component styles in UIManager, which every component had been re-parsing from the theme. That took component creation from 131ms to 17ms and resident memory from 247MB to 144MB.", + "cold_start_decomposition": "Timed to a COMPLETE first screen on both sides, not to Flutter's warm-up frame: runApp schedules a frame before the root widget attaches, so Flutter's literal first frame paints 26 elements in ~10ms and comparing against it measures nothing. The reference build prints FIRSTCONTENT once its element tree stops growing (3003 elements) and that is what is timed. Codename One builds synchronously, so its first frame is already its content frame.", + "memory_note": "Physical footprint, not RSS -- the metric changed on 2026-08-23 and the change flips nothing: Codename One loses on memory under either. RSS was abandoned because it was not reproducible (151/207/219MB for one unchanged binary in one afternoon), while footprint for that state read 169.7MB every time.\\n\\nThe run-to-run BIMODALITY that made these numbers unquotable for most of this work is gone. It was the rounded image copies: the runtime rounded a picture's corners by reading it back with getRGB, clearing the alpha outside the arcs and uploading a SECOND image, 32 times during the first frame. That churn drove the start-up peak to ~660MB, and since libmalloc here never returns the pages a peak touched, where a run's peak landed decided what it kept -- anywhere from 121MB to 274MB. Rounding in the fragment shader instead removed the copies entirely (createImageFromARGBImpl: 32 calls -> 0, the CG raster region gone from the process, MALLOC_SMALL (empty) 46MB -> 464KB), took the peak to ~546MB, and four consecutive launches now settle at 154.9, 160.0, 160.0, 160.5MB.\\n\\nWhat remains is graphics buffering: an offscreen screenTexture for partial repaint blitted into CAMetalLayer drawables, against a competing toolchain that renders into one surface it owns.", + "note": "Codename One wins all three size measures and loses cold start and memory. Memory went from 0.45x of the competitor's figure to 0.76x across this work, on the tree AFTER merging origin/master (four collector commits under issue #5537); every macOS figure recorded before that merge is void.\\n\\nLanded against the two losses: a LAZY string constant pool in ParparVM (initConstantPool was materialising every string literal before main -- 38,238 of 113,824 live objects were Strings; invokeMain 119ms -> 46ms); a fix for an unbounded leak in the collector's force-visited side table, and a second fix keeping that table's sweep out of the allocator after it cost 36% of the objectAllocation benchmark; an image LOCK so an EncodedImage on screen is not decoded, collected and decoded again frame after frame; dropping the platform's decoded copy of every EncodedImage, which the encoded bytes already make recoverable; and rounding image corners in the fragment shader rather than building a rounded copy of the bitmap.\\n\\niOS is deliberately NOT measured: the competitor's iOS build is always debug-built, so a release-vs-debug comparison would be meaningless in Codename One's favour." + }, + "ios": { + "status": "pending", + "blocked_on": "needs the iOS build rebuilt and driven on a device or simulator" + }, + "android": { + "status": "pending", + "blocked_on": "needs an APK and a Flutter AOT build measured on the same device" + }, + "windows": { + "status": "pending", + "blocked_on": "cannot be built on this machine; needs a CI runner" + }, + "linux": { + "status": "pending", + "blocked_on": "cannot be built on this machine; needs a CI runner" + }, + "web": { + "status": "pending", + "blocked_on": "the JavaScript port is measured for the record but is not gated; a loss here is expected and accepted" + } + } +} \ No newline at end of file diff --git a/scripts/hellocodenameone/conformance/publish_port_status.py b/scripts/hellocodenameone/conformance/publish_port_status.py index 34d552979f4..e6875bae292 100644 --- a/scripts/hellocodenameone/conformance/publish_port_status.py +++ b/scripts/hellocodenameone/conformance/publish_port_status.py @@ -57,7 +57,13 @@ def stamp(value): return parsed -def publish(report, repo): +def publish(report, repo, target=None, message=None): + """Writes `report` to `target` on the data branch, newest generated_at wins. + + `target` defaults to the per-port file, ports/.json. Other data the + website reads from the same branch (the Flutter benchmark) passes its own + path and reuses this compare-and-swap rather than a second copy of it. + """ branch = "port-status-data" root = f"repos/{repo}" ref = f"{root}/git/ref/heads/{branch}" @@ -75,11 +81,14 @@ def publish(report, repo): raise api(ref) # A concurrent creator must actually have created the branch. - target = f"ports/{report['port']}.json" + if target is None: + target = f"ports/{report['port']}.json" + if message is None: + message = f"Update {report['port']} compliance status" endpoint = f"{root}/contents/{target}" generated = stamp(report["generated_at"]) for attempt in range(3): - payload = {"message": f"Update {report['port']} compliance status", "branch": branch, + payload = {"message": message, "branch": branch, "content": base64.b64encode(json.dumps(report).encode()).decode()} try: existing = api(f"{endpoint}?ref={branch}") diff --git a/scripts/hellocodenameone/conformance/test_publish_port_status.py b/scripts/hellocodenameone/conformance/test_publish_port_status.py index 74bb95c385b..e19a42a9e9d 100644 --- a/scripts/hellocodenameone/conformance/test_publish_port_status.py +++ b/scripts/hellocodenameone/conformance/test_publish_port_status.py @@ -63,6 +63,15 @@ def test_conflict_uses_fresh_sha_when_ours_is_still_newer(self): publisher.publish(self.report, "owner/repo") self.assertEqual("fresh", api.call_args.args[2]["sha"]) + def test_other_data_can_name_its_own_path_on_the_branch(self): + # The Flutter benchmark shares this compare-and-swap but is not a port + # report, so it is written beside ports/ rather than into it. + with patch.object(publisher, "api", side_effect=[{}, publisher.ApiError("missing", 404), {}]) as api: + publisher.publish(self.report, "owner/repo", target="benchmarks/flutter.json", + message="Update the Flutter benchmark") + self.assertTrue(api.call_args.args[0].endswith("/contents/benchmarks/flutter.json")) + self.assertEqual("Update the Flutter benchmark", api.call_args.args[2]["message"]) + def test_only_404_allows_creating_a_report(self): with patch.object(publisher, "api", side_effect=[{}, publisher.ApiError("missing", 404), {}]) as api: publisher.publish(self.report, "owner/repo") diff --git a/scripts/initializr/common/src/main/resources/skill/SKILL.md b/scripts/initializr/common/src/main/resources/skill/SKILL.md index 75ba2b06a90..3e0e75cb0a2 100644 --- a/scripts/initializr/common/src/main/resources/skill/SKILL.md +++ b/scripts/initializr/common/src/main/resources/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: codename-one -description: Build and modify Codename One cross-platform mobile apps (Java 17, Maven or Gradle, ParparVM/Android/iOS/JavaScript). Use when the project contains a `codenameone_settings.properties` (in `common/` for Maven, at the root for Gradle), a `settings.gradle.kts` applying the `com.codenameone` plugin, depends on `com.codenameone:codenameone-core`, edits CSS files under `src/main/css/`, calls `cn1:run`, `cn1:test`, `cn1:build` or `./gradlew run`, references `com.codename1.ui.*` / `com.codename1.testing.*`, works in the `backend/` project (`cn1:backend`, `./gradlew runBackend`, `com.codename1.backend.*`, `@RestController`, `@Service`, `@Transactional`, `@Scheduled`), or when the user asks to build a UI, write screen tests, generate screenshots, build a server or full-stack feature, or compare to Swing/HTML. +description: Build and modify Codename One cross-platform mobile apps (Java 17, Maven or Gradle, ParparVM/Android/iOS/JavaScript). Use when the project contains a `codenameone_settings.properties` (in `common/` for Maven, at the root for Gradle), a `settings.gradle.kts` applying the `com.codenameone` plugin, depends on `com.codenameone:codenameone-core`, edits CSS files under `src/main/css/`, calls `cn1:run`, `cn1:test`, `cn1:build` or `./gradlew run`, references `com.codename1.ui.*` / `com.codename1.testing.*`, works in the `backend/` project (`cn1:backend`, `./gradlew runBackend`, `com.codename1.backend.*`, `@RestController`, `@Service`, `@Transactional`, `@Scheduled`), or when the user asks to build a UI, write screen tests, generate screenshots, build a server or full-stack feature, or compare to Swing/HTML. Also use when the project has Dart under `common/src/main/flutter/`, runs the `transcode-flutter` goal, references `com.codename1.flutter.*` / `FlutterUI`, or the user asks to port or embed Flutter/Dart code. metadata: type: skill --- @@ -31,6 +31,7 @@ This skill teaches you how to write code for a Codename One (CN1) cross-platform - `references/html-css-cheatsheet.md` — Converting common HTML/CSS snippets to CN1 components + CSS. - `references/android-to-cn1.md` — Porting Android (XML + Kotlin/Java) screens to Codename One. - `references/react-to-cn1.md` — Bringing a **React / JSX + CSS design** (e.g. a Claude-generated bundle: `*.jsx` + `tokens.css` + `styles.css`) into Codename One: the component/hook/CSS mapping, the `tokens.css` -> `theme.css` token workflow, dark-mode re-skinning, and the **render-on-desktop-then-inspect** loop. Read this when the user hands you a React/HTML design to rebuild in CN1. +- `references/flutter-to-cn1.md` — Bringing **Dart / Flutter widget source** into Codename One. Unlike the other porting guides this is mostly not a rewrite: the `transcode-flutter` Maven goal compiles Dart widgets to Java at build time (no Dart SDK involved), and `FlutterUI.wrap` / `FlutterUI.runApp` mount the result. Read this when the user has Dart or Flutter code, asks to embed a Flutter screen in a CN1 app, or asks whether a Flutter app can be ported. - `references/testing-and-screenshots.md` — `AbstractTest`, `TestUtils`, `screenshotTest`, the `cn1:test` Maven goal, the screenshot tolerance algorithm. - `references/mockup-comparison.md` — Building a screen to **match a designer mockup**: `tools/CompareToMockup.java` scores a render against a mockup image and prints a similarity % (with partial/region masking so device chrome doesn't sabotage the score), and `tools/DesignImport.java` turns a Figma/Sketch/Adobe XD file **or an HTML/React design's `tokens.css`/`styles.css`** (e.g. a Claude-generated mockup) into a starter `theme.css` + tokens + layout map. Read this when the user gives you a mockup, a Figma/Sketch/XD file, an HTML/CSS design bundle, or asks "how close is this screen to the design". - `references/junit-testing.md` — Standard JUnit 5 tests against the simulator via `@CodenameOneTest`. Annotations (`@RunOnEdt`, `@Theme`, `@DarkMode`, `@LargerText`, `@Orientation`, `@RTL`, `@SimulatorProperty`), how it coexists with `cn1:test`, and why a headless CI runner has to be configured with Xvfb (or accepts that JUnit test classes will be skipped). diff --git a/scripts/initializr/common/src/main/resources/skill/references/flutter-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/flutter-to-cn1.md new file mode 100644 index 00000000000..b0f58f813b8 --- /dev/null +++ b/scripts/initializr/common/src/main/resources/skill/references/flutter-to-cn1.md @@ -0,0 +1,150 @@ +# Flutter / Dart -> Codename One + +Codename One compiles **Dart widget source to Java at build time** and renders +it through its own pipeline. There is no Dart VM, no embedded engine and no +platform view in the result -- a Dart screen becomes ordinary CN1 components. + +That makes this different from the other porting guides in this directory: +`android-to-cn1.md` and `react-to-cn1.md` are about *rewriting* a design in CN1. +This one is mostly about *keeping* the Dart and making the build consume it. +Rewrite only what the transpiler refuses. + +> Read alongside `references/build-and-run.md` (Maven goals, JDK matrix) and +> `references/ui-components.md` (for the parts you do hand-write). + +## Decide which of the two shapes you are in + +| You have | Use | Result | +| --- | --- | --- | +| A Dart widget or screen you want inside an existing CN1 app | `FlutterUI.wrap(widget)` | a `Container` you add anywhere; the surrounding screen keeps its own theme | +| A Dart application you want to run as the app | `FlutterUI.runApp(widget)` | a new `Form` is created, the Material base theme is installed, and it is shown | + +Start with `wrap` whenever the answer is not obviously "the whole app". It is +reversible, it touches one screen, and it will surface the unsupported +constructs in that screen before anyone commits to the rest. + +## Wiring the build + +1. Add the runtime to `common/pom.xml`. Generated projects already contain it + commented out: + +```xml + + com.codenameone + codenameone-flutter-runtime + ${cn1.version} + +``` + +2. Put the Dart under `common/src/main/flutter/`. + +3. Build normally. The `transcode-flutter` goal is already bound to + `generate-sources` in the generated `pom.xml`, and is a **silent no-op** + while `src/main/flutter` does not exist. + +**No Dart SDK is needed.** The transpiler is Java; the Dart is input, never +executed. Do not add a Flutter SDK to the project or to CI on account of this. + +Useful properties: + +| Property | Default | +| --- | --- | +| `cn1.flutter.sourceDir` | `src/main/flutter` | +| `cn1.flutter.outputDir` | `target/generated-sources/flutter` | +| `cn1.flutter.package` | `com.codename1.generated.flutter` | + +4. Optional, for a `runApp` application: drop the platform theme. Its widgets + draw everything themselves, so a native theme underneath is dead weight + (a few hundred KB on iOS and Android). This is the generic setting for + any app whose own theme is complete, not something specific to Flutter: + * set `includeNativeBool: false` in the `#Constants` block of + `common/src/main/css/theme.css`, so the theme stops asking for a native + base at run time; + * set the build hint `codename1.arg.nativeTheme=custom` (or + `@Build(nativeTheme = ThemeMode.CUSTOM)`), so no build packages a platform + theme and no port installs one. + A per-platform hint outranks it, and generated projects set + `codename1.arg.ios.themeMode=modern` and `codename1.arg.and.themeMode=modern` + in `codenameone_settings.properties`: change those to `custom` or delete them. + + Leave both alone if you `wrap` widgets inside screens that rely on the + native theme. + +## The port loop that actually converges + +Do not read the whole Dart codebase and plan. Let the build tell you what it +cannot do, because that list is exact and the reading is guesswork. + +1. Copy the Dart in. +2. `mvn -pl common install`. The transpiler **fails the build** on a construct + outside the supported subset and names it with a milestone code. +3. Fix or stub that one construct. Re-run. +4. Repeat until it compiles. Only then run it. +5. Run in the simulator and read the error inventory: widgets that are not + implemented are **recorded**, not silently skipped, so a screen missing + something says so rather than rendering blank. + +Two failure modes to recognise, because they look alike and are not: + +* **Transpile failure** -- a build error with a milestone code. The Dart uses a + language or library feature that is not supported yet. This is a wall; work + around it in the Dart. +* **Runtime gap** -- it builds and runs, but a widget is missing or wrong. This + is in the runtime library, and the error inventory names the widget. + +## What tends to need hand-work + +* **Anything that is not UI.** Platform channels, plugins, `dart:io`, + `dart:ffi`. Replace with the CN1 equivalent (`references/native-interfaces.md` + for native code, the IO/networking section of `references/java-api-subset.md` + for the rest). +* **Asset declarations.** Flutter's `pubspec.yaml` asset section has no + counterpart; assets move to the CN1 project's own resources. +* **Packages from pub.dev.** Only the framework subset is transpiled. A + third-party package has to be replaced or its source brought in and + transpiled with everything else. +* **Fonts.** A Dart `TextStyle` naming a bundled font needs that font present + in the CN1 project the normal way. + +## Verifying a port, rather than eyeballing it + +If you have the original running, compare frames rather than opinions. The +pattern used to build this feature: + +* Render the same route on both sides at the **same framebuffer size**, with + the same safe-area insets. Rescaling either side to match the other blurs one + of them and invalidates the comparison. +* Diff at absolute position and score the percentage of differing pixels. +* When a screen scores badly, check for a **uniform offset first**. A whole + page shifted by a few pixels scores like a broken screen while nothing is + actually wrong with the content; one spacing value upstream is usually the + cause. + +See `references/testing-and-screenshots.md` and +`references/mockup-comparison.md` for the CN1-side tooling that does this. + +## Measuring the result + +`scripts/flutter-bench/run_bench.py` in the Codename One repository builds one +application both ways and reports installed size, executable code, download +size, cold start and idle memory per platform. If you are asked whether a port +is "worth it", measure it with that rather than asserting a direction -- it +reports the metrics Codename One loses as well as the ones it wins. + +Two of its rules are worth copying into any comparison you make yourself: + +* **Interleave the runs** and record the machine's load. A ratio measured under + different load twice is not a ratio. +* **Start-up is a bracket.** The two runtimes do not expose the same event, so + a single number requires picking one, and picking one is how a comparison + ends up flattering whoever wrote it. + +## Gotchas + +* A `wrap`ped subtree deliberately does **not** install the Material base + theme, so embedding a widget will not restyle the screen around it. If the + widget looks unstyled, that is why -- style it through the CN1 theme. +* CN1 is single-threaded on the EDT. Dart's `async`/`await` transpiles, but + anything that assumed a separate isolate does not. +* `FlutterUI.runApp` must be called with the display initialized, i.e. from the + CN1 lifecycle, not from a static initializer. diff --git a/scripts/ios/screenshots-metal-27/graphics-clip.png b/scripts/ios/screenshots-metal-27/graphics-clip.png index de2367437c7..dc35b26ea8f 100644 Binary files a/scripts/ios/screenshots-metal-27/graphics-clip.png and b/scripts/ios/screenshots-metal-27/graphics-clip.png differ diff --git a/scripts/ios/screenshots-metal/graphics-clip.png b/scripts/ios/screenshots-metal/graphics-clip.png index e3abda26954..27e4d9face5 100644 Binary files a/scripts/ios/screenshots-metal/graphics-clip.png and b/scripts/ios/screenshots-metal/graphics-clip.png differ diff --git a/scripts/ios/screenshots-tv/graphics-clip.png b/scripts/ios/screenshots-tv/graphics-clip.png index 613a7a3cc8d..3fd956ed3dc 100644 Binary files a/scripts/ios/screenshots-tv/graphics-clip.png and b/scripts/ios/screenshots-tv/graphics-clip.png differ diff --git a/scripts/ios/screenshots-watch/graphics-clip-direct-aa-off.png b/scripts/ios/screenshots-watch/graphics-clip-direct-aa-off.png index 4d70ce3a775..795ab7a5342 100644 Binary files a/scripts/ios/screenshots-watch/graphics-clip-direct-aa-off.png and b/scripts/ios/screenshots-watch/graphics-clip-direct-aa-off.png differ diff --git a/scripts/ios/screenshots-watch/graphics-clip-direct-aa-on.png b/scripts/ios/screenshots-watch/graphics-clip-direct-aa-on.png index 4d70ce3a775..795ab7a5342 100644 Binary files a/scripts/ios/screenshots-watch/graphics-clip-direct-aa-on.png and b/scripts/ios/screenshots-watch/graphics-clip-direct-aa-on.png differ diff --git a/scripts/ios/screenshots-watch/graphics-clip-image-aa-off.png b/scripts/ios/screenshots-watch/graphics-clip-image-aa-off.png index acb41f8b78d..240f407e22f 100644 Binary files a/scripts/ios/screenshots-watch/graphics-clip-image-aa-off.png and b/scripts/ios/screenshots-watch/graphics-clip-image-aa-off.png differ diff --git a/scripts/ios/screenshots-watch/graphics-clip-image-aa-on.png b/scripts/ios/screenshots-watch/graphics-clip-image-aa-on.png index acb41f8b78d..a3b52eed71f 100644 Binary files a/scripts/ios/screenshots-watch/graphics-clip-image-aa-on.png and b/scripts/ios/screenshots-watch/graphics-clip-image-aa-on.png differ diff --git a/scripts/javascript/screenshots/chart-time.png b/scripts/javascript/screenshots/chart-time.png index d977befb074..d1e7122702e 100644 Binary files a/scripts/javascript/screenshots/chart-time.png and b/scripts/javascript/screenshots/chart-time.png differ diff --git a/scripts/linux/screenshots-arm/ImageViewerNavigationModes.png b/scripts/linux/screenshots-arm/ImageViewerNavigationModes.png index 58536821c1e..ab399e9c940 100644 Binary files a/scripts/linux/screenshots-arm/ImageViewerNavigationModes.png and b/scripts/linux/screenshots-arm/ImageViewerNavigationModes.png differ diff --git a/scripts/linux/screenshots-arm/graphics-draw-image-rect.png b/scripts/linux/screenshots-arm/graphics-draw-image-rect.png index cfdd331787e..34c4ad11f07 100644 Binary files a/scripts/linux/screenshots-arm/graphics-draw-image-rect.png and b/scripts/linux/screenshots-arm/graphics-draw-image-rect.png differ diff --git a/scripts/linux/screenshots/ImageViewerNavigationModes.png b/scripts/linux/screenshots/ImageViewerNavigationModes.png index 58536821c1e..ab399e9c940 100644 Binary files a/scripts/linux/screenshots/ImageViewerNavigationModes.png and b/scripts/linux/screenshots/ImageViewerNavigationModes.png differ diff --git a/scripts/linux/screenshots/graphics-draw-image-rect.png b/scripts/linux/screenshots/graphics-draw-image-rect.png index cfdd331787e..34c4ad11f07 100644 Binary files a/scripts/linux/screenshots/graphics-draw-image-rect.png and b/scripts/linux/screenshots/graphics-draw-image-rect.png differ diff --git a/scripts/mac-catalyst/screenshots/graphics-clip.png b/scripts/mac-catalyst/screenshots/graphics-clip.png index 13fc5a65605..750e16f440c 100644 Binary files a/scripts/mac-catalyst/screenshots/graphics-clip.png and b/scripts/mac-catalyst/screenshots/graphics-clip.png differ diff --git a/scripts/macos/screenshots/graphics-clip.png b/scripts/macos/screenshots/graphics-clip.png index 014950057ea..5c6d64cdf11 100644 Binary files a/scripts/macos/screenshots/graphics-clip.png and b/scripts/macos/screenshots/graphics-clip.png differ diff --git a/scripts/run-ios-native-tests.sh b/scripts/run-ios-native-tests.sh index 238e6d1f9c1..0abcd0e3a80 100755 --- a/scripts/run-ios-native-tests.sh +++ b/scripts/run-ios-native-tests.sh @@ -212,6 +212,17 @@ if [ -f "$PODFILE_LOCK" ] && grep -q "GoogleMLKit/" "$PODFILE_LOCK"; then ) ri_log "Google ML Kit detected; selecting its supported x86_64 simulator slice" fi +# The same watch-simulator pin run-ios-ui-tests.sh applies (see the comment there): the +# embedded watch app otherwise compiles an x86_64 slice nothing on an arm64 runner can +# run, and when this step shares the UI step's derived data a different setting would +# also rebuild the watch objects that step already produced. +WATCH_SIM_EXCLUDED_ARCHS="x86_64" +if [ "$(uname -m 2>/dev/null)" = "x86_64" ] || [ ${#SIMULATOR_ARCH_ARGS[@]} -gt 0 ]; then + WATCH_SIM_EXCLUDED_ARCHS="arm64" +fi +ARCH_XCCONFIG="$(mktemp -t cn1-ios-archs).xcconfig" +printf 'EXCLUDED_ARCHS[sdk=watchsimulator*] = %s\n' "$WATCH_SIM_EXCLUDED_ARCHS" > "$ARCH_XCCONFIG" +DERIVED_ARGS+=(-xcconfig "$ARCH_XCCONFIG") ri_log "Running xcodebuild test (scheme=$TEST_SCHEME, destination=$DESTINATION)" set +e xcodebuild \ diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index c71c7e71771..7c71b12e634 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -680,6 +680,25 @@ XCODE_BUILD_CMD=( -destination-timeout 120 -derivedDataPath "$DERIVED_DATA_DIR" ) +# Every simulator build gets an xcconfig, even when ARCHS is not forced, because of +# the watch app a companion project embeds. Its simulator ARCHS is $(ARCHS_STANDARD) +# with ONLY_ACTIVE_ARCH, which reads as "follow the host" -- but the run destination +# is an iPhone, so the watch target, built for a different platform, has no active +# arch and compiles every standard one. On an arm64 runner that is an x86_64 watch +# slice nothing can run: a full second ParparVM translation, a third of this step's +# compile (measured: 3206 x86_64 units beside 3206 arm64 ones). Excluding every +# simulator arch but BUILD_ARCH keeps the watch on the slice the app itself uses. +# EXCLUDED_ARCHS rather than ARCHS, because the watch target sets ARCHS itself and +# a target setting outranks an xcconfig; nothing sets its EXCLUDED_ARCHS. +# run-ios-native-tests.sh writes the same line: it reuses this derived data, and a +# watch whose settings differ is rebuilt from scratch. +case "$BUILD_ARCH" in + x86_64) WATCH_SIM_EXCLUDED_ARCHS="arm64" ;; + *) WATCH_SIM_EXCLUDED_ARCHS="x86_64" ;; +esac +ARCH_XCCONFIG="$(mktemp -t cn1-ios-archs).xcconfig" +printf 'EXCLUDED_ARCHS[sdk=watchsimulator*] = %s\n' "$WATCH_SIM_EXCLUDED_ARCHS" > "$ARCH_XCCONFIG" +XCODE_BUILD_CMD+=(-xcconfig "$ARCH_XCCONFIG") if [ "$USE_GENERIC_BUILD_DESTINATION" = "true" ] || [ "$FORCE_SIMULATOR_ARCH" = "true" ]; then ri_log "Forcing simulator ARCHS=$BUILD_ARCH" # The override has to be scoped to the simulator SDK: unscoped, it is forced on @@ -695,17 +714,13 @@ if [ "$USE_GENERIC_BUILD_DESTINATION" = "true" ] || [ "$FORCE_SIMULATOR_ARCH" = # resources. An xcconfig is where the conditional syntax is actually evaluated, # and it sits below target-level settings, so the watch target's own # ARCHS[sdk=watchos*]=arm64_32 still wins where it applies. - ARCH_XCCONFIG="$(mktemp -t cn1-ios-archs).xcconfig" { printf 'ARCHS[sdk=iphonesimulator*] = %s\n' "$BUILD_ARCH" printf 'EXCLUDED_ARCHS[sdk=iphonesimulator*] = %s\n' "$SIMULATOR_EXCLUDED_ARCHS" - } > "$ARCH_XCCONFIG" - ri_log "Simulator arch xcconfig -> $ARCH_XCCONFIG" - XCODE_BUILD_CMD+=( - -xcconfig "$ARCH_XCCONFIG" - "ONLY_ACTIVE_ARCH=YES" - ) + } >> "$ARCH_XCCONFIG" + XCODE_BUILD_CMD+=("ONLY_ACTIVE_ARCH=YES") fi +ri_log "Simulator arch xcconfig -> $ARCH_XCCONFIG: $(tr '\n' ';' < "$ARCH_XCCONFIG")" # Optimize the translated C (Xcode's Debug config defaults to -O0). With -O0 the # scalar baseline is unvectorized, so the SIMD benchmark overstates the speedup; # -O2 lets the compiler auto-vectorize scalar, making the SIMD-vs-scalar diff --git a/scripts/website/sync_port_status_reports.sh b/scripts/website/sync_port_status_reports.sh index 7debbc3c477..66debd0638d 100755 --- a/scripts/website/sync_port_status_reports.sh +++ b/scripts/website/sync_port_status_reports.sh @@ -110,3 +110,21 @@ if git -C "${REPO_ROOT}" show "FETCH_HEAD:evidence/environment.json" > "${enviro else echo "No valid persisted browser evidence; using the checked-in first-run snapshot." >&2 fi + +# The Flutter benchmark, published nightly by flutter-bench.yml. There is no +# checked-in fallback: without a published run the page simply omits the +# section, rather than presenting numbers nobody measured. +benchmark_candidate="${tmp_dir}/flutter_benchmark.json" +benchmark_target="${REPO_ROOT}/docs/website/data/port_status_flutter_benchmark.json" +if git -C "${REPO_ROOT}" show "FETCH_HEAD:benchmarks/flutter.json" > "${benchmark_candidate}" 2>/dev/null && \ + jq -e ' + .schema_version == 1 and + (.generated_at | type == "string") and + (.platforms | type == "object") and + any(.platforms[]; .status == "measured") + ' "${benchmark_candidate}" >/dev/null; then + cp "${benchmark_candidate}" "${benchmark_target}" + echo "Resolved the Flutter benchmark from ${DATA_REF}." +else + echo "No published Flutter benchmark; the page omits that section." >&2 +fi diff --git a/scripts/website/validate_port_status.mjs b/scripts/website/validate_port_status.mjs index 3def688f4e9..07a0b21ef38 100644 --- a/scripts/website/validate_port_status.mjs +++ b/scripts/website/validate_port_status.mjs @@ -119,9 +119,36 @@ function validate() { fail(`deployment and performance evidence is missing ${required}`); } } - if (/flutter/i.test(pageText)) { + // The Flutter benchmark is the one place this page may name another + // framework, and only inside its own marked section: it measures the same + // Flutter application built by Flutter and by Codename One on each port, so + // it belongs beside the ports it measures. Everywhere else the rule stands, + // so the check runs on the page with that section cut out rather than being + // dropped. + const benchmarkSections = Array.from( + page.matchAll(/]*\bdata-flutter-benchmark(?=[\s=>]))[^>]*>[\s\S]*?<\/section>/gi), + (match) => match[0]); + if (benchmarkSections.length > 1) { + fail("the Flutter benchmark must render as a single section"); + } + const textOutsideBenchmark = benchmarkSections + .reduce((html, section) => html.replace(section, " "), page) + .replace(/<[^>]+>/g, " ").replace(/\s+/g, " "); + if (/flutter/i.test(textOutsideBenchmark)) { fail("the Port Status page must not contain unrelated framework-comparison language"); } + for (const section of benchmarkSections) { + // Its own attributes only: the evidence counts above must not see it. + if (/\bdata-(?:performance|feature|deployment)-(?:row|cell)\b|\bdata-port=/i.test(section)) { + fail("the Flutter benchmark section reuses the compliance matrix's counted attributes"); + } + const rows = Array.from(section.matchAll(/]*\bdata-flutter-benchmark-row=[^>]*>([\s\S]*?)<\/tr>/gi)); + for (const [, row] of rows) { + if (!/\b\d+\.\d\dx\b/.test(row.replace(/<[^>]+>/g, " "))) { + fail("every Flutter benchmark row must state its ratio"); + } + } + } const portCards = countMatches(page, /\bdata-port-card(?:=|\s|>)/g); const featureRows = countMatches(page, /\bdata-feature-row(?:=|\s|>)/g); diff --git a/scripts/windows/screenshots/MorphElementMorphScreenshotTest.png b/scripts/windows/screenshots/MorphElementMorphScreenshotTest.png index dbe149ca4dd..ee4a791dd20 100644 Binary files a/scripts/windows/screenshots/MorphElementMorphScreenshotTest.png and b/scripts/windows/screenshots/MorphElementMorphScreenshotTest.png differ diff --git a/scripts/windows/screenshots/graphics-draw-image-rect.png b/scripts/windows/screenshots/graphics-draw-image-rect.png index 2f13c818e76..aae7f436401 100644 Binary files a/scripts/windows/screenshots/graphics-draw-image-rect.png and b/scripts/windows/screenshots/graphics-draw-image-rect.png differ diff --git a/tests/core.sh b/tests/core.sh index eaafc4b18a7..2ee6486a80f 100644 --- a/tests/core.sh +++ b/tests/core.sh @@ -16,6 +16,7 @@ mvn com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-project -DgroupId=com.codenameone.tests \ -Dversion=1.0-SNAPSHOT \ -DinteractiveMode=false \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DsourceProject=$ANT_PROJECT_DIR \ -Dcn1Version=$CN1_VERSION \ -e diff --git a/tests/env.sh b/tests/env.sh index 04441e8789e..5ea7ccd579a 100644 --- a/tests/env.sh +++ b/tests/env.sh @@ -7,4 +7,12 @@ fi if [ ! -d $SCRIPTPATH/build ]; then mkdir $SCRIPTPATH/build -fi \ No newline at end of file +fi + +# The javaVersion to generate archetype projects with, derived from the running +# JDK. This suite runs on JDK 8 on some legs and the archetype now defaults to +# 17, so a generated project would target a release this JDK cannot produce. +if [ -z "${CN1_ARCHETYPE_JAVA_VERSION:-}" ]; then + CN1_ARCHETYPE_JAVA_VERSION="$(bash "$SCRIPTPATH/../scripts/ci/archetype-java-version.sh")" +fi +export CN1_ARCHETYPE_JAVA_VERSION diff --git a/tests/signin-demo.sh b/tests/signin-demo.sh index 4aba758243d..856ab7a92c6 100644 --- a/tests/signin-demo.sh +++ b/tests/signin-demo.sh @@ -16,6 +16,7 @@ mvn com.codenameone:codenameone-maven-plugin:${CN1_VERSION}:generate-app-project -DgroupId=com.codenameone.tests \ -Dversion=1.0-SNAPSHOT \ -DinteractiveMode=false \ + -DjavaVersion=$CN1_ARCHETYPE_JAVA_VERSION \ -DsourceProject=$ANT_PROJECT_DIR \ -Dcn1Version=$CN1_VERSION \ -e diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 781eb2caa60..b12f1062420 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -3305,6 +3305,15 @@ extern CN1_NORETURN void cn1ThrowArrayIndexOrDie(CODENAME_ONE_THREAD_STATE, int // usually reads adjacent heap rather than faulting, so no signal ever arrives. extern CN1_NORETURN void cn1ThrowNullPointerOrDie(CODENAME_ONE_THREAD_STATE); +/* Called from the stub of a method the translator culled, only when the translation + * ran with CN1_CULL_TRAP (see BytecodeMethod.appendMethodC). Reaching one means the + * cull removed live code, so this dies loudly, naming the method. */ +static inline void cn1CulledMethodCalled(const char *method) { + fprintf(stderr, "CN1 FATAL: called %s, which the translator culled as unreachable\n", method); + fflush(stderr); + abort(); +} + /* Constructing throw helpers for the paths that must RETURN rather than die: a * frameless method hands the pending exception back to its caller's frame, and the * expression forms have to yield a value. Each allocates its own exception so the diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index aa46ccbbac5..9e73577af23 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -19137,6 +19137,7 @@ void initConstantPool() { // it will wait two seconds unless an explicit GC occurs java_lang_System_startGCThread__(threadStateData); finishedNativeAllocations(); + cn1StartupPhase("constantPoolReady"); } JAVA_OBJECT utf8String = NULL; diff --git a/vm/ByteCodeTranslator/src/cn1_intrinsics.h b/vm/ByteCodeTranslator/src/cn1_intrinsics.h index 7d1c3e3b8e8..58b869309a1 100644 --- a/vm/ByteCodeTranslator/src/cn1_intrinsics.h +++ b/vm/ByteCodeTranslator/src/cn1_intrinsics.h @@ -447,4 +447,41 @@ static inline JAVA_CHAR cn1InlStrCharAt(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT s #endif // CN1_HAVE_SB_INTRINSICS +#if defined(__has_include) +#if __has_include("dart_core_DartLongList.h") +#include "dart_core_DartLongList.h" +#define CN1_HAVE_DLL_INTRINSICS 1 +#endif +#endif + +#ifdef CN1_HAVE_DLL_INTRINSICS + +// Dart List element access. The bounds test is against the LOGICAL length +// (which can be smaller than the backing array), exactly as DartLongList does; +// out-of-range falls through to the out-of-line method so RangeError.indexError +// stays single-sourced there. + +static inline JAVA_LONG cn1InlDllGet(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT self, JAVA_LONG index) { + struct obj__dart_core_DartLongList* t = (struct obj__dart_core_DartLongList*)self; + if(__builtin_expect(self != JAVA_NULL && + index >= 0 && index < (JAVA_LONG)t->dart_core_DartLongList_len, 1)) { + JAVA_ARRAY arr = (JAVA_ARRAY)t->dart_core_DartLongList_a; + return ((JAVA_ARRAY_LONG*)CN1_ARRAY_DATA(arr))[(JAVA_INT)index]; + } + return dart_core_DartLongList_getLong___long_R_long(threadStateData, self, index); +} + +static inline JAVA_LONG cn1InlDllSet(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT self, JAVA_LONG index, JAVA_LONG value) { + struct obj__dart_core_DartLongList* t = (struct obj__dart_core_DartLongList*)self; + if(__builtin_expect(self != JAVA_NULL && + index >= 0 && index < (JAVA_LONG)t->dart_core_DartLongList_len, 1)) { + JAVA_ARRAY arr = (JAVA_ARRAY)t->dart_core_DartLongList_a; + ((JAVA_ARRAY_LONG*)CN1_ARRAY_DATA(arr))[(JAVA_INT)index] = value; + return value; + } + return dart_core_DartLongList_setLong___long_long_R_long(threadStateData, self, index, value); +} + +#endif // CN1_HAVE_DLL_INTRINSICS + #endif // CN1_INTRINSICS_H diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index 74f70217e71..2b37037e8bc 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -1850,7 +1850,6 @@ public String generateCCode(List allClasses) { if (ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_MACOS) { b.append(" [NSApplication sharedApplication];\n"); b.append(" [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];\n"); - b.append(" CN1MacInstallMainMenu();\n"); b.append(" CN1MacInstallAppDelegate();\n"); b.append(" dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{\n"); // OPEN QUESTION, INHERITED AND DELIBERATELY NOT ANSWERED HERE. @@ -1887,6 +1886,24 @@ public String generateCCode(List allClasses) { // is defined in nativeMethods.m and appears in no // header; without that it is an implicit declaration. b.append(" });\n"); + // The main window, built HERE: after the VM boot is already + // dispatched (so it overlaps it instead of delaying it) and before + // [NSApp run] (so it does not wait for the run loop to finish + // launching, which on a slow machine is the longest wait on the + // path to the first frame). Weak, so a port without the function + // still links and keeps the queued build in CN1MacInstallAppDelegate. + b.append(" { extern void CN1MacBuildMainWindowBeforeRun(void) __attribute__((weak_import));\n"); + b.append(" if (CN1MacBuildMainWindowBeforeRun) CN1MacBuildMainWindowBeforeRun(); }\n"); + // The menu bar, AFTER the window and its first frame rather than + // before everything. Building it is 17-22ms of main thread on the + // benchmark runner (the first separator item loads the system + // symbol-image catalog), and in front of the window build it delayed + // both the window and the dispatch of the VM boot above by that much. + // A menu bar only has to exist before the user can reach it, which is + // once [NSApp run] starts taking events -- still after this line. + // CN1MacInstallMainMenu is idempotent, so an application that + // published menu commands in the meantime keeps them. + b.append(" CN1MacInstallMainMenu();\n"); b.append(" [NSApp run];\n}\n\n"); } else { b.append(" "); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 746150c4687..a6780080317 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -961,6 +961,16 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File replaceInFile(projectWorkspaceData, "KitchenSink", appName); + // The iOS template sets COMPRESS_PNG_FILES = NO and STRIP_PNG_TEXT = NO. Both are + // needed: Xcode's copypng treats -strip-PNG-text as a request to compress too, so + // with STRIP_PNG_TEXT on (its Release default) the first setting alone changes + // nothing. Xcode's default rewrites every + // loose PNG in the bundle into Apple's CgBI form -- premultiplied, byte-swapped + // and recompressed with a weaker filter -- which made an application's PNGs larger, + // not smaller: 145 photographic PNGs of the Flutter gallery went from 14.4MB to + // 20.0MB. Nothing here needs CgBI. Images decode through ImageIO, which reads a + // standard PNG just as well, and Image.isPNG only checks the signature both forms + // share. An app that wants CgBI can put its images in an asset catalog. File projectPbx = new File(xcproj, "project.pbxproj"); copy(ByteCodeTranslator.class.getResourceAsStream(templateRoot + "/template.xcodeproj/project.pbxproj"), new FileOutputStream(projectPbx)); @@ -1439,6 +1449,65 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app writer.append("else()\n"); writer.append(" target_compile_options(${PROJECT_NAME} PRIVATE -fwrapv -fno-strict-aliasing -fno-builtin-fmod -fno-builtin-fmodf)\n"); writer.append("endif()\n"); + // CODE SIZE: an LLVM inline threshold of 50 on optimized builds (the default is + // 225 at -O2, 250 at -O3). The translator emits one small C function per Java + // method plus per-call helpers, and at the default threshold clang inlines them + // into every caller -- most of the __text of a translated app is those copies. + // Measured on the transpiled Flutter gallery (macOS arm64, thin LTO): __text + // 20.3MB -> 16.4MB (-19%), cold start unchanged, the vm/benchmarks compute + // geomean within 2%; -Os reached a similar size but lost 9% on compute + // (it disables loop vectorization). The Xcode templates carry the same flag on + // their Release configurations, compile and LTO link. Clang only: gcc has no + // -mllvm, and clang-cl takes it through /clang:. + // + // GENERATED SOURCES ONLY. The runtime's hand-written C -- the allocator, the + // collector, the write barriers, nativeMethods.c -- stays at the compiler's + // default: it is five files of a few thousand, so it is not where the size is, + // and it is where a lost inline costs time on every allocation. Applied to the + // whole target, the Windows x64 gate's objectAllocation rose 33% in time and + // 23% in peak memory. The translator already records which sources it + // generated in cn1-source-manifest.txt beside this file, so CMake reads that + // at configure time rather than guessing from file names (java_io_File_runtime.c + // is hand-written and named like a class). A source COMPILE_OPTIONS list rather + // than SHELL:, which the per-source property does not accept; the two words are + // the only -mllvm in a file's options, so de-duplication cannot split them. + writer.append("set(CN1_MANIFEST \"${CMAKE_CURRENT_SOURCE_DIR}/cn1-source-manifest.txt\")\n"); + // GCC gets its own limit. The host cc of a Linux build without zig is gcc (the + // Flutter benchmark's runner is one), and none of the clang flags above reach + // it -- measured, the Linux gallery's code did not move at all. The GCC knob is + // max-inline-insns-single (the size limit on functions a caller may inline): + // 20 took the gallery's generated object code from 26.45MB to 23.90MB (-9.6%) + // with every vm/benchmarks workload within noise. max-inline-insns-auto would + // shrink more but stops -O3 inlining a recursive call into itself, which cost + // the recursion benchmark 52%; -O2 costs the same. Turning off -O3's cloning, + // unswitching, peeling and loop versioning bought 0.2% and was left alone. + writer.append("if(EXISTS \"${CN1_MANIFEST}\" AND (MSVC OR CMAKE_C_COMPILER_ID MATCHES \"Clang\" OR CMAKE_C_COMPILER_ID STREQUAL \"GNU\"))\n"); + writer.append(" if(MSVC)\n"); + writer.append(" set(CN1_SIZE_OPTIONS \"$<$:/clang:-mllvm>;$<$:/clang:-inline-threshold=50>\")\n"); + writer.append(" elseif(CMAKE_C_COMPILER_ID STREQUAL \"GNU\")\n"); + writer.append(" set(CN1_SIZE_OPTIONS \"$<$:--param=max-inline-insns-single=20>\")\n"); + writer.append(" else()\n"); + writer.append(" set(CN1_SIZE_OPTIONS \"$<$:-mllvm>;$<$:-inline-threshold=50>\")\n"); + // -O2 rather than CMake's Release -O3 for the generated sources, when there is + // no LTO (the default; zig cc on Linux). Measured with clang on the gallery's + // generated objects at threshold 50: .text 22.43MB -> 21.49MB (-4.2%), and + // the vm/benchmarks compute geomean went 59.44 -> 57.58ms -- faster, not + // slower, recursion most of all (161 -> 118ms), since -O3's extra cloning + // and unrolling of small translated functions costs i-cache here. Placed + // after CMAKE_C_FLAGS_RELEASE on the command line, so it wins. Not with LTO, + // which was not measured this way, and not for MSVC: clang-cl is already /O2. + writer.append(" if(NOT CN1_ENABLE_LTO)\n"); + writer.append(" list(APPEND CN1_SIZE_OPTIONS \"$<$:-O2>\")\n"); + writer.append(" endif()\n"); + writer.append(" endif()\n"); + writer.append(" file(STRINGS \"${CN1_MANIFEST}\" CN1_GENERATED_LINES REGEX \"^[^#|]+\\\\.c\\\\|generated\\\\|\")\n"); + writer.append(" set(CN1_GENERATED_SOURCES \"\")\n"); + writer.append(" foreach(CN1_LINE IN LISTS CN1_GENERATED_LINES)\n"); + writer.append(" string(REGEX REPLACE \"\\\\|.*$\" \"\" CN1_NAME \"${CN1_LINE}\")\n"); + writer.append(" list(APPEND CN1_GENERATED_SOURCES \"${CN1_APP_SOURCE_ROOT}/${CN1_NAME}\")\n"); + writer.append(" endforeach()\n"); + writer.append(" set_source_files_properties(${CN1_GENERATED_SOURCES} PROPERTIES COMPILE_OPTIONS \"${CN1_SIZE_OPTIONS}\")\n"); + writer.append("endif()\n"); if (executable && !windows) { // ThinLTO for the Release Linux executable: the translator emits one // C function per Java method, so cross-TU inlining is where the @@ -1453,6 +1522,20 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app writer.append("if(CMAKE_C_COMPILER_ID MATCHES \"Clang\")\n"); writer.append(" target_compile_options(${PROJECT_NAME} PRIVATE $<$:-flto=thin>)\n"); writer.append(" target_link_options(${PROJECT_NAME} PRIVATE $<$:-flto=thin>)\n"); + // The ThinLTO backend is where cross-translation-unit inlining happens, + // so it needs the same threshold as the compile above or it re-inlines + // everything the compile left alone. A link-time -mllvm is NOT forwarded + // by the clang driver (it warns "argument unused"); -plugin-opt=- is + // the spelling both the LLVM gold plugin (under GNU ld or gold) and lld + // hand to LLVM as a command-line option. + // + // Not under zig cc (the musl and cross-arch toolchain): it identifies as + // Clang, but its linker driver accepts a fixed set of arguments and fails + // the link with "unsupported linker arg: -plugin-opt". Those builds keep + // the compile-side threshold above and LLVM's default at link time. + writer.append(" if(NOT CMAKE_C_COMPILER MATCHES \"zig\")\n"); + writer.append(" target_link_options(${PROJECT_NAME} PRIVATE $<$:-Wl,-plugin-opt=-inline-threshold=50>)\n"); + writer.append(" endif()\n"); writer.append("endif()\n"); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index 25dbe3c6cdf..77468d0811d 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -140,6 +140,7 @@ public static void setDependencyGraph(MethodDependencyGraph dependencyGraph) { private final static Set virtualMethodsInvoked = new TreeSet(); private String desc; private boolean eliminated; + private boolean bodyCulled; private boolean barebone; private boolean disableDebugInfo; private boolean disableNullAndArrayBoundsChecks; @@ -176,6 +177,14 @@ public static boolean isOnDeviceDebug() { return onDeviceDebug; } + /// -Dcn1.cull.trap=true or CN1_CULL_TRAP=1: culled-method stubs abort, naming the + /// method, instead of returning 0. See appendMethodC. The dotted key is what makes + /// Util.getProperty's environment name CN1_CULL_TRAP. + static boolean isCullTrapEnabled() { + String v = Util.getProperty("cn1.cull.trap", "false"); + return "true".equalsIgnoreCase(v) || "1".equals(v); + } + public boolean isBarebone() { return barebone; } @@ -1759,12 +1768,23 @@ public void appendMethodC(StringBuilder b) { if(nativeMethod) { return; } - if (onDeviceDebug && !eliminated) { + if (onDeviceDebug && !eliminated && !bodyCulled) { appendFrameInfoStruct(b); } appendCMethodPrefix(b, ""); b.append(" {\n"); - if(eliminated) { + if(eliminated || bodyCulled) { + // A culled method of a surviving class is still emitted, because dispatch + // tables name it -- and a stub that returns 0 turns a wrong cull into a silent + // null. CN1_CULL_TRAP (our CI sets it, as it does CN1_NATIVE_VERIFY) makes the + // stub abort with the method's name instead, so a cull that removed something + // live fails the suite that reached it. Abort, not a thrown Error: a + // catch (Throwable) would swallow that, and does in exactly the defensive code + // most likely to be the caller. + if (isCullTrapEnabled()) { + b.append(" cn1CulledMethodCalled(\"").append(clsName).append('.') + .append(methodName).append(desc.replace('"', '\'')).append("\");\n"); + } if(returnType.isVoid()) { b.append(" return;\n}\n\n"); } else { @@ -3884,6 +3904,19 @@ public void setEliminated(boolean eliminated) { this.eliminated = eliminated; } + /// Replaces the body with the culled-method stub but keeps the method: an instance + /// method live code calls that can never run, because nothing allocates its class + /// (ReachabilityCull.Allocation). The call site still names its virtual_ dispatcher, + /// so the method has to exist; what its body would have called does not. + void cullBody() { + bodyCulled = true; + instructions.clear(); + } + + boolean isBodyCulled() { + return bodyCulled; + } + private int varCounter = 0; diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/InlineIntrinsics.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/InlineIntrinsics.java index 8db11b7829a..02d708b7248 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/InlineIntrinsics.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/InlineIntrinsics.java @@ -59,6 +59,10 @@ public final class InlineIntrinsics { RENAMES.put("java_lang_String_hashCode___R_int", "cn1InlStrHash"); RENAMES.put("java_lang_String_equals___java_lang_Object_R_boolean", "cn1InlStrEquals"); RENAMES.put("java_lang_String_charAt___int_R_char", "cn1InlStrCharAt"); + // Dart List index accesses (DartLongList) -- inlined to raw long[] access + // in a hot loop; see cn1InlDllGet/Set in cn1_intrinsics.h (__has_include-guarded). + RENAMES.put("dart_core_DartLongList_getLong___long_R_long", "cn1InlDllGet"); + RENAMES.put("dart_core_DartLongList_setLong___long_long_R_long", "cn1InlDllSet"); } private InlineIntrinsics() { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index 5cc786e21de..97949060809 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -294,6 +294,7 @@ public int compare(ByteCodeClass a, ByteCodeClass b) { List chunks = new ArrayList(); StringBuilder current = new StringBuilder(); chunks.add(current); + JavascriptMethodGenerator.resetAllocationFunctions(); for (ByteCodeClass cls : sorted) { String code = cls.generateJavascriptCode(classes); if (current.length() > 0 && current.length() + code.length() > CLASS_CHUNK_MAX_BYTES) { @@ -304,6 +305,10 @@ public int compare(ByteCodeClass a, ByteCodeClass b) { } StringBuilder tail = chunks.get(chunks.size() - 1); + // After every class registration (this is the last chunk, and it loads last), so + // each allocation function can capture its classDef at load; see + // JavascriptMethodGenerator.newObjectExpression. + JavascriptMethodGenerator.appendAllocationFunctions(tail); ByteCodeClass mainClass = ByteCodeClass.getMainClass(); if (mainClass != null) { tail.append("jvm.setMain(\"").append(mainClass.getClsName()).append("\", \"") @@ -420,7 +425,8 @@ private static void minifyGeneratedIdentifiers(java.util.List chunkStrin // screenshot runner (lambda2RunBridge:missingDispatch). Use the // regex-based quoted-token collector for those sources instead. java.util.Set stringTokens = collectStringLiteralCn1Tokens(chunkStrings); - stringTokens.addAll(collectBridgeReferencedCn1Tokens()); + java.util.Set bridgeTokens = collectBridgeReferencedCn1Tokens(); + stringTokens.addAll(bridgeTokens); // installNativeBindings overrides BOTH global[name] and the CONSTRUCTED // global[name + "__impl"] (the static-method body) -- see parparvm_runtime.js. // The "__impl" variant never appears as a literal string, so add it for every @@ -437,6 +443,8 @@ private static void minifyGeneratedIdentifiers(java.util.List chunkStrin // (getImpl/valueOfHeap/...) as BARE identifiers -- invisible to the // string-literal scans above -- so protect them from renaming explicitly. excluded.addAll(JavascriptMethodGenerator.RUNTIME_DELEGATE_IDENTIFIERS); + // Field names the allocation functions write as bare keys: renaming is whole-token. + excluded.addAll(JavascriptMethodGenerator.ALLOCATION_FUNCTION_KEYS); defs.removeAll(excluded); // Prefix protection: some bridge names are CONSTRUCTED at runtime by string // concatenation, so the full identifier never appears as a literal -- only @@ -457,8 +465,15 @@ private static void minifyGeneratedIdentifiers(java.util.List chunkStrin // the screenshot runner's lambda stem, is 77 chars). final int MIN_PREFIX_PROTECT_LEN = 16; if (!defs.isEmpty()) { + // Stems come from the bridge sources only. The bundle's own cn1_ literals are + // complete names -- jvm.setMain's main method, and above all every instance + // field's property name in the f: lists -- and the exact-match protection above + // covers them. Used as stems they protected every method whose name extends a + // field's: a field ``strokeWidth`` protected ``strokeWidth_R_double`` and + // ``strokeWidth_double``, the getter and setter a transpiled Dart property + // always has, which left 5,211 functions of the Flutter gallery unminified. java.util.List prefixTokens = new java.util.ArrayList(); - for (String t : stringTokens) { + for (String t : bridgeTokens) { if (t.length() >= MIN_PREFIX_PROTECT_LEN) { prefixTokens.add(t); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java index dae0db383c5..676e253d839 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java @@ -708,6 +708,50 @@ private static boolean emitsMonitor(BytecodeMethod method) { * calls that implementation directly, so a signature-wide "suspending" * would put ``yield*`` in front of a plain function. */ + /// A virtual call site's direct target, as {function id, "1" if it suspends}, or null. + /// + /// monomorphicDispatch only devirtualizes a signature declared ONCE in the whole + /// program, which almost nothing is: Integer.intValue() shares its signature with + /// every Number, HashMap.get(Object) with every Map. Every such call went through + /// cn1_ivResolve -- a methods-map probe per call -- even where the receiver's type + /// leaves one possible body: a final class, or a cone in which RTA sees one + /// instantiated implementation. This asks the suspension analysis's own dispatch + /// model for that body (DispatchModel.directTarget), so the direct call and the + /// site's classification come from one answer. The m: entry stays: only + /// monomorphicDispatch prunes it, and the bridge can still dispatch through it. + /// Kill switch: -Dparparvm.js.sitedevirt.off. + private static String[] siteDirectTarget(Invoke invoke, String receiver) { + if (invoke == null || System.getProperty("parparvm.js.sitedevirt.off") != null) { + return null; + } + JavascriptSuspensionAnalysis.DispatchModel model = JavascriptSuspensionAnalysis.exportedDispatchModel; + Map idx = classIndex; + if (model == null || idx == null) { + return null; + } + BytecodeMethod m = null; + String exactOwner = exactLocalType(receiver); + if (exactOwner != null) { + m = model.exactTarget(exactOwner, invoke.getName(), invoke.getDesc()); + // The site was classified from the declared type's cone, which holds this + // body; a generator body at a site classified synchronous would need a drive. + if (m != null && m.isJavascriptSuspending() && !isInvokeSuspending(invoke)) { + m = null; + } + } + if (m == null) { + m = model.directTarget(invoke.getOwner(), invoke.getName(), invoke.getDesc()); + } + if (m == null) { + return null; + } + ByteCodeClass c = idx.get(m.getClsName()); + if (c == null || isJsoBridgeType(c, idx)) { + return null; + } + return new String[]{jsMethodIdentifier(c, m), m.isJavascriptSuspending() ? "1" : "0"}; + } + private static boolean isDevirtualizedInvokeSuspending(String dispatchId, boolean signatureAnswer) { java.util.Map known = monomorphicSuspending; if (known == null) { @@ -1101,7 +1145,9 @@ static String generateClassJavascript(ByteCodeClass cls, List all String nativeId = jsMethodIdentifier(cls, method); NATIVE_METHOD_IDENTIFIERS.add(nativeId); NATIVE_METHOD_IDENTIFIERS.add(nativeId + "__impl"); - appendNativeStubIfNeeded(methodsOut, cls, method); + if (!appendTranslatedDelegate(methodsOut, cls, method)) { + appendNativeStubIfNeeded(methodsOut, cls, method); + } if (!method.isStatic() && !method.isConstructor()) { String jsMethodName = jsMethodIdentifier(cls, method); String dispatchId = JavascriptNameUtil.dispatchMethodIdentifier(method.getMethodName(), method.getSignature()); @@ -1474,9 +1520,7 @@ private static String renderStaticConstant(ByteCodeField field) { return ((Boolean) value).booleanValue() ? "1" : "0"; } if (value instanceof Long) { - // Java long == hi/lo Long object: _Llit(lowInt, highInt). - long lv = (Long) value; - return "_Llit(" + ((int) lv) + ", " + ((int) (lv >>> 32)) + ")"; + return javascriptLongLiteral((Long) value); } if (value instanceof Number) { return value.toString(); @@ -1611,6 +1655,10 @@ private static String applyVirtualRule(String s, String pattern, String replacem s = s.replaceAll( dvPattern.replace("yield\\* _dv", "_dw"), dvReplacement.replace("yield* _dv", "_dw")); + // _dn is _dw without the generator drive; same argument shape. + s = s.replaceAll( + dvPattern.replace("yield\\* _dv", "_dn"), + dvReplacement.replace("yield* _dv", "_dn")); } return s; } @@ -1666,7 +1714,14 @@ private static String applyMethodPeephole(CharSequence body) { s = s.replaceAll( "stack\\.p\\(([a-zA-Z_\\$][\\w\\$]*(?:\\[\\d+\\])*)\\);?\\s*locals\\[(\\d+)\\] = stack\\.q\\(\\);", "locals[$2] = $1;"); - // Rule 4: IADD/ISUB/IMUL/IAND/IOR/IXOR with int-coercion. + // Rule 4b: IMUL, which is Math.imul rather than a coerced multiply. + // stack.p(X); stack.p(Y); + // { let b = stack.q(); let a = stack.q(); stack.p(Math.imul(a, b)); } + // -> stack.p(Math.imul(X, Y)); + s = s.replaceAll( + "stack\\.p\\(([^;(){},]+)\\);?\\s*stack\\.p\\(([^;(){},]+)\\);?\\s*\\{\\s*let b = stack\\.q\\(\\);\\s*let a = stack\\.q\\(\\);\\s*stack\\.p\\(Math\\.imul\\(a, b\\)\\);\\s*\\}", + "stack.p(Math.imul($1, $2));"); + // Rule 4: IADD/ISUB/IAND/IOR/IXOR with int-coercion. // stack.p(X); stack.p(Y); // { let b = stack.q(); let a = stack.q(); stack.p((a|0) OP (b|0)); } // → stack.p(((X)|0) OP ((Y)|0)); @@ -2834,6 +2889,8 @@ private static int countWholeIdentifier(String body, String ident) { private static void appendMethodImpl(StringBuilder out, StringBuilder regs, ByteCodeClass cls, BytecodeMethod method) { List instructions = method.getInstructions(); Map labelToIndex = buildLabelMap(instructions); + currentLoopNewSites = loopNewSites(instructions, labelToIndex); + currentExactLocals = exactLocals(method, instructions); String jsMethodName = jsMethodIdentifier(cls, method); String jsMethodBodyName = jsMethodBodyIdentifier(cls, method); boolean wrappedStaticMethod = isWrappedStaticMethod(method); @@ -4682,11 +4739,17 @@ private static boolean appendStraightLineBasicInstruction(StringBuilder out, Byt case Opcodes.ICONST_5: out.append(" ").append(ctx.push(Integer.toString(instruction.getOpcode() - Opcodes.ICONST_0))).append(";\n"); return true; + // A long in the safe range is a plain number, so 0L and 1L are the literals + // rather than the runtime's _L0/_L1 constants. Reading a top-level const is a + // context-slot load V8 does not fold here: an accumulator seeded from one + // (`long s = 0; for (...) s += a[i];`) is typed as any number and every + // _Ladd in the loop keeps its type tests -- 2.5x slower in node on + // arraySequential's reduction than the same loop seeded with 0. case Opcodes.LCONST_0: - out.append(" ").append(ctx.push("_L0")).append(";\n"); + out.append(" ").append(ctx.push("0")).append(";\n"); return true; case Opcodes.LCONST_1: - out.append(" ").append(ctx.push("_L1")).append(";\n"); + out.append(" ").append(ctx.push("1")).append(";\n"); return true; case Opcodes.FCONST_0: case Opcodes.DCONST_0: @@ -4852,7 +4915,11 @@ private static boolean appendStraightLineBasicInstruction(StringBuilder out, Byt case Opcodes.ISUB: return emitBinary(out, ctx, "((%s|0) - (%s|0))"); case Opcodes.IMUL: - return emitBinary(out, ctx, "((%s|0) * (%s|0))"); + // Math.imul, never (a|0) * (b|0): once the true product passes 2^53 a + // JavaScript multiply has already rounded it, so no |0 afterwards can + // recover the low 32 bits Java keeps. vm/benchmarks' intArithmetic and + // quicksortBench returned wrong checksums in the browser because of it. + return emitBinary(out, ctx, "Math.imul(%s, %s)"); case Opcodes.LADD: return emitBinary(out, ctx, "_Ladd(%s, %s)"); case Opcodes.FADD: @@ -4988,6 +5055,24 @@ private static boolean appendStraightLineBasicInstruction(StringBuilder out, Byt case Opcodes.SALOAD: { String idx = ctx.pop(); String arr = ctx.pop(); + java.util.Set hotArraySites = currentLoopNewSites; + if (hotArraySites != null && hotArraySites.contains(instruction) + && isPureOperand(arr) && isPureOperand(idx)) { + // The bounds test as a statement and the load as a plain element read. + // Through _A the load is one site shared by every array access in the + // program -- int, double and reference arrays alike -- so its feedback + // is megamorphic in any real application. The failing case still goes + // through _A, which throws exactly what it always did (a null array is + // a NullPointerException, a bad index ArrayIndexOutOfBoundsException). + // A statement rather than `ok ? a[i] : _A(a, i)`: the conditional merges + // an unboxed element with _A's tagged result and boxes every load. In + // headless Chrome on an 8M-element int reduction: shared _A 98ms, the + // conditional 212ms, this form 38ms, a bare `a[i]` 27ms. + out.append(" if(").append(arr).append("==null||").append(idx).append(">>>0>=") + .append(arr).append(".length)_A(").append(arr).append(",").append(idx).append(");\n"); + out.append(" ").append(ctx.push(arr + "[" + idx + "]")).append(";\n"); + return true; + } out.append(" ").append(ctx.push("_A(" + arr + ", " + idx + ")")).append(";\n"); return true; } @@ -5002,6 +5087,17 @@ private static boolean appendStraightLineBasicInstruction(StringBuilder out, Byt String value = ctx.pop(); String idx = ctx.pop(); String arr = ctx.pop(); + java.util.Set hotArraySites = currentLoopNewSites; + if (hotArraySites != null && hotArraySites.contains(instruction) + && isPureOperand(arr) && isPureOperand(idx)) { + // As for the loads above, inside loops only. The value is evaluated once on either branch, + // after the array and index, as Java evaluates it. + out.append(" if(").append(arr).append("==null||").append(idx).append(">>>0>=") + .append(arr).append(".length)_T(").append(arr).append(",").append(idx).append(",") + .append(value).append(");else ").append(arr).append("[").append(idx).append("]=") + .append(value).append(";\n"); + return true; + } out.append(" _T(").append(arr).append(", ").append(idx).append(", ").append(value).append(");\n"); return true; } @@ -5084,13 +5180,22 @@ private static boolean appendStraightLineVarInstruction(StringBuilder out, VarOp private static boolean appendStraightLineLdcInstruction(StringBuilder out, Ldc instruction, StraightLineContext ctx) { Object value = instruction.getValue(); if (value instanceof String) { - out.append(" ").append(ctx.push("_L(\"" + JavascriptNameUtil.escapeJs((String) value) + "\")")).append(";\n"); + String literal = "_L(\"" + JavascriptNameUtil.escapeJs((String) value) + "\")"; + java.util.Set loopSites = currentLoopNewSites; + if (loopSites != null && loopSites.contains(instruction)) { + // A literal read in a loop keeps its String in a slot of its own. _L looks + // the text up in the literal table on every read -- a dictionary lookup + // over every literal in the program (4,675 in the transpiled gallery), + // repeated per iteration. Outside loops the lookup runs once and the + // slot's ~20 bytes would not pay for themselves. + String slot = "_Lc[" + literalCacheSlot((String) value) + "]"; + literal = "(" + slot + "||(" + slot + "=" + literal + "))"; + } + out.append(" ").append(ctx.push(literal)).append(";\n"); return true; } if (value instanceof Long) { - // Java long == hi/lo Long object: emit _Llit(lowInt, highInt). - long lv = (Long) value; - out.append(" ").append(ctx.push("_Llit(" + ((int) lv) + ", " + ((int) (lv >>> 32)) + ")")).append(";\n"); + out.append(" ").append(ctx.push(javascriptLongLiteral((Long) value))).append(";\n"); return true; } if (value instanceof Integer || value instanceof Float || value instanceof Double) { @@ -5164,7 +5269,7 @@ private static boolean appendStraightLineTypeInstruction(StringBuilder out, Type // suspending runs on the trampoline; ``_O``'s own // call then finds the class initialized and returns. (#5774) appendStraightLineEnsureClassInitialized(out, ctx, typeName); - out.append(" ").append(ctx.push("_O(\"" + typeName + "\")")).append(";\n"); + out.append(" ").append(ctx.push(newObjectExpression(typeName, instruction))).append(";\n"); return true; case Opcodes.ANEWARRAY: { String size = ctx.pop(); @@ -5403,9 +5508,41 @@ private static boolean appendStraightLineInvokeInstruction(StringBuilder out, In // A devirtualized site follows its TARGET, not the signature. boolean devSuspending = monoImpl != null ? isDevirtualizedInvokeSuspending(dispatchId, suspending) : suspending; - String devBase = monoImpl != null ? (devSuspending ? "_dv" : "_dw") : (suspending ? "_v" : "_w"); + if (monoImpl == null) { + String[] direct = siteDirectTarget(invoke, target); + if (direct != null) { + monoImpl = direct[0]; + devSuspending = "1".equals(direct[1]); + } + } + // A generator target at a site classified synchronous (see + // JavascriptSuspensionAnalysis.isDrivenSignature) is driven. + boolean devDriven = monoImpl != null && devSuspending && !suspending; + if (devDriven) { + devSuspending = false; + } + // _dn: a direct call to a synchronous body needs no generator + // drive (see _dn0 in parparvm_runtime.js). + String devBase = monoImpl != null ? (devDriven ? "_dw" : devSuspending ? "_dv" : "_dn") : (suspending ? "_v" : "_w"); String devSecond = monoImpl != null ? monoImpl : ("\"" + dispatchId + "\""); StringBuilder callExpr = new StringBuilder(); + if ("_dn".equals(devBase) && argsArePure(argValues)) { + // A direct call V8 can see at the site: `impl(_nn(t), ...)`. Through + // _dn* the body is a parameter of a helper every direct call in the + // program shares, so the inner call's feedback is megamorphic, the + // body is not inlined, and the receiver escapes into it -- a `new` + // whose fields are read through getters is allocated for real. On + // valueEscape's shape that is 7ms against 107ms in node. The null + // check moves ahead of the arguments, which is only unobservable + // when they are pure (locals and constants), so only then. + callExpr.append("(").append(monoImpl).append("(_nn(").append(target).append(")"); + for (int ai = 0; ai < argValues.length; ai++) { + callExpr.append(", ").append(argValues[ai]); + } + callExpr.append("))"); + out.append(" ").append(ctx.pushComposite(callExpr.toString(), false)).append(";\n"); + return true; + } callExpr.append(devSuspending ? "(yield* " : "(").append(devBase) .append(argValues.length <= 4 ? String.valueOf(argValues.length) : "N") .append("(").append(target).append(", ").append(devSecond); @@ -5425,12 +5562,12 @@ private static boolean appendStraightLineInvokeInstruction(StringBuilder out, In out.append(" ").append(ctx.pushComposite(callExpr.toString(), false)).append(";\n"); } else { out.append(" {\n"); - appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues, suspending); + appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues, suspending, invoke); out.append(" ").append(ctx.push("__result")).append(";\n"); out.append(" }\n"); } } else { - appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues, suspending); + appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues, suspending, invoke); } return true; } @@ -5881,6 +6018,52 @@ private static String jsStaticMethodBodyIdentifier(String owner, String name, St return JavascriptNameUtil.methodIdentifier(owner, name, desc) + "__impl"; } +/// Emits a native the JavaScript target implements by calling its pure-Java twin +/// (JavascriptNativeRegistry.TRANSLATED_DELEGATES), and answers whether it did. The +/// native is a plain function exactly when the twin is: the suspension analysis +/// records the call as an edge, so a suspending twin always makes the native +/// suspending. The reverse cannot be emitted -- a synchronous body cannot yield* +/// into a generator -- and would mean the analysis and the emitter disagree, so it +/// fails the translation rather than producing a native that returns a generator. +private static boolean appendTranslatedDelegate(StringBuilder out, ByteCodeClass cls, BytecodeMethod method) { + String twinName = JavascriptNativeRegistry.translatedDelegateTwin(cls.getClsName(), method); + if (twinName == null) { + return false; + } + BytecodeMethod twin = null; + for (BytecodeMethod candidate : cls.getMethods()) { + if (!candidate.isEliminated() && !candidate.isNative() + && twinName.equals(candidate.getMethodName()) + && method.getSignature().equals(candidate.getSignature()) + && method.isStatic() == candidate.isStatic()) { + twin = candidate; + break; + } + } + if (twin == null) { + throw new IllegalStateException("JavaScript delegate twin " + cls.getClsName() + "." + twinName + + method.getSignature() + " of native " + method.getMethodName() + " is missing"); + } + boolean suspending = method.isJavascriptSuspending(); + boolean twinSuspending = twin.isJavascriptSuspending(); + if (twinSuspending && !suspending) { + throw new IllegalStateException("native " + cls.getClsName() + "." + method.getMethodName() + + " was classified synchronous but its twin " + twinName + " suspends"); + } + String twinFn = twin.isStatic() && !shouldEmitStaticWrapper(twin) + ? jsMethodBodyIdentifier(cls, twin) : jsMethodIdentifier(cls, twin); + out.append(suspending ? "function* " : "function ").append(jsMethodIdentifier(cls, method)).append("("); + appendMethodParameters(out, method); + out.append("){\n"); + if (twin.isStatic() && !shouldEmitStaticWrapper(twin) && classNeedsInitialization(cls.getClsName())) { + out.append(classInitGuard(cls.getClsName(), suspending, " ")); + } + out.append(" return ").append(twinSuspending ? "yield* " : "").append(twinFn).append("("); + appendMethodParameters(out, method); + out.append(");\n}\n"); + return true; + } + private static void appendNativeStubIfNeeded(StringBuilder out, ByteCodeClass cls, BytecodeMethod method) { String jsMethodName = jsMethodIdentifier(cls, method); if (method.isJsBodyMethod()) { @@ -6673,10 +6856,10 @@ private static void appendBasicInstruction(StringBuilder out, BytecodeMethod met .append(index + 1).append("; break;\n"); return; case Opcodes.LCONST_0: - out.append(" stack.p(_L0); pc = ").append(index + 1).append("; break;\n"); + out.append(" stack.p(0); pc = ").append(index + 1).append("; break;\n"); return; case Opcodes.LCONST_1: - out.append(" stack.p(_L1); pc = ").append(index + 1).append("; break;\n"); + out.append(" stack.p(1); pc = ").append(index + 1).append("; break;\n"); return; case Opcodes.FCONST_0: case Opcodes.DCONST_0: @@ -6747,7 +6930,8 @@ private static void appendBasicInstruction(StringBuilder out, BytecodeMethod met out.append(" { let b = stack.q(); let a = stack.q(); stack.p((a|0) - (b|0)); pc = ").append(index + 1).append("; break; }\n"); return; case Opcodes.IMUL: - out.append(" { let b = stack.q(); let a = stack.q(); stack.p((a|0) * (b|0)); pc = ").append(index + 1).append("; break; }\n"); + // Math.imul: see the structured emitter's IMUL. + out.append(" { let b = stack.q(); let a = stack.q(); stack.p(Math.imul(a, b)); pc = ").append(index + 1).append("; break; }\n"); return; case Opcodes.LADD: out.append(" { let b = stack.q(); let a = stack.q(); stack.p(_Ladd(a, b)); pc = ").append(index + 1).append("; break; }\n"); @@ -6995,10 +7179,8 @@ private static void appendLdcInstruction(StringBuilder out, Ldc instruction, int return; } if (value instanceof Long) { - // Java long == hi/lo Long object: emit _Llit(lowInt, highInt). - long lv = (Long) value; - out.append(" stack.p(_Llit(").append((int) lv).append(", ").append((int) (lv >>> 32)) - .append(")); pc = ").append(index + 1).append("; break;\n"); + out.append(" stack.p(").append(javascriptLongLiteral((Long) value)) + .append("); pc = ").append(index + 1).append("; break;\n"); return; } if (value instanceof Integer || value instanceof Float || value instanceof Double) { @@ -7028,7 +7210,7 @@ private static void appendTypeInstruction(StringBuilder out, TypeInstruction ins // See the straight-line NEW case: ``_O`` only ever drives a // clinit synchronously, so the guard goes first. (#5774) appendInterpreterEnsureClassInitialized(out, typeName, false); - out.append(" stack.p(_O(\"").append(typeName).append("\")); pc = ").append(index + 1).append("; break;\n"); + out.append(" stack.p(").append(newObjectExpression(typeName, instruction)).append("); pc = ").append(index + 1).append("; break;\n"); return; case Opcodes.ANEWARRAY: out.append(" stack.p(_j(stack.q(), \"").append(typeName) @@ -7319,10 +7501,21 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in // one emitted through the interpreter rather than structured -- // could still ``yield*`` a plain function or drive a generator // synchronously at precisely the sites this map exists for. + boolean siteSusp = susp; if (monoImpl != null) { susp = isDevirtualizedInvokeSuspending(dispatchId, susp); + } else { + String[] direct = siteDirectTarget(invoke, null); + if (direct != null) { + monoImpl = direct[0]; + susp = "1".equals(direct[1]); + } } - String iv = monoImpl != null ? (susp ? "_dv" : "_dw") : (susp ? "_v" : "_w"); + boolean driven = monoImpl != null && susp && !siteSusp; + if (driven) { + susp = false; + } + String iv = monoImpl != null ? (driven ? "_dw" : susp ? "_dv" : "_dn") : (susp ? "_v" : "_w"); String ivSecond = monoImpl != null ? monoImpl : ("\"" + dispatchId + "\""); String yk = susp ? "yield* " : ""; // Fast path for 0-arg virtual dispatch: inline the @@ -7559,6 +7752,13 @@ private static void appendCompactVirtualDispatch(StringBuilder out, String inden private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId, int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions, boolean suspending) { + appendCompactVirtualDispatch(out, indent, methodId, argCount, hasReturn, targetExpr, argsFromStack, + argExpressions, suspending, null); + } + + private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId, + int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions, + boolean suspending, Invoke invoke) { // Monomorphic devirtualization: a dispatch id with exactly one // concrete impl resolves to that body for any receiver, so call // it directly through the ``_dv*`` / ``_dw*`` family (impl @@ -7570,10 +7770,21 @@ private static void appendCompactVirtualDispatch(StringBuilder out, String inden String monoImpl = mono == null ? null : mono.get(methodId); // Devirtualized: follow the TARGET's suspending-ness, not the // signature's -- see isDevirtualizedInvokeSuspending. + boolean siteSuspending = suspending; if (monoImpl != null) { suspending = isDevirtualizedInvokeSuspending(methodId, suspending); + } else { + String[] direct = siteDirectTarget(invoke, targetExpr); + if (direct != null) { + monoImpl = direct[0]; + suspending = "1".equals(direct[1]); + } + } + boolean driven = monoImpl != null && suspending && !siteSuspending; + if (driven) { + suspending = false; } - String base = monoImpl != null ? (suspending ? "_dv" : "_dw") : (suspending ? "_v" : "_w"); + String base = monoImpl != null ? (driven ? "_dw" : suspending ? "_dv" : "_dn") : (suspending ? "_v" : "_w"); // The second helper argument: bareword impl fn for devirt, else // the quoted dispatch-id string. String secondArg = monoImpl != null ? monoImpl : ("\"" + methodId + "\""); @@ -7591,6 +7802,20 @@ private static void appendCompactVirtualDispatch(StringBuilder out, String inden variadic = true; break; } + if ("_dn".equals(base) && !argsFromStack && argsArePure(argExpressions == null ? new String[0] : argExpressions)) { + // The site-visible direct call, as in the straight-line invoke: see the + // comment there. + out.append(indent); + if (hasReturn) { + out.append("let __result = "); + } + out.append(monoImpl).append("(_nn(").append(targetExpr).append(")"); + for (int i = 0; i < argCount; i++) { + out.append(", ").append(argExpressions[i]); + } + out.append(");\n"); + return; + } out.append(indent); if (hasReturn && argsFromStack) { out.append("stack.p(").append(yieldKw).append(helper).append("(").append(targetExpr).append(", ").append(secondArg); @@ -11008,4 +11233,373 @@ private static boolean emitPushArg(String body, int from, int to, int[] depthHol depthHolder[0]++; return true; } + /// The JavaScript expression for a long constant, in the runtime's canonical form + /// (see the long section of parparvm_runtime.js): a plain number literal when the + /// value is a safe integer, which is what the runtime's own helpers return for it, + /// and an _Llit(lowInt, highInt) record otherwise. Emitting _Llit for every + /// constant cost a call per evaluation, inside loops included. A negative number + /// is parenthesized so it cannot merge with an operator it is spliced after. + static String javascriptLongLiteral(long value) { + final long maxSafe = 9007199254740991L; + if (value >= -maxSafe && value <= maxSafe) { + return value < 0 ? "(" + value + ")" : Long.toString(value); + } + return "_Llit(" + ((int) value) + ", " + ((int) (value >>> 32)) + ")"; + } + + /// Classes a translated ``new`` allocates through a per-class allocation function + /// (see newObjectExpression), in first-use order; appendAllocationFunctions emits them. + static final java.util.LinkedHashSet ALLOCATION_FUNCTION_CLASSES = new java.util.LinkedHashSet(); + + /// Field property names written as bare object-literal keys by the allocation + /// functions. JavascriptBundleWriter.minifyGeneratedIdentifiers renames whole tokens, + /// so one of these that happened to spell a generated function name (a field + /// ``done`` beside a method ``void done()``) would be renamed with it; it excludes them. + static final java.util.Set ALLOCATION_FUNCTION_KEYS = new java.util.HashSet(); + + static void resetAllocationFunctions() { + ALLOCATION_FUNCTION_CLASSES.clear(); + ALLOCATION_FUNCTION_KEYS.clear(); + } + + /// The expression a translated ``new typeName`` allocates with. + /// + /// jvm.newObject builds an object by walking a cached field list and assigning each + /// default through a computed key. V8 cannot see through that: the allocation is a + /// call it will not inline and the stores are keyed, so every ``new`` in a loop + /// allocates for real. Measured in node on the valueEscape shape (allocate, read two + /// fields, drop), 8M iterations: 220-250ms through the generic path, 14ms for a + /// function returning a literal with the field names as literal keys -- V8 inlines it + /// and removes the allocation, as it does for dart2js's own output -- and ~80ms with + /// the same keys computed. + /// + /// So a class gets its own allocation function returning exactly that literal. Not + /// for a Throwable, whose construction has to capture the stack (jvm.newObject does); + /// not for an abstract class or an interface, which bytecode cannot instantiate; not + /// when any class on the chain is unknown here, since the field list would be + /// incomplete; and not under -Dparparvm.js.manglefields=1, which renames field names + /// only where they appear as quoted strings. The class-initialization guard the NEW + /// site already emits (appendStraightLineEnsureClassInitialized) is what runs a + /// static initializer; newObject's own check only mattered for a class without one. + static String newObjectExpression(String typeName, Instruction site) { + java.util.Set loopSites = currentLoopNewSites; + boolean hot = BOXED_TYPES.contains(typeName) || (loopSites != null && loopSites.contains(site)); + if (!hot || !allocationFunctionEligible(typeName)) { + return "_O(\"" + typeName + "\")"; + } + ALLOCATION_FUNCTION_CLASSES.add(typeName); + return allocationFunctionName(typeName) + "()"; + } + + /// The NEW, array load and array store instructions of the method being emitted that sit + /// inside a loop -- between a + /// backward branch and its target -- or null. Only those get an allocation function. + /// The function's win is V8 removing, or at least cheaply building, an allocation it + /// sees many times; a ``new`` that runs once per call -- a listener, a UI component, a + /// lambda -- gains nothing from it, and on the transpiled Flutter gallery giving every + /// ``new`` one cost ~1MB of executable code (2,979 functions). An allocation reached + /// from a loop only through a call (Integer.valueOf's ``new Integer``) is not + /// covered; that is the price of keeping the bundle size. + private static java.util.Set currentLoopNewSites; + + /// True when evaluating every argument can neither throw nor have an effect -- local + /// reads and literals -- so a null check may run before them without changing what + /// a program can observe. + private static boolean argsArePure(String[] args) { + for (int i = 0; i < args.length; i++) { + if (!StraightLineContext.isDeferrable(args[i]) && !isStackSlotName(args[i])) { + return false; + } + } + return true; + } + + /// A local, a stack slot or a literal: re-reading it costs nothing and cannot throw. + private static boolean isPureOperand(String expr) { + return StraightLineContext.isDeferrable(expr) || isStackSlotName(expr); + } + + private static boolean isStackSlotName(String expr) { + if (expr == null || expr.length() < 2 || expr.charAt(0) != 's') { + return false; + } + for (int i = 1; i < expr.length(); i++) { + if (!Character.isDigit(expr.charAt(i))) { + return false; + } + } + return true; + } + + private static final Map LITERAL_CACHE_SLOTS = new HashMap(); + + /// The `_Lc` index of a string literal read inside a loop: one slot per distinct text. + private static synchronized int literalCacheSlot(String text) { + Integer slot = LITERAL_CACHE_SLOTS.get(text); + if (slot == null) { + slot = Integer.valueOf(LITERAL_CACHE_SLOTS.size()); + LITERAL_CACHE_SLOTS.put(text, slot); + } + return slot.intValue(); + } + + /// Local slots of the method being emitted whose class is known exactly, or null. + private static Map currentExactLocals; + + /// The slots every store to which is ``new X(...)`` -- an ASTORE right after the + /// ``X.`` call -- mapped to X; parameters, whose first value comes from the + /// caller, never qualify. A virtual call on such a local reaches X's own body however + /// many overrides the declared type has elsewhere in the program: a ``new HashMap()`` + /// in an application that also has a LinkedHashMap. CHA answers that site with a + /// lookup by name on every call; this answers it with a direct call. + /// + /// The ASTORE must follow the ```` with nothing between them but line and + /// variable metadata. A label would mean another path reaches the store, and the value + /// that path brings is unknown. + private static Map exactLocals(BytecodeMethod method, List instructions) { + if (System.getProperty("parparvm.js.exactlocals.off") != null) { + return null; + } + int firstLocal = method.isStatic() ? 0 : 1; + for (ByteCodeMethodArg arg : method.getArguments()) { + char q = arg.getQualifier(); + firstLocal += (q == 'l' || q == 'd') ? 2 : 1; + } + Map exact = null; + java.util.Set rejected = null; + for (int i = 0; i < instructions.size(); i++) { + Instruction ins = instructions.get(i); + if (!(ins instanceof VarOp)) { + continue; + } + int op = ins.getOpcode(); + if (op < Opcodes.ISTORE || op > Opcodes.ASTORE) { + continue; + } + Integer slot = Integer.valueOf(((VarOp) ins).getIndex()); + String type = null; + if (op == Opcodes.ASTORE && slot.intValue() >= firstLocal) { + int p = i - 1; + while (p >= 0 && (instructions.get(p) instanceof LineNumber + || instructions.get(p) instanceof LocalVariable)) { + p--; + } + if (p >= 0 && instructions.get(p) instanceof Invoke) { + Invoke init = (Invoke) instructions.get(p); + if (init.getOpcode() == Opcodes.INVOKESPECIAL && "".equals(init.getName())) { + type = init.getOwner(); + } + } + } + if (rejected != null && rejected.contains(slot)) { + continue; + } + String known = exact == null ? null : exact.get(slot); + if (type == null || (known != null && !known.equals(type))) { + if (rejected == null) { + rejected = new HashSet(); + } + rejected.add(slot); + if (exact != null) { + exact.remove(slot); + } + continue; + } + if (exact == null) { + exact = new HashMap(); + } + exact.put(slot, type); + } + return exact == null || exact.isEmpty() ? null : exact; + } + + /// The exact class of a receiver expression that is a plain local read, or null. + private static String exactLocalType(String receiver) { + Map exact = currentExactLocals; + if (exact == null || receiver == null || receiver.length() < 2 || receiver.charAt(0) != 'l') { + return null; + } + int slot = 0; + for (int i = 1; i < receiver.length(); i++) { + char c = receiver.charAt(i); + if (c < '0' || c > '9' || i > 6) { + return null; + } + slot = slot * 10 + (c - '0'); + } + return exact.get(Integer.valueOf(slot)); + } + + /// The boxed types. Their instances are allocated in their own valueOf factories, never + /// in the loop that autoboxes, so the in-loop rule never sees them -- yet boxing is the + /// commonest allocation a loop does (hashMapChurn spent 21% of its time building + /// Integers through jvm.newObject). Eight one-field classes, so the cost is a few hundred + /// bytes in any bundle. + private static final java.util.Set BOXED_TYPES = new java.util.HashSet(java.util.Arrays.asList( + "java_lang_Integer", "java_lang_Long", "java_lang_Double", "java_lang_Float", + "java_lang_Short", "java_lang_Character", "java_lang_Byte", "java_lang_Boolean")); + + private static java.util.Set loopNewSites(List instructions, + Map labelToIndex) { + java.util.Set out = java.util.Collections.newSetFromMap( + new java.util.IdentityHashMap()); + if (instructions == null) { + return out; + } + boolean[] inLoop = null; + for (int i = 0; i < instructions.size(); i++) { + Instruction in = instructions.get(i); + java.util.List