diff --git a/scripts/initializr/common/src/main/resources/skill/SKILL.md b/scripts/initializr/common/src/main/resources/skill/SKILL.md index d71c510541..f1c4d507c5 100644 --- a/scripts/initializr/common/src/main/resources/skill/SKILL.md +++ b/scripts/initializr/common/src/main/resources/skill/SKILL.md @@ -38,6 +38,9 @@ This skill teaches you how to write code for a Codename One (CN1) cross-platform - `references/native-interfaces.md` — Authoring native interfaces for iOS/Android/JavaScript/Desktop with `cn1:generate-native-interfaces` and platform callbacks. - `references/cn1libs.md` — Creating, packaging, and consuming Codename One libraries (Maven and legacy `.cn1lib`). - `references/ai-and-speech.md` — LLM client (`com.codename1.ai`), `ChatView`, `SpeechRecognizer`, `TextToSpeech`, non-prompting `SecureStorage` overloads, the ML Kit cn1libs, and the simulator's offline Ollama redirect. Read this when the user asks for chat, voice, embeddings, image generation, barcode/document/face detection, or wants to store an LLM API key. +- `references/printing.md` — Cross-platform printing (`com.codename1.printing`): `Printer.printPDF` / `printImage` / `print`, the `PrintResult` outcome, and per-platform caveats (iOS AirPrint, Android, desktop, native Windows, web). Read this when the user wants to print a document, report, image, or the current screen. +- `references/games.md` — Game development (`com.codename1.gaming`): the `GameView` update loop, `Sprite` / `AnimatedSprite` / `SpriteSheet`, pollable `GameInput`, `TouchControls`, `SoundPool`, and 2D `com.codename1.gaming.physics` (Box2D). Read this for arcade/casual/scroller/board games or any real-time animated canvas. +- `references/3d-graphics.md` — Portable GPU 3D (`com.codename1.gpu`): the `RenderView` + `Renderer` loop, declarative `Material` / `VertexFormat` (engine-generated shaders — no GLSL), `Primitives`, `GltfLoader` for glTF models, `Camera` / `Light` / `Matrix4`, and platform backends. Read this for product viewers, 3D scenes, or custom GPU rendering. - `references/snapshot-builds.md` — Edge case: compiling against a Codename One SNAPSHOT from git. - `references/debugging.md` — `jdb`-attach workflow for an agent: start the simulator paused, set breakpoints, dump locals, drive the session non-interactively from a script. - `tools/` — runnable Java 17 single-file utilities. `tools/IsApiSupported.java` answers "is this `java.*` class in the CN1 subset?"; `tools/IsCssValid.java` answers "does this `theme.css` compile?"; `tools/CompareToMockup.java` scores a rendered screenshot against a designer mockup (similarity %, with region masking); `tools/DesignImport.java` turns a Figma/Sketch/Adobe XD design — or an HTML/React design's `tokens.css`/`styles.css` (Claude-generated mockups) — into starter CN1 CSS + tokens + a layout map. **`tools/DumpForm.java`** boots the app in **desktop mode** and dumps a model of the current screen, which **`tools/DescribeForm.java`** (vision-free outline), **`tools/AlignmentCheck.java`** (designer alignment guides) and **`tools/GuiLint.java`** (nested scroll, opaque text/containers, image borders) analyse. **`tools/UpdateSkills.java`** self-updates this whole skill from GitHub. Run with `java tools/.java `. @@ -303,6 +306,9 @@ If you cannot run the simulator (e.g. headless environment), **say so explicitly | "How do I create / consume a cn1lib" | `references/cn1libs.md` | | "Add a chatbot" / "Integrate OpenAI/Ollama/Anthropic" / "Stream LLM tokens" / "Generate an image" / "Embed text" | `references/ai-and-speech.md` | | "Read voice input" / "Speak text aloud" / "Add a voice button to my chat" | `references/ai-and-speech.md` | +| "Print a document / report / image" / "print the current screen" / AirPrint | `references/printing.md` | +| "Build a game" / sprites, game loop, joystick, collisions / `GameView` | `references/games.md` | +| "Render 3D" / "show a 3D model" / glTF / `RenderView` / custom GPU drawing | `references/3d-graphics.md` | | "Scan a barcode" / "Detect a face" / "Crop a document photo" via ML Kit | `references/ai-and-speech.md` | | "Store an LLM API key" / non-prompting SecureStorage | `references/ai-and-speech.md` | | "Build against a Codename One SNAPSHOT from git" | `references/snapshot-builds.md` | diff --git a/scripts/initializr/common/src/main/resources/skill/references/3d-graphics.md b/scripts/initializr/common/src/main/resources/skill/references/3d-graphics.md new file mode 100644 index 0000000000..8feccdecf8 --- /dev/null +++ b/scripts/initializr/common/src/main/resources/skill/references/3d-graphics.md @@ -0,0 +1,127 @@ +# Portable 3D / GPU Graphics Reference + +`com.codename1.gpu` is a portable, **engine-managed-shader** 3D graphics surface. The headline: **you never write shader source.** You describe geometry (`Mesh` + `VertexFormat`) and surface appearance (`Material`) declaratively, and the engine generates the right shader for each platform (GLSL on OpenGL ES / WebGL, Metal Shading Language on iOS). The GPU surface is hosted by `RenderView`, an ordinary CN1 `Component`. + +Use this for product viewers, data visualizations, custom 3D scenes, AR-style overlays — anything needing real GPU geometry. For 2D sprite games, prefer `com.codename1.gaming` (see `references/games.md`), which is built on top of this. + +## The render loop + +`new RenderView(Renderer)` drives four callbacks on the **GPU thread** (never touch CN1 UI from inside them). Allocate in `onInit`, configure projection in `onResize`, draw in `onFrame`: + +```java +import com.codename1.gpu.*; +import com.codename1.ui.layouts.BorderLayout; + +RenderView view = new RenderView(new Renderer() { + private final Camera camera = new Camera(); + private Mesh cube; + private Material material; + + public void onInit(GraphicsDevice device) { + cube = Primitives.cube(device, 1.6f); + material = new Material(Material.Type.PHONG).setColor(0xff3366ff).setShininess(24f); + camera.setPerspective(45f, 0.1f, 100f) + .setPosition(2.6f, 2.1f, 3.4f) + .setTarget(0f, 0f, 0f); + device.setLight(new Light().setDirection(-0.4f, -1f, -0.55f)); + } + + public void onResize(GraphicsDevice device, int w, int h) { + camera.setAspect((float) w / Math.max(1, h)); + device.setViewport(0, 0, w, h); + } + + public void onFrame(GraphicsDevice device) { + device.clear(0xff101018, true, true); // color, clearColor?, clearDepth? + device.setCamera(camera); + float[] model = Matrix4.rotation((float) Math.toRadians(25), 0.35f, 1f, 0.12f); + device.draw(cube, material, model); // null model = identity + } + + public void onDispose(GraphicsDevice device) { } +}); + +if (view.isSupported()) { // false where there is no GPU backend + view.setContinuous(true); // animate every frame; else call requestRender() + form.add(BorderLayout.CENTER, view); +} +``` + +**Always gate on `view.isSupported()`.** When it's `false` the view shows a placeholder instead of crashing — branch to a 2D fallback if 3D is essential to the screen. + +## Class map + +| Role | Class | Key members | +| --- | --- | --- | +| Host component | `RenderView` | `new RenderView(Renderer)`, `isSupported()`, `setContinuous(boolean)`, `requestRender()`, `getRenderer()` | +| Your callbacks | `Renderer` (interface) | `onInit` / `onResize(w,h)` / `onFrame` / `onDispose`, all taking `GraphicsDevice` | +| Command surface | `GraphicsDevice` | `createVertexBuffer/IndexBuffer/Texture`, `clear`, `setViewport`, `setCamera`, `setLight`, `draw(mesh, material, modelMatrix)`, `dispose(...)`, `getCapabilities()` | +| Geometry | `Mesh` | `new Mesh(vb, primitiveType)` or `new Mesh(vb, ib, primitiveType)`; `isIndexed()` | +| Vertex data | `VertexBuffer` / `IndexBuffer` | `getData()` (write directly, then `setDirty()`), `setData(...)`; indices are 16-bit unsigned (max 65536 verts) | +| Layout | `VertexFormat` / `VertexAttribute` | presets `POSITION`, `POSITION_TEXCOORD`, `POSITION_NORMAL`, `POSITION_NORMAL_TEXCOORD`; `VertexAttribute.Usage` = POSITION/NORMAL/TEXCOORD/COLOR | +| Primitive kind | `PrimitiveType` | POINTS, LINES, LINE_STRIP, TRIANGLES, TRIANGLE_STRIP | +| Appearance | `Material` | `Type` UNLIT/LAMBERT/PHONG/SPRITE/SKYBOX; `setColor(argb)`, `setTexture(t)`, `setShininess(f)`, `setRenderState(rs)` | +| Pipeline state | `RenderState` | `opaque()` / `transparent()`; `setDepthTest`, `setDepthWrite`, `setBlendMode` (NONE/ALPHA/ADDITIVE), `setCullMode` (BACK/FRONT/NONE) | +| Lighting | `Light` | one directional light + ambient: `setDirection(x,y,z)`, `setColor(argb)`, `setAmbientColor(argb)` | +| View/projection | `Camera` | `setPerspective(fovDeg, near, far)` / `setOrthographic(...)`, `setAspect`, `setPosition`/`setTarget`/`setUp` | +| Math | `Matrix4` | `float[16]` helpers: `identity`, `multiply(a,b,dst)`, `perspective`, `ortho`, `lookAt`, `translation`, `scaling`, `rotation`, `invert`, `normalMatrix` | +| Built-in meshes | `Primitives` | `cube(device, size)`, `quad(device, size)` (both `POSITION_NORMAL_TEXCOORD`) | +| Model loading | `GltfLoader` | `load(device, bytes/stream)` → `Mesh`; `loadModel(...)` → `GltfModel` (`getMesh()` + `getBaseColorTexture()`) | +| Textures | `Texture` | `setWrap(CLAMP/REPEAT)`, `setFilter(NEAREST/LINEAR)`; create via `device.createTexture(image)` | +| Capabilities | `GpuCapabilities` | `getMaxTextureSize()`, `isShaderLevel3()`, `isDepthTextureSupported()`, `isIntIndicesSupported()`, `getRendererName()` | + +Prefer `device.createVertexBuffer(...)` / `createIndexBuffer(...)` / `createTexture(...)` over the raw constructors — they return device-owned, SIMD-aligned resources the device disposes for you. + +## Loading a glTF model + +`GltfLoader` reads `.glb` (binary) and `.gltf` (JSON). It pulls POSITION (required), NORMAL (computed if absent), and TEXCOORD_0 into the standard `POSITION_NORMAL_TEXCOORD` layout; materials beyond base-color texture, skinning, and animation are ignored. + +```java +public void onInit(GraphicsDevice device) { + InputStream in = Display.getInstance().getResourceAsStream(getClass(), "/boombox.glb"); + GltfLoader.GltfModel loaded = GltfLoader.loadModel(device, in); + mesh = loaded.getMesh(); + material = new Material(Material.Type.PHONG).setShininess(16f); + Texture tex = loaded.getBaseColorTexture(); + if (tex != null) material.setTexture(tex); + + camera.setPerspective(45f, 0.1f, 100f).setPosition(1.9f, 1.5f, 2.6f).setTarget(0,0,0); + device.setLight(new Light().setDirection(-0.4f, -0.7f, -0.6f)); +} +``` + +Bundle the `.glb` at the top of `common/src/main/resources/` (flat namespace — see `references/java-api-subset.md`). + +## Building custom geometry + +```java +VertexBuffer vb = device.createVertexBuffer(VertexFormat.POSITION_NORMAL_TEXCOORD, vertexCount); +float[] data = vb.getData(); // interleaved per-vertex floats; write in place +// ... fill data ... +vb.setDirty(); // mark for re-upload after writing +IndexBuffer ib = device.createIndexBuffer(indexCount); +ib.setData(intIndices); // validated to 0..65535 +Mesh mesh = new Mesh(vb, ib, PrimitiveType.TRIANGLES); +``` + +For transforms, compose `Matrix4` helpers (column-major `float[16]`); the destination of `multiply(a, b, dst)` must not alias `a` or `b`. + +## Platform support + +| Platform | Backend | +| --- | --- | +| Android | OpenGL ES 2+ (`GLSurfaceView`) | +| iOS | Metal (`MTKView`) | +| JavaScript / web | WebGL 1.0+ | +| JavaSE simulator | Real OpenGL via JOGL when present; pure-Java software rasterizer fallback otherwise | + +Backends differ in capability — query `device.getCapabilities()` before using advanced features: `isShaderLevel3()` (GLSL ES 3 / WebGL2), `isDepthTextureSupported()` (shadow maps), `isIntIndicesSupported()` (32-bit indices; WebGL 1 is 16-bit only), `getMaxTextureSize()`. No build hint or extra dependency is required; the API is part of `codenameone-core`. + +## What NOT to do + +- Don't touch CN1 UI (`Form`, components, `callSerially` targets) from a `Renderer` callback — they run on the GPU thread, not the EDT. +- Don't skip `isSupported()` — assume some target has no GPU and provide a fallback. +- Don't write GLSL/Metal by hand — express it through `Material` + `VertexFormat`; there is no hook for raw shader source. +- Don't exceed 65536 vertices per `IndexBuffer` unless `isIntIndicesSupported()` is true. +- Don't allocate meshes/textures in `onFrame` — create them once in `onInit` and reuse. +- Don't forget `setDirty()` after writing into a buffer's backing array, or the GPU keeps the stale copy. diff --git a/scripts/initializr/common/src/main/resources/skill/references/build-hints.md b/scripts/initializr/common/src/main/resources/skill/references/build-hints.md index 56207e2860..a69291079a 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/build-hints.md +++ b/scripts/initializr/common/src/main/resources/skill/references/build-hints.md @@ -40,6 +40,7 @@ When in doubt, search the developer guide for the exact key name — there are h | `codename1.arg.ios.statusbar_hidden=true` | Hide the iOS status bar. | | `codename1.arg.ios.beforeFinishLaunching=...` | Native code inserted before iOS's `application:didFinishLaunchingWithOptions:` returns. | | `codename1.arg.ios.newStorageLocation=true` | Use modern iOS storage paths (recommended for new apps). | +| `codename1.arg.ios.wallet.extension=true` | Generate an Apple Wallet issuer-provisioning extension (iOS 14+). See *Apple Wallet issuer provisioning* below. | ## Android @@ -87,6 +88,21 @@ Any `ios.NSUsageDescription` key is forwarded into the generated `Info.plis `ios.plistInject` remains the escape hatch for raw XML snippets that don't have a dedicated `ios.NS*` hint. +## Apple Wallet issuer provisioning (iOS) + +Card issuers can surface their cards inside the iOS Wallet app via a *non-UI issuer-provisioning extension* (plus an optional login-UI extension). Setting these hints makes the iOS build auto-generate the Objective-C extension target(s), wire the entitlements, and share data with the host app through an App Group. The host app publishes pass entries via the new `com.codename1.payment.WalletExtension` / `WalletPassEntry` Java API; the generated extension answers Wallet's ~100 ms status callback from that shared data. + +| Hint (`codename1.arg.` prefix) | Effect | +| --- | --- | +| `ios.wallet.extension=true` | **Enable.** Emit the issuer-provisioning extension. | +| `ios.wallet.appGroup=group.com.example.app` | **Required.** Shared App Group ID (must start with `group.`) the host app and extension exchange pass data through. | +| `ios.wallet.issuerEndpoint=https://...` | **Required.** Your HTTPS issuer endpoint that returns the encrypted pass payload. | +| `ios.wallet.includeUI=true` | Also generate the login-UI extension (set `ios.wallet.authEndpoint=...` for its login URL). | +| `ios.wallet.nonuiExtensionName=...` / `ios.wallet.uiExtensionName=...` | Override the default extension target names. | +| `ios.wallet.*Inject=...raw ObjC...` | Inject custom Objective-C at marker points (`generateRequestInject`, `generateResponseInject`, `statusInject`, `uiViewDidLoadInject`, etc.) in the generated extension code. | + +This is an advanced, issuer-only feature — most apps never need it. The compiled native code is gated behind a build define, so leaving the hints unset is a no-op. + ## JavaScript / web | Hint | Effect | diff --git a/scripts/initializr/common/src/main/resources/skill/references/games.md b/scripts/initializr/common/src/main/resources/skill/references/games.md new file mode 100644 index 0000000000..e47df032bc --- /dev/null +++ b/scripts/initializr/common/src/main/resources/skill/references/games.md @@ -0,0 +1,166 @@ +# Game Development Reference + +`com.codename1.gaming` is a game-oriented surface that fits how games are written: a tight update loop, sprite primitives, pollable input, low-latency sound, and optional 2D rigid-body physics. Rendering is GPU-driven (it sits on top of `com.codename1.gpu` — see `references/3d-graphics.md`), so **there is no `paint()` to implement and no frame rate to manage**. You move objects in `update(deltaSeconds)`; the engine draws them. + +Use this for arcade / casual / scroller / board games and any real-time animated canvas. For static UI, use normal CN1 components — not this. + +## The shape of a game + +Subclass `GameView`, populate the scene, advance state in `update`, add the view to a `Form`, and `start()`: + +```java +import com.codename1.gaming.*; +import com.codename1.ui.*; +import com.codename1.ui.layouts.BorderLayout; + +class MyGame extends GameView { + private final Sprite player = new Sprite(playerImage); + + MyGame() { + getScene().add(player); + player.setPosition(160, 240); + } + + @Override + protected void update(double dt) { // dt = seconds since last frame + GameInput in = getInput(); + if (in.isGameKeyDown(Display.GAME_RIGHT)) player.setX(player.getX() + 200 * dt); + if (in.isGameKeyDown(Display.GAME_LEFT)) player.setX(player.getX() - 200 * dt); + } +} + +Form f = new Form("Game", new BorderLayout()); +MyGame game = new MyGame(); +f.add(BorderLayout.CENTER, game); +f.show(); +game.start(); // begins the loop; stop()/pause()/resume() control it +``` + +Multiply movement by `dt` so speed is frame-rate independent. For deterministic physics use a fixed step: `setFixedTimestep(1.0/60)` (then `getInterpolationAlpha()` gives a 0..1 blend for smooth rendering between steps). + +## GameView — the loop and the world + +| Method | Purpose | +| --- | --- | +| `protected abstract void update(double dt)` | Override. Advance game state; `dt` is seconds (or the fixed step). | +| `Scene getScene()` | The z-ordered sprite collection to `add`/`remove`/`clear`. | +| `GameInput getInput()` | Pollable keyboard / pointer / analog input. | +| `TouchControls getControls()` | On-screen joystick + buttons for touch devices. | +| `GameCamera getCamera()` | 2D ortho (default) or 3D perspective view. | +| `Light getLight()` | Directional light for lit 3D models. | +| `void addModel(Model)` / `removeModel(Model)` | Add/remove a 3D mesh (perspective rendering). | +| `void setClearColor(int argb)` | Background clear color. | +| `void start()` / `stop()` / `pause()` / `resume()` | Loop lifecycle. `isRunning()` / `isPaused()` query it. | +| `void setFixedTimestep(double s)` | Switch to a deterministic fixed step (0 = variable, the default). | +| `protected void onSetup(GraphicsDevice device)` | Override to allocate GPU resources once, before the first frame. | + +`GameView` *is* a CN1 `Component`, so it lives inside a normal `Form`/layout. Call `stop()` in your form's cleanup so the loop doesn't run in the background. + +## GameInput — poll, don't listen + +All state is read on the EDT inside `update`. Levels vs. edges: + +- `isKeyDown(int keyCode)` / `isGameKeyDown(int gameAction)` — held this frame. Game actions: `Display.GAME_UP/DOWN/LEFT/RIGHT/FIRE`. +- `wasKeyPressed(int)` / `wasKeyReleased(int)` — true only on the single transition frame (cleared after `update`). +- `getPointerX()` / `getPointerY()` — pointer position relative to the view. +- `isPointerDown()` / `wasPointerPressed()` / `wasPointerReleased()` — pointer level + edges. +- `getAxisX()` / `getAxisY()` — analog `-1..1` from the virtual joystick (x right, y down). + +## Sprites + +`Sprite` is a lightweight holder: image + position + rotation + scale + tint + normalized anchor. The renderer turns each into a GPU quad per frame. + +```java +Sprite s = new Sprite(image); +s.setPosition(x, y); // anchor point; default anchor is center (0.5, 0.5) +s.setRotation(45f); // degrees, clockwise +s.setScale(2f); // or setScale(sx, sy) +s.setColor(0xffff8800); // ARGB tint; opaque white = no tint +s.setAlpha(180); // 0..255 +s.setZOrder(10); // higher draws on top +boolean hit = s.intersects(other); // AABB overlap test +``` + +`AnimatedSprite` cycles frames over time: + +```java +SpriteSheet sheet = new SpriteSheet(atlasImage, 32, 32); // grid of 32x32 frames +AnimatedSprite hero = new AnimatedSprite(sheet, new int[]{0,1,2,3}, 0.12); // 0.12s/frame +hero.setLooping(true); +hero.setPlaying(true); +getScene().add(hero); +``` + +`SpriteSheet` slices a texture atlas: `getFrame(index)` or `getFrame(col, row)`, plus `getColumns()/getRows()/getFrameCount()`. Frames are cut and cached on first access. + +**Don't add or remove sprites during `Scene.update()`** (i.e. mid-frame) — mutate the scene from your `update(dt)` body, which runs before the scene advances. Use `Scene.setCameraX/Y` to scroll a 2D world. + +## Touch controls + +Wire on-screen controls once; they feed the same `GameInput` so touch and keyboard code paths are identical: + +```java +TouchControls c = getControls(); +c.addJoystick(88, TouchControls.LEFT, TouchControls.BOTTOM, 30); // read via getAxisX/Y + GAME_* keys +VirtualButton fire = c.addButton(Display.GAME_FIRE, + TouchControls.RIGHT, TouchControls.BOTTOM, 26); +fire.setLabel("A"); // while pressed, isKeyDown(GAME_FIRE) is true +``` + +A `VirtualJoystick` reads as both analog (`getAxisX/Y`) and digital (`isGameKeyDown` past its dead zone, default 0.2). A `VirtualButton` holds its mapped key code down while touched. + +## Low-latency sound + +`MediaManager` is fine for music; for overlapping short effects use `SoundPool` (native low-latency backend where available, MediaManager fallback otherwise): + +```java +SoundPool pool = SoundPool.create(8); // up to 8 simultaneous voices +SoundEffect blip = pool.load("/blip.wav"); +int voice = pool.play(blip); // or play(fx, volume, pan, rate, loop) +// pool.stop(voice); blip.unload(); +``` + +`pool.isNativeAccelerated()` tells you whether pan/rate are honored (they are ignored on the fallback path, but playback still works everywhere). + +## 2D physics (`com.codename1.gaming.physics`) + +An idiomatic wrapper over a Box2D simulation (derived from JBox2D, pure Java, runs on every platform). **Everything is in screen pixels, y-down** — the meter/y-up conversion is internal. Link a body to a sprite and the body drives the sprite each step. + +```java +import com.codename1.gaming.physics.*; + +PhysicsWorld world = new PhysicsWorld(0, 600); // gravity px/s^2, down +PhysicsBody ground = world.createBox(0, 460, 320, 40, BodyType.STATIC); +PhysicsBody crate = world.createBox(160, 0, 32, 32, BodyType.DYNAMIC); +crate.setLinkedSprite(crateSprite); // body pushes its transform into the sprite + +world.addContactListener(new ContactListener() { + public void beginContact(PhysicsContact c) { /* landed */ } + public void endContact(PhysicsContact c) { } +}); + +// in update(dt): +world.step((float) dt); // integrates and syncs every linked sprite +``` + +- `BodyType`: `STATIC` (walls/ground), `KINEMATIC` (app-moved platforms), `DYNAMIC` (fully simulated). +- Body factories: `createBox`, `createCircle`, `createPolygon`, `createShape(Shape)`. Joints: `createRevoluteJoint`, `createDistanceJoint`, `createWeldJoint`, `createPrismaticJoint`, `createMouseJoint`. +- `PhysicsBody`: `applyForce`, `setLinearVelocity`, `setAngularVelocity`, `setDensity/setFriction/setRestitution`, `setTransform`. +- Debugging: `world.setDebugDrawFlags(...)` + `world.debugDraw(g)`. +- **Don't create/destroy bodies inside a contact callback** — defer until after `step()` returns. + +## 3D in a game + +`GameView` can render 3D meshes through the camera: switch `getCamera().setMode(GameCamera.MODE_PERSPECTIVE)`, set it up with `setPerspective/setPosition/setTarget`, build a `Model` from a `Mesh` (see `references/3d-graphics.md` for `Primitives`, `GltfLoader`, materials), and `addModel(model)`. For raw 3D without the game loop, use `com.codename1.gpu` directly. + +## Platform support + +The gaming API is part of `codenameone-core` — no extra dependency, no build hint. Rendering uses the platform GPU backend (OpenGL ES on Android, Metal on iOS, WebGL on web, OpenGL/software on the simulator) and degrades gracefully where 3D is unavailable. Box2D physics is pure Java and identical everywhere. + +## What NOT to do + +- Don't implement `paint()` / drive your own frame loop — override `update(dt)` and let `GameView` render. +- Don't move objects by a fixed pixel count per frame — multiply by `dt` (or use a fixed timestep) so speed is consistent across devices. +- Don't mutate the scene or physics-body set mid-step; do it from `update(dt)` (scene) or after `world.step()` returns (bodies). +- Don't reach into `com.codename1.gaming.physics.box2d.*` directly — use the `PhysicsWorld`/`PhysicsBody` wrapper (the box2d package is the internal engine). +- Don't forget to `stop()` the `GameView` when leaving the screen. diff --git a/scripts/initializr/common/src/main/resources/skill/references/java-api-subset.md b/scripts/initializr/common/src/main/resources/skill/references/java-api-subset.md index d7421a25f0..468c4e3b1e 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/java-api-subset.md +++ b/scripts/initializr/common/src/main/resources/skill/references/java-api-subset.md @@ -50,7 +50,7 @@ The runtime authoritative reference is also published as JavaDoc: { + if (result.isFailed()) { + ToastBar.showErrorMessage("Print failed: " + result.getError()); + } else { + ToastBar.showMessage("Sent to printer", FontImage.MATERIAL_PRINT); + } + }); +} +``` + +To print the current screen, snapshot it into an `Image` first +(`Image screenshot = Image.createImage(form.getWidth(), form.getHeight()); form.paintComponent(screenshot.getGraphics());`) +and pass that to `printImage`. + +## Printing a PDF (e.g. a generated report or a download) + +`print*` takes a path in `FileSystemStorage`, so write/download the PDF there first: + +```java +String pdfPath = FileSystemStorage.getInstance().getAppHomePath() + "report.pdf"; +Util.downloadUrlToFileSystemInBackground(reportUrl, pdfPath, e -> { + if (FileSystemStorage.getInstance().exists(pdfPath)) { + Printer.printPDF(pdfPath, result -> { + if (result.isFailed()) { + ToastBar.showErrorMessage("Print failed: " + result.getError()); + } + }); + } +}); +``` + +CN1 has no built-in PDF *generator*. Produce the PDF with a cn1lib or a server endpoint, land it in `FileSystemStorage`, then `printPDF` it. For arbitrary file types use the generic `print(path, mimeType, listener)` overload. + +## Platform behaviour and caveats + +| Platform | Backend | Notes | +| --- | --- | --- | +| iOS | `UIPrintInteractionController` (AirPrint) | Full dialog + cancel detection. | +| Android | `android.print` / `PrintHelper` | API 19+. `isPrintingSupported()` is `false` below that or with no Activity. Image prints report `COMPLETED` best-effort (can't see a dismissed dialog). | +| JavaSE / desktop | `PrinterJob` (images) + `Desktop.print` (PDF) | `false` when headless. | +| Native Windows | Win32 `PrintDlg` + GDI + `Windows.Data.Pdf` | Missing printer / headless surfaces as `FAILED`. | +| JavaScript / web | Blob URL in a hidden iframe + browser print dialog | Always "supported". Cancellation **cannot** be detected (reported as `COMPLETED`); some browsers (Firefox) may render a PDF blank. | + +**Cancellation is best-effort.** Treat `isCompleted()` as "handed off", not "printed". Only iOS and the Windows/Android dialogs reliably report a user cancel; elsewhere a dismissed dialog still comes back `COMPLETED`. Don't build logic that depends on distinguishing cancel from success on web/desktop. + +**No build hints, no permissions.** Printing needs no `codename1.arg.*` key, entitlement, or manifest entry — user confirmation is implicit in the native dialog. Just call `Printer`. + +## What NOT to do + +- Don't call `print*` off a path that isn't in `FileSystemStorage` — `Storage` keys and classpath resources won't resolve. Copy to a `FileSystemStorage` path first (`getAppHomePath() + name`). +- Don't assume `isCompleted()` means a page came out — it means the OS accepted the job. +- Don't try to lay out print content with CN1 layouts expecting pagination; render to a PDF/image and print that. +- Don't poll for a result — the listener fires exactly once on the EDT. diff --git a/scripts/initializr/common/src/main/resources/skill/references/ui-components.md b/scripts/initializr/common/src/main/resources/skill/references/ui-components.md index fe26d44a7f..869a987d42 100644 --- a/scripts/initializr/common/src/main/resources/skill/references/ui-components.md +++ b/scripts/initializr/common/src/main/resources/skill/references/ui-components.md @@ -47,7 +47,7 @@ form.add(BorderLayout.CENTER, col); | `SpanLabel` | Multi-line wrapped text | Use for descriptions/copy | | `Button` | Tappable button | `.pressed` UIID variant for press state | | `TextField` | Single-line input | Use `TextField.setUIID("InitializrField")` to apply CSS | -| `TextArea` | Multi-line input | | +| `TextArea` | Multi-line input | `setGrowByContent(true)` grows the field to fit its text reliably *while editing* (not just after focus leaves). | | `TextComponent` | Material-Design–style input — floating label, optional description / error message, optional action icon | The right default for new form inputs. See *TextComponent* below. | | `Picker` | Native picker (date/time/string/list) | Reads as a button, opens a native sheet on mobile. **Use this instead of `ComboBox`.** | | `Switch` / `CheckBox` / `RadioButton` | Toggles | RadioButton requires a `ButtonGroup` | @@ -60,10 +60,20 @@ form.add(BorderLayout.CENTER, col); | `InfiniteProgress` | Activity spinner | | | `Dialog` | Modal popup | `Dialog.show(...)` blocks current EDT pump | | `Toolbar` | Top bar | Already on every Form | -| `BrowserComponent` | Embedded WebView | Use sparingly; ParparVM/iOS WebView has caveats | +| `BrowserComponent` | Embedded WebView | Use sparingly; ParparVM/iOS WebView has caveats. See *BrowserComponent appearance* below. | **Note on `ComboBox`**: It exists but is **not recommended** in CN1. The dropdown rendering is awkward on touch screens and behaves inconsistently across platforms. Use `Picker` (set `pickerType` to `Display.PICKER_TYPE_STRINGS` for a string-list picker) — it opens a native sheet on iOS, a Material dialog on Android, and a normal popup in the simulator. `ComboBox` is kept only for legacy ports of Swing apps. +### BrowserComponent appearance (light/dark) + +On iOS you can pin the WebView's appearance (the `prefers-color-scheme` the page sees and the native form-control rendering) instead of letting it follow the device: + +```java +browser.setProperty(BrowserComponent.BROWSER_PROPERTY_INTERFACE_STYLE, "light"); // "light" | "dark" | "auto" +``` + +`"auto"` (the default) follows the device theme. Honored on iOS WKWebView; other platforms ignore it. Useful when your embedded HTML is only styled for one mode and you don't want the system flipping it. + ### Package locations — don't trust autocomplete to find these A few components live in package paths that don't match where you'd guess from the type name. Importing from the wrong package gives `cannot find symbol` and the IDE will helpfully offer to import the (deprecated or non-existent) sibling. @@ -226,6 +236,28 @@ ToastBar.showMessage("Saved!", FontImage.MATERIAL_CHECK); For confirmation flows prefer `Dialog.show(...)` overloads — they map to native sheets on iOS. `ToastBar` is the only built-in non-blocking notification. +### `InteractionDialog` — a non-blocking, movable dialog + +`com.codename1.components.InteractionDialog` is a dialog that does **not** block the EDT and lets the user keep interacting with the form behind it (toolbars, lists). Use it for floating panels, in-place editors, or a popover that should coexist with the screen. + +```java +import com.codename1.components.InteractionDialog; + +InteractionDialog dlg = new InteractionDialog("Filters"); +dlg.setLayout(BoxLayout.y()); +dlg.add(new Switch("Unread only")); +dlg.add(new Button("Apply") {{ addActionListener(e -> dlg.dispose()); }}); +// position + size in pixels; show() leaves the rest of the form live +dlg.show(top, left, bottom, right); +``` + +**Stackable mode** (global opt-in): by default, disposing one `InteractionDialog` runs `removeAll()` on the shared layered pane and wipes any *other* interaction dialog still on screen. If your app shows two at once, enable stackable mode once at startup so siblings survive — later-shown dialogs simply layer on top, and the shared pane is cleared only when the last one closes: + +```java +InteractionDialog.setStackable(true); // app-wide; default is false (back-compatible) +boolean on = InteractionDialog.isStackable(); +``` + ## Styling: UIIDs over inline styles A component's **UIID** is its CSS selector. `Button` is the default UIID for `new Button(...)`. Change it to target a CSS rule: